├── README.md ├── Advanced.md ├── chuckle-post-ai.css ├── LICENSE ├── chuckle-post-ai.min.js └── chuckle-post-ai.js /README.md: -------------------------------------------------------------------------------- 1 | > 本库短期内暂时无继续对接tianliGPT的更新计划,请查看最新的 [Post-Abstract-AI](https://github.com/zhheo/Post-Abstract-AI) 项目 2 | 3 | # Post-Summary-AI 4 | 一个较通用的,生成网站内文章**摘要**(简介),并**推荐**相关文章的AI(前端实现),基于tianliGPT后端 5 | 6 | 你可以前往这篇文章查看效果[文章添加预设或实时生成的AI简介](https://www.qcqx.cn/article/17d3383a.html) 7 | 8 | > 该项目理论支持所有类型的网站,无论动态还是静态站,起初该项目是为了个人博客而生的 9 | 10 | *** 11 | 12 | ## 1.效果 13 | 更多的 Post-Summary-AI 部署效果请查看[部署展示](https://github.com/qxchuckle/Post-Summary-AI#8%E9%83%A8%E7%BD%B2%E5%B1%95%E7%A4%BA) 14 | ![image](https://github.com/qxchuckle/Post-Summary-AI/assets/55614189/9dda32f5-f97b-43a1-89a5-83fc014980df) 15 | ![image](https://github.com/qxchuckle/Post-Summary-AI/assets/55614189/3e63a44b-c16f-46aa-98c8-1ba5c05fae78) 16 | 17 | *** 18 | 19 | ## 2.快速上手 20 | 非常简单,引入下面这些代码到你的网站内,并修改配置项后即可 21 | 22 | cdn1.tianli0.top、jsd.onmicrosoft.cn 是公益cdn,若无法访问或为确保资源的稳定,建议下载仓库对应文件至本地引入 23 | 24 | **基本配置**如下,更多**进阶**配置项和**实验性**功能,请查看[进阶操作](https://github.com/qxchuckle/Post-Summary-AI/blob/master/Advanced.md) 25 | 26 | ```html 27 | 28 | 29 | 30 | 40 | ``` 41 | 42 | **4.7**版本开始,无需再手动引入 CSS,JS 会在合适时机自动插入 CSS 43 | 44 | **AI构造函数 `ChucklePostAI({ /* 传入配置对象 */ })` 详解** 45 | 1. `el` **文章内容**所在的元素属性的选择器,也是AI**挂载**的容器,AI将会挂载到该容器的最前面 46 | 2. `key` 驱动AI所必须的key,即是tianliGPT后端服务所必须的**key** 47 | 3. `rec_method` 文章推荐方式,**all**:匹配数据库内所有文章进行推荐,**web**:仅当前站内的文章,**默认all** 48 | 49 | 项目开发不易,可以前往[爱发电](https://afdian.net/a/chuckle)给予我赞助 50 | 51 | *** 52 | 53 | ## 3.注意事项 54 | 1. 若是**动态网站**(静态或服务端渲染网站无需注意此事项),文章内容需要通过后端接口获取后返回给前端展示的,建议将 `new ChucklePostAI()` 放到获取文章成功后的**回调**中(即获取文章成功后才执行某些JS代码),这样可以保证访客在文章出现后,才能去点击按钮获取AI摘要,以免获取到空文章内容,返回错误的摘要。不同的网站有各自的获取文章成功后的回调,请查阅自己网站系统的文档。 55 | 56 | 2. 若网站开启了**PJAX**,可能会存在切换页面后JS插件无法正常执行的问题,若你不知该如何主动适配,可以将 `pjax` 配置项设为 **true**,具体请查看[进阶操作](https://github.com/qxchuckle/Post-Summary-AI/blob/master/Advanced.md)的第 **9** 点。 57 | 58 | *** 59 | 60 | ## 4.tianliGPT-KEY 61 | tianliGPT的key请到[爱发电](https://afdian.net/item/f18c2e08db4411eda2f25254001e7c00)中购买,10元5万字符(常有优惠)。请求过的内容再次请求不会消耗key,可以无限期使用。 62 | 63 | - 相比实时请求OpenAI,使用tianliGPT可以让你请求过的内容不再消耗key,并在国内更快速的获取摘要,适合生产环境。 64 | - key消耗完毕,已经请求过的内容仍然可以继续请求,避免了被恶意请求造成的资金损失和业务停摆。 65 | - 符合中国大陆法律法规。 66 | 67 | **注意事项:** 68 | 1. 购买完成后,进入[管理后台](https://summary.zhheo.com/):summary.zhheo.com ,登录后点击右上角的“添加新网站”,输入密钥即可绑定成功。 69 | 2. 若需要进行**本地调试**,请在管理后台将 127.0.0.1:端口 加入白名单,否则会触发防盗KEY,无法正常获取摘要。 70 | 71 | *** 72 | 73 | ## 5.版本升级 74 | 修改引入资源的版本号,版本号可在[releases](https://github.com/qxchuckle/Post-Summary-AI/releases)查看 75 | ![image](https://github.com/qxchuckle/Post-Summary-AI/assets/55614189/7e9d3ef9-bdfa-40f7-bd97-9183a02e96d8) 76 | 77 | *** 78 | 79 | ## 6.进阶操作 80 | 摘要AI的**进阶用法**,以及一些**实验性**功能:[进阶操作](https://github.com/qxchuckle/Post-Summary-AI/blob/master/Advanced.md) 81 | 82 | *** 83 | 84 | ## 7.技术支持 85 | 若你的网站接入该项目有困难,可以提 [issues](https://github.com/qxchuckle/Post-Summary-AI/issues),简单讲述你所遇到的困难,并附上**网站地址**,你将会获得快速的回复。 86 | 87 | 工单系统、反馈互动社区:https://support.qq.com/product/600565 88 | 89 | 也可以加入**QQ频道**:点击链接加入讨论子频道【TianliGPT 问题交流】:https://pd.qq.com/s/7cx85i9l0 90 | 91 | *** 92 | 93 | ## 8.部署展示 94 | 这里展示已经成功部署 Post-Summary-AI 的网站,若你已成功部署,可以提 [issues](https://github.com/qxchuckle/Post-Summary-AI/issues),会将你展示于此 95 | 96 | 1. [MoyuqLのBlog](https://blog.moyuql.top/) 97 | 2. [佳凌雾杨的日记](https://www.chukogals.top/) 98 | 3. [王卓Sco](https://blog.sondy.top/) 99 | 4. [Teink](https://te.ink/) 100 | 101 | *** 102 | 103 | ## 9.同类友情项目 104 | [Post-Abstract-AI](https://github.com/zhheo/Post-Abstract-AI) 105 | 106 | 友情项目和本项目都是基于tianliGPT的AI摘要前端实现,可自行选择适合你网站的项目。 107 | 108 | ![image](https://github.com/qxchuckle/Post-Summary-AI/assets/55614189/352ebdec-c43a-40a7-8060-30230ed5aa0d) 109 | 110 | -------------------------------------------------------------------------------- /Advanced.md: -------------------------------------------------------------------------------- 1 | ## 进阶操作 2 | 这里是摘要AI的进阶用法,以及一些实验性功能 3 | 4 | **0、自定义文章标题所在的元素属性的选择器** 5 | `title_el` 可以指定文章标题所在的元素,标题将用于AI推荐和后续社区建设展示,无该配置项或为空,则默认获取浏览器标题 6 | 7 | ```js 8 | new ChucklePostAI({ 9 | // ...... 10 | title_el: '.post-title', 11 | }) 12 | ``` 13 | 14 | *** 15 | 16 | **1、自定义界面信息,修改AI名称和自我介绍等**,新增 `interface` 配置项。 17 | 18 | ```js 19 | new ChucklePostAI({ 20 | // ...... 21 | interface: { 22 | name: "QX-AI", // AI名称 23 | introduce: "我是文章辅助AI: QX-AI,点击下方的按钮,让我生成本文简介、推荐相关文章等。", // 自我介绍 24 | version: "GPT-4", // 右上角GPT版本文字 25 | button: ["介绍自己", "推荐相关文章", "生成AI简介", "矩阵穿梭"], // 四个按钮文本 26 | }, 27 | }) 28 | ``` 29 | 30 | *** 31 | 32 | **2、获取文章内容时,排除某些元素及其子元素的内容** 33 | 你可能不需要文章内一些元素的内容去生成摘要,或者这些内容对于生成摘要并没有帮助,比如代码框、版权信息等等。 34 | 35 | 如有需要可以使用 `exclude` 配置项。 36 | 37 | 往数组中加入需要排除的元素的 **className**,摘要AI会自动跳过对该**元素及其子元素**内容的获取。 38 | 39 | ```js 40 | new ChucklePostAI({ 41 | // ...... 42 | exclude: ['post-ai', 'highlight', 'Copyright-Notice', 'post-series', 'mini-sandbox'], 43 | }) 44 | ``` 45 | 46 | 以上是该配置项的默认值,建议保留对 **post-ai** 也就是摘要元素本身的排除,【todo】后续版本会将其默认进行排除,无论是否做了配置,但考虑到兼容性,在配置项中最好也做排除 47 | 48 | *** 49 | 50 | **3、黑名单:让指定页面、文章不显示摘要AI** 51 | 可能你想让某篇文章没有摘要AI,这个 `eliminate` 配置就对你有用。通过匹配当前 URL 中**唯一标识**的关键字符串实现排除。 52 | 53 | ```js 54 | new ChucklePostAI({ 55 | // ...... 56 | eliminate: [], 57 | }) 58 | ``` 59 | 60 | 例如你想让 https://www.qcqx.cn/article/544ba770.html 这篇文章不显示摘要AI,544ba770 是可以唯一标识该文章的字符串,则将其加入到数组中 61 | 62 | 当然,你也可以将除去域名外的路径,填入数组中。下面展示了多种写法。 63 | 64 | ```js 65 | eliminate: ['544ba770', '/article/544ba770.html'], 66 | ``` 67 | 68 | 另外,当 `el` 配置项无法区分一般页面和文章页面时,这个配置项也会有用。 69 | 70 | *** 71 | 72 | **4、白名单:只让指定页面显示摘要AI** 73 | 当你需要确定在某类页面才有摘要AI时,`whitelist` 这个配置项会对你有用。 74 | 75 | ```js 76 | new ChucklePostAI({ 77 | // ...... 78 | whitelist: [], 79 | }) 80 | ``` 81 | 82 | 和黑名单差不多的用法,例如你只想让 https://www.qcqx.cn/article/***.html 这种文章页面显示摘要AI,article 是可以唯一标识该类页面的字符串,则将其加入到数组中。 83 | 84 | 下面展示了多种写法。 85 | 86 | ```js 87 | whitelist: ['article', '/article/'], 88 | ``` 89 | 90 | *** 91 | 92 | **5、摘要AI挂载后直接请求并显示摘要** 93 | 默认是显示摘要AI的自我介绍,需要访客点击指定按钮后才显示摘要,但你可能想要**直接**显示摘要,那么 `summary_directly` 配置项正合你意。 94 | 95 | 将该配置项设置为 true 后,摘要AI会在挂载完后立刻请求并显示摘要。 96 | 97 | ```js 98 | new ChucklePostAI({ 99 | // ...... 100 | summary_directly: true, 101 | }) 102 | ``` 103 | 104 | > 不推荐前后端分离的网站打开此配置项(服务端渲染的动态站不受此影响),当文章还没从后端返回到前端渲染,摘要AI将获取到空的文章内容,当然大部分动态站都有文章加载好后再执行部分JS的配置,将 `new ChucklePostAI()` 放入其中,即可避免上述问题。 105 | 106 | *** 107 | 108 | **6、控制打字机效果** 109 | 虽然默认开启的打字机效果挺Cool,但你不喜欢,也可以用 `typewriter` 去关闭它 110 | 111 | false 关闭,true 开启,默认是开启 112 | 113 | ```js 114 | new ChucklePostAI({ 115 | // ...... 116 | typewriter: false, 117 | }) 118 | ``` 119 | 120 | *** 121 | 122 | **7、控制打字机效果的速度** 123 | 本项目自己实现了一套打字机效果,如果你对默认打字机的速度不满意,可以使用 `speed` 配置项自定义速度 124 | 125 | 单位ms,即输出一个字所用的基准时间,默认20ms 126 | 127 | ```js 128 | new ChucklePostAI({ 129 | // ...... 130 | speed: 20, 131 | }) 132 | ``` 133 | 134 | *** 135 | 136 | **8、隐藏矩阵穿梭按钮** 137 | 矩阵穿梭,随机跳转到一个接入了AI摘要的网站,但或许你并不需要矩阵穿梭的功能和按钮,那就用 `hide_shuttle` 配置项去掉吧 138 | 139 | ```js 140 | new ChucklePostAI({ 141 | // ...... 142 | hide_shuttle: true, 143 | }) 144 | ``` 145 | 146 | *** 147 | 148 | **9、适配PJAX** 149 | 若网站开启了**PJAX**,可能会存在切换页面后JS插件无法正常执行的问题。 150 | 151 | 本项目提供了一个可选的 `pjax` 适配选项。 152 | 153 | ```js 154 | new ChucklePostAI({ 155 | // ...... 156 | pjax: true, 157 | }) 158 | ``` 159 | 160 | 若仍然存在 pjax 导致的插件执行问题,请提 Issues 161 | 162 | *** 163 | 164 | **10、自定义css** 165 | 由于不同网站夜间模式适配问题,以及对css自定义的需求,这里提供了 `css` 的配置项。 166 | 167 | 你可以下载仓库中的 `chuckle-post-ai.css` ,修改完后压缩填入 `css` 配置项中 168 | 169 | ```js 170 | new ChucklePostAI({ 171 | // ...... 172 | css: `你的自定义css`, 173 | }) 174 | ``` 175 | 176 | *** 177 | 178 | **11、自定义文章截取字数及比例** 179 | 由于GPT有字数限制,所以对于较长的文章,将会按照一定比例,在文章的前中后截取一定字数的内容后,提交给后端生成摘要,默认为 1000 字数,按 5:3:2 截取 180 | 181 | 可以通过 `total_length` 设置文章截取字数,`ratio_string` 设置前中后截取比例。你可以降低字符数来让扣费变得更少,也可以增加字符数让摘要变得更准确。上限为5000,配置超过5000会重置为上限。 182 | 183 | ⚠️危险:更改此变量损失已消耗过的key,因为你提交的内容发生了变化。 184 | 185 | ```js 186 | new ChucklePostAI({ 187 | // ...... 188 | total_length: 1000, 189 | ratio_string: "5:3:2", 190 | }) 191 | ``` 192 | 193 | 通过实践标明,1000字的文章截取,GPT就能很好地生成摘要。 194 | 195 | *** 196 | 197 | **12、插入额外CSS** 198 | `additional_css` 插入额外的CSS以自定义局部样式,若无法覆盖,最好加上 !important 199 | 200 | ```js 201 | new ChucklePostAI({ 202 | // ...... 203 | additional_css: `#post-ai .ai-btn-box{display: none!important;}`, // 案例 204 | }) 205 | ``` 206 | 207 | *** 208 | 209 | **12、切换简介相关配置** 210 | 5.5版本后,允许点击“切换”按钮切换简介,该功能默认开启。 211 | 212 | **注意:** 切换简介本质上是经过一些处理后,重新生成一份简介,这将消耗key字数。 213 | 214 | `summary_toggle` 控制是否开启切换简介功能,`summary_num` 切换时允许生成的简介总数,默认3个,切换简介将会在该个数内重复循环。 215 | 216 | ```js 217 | new ChucklePostAI({ 218 | // ...... 219 | summary_toggle: true, 220 | summary_num: 3, 221 | }) 222 | ``` 223 | 224 | *** 225 | 226 | **12、摘要语音朗读** 227 | 5.6版本后,允许点击"唱片"图标朗读生成好的摘要,`summary_speech` 控制是否启用该功能,默认开启, 228 | 229 | ```js 230 | new ChucklePostAI({ 231 | // ...... 232 | summary_speech: true, 233 | }) 234 | ``` 235 | 236 | *** 237 | 238 | ## 实验性功能 239 | **以下是实验性功能,不保证其稳定性,但都已经过测试** 240 | 241 | **1、自动挂载:自动获取文章内容所在容器元素,而无需el配置项** 242 | 主动去审查元素,找到适合的容器,这对于萌新来说,有一些难度。 243 | 244 | 开启 `auto_mount` 将不需要 el 配置项,JS会自动通过一些算法找到文章所在的容器元素,经测试兼容大部分网站。开启该功能后,el 配置项将无效。 245 | 246 | **注意**:前后端分离的、文章内容需要通过后端接口获取的网站,不建议开启此功能,除非已经将 `new ChucklePostAI()` 放到获取文章成功后的**回调**中(即获取文章成功后才执行某些JS代码)。 247 | 248 | ```js 249 | new ChucklePostAI({ 250 | // ...... 251 | auto_mount: true, 252 | }) 253 | ``` 254 | 255 | 若你一不小心 el 配置项为空,该功能也会启用,无论你是否主动配置为 true。 256 | 257 | 有了这个实验性的功能,现在你的配置项可以非常得简单。 258 | 259 | ```js 260 | new ChucklePostAI({ 261 | auto_mount: true, // 开启自动挂载 262 | key: '123456', // key还是必须要的 263 | whitelist: ['article', '/article/'], // 白名单是必要的 264 | }) 265 | ``` 266 | 267 | 是的,`whitelist` 白名单是必要的,请查看进阶操作第四点,不然所有的页面都会尝试自动挂载。 268 | 269 | > 若文章内容实在是太少,一张图,几句话,可能无法正确挂载 270 | 271 | *** 272 | -------------------------------------------------------------------------------- /chuckle-post-ai.css: -------------------------------------------------------------------------------- 1 | :root{ 2 | --ai-font-color:#353535; 3 | --ai-post-bg: #f1f3f8; 4 | --ai-content-bg: #fff; 5 | --ai-content-border: 1px solid #e3e8f7; 6 | --ai-border:1px solid #e3e8f7bd; 7 | --ai-tag-bg:rgba(48, 52, 63, 0.80); 8 | --ai-cursor: #333; 9 | --ai-btn-bg: rgba(48, 52, 63, 0.75); 10 | --ai-title-color: #4c4948; 11 | --ai-btn-color: #fff; 12 | --ai-speech-content: #fff; 13 | } 14 | [data-theme=dark], .theme-dark, body.dark, body.dark-theme { 15 | --ai-font-color:rgba(255,255,255,0.9); 16 | --ai-post-bg:#30343f; 17 | --ai-content-bg: #1d1e22; 18 | --ai-content-border: 1px solid #42444a; 19 | --ai-border:1px solid #3d3d3f; 20 | --ai-tag-bg:#1d1e22; 21 | --ai-cursor: rgb(255, 255, 255, 0.9); 22 | --ai-btn-bg: #1d1e22; 23 | --ai-title-color: rgba(255,255,255,0.86); 24 | --ai-btn-color: rgb(255, 255, 255, 0.9); 25 | --ai-speech-content: #1d1e22; 26 | } 27 | #post-ai.post-ai{ 28 | background: var(--ai-post-bg); 29 | border-radius: 12px; 30 | padding: 10px 12px 11px; 31 | line-height: 1.3; 32 | border: var(--ai-border); 33 | margin-top: 10px; 34 | margin-bottom: 6px; 35 | transition: all 0.3s; 36 | -webkit-transition: all 0.3s; 37 | -moz-transition: all 0.3s; 38 | -ms-transition: all 0.3s; 39 | -o-transition: all 0.3s; 40 | } 41 | #post-ai .ai-title{ 42 | display: flex; 43 | color: var(--ai-title-color); 44 | border-radius: 8px; 45 | align-items: center; 46 | padding: 0 6px; 47 | position: relative; 48 | } 49 | #post-ai .ai-title i{ 50 | font-weight: 800; 51 | } 52 | #post-ai .ai-title-text{ 53 | font-weight: bold; 54 | margin-left: 8px; 55 | font-size: 17px; 56 | } 57 | #post-ai .ai-tag{ 58 | font-size: 12px; 59 | background-color: var(--ai-tag-bg); 60 | color: var(--ai-btn-color); 61 | border-radius: 4px; 62 | margin-left: auto; 63 | line-height: 1; 64 | padding: 4px 5px; 65 | border: var(--ai-border); 66 | } 67 | #post-ai .ai-explanation{ 68 | margin-top: 10px; 69 | padding: 8px 12px; 70 | background: var(--ai-content-bg); 71 | border-radius: 8px; 72 | border: var(--ai-content-border); 73 | font-size: 15.5px; 74 | line-height: 1.4; 75 | color: var(--ai-font-color); 76 | } 77 | #post-ai .ai-cursor{ 78 | display: inline-block; 79 | width: 7px; 80 | background: var(--ai-cursor); 81 | height: 16px; 82 | margin-bottom: -2px; 83 | opacity: 0.95; 84 | margin-left: 3px; 85 | transition: all 0.3s; 86 | -webkit-transition: all 0.3s; 87 | -moz-transition: all 0.3s; 88 | -ms-transition: all 0.3s; 89 | -o-transition: all 0.3s; 90 | } 91 | #post-ai .ai-btn-box{ 92 | font-size: 15.5px; 93 | width: 100%; 94 | display: flex; 95 | flex-direction: row; 96 | flex-wrap: wrap; 97 | } 98 | #post-ai .ai-btn-item{ 99 | padding: 5px 10px; 100 | margin: 10px 16px 0px 5px; 101 | width: fit-content; 102 | line-height: 1; 103 | background: var(--ai-btn-bg); 104 | border: var(--ai-border); 105 | color: var(--ai-btn-color); 106 | border-radius: 6px 6px 6px 0; 107 | -webkit-border-radius: 6px 6px 6px 0; 108 | -moz-border-radius: 6px 6px 6px 0; 109 | -ms-border-radius: 6px 6px 6px 0; 110 | -o-border-radius: 6px 6px 6px 0; 111 | user-select: none; 112 | transition: all 0.3s; 113 | -webkit-transition: all 0.3s; 114 | -moz-transition: all 0.3s; 115 | -ms-transition: all 0.3s; 116 | -o-transition: all 0.3s; 117 | cursor: pointer; 118 | } 119 | #post-ai .ai-btn-item:hover{ 120 | background: #49b0f5dc; 121 | } 122 | #post-ai .ai-recommend{ 123 | display: flex; 124 | flex-direction: row; 125 | flex-wrap: wrap; 126 | } 127 | #post-ai .ai-recommend-item{ 128 | width: 50%; 129 | margin-top: 2px; 130 | } 131 | #post-ai .ai-recommend-item a{ 132 | border-bottom: 2px solid #4c98f7; 133 | padding: 0 .2em; 134 | color: #4c98f7; 135 | font-weight: 700; 136 | text-decoration: none; 137 | transition: all 0.3s; 138 | -webkit-transition: all 0.3s; 139 | -moz-transition: all 0.3s; 140 | -ms-transition: all 0.3s; 141 | -o-transition: all 0.3s; 142 | } 143 | #post-ai .ai-recommend-item a:hover{ 144 | background-color: #49b1f5; 145 | border-bottom: 2px solid #49b1f5; 146 | color: #fff; 147 | border-radius: 5px; 148 | } 149 | @media screen and (max-width: 768px){ 150 | #post-ai .ai-btn-box{ 151 | justify-content: center; 152 | } 153 | } 154 | #post-ai .ai-title>svg { 155 | width: 21px; 156 | height: 21px; 157 | } 158 | #post-ai .ai-title>svg path{ 159 | fill: var(--ai-font-color); 160 | } 161 | #post-ai .ai-Toggle{ 162 | font-size: 12px; 163 | border: var(--ai-border); 164 | background: var(--ai-btn-bg); 165 | color: var(--ai-btn-color); 166 | padding: 3px 4px; 167 | border-radius: 4px; 168 | margin-left: 6px; 169 | cursor: pointer; 170 | -webkit-transition: .3s; 171 | -moz-transition: .3s; 172 | -o-transition: .3s; 173 | -ms-transition: .3s; 174 | transition: .3s; 175 | font-weight: bolder; 176 | pointer-events: none; 177 | opacity: 0; 178 | } 179 | #post-ai .ai-Toggle:hover{ 180 | background: #49b0f5dc; 181 | } 182 | #post-ai .ai-speech-box{ 183 | width: 21px; 184 | height: 21px; 185 | background: var(--ai-font-color); 186 | margin-left: 7px; 187 | border-radius: 50%; 188 | display: flex; 189 | flex-direction: row; 190 | flex-wrap: wrap; 191 | align-content: center; 192 | justify-content: center; 193 | pointer-events:none; 194 | opacity:0; 195 | -webkit-transition:.3s; 196 | -moz-transition:.3s; 197 | -o-transition:.3s; 198 | -ms-transition:.3s; 199 | transition:.3s; 200 | cursor: pointer; 201 | } 202 | #post-ai .ai-speech-content{ 203 | width: 8px; 204 | background: var(--ai-speech-content); 205 | height: 8px; 206 | border-radius: 50%; 207 | -webkit-transition:.3s; 208 | -moz-transition:.3s; 209 | -o-transition:.3s; 210 | -ms-transition:.3s; 211 | transition:.3s; 212 | } 213 | #post-ai .ai-speech-box:hover .ai-speech-content{ 214 | background: #49b0f5; 215 | } 216 | @keyframes ai_breathe { 217 | 0% { 218 | transform: scale(0.9); 219 | -webkit-transform: scale(0.9); 220 | -moz-transform: scale(0.9); 221 | -ms-transform: scale(0.9); 222 | -o-transform: scale(0.9); 223 | } 224 | 50% { 225 | transform: scale(1); 226 | -webkit-transform: scale(1); 227 | -moz-transform: scale(1); 228 | -ms-transform: scale(1); 229 | -o-transform: scale(1); 230 | } 231 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /chuckle-post-ai.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Minified by jsDelivr using Terser v5.19.2. 3 | * Original file: /gh/qxchuckle/Post-Summary-AI@6.0/chuckle-post-ai.js 4 | * 5 | * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files 6 | */ 7 | function ChucklePostAI(t){function e(t){const e=document.querySelector(".post-ai");e&&e.parentElement.removeChild(e);const o=window.location.href;if(t.eliminate&&t.eliminate.length&&t.eliminate.some((t=>o.includes(t))))return void console.log("Post-Summary-AI 已排除当前页面(黑名单)");if(t.whitelist&&t.whitelist.length&&!t.whitelist.some((t=>o.includes(t))))return void console.log("Post-Summary-AI 已排除当前页面(白名单)");let n="";n=!t.auto_mount&&t.el?document.querySelector(t.el?t.el:"#post #article-container"):function(){function t(e){let o=1;for(const n of e.children)o+=t(n);return o}function e(t){if(["IFRAME","FOOTER","HEADER","BLOCKQUOTE"].includes(t.tagName))return!0;const e=["aplayer","comment"];return Array.from(t.classList).some((t=>e.some((e=>t.includes(e)))))}function o(t){const o={H1:1.5,H2:1,H3:.5,P:1};let n=0,i=null;function r(t){if(e(t))return;let a=0;for(const e of t.children)e.tagName in o&&(a+=o[e.tagName]);a>n&&(n=a,i=t);for(const e of t.children)r(e)}return r(t),i}function n(){const n=[document.body];let i=0,r=null;for(;n.length>0;){const o=n.shift();if(e(o))continue;const a=t(o);a>i&&(i=a,r=o);for(const t of o.children)n.push(t)}return o(r)}return n()}();const i=document.querySelector(t.title_el)?document.querySelector(t.title_el).textContent:document.title;if(!n)return;const r={name:"QX-AI",introduce:"我是文章辅助AI: QX-AI,点击下方的按钮,让我生成本文简介、推荐相关文章等。",version:"GPT-4",button:["介绍自己","推荐相关文章","生成AI简介","矩阵穿梭"],...t.interface};!function(){const e="qx-ai-style";if(document.getElementById(e))return;const o=document.createElement("style");o.id=e,o.textContent=t.css||":root{--ai-font-color:#353535;--ai-post-bg:#f1f3f8;--ai-content-bg:#fff;--ai-content-border:1px solid #e3e8f7;--ai-border:1px solid #e3e8f7bd;--ai-tag-bg:rgba(48,52,63,0.80);--ai-cursor:#333;--ai-btn-bg:rgba(48,52,63,0.75);--ai-title-color:#4c4948;--ai-btn-color:#fff;--ai-speech-content:#fff;}[data-theme=dark],.theme-dark,body.dark,body.dark-theme{--ai-font-color:rgba(255,255,255,0.9);--ai-post-bg:#30343f;--ai-content-bg:#1d1e22;--ai-content-border:1px solid #42444a;--ai-border:1px solid #3d3d3f;--ai-tag-bg:#1d1e22;--ai-cursor:rgb(255,255,255,0.9);--ai-btn-bg:#1d1e22;--ai-title-color:rgba(255,255,255,0.86);--ai-btn-color:rgb(255,255,255,0.9);--ai-speech-content:#1d1e22;}#post-ai.post-ai{background:var(--ai-post-bg);border-radius:12px;padding:10px 12px 11px;line-height:1.3;border:var(--ai-border);margin-top:10px;margin-bottom:6px;transition:all 0.3s;-webkit-transition:all 0.3s;-moz-transition:all 0.3s;-ms-transition:all 0.3s;-o-transition:all 0.3s;}#post-ai .ai-title{display:flex;color:var(--ai-title-color);border-radius:8px;align-items:center;padding:0 6px;position:relative;}#post-ai .ai-title i{font-weight:800;}#post-ai .ai-title-text{font-weight:bold;margin-left:8px;font-size:17px;}#post-ai .ai-tag{font-size:12px;background-color:var(--ai-tag-bg);color:var(--ai-btn-color);border-radius:4px;margin-left:auto;line-height:1;padding:4px 5px;border:var(--ai-border);}#post-ai .ai-explanation{margin-top:10px;padding:8px 12px;background:var(--ai-content-bg);border-radius:8px;border:var(--ai-content-border);font-size:15.5px;line-height:1.4;color:var(--ai-font-color);}#post-ai .ai-cursor{display:inline-block;width:7px;background:var(--ai-cursor);height:16px;margin-bottom:-2px;opacity:0.95;margin-left:3px;transition:all 0.3s;-webkit-transition:all 0.3s;-moz-transition:all 0.3s;-ms-transition:all 0.3s;-o-transition:all 0.3s;}#post-ai .ai-btn-box{font-size:15.5px;width:100%;display:flex;flex-direction:row;flex-wrap:wrap;}#post-ai .ai-btn-item{padding:5px 10px;margin:10px 16px 0px 5px;width:fit-content;line-height:1;background:var(--ai-btn-bg);border:var(--ai-border);color:var(--ai-btn-color);border-radius:6px 6px 6px 0;-webkit-border-radius:6px 6px 6px 0;-moz-border-radius:6px 6px 6px 0;-ms-border-radius:6px 6px 6px 0;-o-border-radius:6px 6px 6px 0;user-select:none;transition:all 0.3s;-webkit-transition:all 0.3s;-moz-transition:all 0.3s;-ms-transition:all 0.3s;-o-transition:all 0.3s;cursor:pointer;}#post-ai .ai-btn-item:hover{background:#49b0f5dc;}#post-ai .ai-recommend{display:flex;flex-direction:row;flex-wrap:wrap;}#post-ai .ai-recommend-item{width:50%;margin-top:2px;}#post-ai .ai-recommend-item a{border-bottom:2px solid #4c98f7;padding:0 .2em;color:#4c98f7;font-weight:700;text-decoration:none;transition:all 0.3s;-webkit-transition:all 0.3s;-moz-transition:all 0.3s;-ms-transition:all 0.3s;-o-transition:all 0.3s;}#post-ai .ai-recommend-item a:hover{background-color:#49b1f5;border-bottom:2px solid #49b1f5;color:#fff;border-radius:5px;}@media screen and (max-width:768px){#post-ai .ai-btn-box{justify-content:center;}}#post-ai .ai-title>svg{width:21px;height:21px;}#post-ai .ai-title>svg path{fill:var(--ai-font-color);}#post-ai .ai-Toggle{font-size:12px;border:var(--ai-border);background:var(--ai-btn-bg);color:var(--ai-btn-color);padding:3px 4px;border-radius:4px;margin-left:6px;cursor:pointer;-webkit-transition:.3s;-moz-transition:.3s;-o-transition:.3s;-ms-transition:.3s;transition:.3s;font-weight:bolder;pointer-events:none;opacity:0;}#post-ai .ai-Toggle:hover{background:#49b0f5dc;}#post-ai .ai-speech-box{width:21px;height:21px;background:var(--ai-font-color);margin-left:7px;border-radius:50%;display:flex;flex-direction:row;flex-wrap:wrap;align-content:center;justify-content:center;pointer-events:none;opacity:0;-webkit-transition:.3s;-moz-transition:.3s;-o-transition:.3s;-ms-transition:.3s;transition:.3s;cursor:pointer;}#post-ai .ai-speech-content{width:8px;background:var(--ai-speech-content);height:8px;border-radius:50%;-webkit-transition:.3s;-moz-transition:.3s;-o-transition:.3s;-ms-transition:.3s;transition:.3s;}#post-ai .ai-speech-box:hover .ai-speech-content{background:#49b0f5;}@keyframes ai_breathe{0%{transform:scale(0.9);-webkit-transform:scale(0.9);-moz-transform:scale(0.9);-ms-transform:scale(0.9);-o-transform:scale(0.9);}50%{transform:scale(1);-webkit-transform:scale(1);-moz-transform:scale(1);-ms-transform:scale(1);-o-transform:scale(1);}}",t.additional_css&&(o.textContent+=t.additional_css),document.head.appendChild(o)}();const a=document.createElement("div");a.className="post-ai",a.setAttribute("id","post-ai"),n.insertBefore(a,n.firstChild),a.innerHTML=`
\n \n \n
${r.name}
\n
切换简介
\n
\n
\n
\n
${r.version}
\n
\n
${r.name}初始化中...
\n
\n
${r.button[0]}
\n
${r.button[1]}
\n
${r.button[2]}
\n
${r.button[3]}
\n
`;let s=!0,l=document.querySelector(".ai-explanation"),c=document.querySelector(".post-ai"),d=document.querySelectorAll(".ai-btn-item"),m=document.querySelector(".ai-Toggle"),p=document.querySelector(".ai-speech-box"),u="",f="",g=600,h=0,b=0,y=t.speed||20,x=7.5*y,w=[],v=0,S=!1,k=new AbortController,E=k.signal,I="",C="";const L=t.summary_toggle??!0,A=t.summary_speech??!0;let T=0,$=!1,_="",N="",O=!1;const q=t.summary_num||3,j=!0,z="填入chatGPT的apiKey",P=`https://${window.location.host}/`,M=t.key?t.key:"123456",R=t=>{if(s)if(R.start||(R.start=t),v=t-R.start,v>=y){if(R.start=t,h{requestAnimationFrame(R)}),e)}}else requestAnimationFrame(R)},H=new IntersectionObserver((t=>{let e=t[0].isIntersecting;s=e,s&&(g=0===h?200:20,w[1]=setTimeout((()=>{b&&(h=0,b=0),0===h&&(l.innerHTML=u.charAt(0)),requestAnimationFrame(R)}),g))}),{threshold:0});function J(t=!0,e="生成中. . ."){h=0,b=1,w.length&&w.forEach((t=>{t&&clearTimeout(t)})),s=!1,v=0,l.innerHTML=t?e:"请等待. . .",S||k.abort(),u="",f="",L&&(m.style.opacity="0",m.style.pointerEvents="none"),A&&(Z(),p.style.opacity="0",p.style.pointerEvents="none"),H.disconnect()}function G(e,o=!0){t.hasOwnProperty("typewriter")&&!t.typewriter?l.innerHTML=e:(J(o),u=e,f=u.length,H.observe(c))}function B(){G(r.introduce)}function F(){J(),w[2]=setTimeout((async()=>{let e=await async function(){S=!1,k=new AbortController,E=k.signal;let e="",o="",n="";const i={signal:E,method:"GET",headers:{"content-type":"application/x-www-form-urlencoded"}};if(sessionStorage.getItem("recommendList"))n=JSON.parse(sessionStorage.getItem("recommendList"));else{try{if(e=await fetch(`https://summary.tianli0.top/recommends?url=${encodeURIComponent(window.location.href)}&author=${t.rec_method?t.rec_method:"all"}`,i),S=!0,429===e.status&&G("请求过于频繁,请稍后再请求AI。"),!e.ok)throw new Error("Response not ok")}catch(t){return"AbortError"===t.name||(console.error("Error occurred:",t),G("获取推荐出错了,请稍后再试。")),S=!0,!1}n=await e.json(),sessionStorage.setItem("recommendList",JSON.stringify(n))}if(n.hasOwnProperty("success")&&!n.success)return!1;o="推荐文章:
",o+='
',n.forEach(((t,e)=>{o+=``})),o+="
";return o}();""===e||!1===e?G(`${r.name}未能找到任何可推荐的文章。`):e&&(l.innerHTML=e)}),200)}async function U(){J();const t=Y(n),e=await tt(t,j);e&&(G(e.summary),L&&(m.style.opacity="1",m.style.pointerEvents="auto",Q()))}async function D(){J(!0,"矩阵穿梭中. . ."),S=!1,k=new AbortController,E=k.signal;let t="",e="";const o={signal:E,method:"GET",headers:{"content-type":"application/x-www-form-urlencoded"}};if(sessionStorage.getItem("matrixShuttle"))e=JSON.parse(sessionStorage.getItem("matrixShuttle"));else{try{if(t=await fetch("https://summary.tianli0.top/websites_used",o),S=!0,429===t.status&&G("请求过于频繁,请稍后再请求AI。"),!t.ok)throw new Error("Response not ok")}catch(t){return"AbortError"===t.name||(console.error("Error occurred:",t),G("矩阵穿梭失败了,请稍后再试。")),S=!0,!1}e=await t.json(),sessionStorage.setItem("matrixShuttle",JSON.stringify(e))}const n=function(t){if(0===t.length)return null;const e=function(t){const e=new Uint32Array(1);return window.crypto.getRandomValues(e),e[0]%t}(t.length);return t[e]}(e.websites);n?(G(`正在前往 ${n} ,已有 ${e.count} 个网站接入AI摘要。`),w[2]=setTimeout((()=>{window.open(`https://${n}`,"_blank")}),100*y)):G("没有可以穿梭的网站。")}function Z(t=!1){A&&(_&&(_.pause(),_.remove()),_=null,p.style.opacity="1",p.style.animation="",t&&(N=null))}function Q(){A&&(p.style.opacity="1",p.style.animation="",p.style.pointerEvents="auto")}async function X(e){if(!A)return;const o=URL.createObjectURL(e);if(_=new Audio(o),_.play(),t.pjax){function n(){_.pause(),_.remove(),document.removeEventListener("pjax:complete",n)}document.removeEventListener("pjax:complete",n),document.addEventListener("pjax:complete",n)}p.style.opacity="0.4",p.style.animation="ai_breathe .7s linear infinite",_.removeEventListener("ended",K),_.addEventListener("ended",K)}function K(){Z()}function V(e){const o=t.exclude?t.exclude:["highlight","Copyright-Notice","post-ai","post-series","mini-sandbox"];o.includes("post-ai")||o.push("post-ai");const n=["script","style","iframe","embed","video","audio","source","canvas","img","svg","hr","input","form"];let i="";for(let t of e.childNodes)if(t.nodeType===Node.TEXT_NODE)i+=t.textContent.trim();else if(t.nodeType===Node.ELEMENT_NODE){let e=!1;for(let n of t.classList)if(o.includes(n)){e=!0;break}let r=n.includes(t.tagName.toLowerCase());if(!e&&!r){i+=V(t)}}return i.replace(/\s+/g,"")}function W(t){const e=t.querySelectorAll("h1, h2, h3, h4"),o=[];for(let t=0;tt+e),0),r=Math.min(t.length,e),a=n.map((t=>Math.floor(r*t/i))),s=t.substring(0,a[0]),l=(t.length-300)/2;return s+t.substring(l,l+a[1])+t.substring(t.length-a[2])}(V(e),o,i)}`}else n=`${V(e)}`;return n}async function tt(t,e=!0){if(!M)return"没有获取到key,代码可能没有安装正确,详细请查看文档。";if("123456"===M)return"请购买 key 使用,如果你能看到此条内容,则说明代码安装正确。";S=!1,k=new AbortController,E=k.signal;let o="";if(sessionStorage.getItem("summary"))return JSON.parse(sessionStorage.getItem("summary"));if(e){try{if(o=await fetch("https://summary.tianli0.top/",{signal:E,method:"POST",headers:{"Content-Type":"application/json",Referer:P},body:JSON.stringify({content:t,key:M,title:i,url:window.location.href,user_openid:I})}),S=!0,429===o.status&&G("请求过于频繁,请稍后再请求AI。"),!o.ok)throw new Error("Response not ok")}catch(t){return"AbortError"===t.name||("localhost"===window.location.hostname||"127.0.0.1"===window.location.hostname?G(`${r.name}请求tianliGPT出错了,你正在本地进行调试,请前往summary.zhheo.com添加本地域名(127.0.0.1:端口)的白名单。`):G(`${r.name}请求tianliGPT出错了,请稍后再试。`)),S=!0,""}const e=await o.json();return C=e.id,sessionStorage.setItem("summary",JSON.stringify(e)),Z(!0),e}{const e=`你是一个摘要生成工具,你需要解释我发送给你的内容,不要换行,不要超过200字,只需要介绍文章的内容,不需要提出建议和缺少的东西。请用中文回答,文章内容为:${t}`,n="https://api.openai.com/v1/chat/completions";try{if(o=await fetch(n,{signal:E,method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${z}`},body:JSON.stringify({model:"gpt-3.5-turbo",messages:[{role:"user",content:e}]})}),S=!0,429===o.status&&G("请求过于频繁,请稍后再请求AI。"),!o.ok)throw new Error("Response not ok")}catch(t){return console.error("Error occurred:",t),G(`${r.name}请求chatGPT出错了,请稍后再试。`),S=!0,""}const i=(await o.json()).choices[0].message.content;return sessionStorage.setItem("summary",i),i}}!async function(){sessionStorage.removeItem("recommendList"),sessionStorage.removeItem("backupsSummary");for(let t=0;t{t.hide_shuttle&&n===d.length-1?o.style.display="none":o.addEventListener("click",(()=>{e[n]()}))})),m=document.querySelector(".ai-Toggle"),L?m.addEventListener("click",(()=>{!async function(){J(),N=null,T=(T+1)%q;const t=Y(n)+"#".repeat(T);let e="";1!==T||$||(sessionStorage.setItem("backupsSummary",sessionStorage.getItem("summary")),$=!0),sessionStorage.getItem(`summary${"#".repeat(T)}`)?(e=JSON.parse(sessionStorage.getItem(`summary${"#".repeat(T)}`)),C=e.id,0===T?sessionStorage.setItem("summary",sessionStorage.getItem("backupsSummary")):sessionStorage.setItem("summary",sessionStorage.getItem(`summary${"#".repeat(T)}`))):(sessionStorage.removeItem("summary"),e=await tt(t,j),e&&sessionStorage.setItem(`summary${"#".repeat(T)}`,JSON.stringify(e))),e&&(G(e.summary),m.style.opacity="1",m.style.pointerEvents="auto",Q())}()})):m.style.display="none",p=document.querySelector(".ai-speech-box"),A?p.addEventListener("click",(()=>{!async function(){if(!C)return;let t="";if(!N||_)if(_&&_)O?(O=!1,_.play(),p.style.opacity="0.4",p.style.animation="ai_breathe .7s linear infinite"):(O=!0,_.pause(),p.style.opacity="1",p.style.animation="");else{const e={method:"GET",headers:{"Content-Type":"application/json",Referer:P}},o=new URLSearchParams({key:M,id:C});try{p.style.pointerEvents="none",p.style.opacity="0.4",t=await fetch(`https://summary.tianli0.top/audio?${o}`,e),403===t.status?console.error("403 refer与key不匹配。"):500===t.status?console.error("500 系统内部错误"):(N=await t.blob(),p.style.pointerEvents="auto",await X(N))}catch(t){console.log("摘要语音请求出错:",t),p.style.opacity="1",p.style.pointerEvents="auto"}}else await X(N)}()})):p.style.display="none",t.summary_directly?U():B(),I=localStorage.getItem("visitorId")||await async function(){try{const t=await import("https://openfpcdn.io/fingerprintjs/v4"),e=await t.default.load(),o=(await e.get()).visitorId;return localStorage.setItem("visitorId",o),o}catch(t){return console.error("生成ID失败"),null}}()}()}e(t),t.pjax&&document.addEventListener("pjax:complete",(()=>{setTimeout((()=>{e(t)}),0)}))}window.hasOwnProperty("aiExecuted")||(console.log("%cPost-Summary-AI 文章摘要AI生成工具:%chttps://github.com/qxchuckle/Post-Summary-AI%c","border:1px #888 solid;border-right:0;border-radius:5px 0 0 5px;padding: 5px 10px;color:white;background:#4976f5;margin:10px 0","border:1px #888 solid;border-left:0;border-radius:0 5px 5px 0;padding: 5px 10px;",""),window.aiExecuted="chuckle"),"undefined"!=typeof ai_option&&(console.log("正在使用旧版本配置方式,请前往项目仓库查看最新配置写法"),new ChucklePostAI(ai_option)); 8 | //# sourceMappingURL=/sm/d8805995aeade909c9ddf5f006de613f0a2320c0a4e54a07ef0705bbdec40547.map -------------------------------------------------------------------------------- /chuckle-post-ai.js: -------------------------------------------------------------------------------- 1 | if(!window.hasOwnProperty("aiExecuted")){ 2 | console.log(`%cPost-Summary-AI 文章摘要AI生成工具:%chttps://github.com/qxchuckle/Post-Summary-AI%c`, "border:1px #888 solid;border-right:0;border-radius:5px 0 0 5px;padding: 5px 10px;color:white;background:#4976f5;margin:10px 0", "border:1px #888 solid;border-left:0;border-radius:0 5px 5px 0;padding: 5px 10px;",""); 3 | window.aiExecuted = "chuckle"; 4 | } 5 | function ChucklePostAI(AI_option) { 6 | MAIN(AI_option); 7 | if(AI_option.pjax){ 8 | document.addEventListener('pjax:complete', ()=>{ 9 | setTimeout(()=>{ 10 | MAIN(AI_option); 11 | }, 0); 12 | }); 13 | } 14 | function MAIN(AI_option) { 15 | // 如果有则删除 16 | const box = document.querySelector(".post-ai"); 17 | if (box) { 18 | box.parentElement.removeChild(box); 19 | } 20 | const currentURL = window.location.href; 21 | // 排除页面 22 | if(AI_option.eliminate && AI_option.eliminate.length && AI_option.eliminate.some(item => currentURL.includes(item))){ 23 | console.log("Post-Summary-AI 已排除当前页面(黑名单)"); 24 | return; 25 | } 26 | if(AI_option.whitelist && AI_option.whitelist.length && !AI_option.whitelist.some(item => currentURL.includes(item))){ 27 | console.log("Post-Summary-AI 已排除当前页面(白名单)"); 28 | return; 29 | } 30 | // 获取挂载元素,即文章内容所在的容器元素 31 | let targetElement = ""; 32 | // 若el配置不存在则自动获取,如果auto_mount配置为真也自动获取 33 | if(!AI_option.auto_mount && AI_option.el){ 34 | targetElement = document.querySelector(AI_option.el ? AI_option.el : '#post #article-container'); 35 | }else{ 36 | targetElement = getArticleElements(); 37 | } 38 | // 获取文章标题,默认获取网页标题 39 | const post_title = document.querySelector(AI_option.title_el) ? document.querySelector(AI_option.title_el).textContent : document.title; 40 | if (!targetElement) { 41 | return; 42 | }; 43 | const interface = { 44 | name: "QX-AI", 45 | introduce: "我是文章辅助AI: QX-AI,点击下方的按钮,让我生成本文简介、推荐相关文章等。", 46 | version: "GPT-4", 47 | button: ["介绍自己", "推荐相关文章", "生成AI简介", "矩阵穿梭"], 48 | ...AI_option.interface 49 | } 50 | insertCSS(); // 插入css 51 | // 插入html结构 52 | const post_ai_box = document.createElement('div'); 53 | post_ai_box.className = 'post-ai'; 54 | post_ai_box.setAttribute('id', 'post-ai'); 55 | targetElement.insertBefore(post_ai_box, targetElement.firstChild); 56 | post_ai_box.innerHTML = `
57 | 58 | 59 |
${interface.name}
60 |
切换简介
61 |
62 |
63 |
64 |
${interface.version}
65 |
66 |
${interface.name}初始化中...
67 |
68 |
${interface.button[0]}
69 |
${interface.button[1]}
70 |
${interface.button[2]}
71 |
${interface.button[3]}
72 |
`; 73 | 74 | // ai主体业务逻辑 75 | let animationRunning = true; // 标志变量,控制动画函数的运行 76 | let explanation = document.querySelector('.ai-explanation'); 77 | let post_ai = document.querySelector('.post-ai'); 78 | let ai_btn_item = document.querySelectorAll('.ai-btn-item'); 79 | let ai_toggle = document.querySelector('.ai-Toggle'); 80 | let ai_speech = document.querySelector('.ai-speech-box'); 81 | let ai_str = ''; 82 | let ai_str_length = ''; 83 | let delay_init = 600; 84 | let i = 0; 85 | let j = 0; 86 | let speed = AI_option.speed || 20; 87 | let character_speed = speed*7.5; 88 | let sto = []; 89 | let elapsed = 0; 90 | let completeGenerate = false; 91 | let controller = new AbortController();//控制fetch 92 | let signal = controller.signal; 93 | let visitorId = ""; // 标识访客ID 94 | let summaryId = ""; // 记录当前摘要ID 95 | const summary_toggle = AI_option.summary_toggle ?? true; 96 | const summary_speech = AI_option.summary_speech ?? true; 97 | let switch_control = 0; 98 | let executedForSwitchControl = false; 99 | let summary_audio = ''; 100 | let audioBlob = ''; 101 | let isPaused = false; 102 | const summary_num = AI_option.summary_num || 3; // 切换时允许生成的摘要总数,默认3个 103 | //默认true,使用tianliGPT,false使用官方api,记得配置Key 104 | const choiceApi = true; 105 | const apiKey = "填入chatGPT的apiKey"; 106 | //tianliGPT的参数 107 | const tlReferer = `https://${window.location.host}/`; 108 | const tlKey = AI_option.key ? AI_option.key : '123456'; 109 | //----------------------------------------------- 110 | const animate = (timestamp) => { 111 | if (!animationRunning) { 112 | return; // 动画函数停止运行 113 | } 114 | if (!animate.start) animate.start = timestamp; 115 | elapsed = timestamp - animate.start; 116 | if (elapsed >= speed) { 117 | animate.start = timestamp; 118 | if (i < ai_str_length - 1) { 119 | let char = ai_str.charAt(i + 1); 120 | let delay = /[,.,。!?!?]/.test(char) ? character_speed : speed; 121 | if (explanation.firstElementChild) { 122 | explanation.removeChild(explanation.firstElementChild); 123 | } 124 | explanation.innerHTML += char; 125 | let div = document.createElement('div'); 126 | div.className = "ai-cursor"; 127 | explanation.appendChild(div); 128 | i++; 129 | if (delay === character_speed) { 130 | document.querySelector('.ai-explanation .ai-cursor').style.opacity = "0"; 131 | } 132 | if (i === ai_str_length - 1) { 133 | observer.disconnect();// 暂停监听 134 | explanation.removeChild(explanation.firstElementChild); 135 | } 136 | sto[0] = setTimeout(() => { 137 | requestAnimationFrame(animate); 138 | }, delay); 139 | } 140 | } else { 141 | requestAnimationFrame(animate); 142 | } 143 | }; 144 | const observer = new IntersectionObserver((entries) => { 145 | let isVisible = entries[0].isIntersecting; 146 | animationRunning = isVisible; // 标志变量更新 147 | if (animationRunning) { 148 | delay_init = i === 0 ? 200 : 20; 149 | sto[1] = setTimeout(() => { 150 | if (j) { 151 | i = 0; 152 | j = 0; 153 | } 154 | if (i === 0) { 155 | explanation.innerHTML = ai_str.charAt(0); 156 | } 157 | requestAnimationFrame(animate); 158 | }, delay_init); 159 | } 160 | }, { threshold: 0 }); 161 | function clearSTO() { 162 | if (sto.length) { 163 | sto.forEach((item) => { 164 | if (item) { 165 | clearTimeout(item); 166 | } 167 | }); 168 | } 169 | } 170 | function resetAI(df = true, str = '生成中. . .') { 171 | i = 0;//重置计数器 172 | j = 1; 173 | clearSTO(); 174 | animationRunning = false; 175 | elapsed = 0; 176 | if (df) { 177 | explanation.innerHTML = str; 178 | } else { 179 | explanation.innerHTML = '请等待. . .'; 180 | } 181 | if (!completeGenerate) { 182 | controller.abort(); 183 | } 184 | ai_str = ''; 185 | ai_str_length = ''; 186 | if(summary_toggle){ 187 | ai_toggle.style.opacity = "0"; 188 | ai_toggle.style.pointerEvents = "none"; 189 | } 190 | if(summary_speech){ 191 | summarySpeechInit(); 192 | ai_speech.style.opacity = "0"; 193 | ai_speech.style.pointerEvents = "none"; 194 | } 195 | observer.disconnect();// 暂停上一次监听 196 | } 197 | function startAI(str, df = true) { 198 | // 如果打字机配置项存在且为false,则关闭打字机,否则默认开启打字机效果 199 | if(AI_option.hasOwnProperty('typewriter') && !AI_option.typewriter){ 200 | explanation.innerHTML = str; 201 | }else{ 202 | resetAI(df); 203 | ai_str = str; 204 | ai_str_length = ai_str.length; 205 | observer.observe(post_ai);//启动新监听 206 | } 207 | } 208 | function aiIntroduce() { 209 | startAI(interface.introduce); 210 | } 211 | function aiRecommend() { 212 | resetAI(); 213 | sto[2] = setTimeout(async() => { 214 | let info = await recommendList(); 215 | if(info === "" || info === false){ 216 | startAI(`${interface.name}未能找到任何可推荐的文章。`); 217 | }else if(info){ 218 | explanation.innerHTML = info; 219 | } 220 | }, 200); 221 | } 222 | async function aiGenerateAbstract() { 223 | resetAI(); 224 | const ele = targetElement; 225 | const content = getTextContent(ele); 226 | const response = await getGptResponse(content, choiceApi);//true使用tianliGPT,false使用官方api 227 | if(response){ 228 | startAI(response.summary); 229 | if(summary_toggle){ 230 | ai_toggle.style.opacity = "1"; 231 | ai_toggle.style.pointerEvents = "auto"; 232 | summarySpeechShow(); 233 | } 234 | } 235 | } 236 | async function switchAbstract() { 237 | resetAI(); 238 | audioBlob = null; 239 | const ele = targetElement; 240 | switch_control = (switch_control + 1) % summary_num; 241 | const content = getTextContent(ele) + "#".repeat(switch_control); 242 | let response = ""; 243 | if(switch_control === 1 && !executedForSwitchControl){ 244 | sessionStorage.setItem('backupsSummary', sessionStorage.getItem('summary')); // 将第一次的简介存起来 245 | executedForSwitchControl = true; 246 | } 247 | if(!sessionStorage.getItem(`summary${"#".repeat(switch_control)}`)){ 248 | sessionStorage.removeItem('summary'); 249 | response = await getGptResponse(content, choiceApi); 250 | if(response){ 251 | sessionStorage.setItem(`summary${"#".repeat(switch_control)}`, JSON.stringify(response)); 252 | } 253 | }else{ 254 | response = JSON.parse(sessionStorage.getItem(`summary${"#".repeat(switch_control)}`)); 255 | summaryId = response.id; 256 | if(switch_control === 0){ 257 | sessionStorage.setItem('summary', sessionStorage.getItem('backupsSummary')); 258 | }else{ 259 | sessionStorage.setItem('summary', sessionStorage.getItem(`summary${"#".repeat(switch_control)}`)); 260 | } 261 | } 262 | if(response){ 263 | startAI(response.summary); 264 | ai_toggle.style.opacity = "1"; 265 | ai_toggle.style.pointerEvents = "auto"; 266 | summarySpeechShow(); 267 | } 268 | } 269 | async function recommendList() { 270 | completeGenerate = false; 271 | controller = new AbortController(); 272 | signal = controller.signal; 273 | let response = ''; 274 | let info = ''; 275 | let data = ''; 276 | const options = { 277 | signal, 278 | method: 'GET', 279 | headers: {'content-type': 'application/x-www-form-urlencoded'}, 280 | }; 281 | // 利用sessionStorage缓存推荐列表,有则缓存中读取,无则获取后缓存 282 | if(sessionStorage.getItem('recommendList')){ 283 | data = JSON.parse(sessionStorage.getItem('recommendList')); 284 | }else{ 285 | try { 286 | response = await fetch(`https://summary.tianli0.top/recommends?url=${encodeURIComponent(window.location.href)}&author=${AI_option.rec_method ? AI_option.rec_method : 'all'}`, options); 287 | completeGenerate = true; 288 | if (response.status === 429) { 289 | startAI('请求过于频繁,请稍后再请求AI。'); 290 | } 291 | if (!response.ok) { 292 | throw new Error('Response not ok'); 293 | } 294 | // 处理响应 295 | } catch (error) { 296 | if (error.name === "AbortError") { 297 | // console.log("请求已被中止"); 298 | }else{ 299 | console.error('Error occurred:', error); 300 | startAI("获取推荐出错了,请稍后再试。"); 301 | } 302 | completeGenerate = true; 303 | return false; 304 | } 305 | // 解析响应并返回结果 306 | data = await response.json(); 307 | sessionStorage.setItem('recommendList', JSON.stringify(data)); 308 | } 309 | if(data.hasOwnProperty("success") && !data.success){ 310 | return false; 311 | }else{ 312 | info = `推荐文章:
`; 313 | info += '
'; 314 | data.forEach((item, index) => { 315 | info += ``; 316 | }); 317 | info += '
' 318 | } 319 | return info; 320 | } 321 | // 矩阵穿梭 322 | async function matrixShuttle(){ 323 | resetAI(true, '矩阵穿梭中. . .'); 324 | completeGenerate = false; 325 | controller = new AbortController(); 326 | signal = controller.signal; 327 | let response = ''; 328 | let data = ''; 329 | const options = { 330 | signal, 331 | method: 'GET', 332 | headers: {'content-type': 'application/x-www-form-urlencoded'}, 333 | }; 334 | if(sessionStorage.getItem('matrixShuttle')){ 335 | data = JSON.parse(sessionStorage.getItem('matrixShuttle')); 336 | }else{ 337 | try { 338 | response = await fetch('https://summary.tianli0.top/websites_used', options); 339 | completeGenerate = true; 340 | if (response.status === 429) { 341 | startAI('请求过于频繁,请稍后再请求AI。'); 342 | } 343 | if (!response.ok) { 344 | throw new Error('Response not ok'); 345 | } 346 | // 处理响应 347 | } catch (error) { 348 | if (error.name === "AbortError") { 349 | // console.log("请求已被中止"); 350 | }else{ 351 | console.error('Error occurred:', error); 352 | startAI("矩阵穿梭失败了,请稍后再试。"); 353 | } 354 | completeGenerate = true; 355 | return false; 356 | } 357 | // 解析响应并返回结果 358 | data = await response.json(); 359 | sessionStorage.setItem('matrixShuttle', JSON.stringify(data)); 360 | } 361 | const randomElement = getRandomElementFromArray(data.websites); 362 | if(randomElement){ 363 | startAI(`正在前往 ${randomElement} ,已有 ${data.count} 个网站接入AI摘要。`); 364 | sto[2] = setTimeout(() => { 365 | window.open(`https://${randomElement}`, '_blank'); 366 | }, speed*100); 367 | }else{ 368 | startAI(`没有可以穿梭的网站。`); 369 | } 370 | } 371 | // 随机返回数组中一个元素 372 | function getRandomElementFromArray(array) { 373 | if (array.length === 0) { 374 | return null; // 返回null表示数组为空 375 | } 376 | const randomIndex = getRandomIndex(array.length); 377 | return array[randomIndex]; 378 | } 379 | function getRandomIndex(max) { 380 | const array = new Uint32Array(1); 381 | window.crypto.getRandomValues(array); 382 | return array[0] % max; 383 | } 384 | async function summarySpeech(){ 385 | if (!summaryId) return; 386 | let response = ''; 387 | if(audioBlob && !summary_audio){ 388 | await summarySpeechPlay(audioBlob); 389 | return; 390 | } 391 | if(summary_audio && summary_audio){ 392 | if(isPaused){ 393 | isPaused = false; 394 | summary_audio.play(); 395 | ai_speech.style.opacity = "0.4"; 396 | ai_speech.style.animation = "ai_breathe .7s linear infinite"; 397 | }else{ 398 | isPaused = true; 399 | summary_audio.pause(); 400 | ai_speech.style.opacity = "1"; 401 | ai_speech.style.animation = ""; 402 | } 403 | return; 404 | }else{ 405 | const options = { 406 | method: 'GET', 407 | headers: { 408 | "Content-Type": "application/json", 409 | "Referer": tlReferer 410 | }, 411 | }; 412 | const requestParams = new URLSearchParams({ 413 | key: tlKey, 414 | id: summaryId, 415 | }); 416 | try { 417 | ai_speech.style.pointerEvents = "none"; 418 | ai_speech.style.opacity = "0.4"; 419 | response = await fetch(`https://summary.tianli0.top/audio?${requestParams}`, options); 420 | if (response.status === 403) { 421 | console.error("403 refer与key不匹配。"); 422 | } else if (response.status === 500) { 423 | console.error("500 系统内部错误"); 424 | }else{ 425 | audioBlob = await response.blob(); 426 | ai_speech.style.pointerEvents = "auto"; 427 | await summarySpeechPlay(audioBlob); 428 | } 429 | }catch (error) { 430 | console.log("摘要语音请求出错:", error); 431 | ai_speech.style.opacity = "1"; 432 | ai_speech.style.pointerEvents = "auto"; 433 | } 434 | } 435 | } 436 | function summarySpeechInit(clBlob = false){ 437 | if(!summary_speech){ return; } 438 | if(summary_audio){ 439 | summary_audio.pause(); 440 | summary_audio.remove(); 441 | } 442 | summary_audio = null; 443 | ai_speech.style.opacity = "1"; 444 | ai_speech.style.animation = ""; 445 | if(clBlob){ 446 | audioBlob = null; 447 | } 448 | } 449 | function summarySpeechShow(){ 450 | if(!summary_speech){ return; } 451 | ai_speech.style.opacity = "1"; 452 | ai_speech.style.animation = ""; 453 | ai_speech.style.pointerEvents = "auto"; 454 | } 455 | async function summarySpeechPlay(audioBlob) { 456 | if(!summary_speech){ return; } 457 | const audioURL = URL.createObjectURL(audioBlob); 458 | summary_audio = new Audio(audioURL); 459 | summary_audio.play(); 460 | if(AI_option.pjax){ 461 | function handlePjaxComplete() { 462 | summary_audio.pause(); 463 | summary_audio.remove(); 464 | document.removeEventListener('pjax:complete', handlePjaxComplete); 465 | } 466 | document.removeEventListener('pjax:complete', handlePjaxComplete); 467 | document.addEventListener('pjax:complete', handlePjaxComplete); 468 | } 469 | ai_speech.style.opacity = "0.4"; 470 | ai_speech.style.animation = "ai_breathe .7s linear infinite"; 471 | summary_audio.removeEventListener("ended", handleSummaryAudioEnded); 472 | summary_audio.addEventListener("ended", handleSummaryAudioEnded); 473 | } 474 | function handleSummaryAudioEnded() { 475 | summarySpeechInit(); 476 | } 477 | //ai首屏初始化,绑定按钮注册事件 478 | async function ai_init() { 479 | // 清除缓存 480 | sessionStorage.removeItem('recommendList'); 481 | sessionStorage.removeItem('backupsSummary'); 482 | for (let i = 0; i < summary_num; i++) { 483 | sessionStorage.removeItem(`summary${"#".repeat(i)}`); 484 | } 485 | explanation = document.querySelector('.ai-explanation'); 486 | post_ai = document.querySelector('.post-ai'); 487 | ai_btn_item = document.querySelectorAll('.ai-btn-item'); 488 | const funArr = [aiIntroduce, aiRecommend, aiGenerateAbstract, matrixShuttle]; 489 | ai_btn_item.forEach((item, index) => { 490 | if(AI_option.hide_shuttle && index === ai_btn_item.length - 1){ 491 | item.style.display = 'none'; 492 | return; 493 | } 494 | item.addEventListener('click', () => { 495 | funArr[index](); 496 | }); 497 | }); 498 | ai_toggle = document.querySelector('.ai-Toggle'); 499 | if(summary_toggle){ 500 | ai_toggle.addEventListener('click', () => { 501 | switchAbstract(); 502 | }); 503 | }else{ 504 | ai_toggle.style.display = 'none'; 505 | } 506 | ai_speech = document.querySelector('.ai-speech-box'); 507 | if(summary_speech){ 508 | ai_speech.addEventListener('click', () => { 509 | summarySpeech(); 510 | }); 511 | }else{ 512 | ai_speech.style.display = 'none'; 513 | } 514 | if(AI_option.summary_directly){ 515 | aiGenerateAbstract(); 516 | }else{ 517 | aiIntroduce(); 518 | } 519 | // 获取或生成访客ID 520 | visitorId = localStorage.getItem('visitorId') || await generateVisitorID(); 521 | } 522 | async function generateVisitorID() { 523 | try { 524 | const FingerprintJS = await import('https://openfpcdn.io/fingerprintjs/v4'); 525 | const fp = await FingerprintJS.default.load(); 526 | const result = await fp.get(); 527 | const visitorId = result.visitorId; 528 | localStorage.setItem('visitorId', visitorId); 529 | return visitorId; 530 | } catch (error) { 531 | console.error("生成ID失败"); 532 | return null; 533 | } 534 | } 535 | //获取某个元素内的所有纯文本,并按顺序拼接返回 536 | function getText(element) { 537 | // 需要排除的元素及其子元素 538 | const excludeClasses = AI_option.exclude ? AI_option.exclude : ['highlight', 'Copyright-Notice', 'post-ai', 'post-series', 'mini-sandbox']; 539 | if (!excludeClasses.includes('post-ai')) { excludeClasses.push('post-ai'); } 540 | const excludeTags = ['script', 'style', 'iframe', 'embed', 'video', 'audio', 'source', 'canvas', 'img', 'svg', 'hr', 'input', 'form'];// 需要排除的标签名数组 541 | let textContent = ''; 542 | for (let node of element.childNodes) { 543 | if (node.nodeType === Node.TEXT_NODE) { 544 | // 如果是纯文本节点则获取内容拼接 545 | textContent += node.textContent.trim(); 546 | } else if (node.nodeType === Node.ELEMENT_NODE) { 547 | let hasExcludeClass = false; 548 | // 遍历类名 549 | for (let className of node.classList) { 550 | if (excludeClasses.includes(className)) { 551 | hasExcludeClass = true; 552 | break; 553 | } 554 | } 555 | let hasExcludeTag = excludeTags.includes(node.tagName.toLowerCase()); // 检查是否是需要排除的标签 556 | // 如果hasExcludeClass和hasExcludeTag都为false,即不包含需要排除的类和标签,可以继续向下遍历子元素 557 | if (!hasExcludeClass && !hasExcludeTag) { 558 | let innerTextContent = getText(node); 559 | textContent += innerTextContent; 560 | } 561 | } 562 | } 563 | // 返回纯文本节点的内容 564 | return textContent.replace(/\s+/g, ''); 565 | } 566 | //获取各级标题 567 | function extractHeadings(element) { 568 | const headings = element.querySelectorAll('h1, h2, h3, h4'); 569 | const result = []; 570 | for (let i = 0; i < headings.length; i++) { 571 | const heading = headings[i]; 572 | const headingText = heading.textContent.trim(); 573 | result.push(headingText); 574 | const childHeadings = extractHeadings(heading); 575 | result.push(...childHeadings); 576 | } 577 | return result.join(";"); 578 | } 579 | //按比例切割字符串 580 | function extractString(str, totalLength = 1000, ratioString = "5:3:2") { 581 | totalLength = Math.min(totalLength, 5000); // 最大5000字数 582 | if (str.length <= totalLength) { return str; } 583 | const ratios = ratioString.split(":").map(Number); 584 | const sumRatios = ratios.reduce((sum, ratio) => sum + ratio, 0); 585 | const availableLength = Math.min(str.length, totalLength); 586 | const partLengths = ratios.map(ratio => Math.floor((availableLength * ratio) / sumRatios)); 587 | const firstPart = str.substring(0, partLengths[0]); 588 | const midStartIndex = (str.length - 300) / 2; // 计算中间部分的起始索引 589 | const middlePart = str.substring(midStartIndex, midStartIndex + partLengths[1]); 590 | const lastPart = str.substring(str.length - partLengths[2]); 591 | const result = firstPart + middlePart + lastPart; 592 | return result; 593 | } 594 | //获得字符串,默认进行切割,false返回原文纯文本 595 | function getTextContent(element, i = true) { 596 | let content; 597 | if (i) { 598 | const totalLength = AI_option.total_length || 1000; 599 | const ratioString = AI_option.ratio_string || "5:3:2"; 600 | content = `文章的各级标题:${extractHeadings(element)}。文章内容的截取:${extractString(getText(element), totalLength, ratioString)}`; 601 | } else { 602 | content = `${getText(element)}`; 603 | } 604 | return content; 605 | } 606 | //发送请求获得简介 607 | async function getGptResponse(content, i = true) { 608 | if (!tlKey) { 609 | return "没有获取到key,代码可能没有安装正确,详细请查看文档。"; 610 | } 611 | if (tlKey === "123456") { 612 | return "请购买 key 使用,如果你能看到此条内容,则说明代码安装正确。"; 613 | } 614 | completeGenerate = false; 615 | controller = new AbortController(); 616 | signal = controller.signal; 617 | let response = ''; 618 | if(sessionStorage.getItem('summary')){ 619 | return JSON.parse(sessionStorage.getItem('summary')); 620 | } 621 | if (i) { 622 | try { 623 | response = await fetch('https://summary.tianli0.top/', { 624 | signal: signal, 625 | method: "POST", 626 | headers: { 627 | "Content-Type": "application/json", 628 | "Referer": tlReferer 629 | }, 630 | body: JSON.stringify({ 631 | content: content, 632 | key: tlKey, 633 | title: post_title, 634 | url: window.location.href, 635 | user_openid: visitorId 636 | }) 637 | }); 638 | completeGenerate = true; 639 | if (response.status === 429) { 640 | startAI('请求过于频繁,请稍后再请求AI。'); 641 | } 642 | if (!response.ok) { 643 | throw new Error('Response not ok'); 644 | } 645 | // 处理响应 646 | } catch (error) { 647 | if (error.name === "AbortError") { 648 | // console.log("请求已被中止"); 649 | }else if(window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1") { 650 | startAI(`${interface.name}请求tianliGPT出错了,你正在本地进行调试,请前往summary.zhheo.com添加本地域名(127.0.0.1:端口)的白名单。`); 651 | }else{ 652 | startAI(`${interface.name}请求tianliGPT出错了,请稍后再试。`); 653 | } 654 | completeGenerate = true; 655 | return ""; 656 | } 657 | // 解析响应并返回结果 658 | const data = await response.json(); 659 | summaryId = data.id; 660 | sessionStorage.setItem('summary', JSON.stringify(data)); 661 | summarySpeechInit(true); 662 | return data; 663 | } else { 664 | const prompt = `你是一个摘要生成工具,你需要解释我发送给你的内容,不要换行,不要超过200字,只需要介绍文章的内容,不需要提出建议和缺少的东西。请用中文回答,文章内容为:${content}`; 665 | const apiUrl = "https://api.openai.com/v1/chat/completions"; 666 | try { 667 | response = await fetch(apiUrl, { 668 | signal: signal, 669 | method: "POST", 670 | headers: { 671 | "Content-Type": "application/json", 672 | "Authorization": `Bearer ${apiKey}` 673 | }, 674 | body: JSON.stringify({ 675 | model: "gpt-3.5-turbo", 676 | messages: [{ "role": "user", "content": prompt }], 677 | }) 678 | }); 679 | completeGenerate = true; 680 | if (response.status === 429) { 681 | startAI('请求过于频繁,请稍后再请求AI。'); 682 | } 683 | if (!response.ok) { 684 | throw new Error('Response not ok'); 685 | } 686 | // 处理响应 687 | } catch (error) { 688 | console.error('Error occurred:', error); 689 | startAI(`${interface.name}请求chatGPT出错了,请稍后再试。`); 690 | completeGenerate = true; 691 | return ""; 692 | } 693 | // 解析响应并返回结果 694 | const data = await response.json(); 695 | const outputText = data.choices[0].message.content; 696 | sessionStorage.setItem('summary', outputText); 697 | return outputText; 698 | } 699 | } 700 | // 实验性功能,自动获取文章内容所在容器元素 701 | function getArticleElements(){ 702 | // 计算元素的后代元素总个数 703 | function countDescendants(element) { 704 | let count = 1; 705 | for (const child of element.children) { 706 | count += countDescendants(child); 707 | } 708 | return count; 709 | } 710 | // 判断是否有要排除的元素 711 | function judgeElement(element) { 712 | const excludedTags = ['IFRAME', 'FOOTER', 'HEADER', 'BLOCKQUOTE']; // 添加要排除的标签 713 | if(excludedTags.includes(element.tagName)){ 714 | return true; 715 | } 716 | const exclusionStrings = ['aplayer', 'comment']; // 排除包含其中字符串的className 717 | return Array.from(element.classList).some(className => exclusionStrings.some(exclusion => className.includes(exclusion))); 718 | } 719 | // 深度搜索,找到得分最高的父元素 720 | function findMaxHeadingParentElement(element) { 721 | const tagScores = { 722 | 'H1': 1.5, 723 | 'H2': 1, 724 | 'H3': 0.5, 725 | 'P': 1 726 | }; 727 | let maxScore = 0; 728 | let maxHeadingParentElement = null; 729 | function dfs(element) { 730 | if (judgeElement(element)) { 731 | return; 732 | } 733 | let score = 0; 734 | for (const child of element.children) { 735 | if (child.tagName in tagScores) { 736 | score += tagScores[child.tagName]; 737 | } 738 | } 739 | if (score > maxScore) { 740 | maxScore = score; 741 | maxHeadingParentElement = element; 742 | } 743 | for (const child of element.children) { 744 | dfs(child); 745 | } 746 | } 747 | dfs(element); 748 | return maxHeadingParentElement; 749 | } 750 | // 广度优先搜索,标记所有元素,并找到得分最高的父元素 751 | function findArticleContentElement() { 752 | const queue = [document.body]; 753 | let maxDescendantsCount = 0; 754 | let articleContentElement = null; 755 | while (queue.length > 0) { 756 | const currentElement = queue.shift(); 757 | // 判断当前元素是否要排除 758 | if (judgeElement(currentElement)) { 759 | continue; 760 | } 761 | const descendantsCount = countDescendants(currentElement); 762 | if (descendantsCount > maxDescendantsCount) { 763 | maxDescendantsCount = descendantsCount; 764 | articleContentElement = currentElement; 765 | } 766 | for (const child of currentElement.children) { 767 | queue.push(child); 768 | } 769 | } 770 | return findMaxHeadingParentElement(articleContentElement); 771 | } 772 | // 返回文章内容所在的容器元素 773 | return findArticleContentElement(); 774 | } 775 | 776 | // 插入css 777 | function insertCSS(){ 778 | const styleId = 'qx-ai-style'; 779 | if(document.getElementById(styleId)) { return; } 780 | const styleElement = document.createElement('style'); 781 | styleElement.id = styleId; 782 | styleElement.textContent = AI_option.css || `:root{--ai-font-color:#353535;--ai-post-bg:#f1f3f8;--ai-content-bg:#fff;--ai-content-border:1px solid #e3e8f7;--ai-border:1px solid #e3e8f7bd;--ai-tag-bg:rgba(48,52,63,0.80);--ai-cursor:#333;--ai-btn-bg:rgba(48,52,63,0.75);--ai-title-color:#4c4948;--ai-btn-color:#fff;--ai-speech-content:#fff;}[data-theme=dark],.theme-dark,body.dark,body.dark-theme{--ai-font-color:rgba(255,255,255,0.9);--ai-post-bg:#30343f;--ai-content-bg:#1d1e22;--ai-content-border:1px solid #42444a;--ai-border:1px solid #3d3d3f;--ai-tag-bg:#1d1e22;--ai-cursor:rgb(255,255,255,0.9);--ai-btn-bg:#1d1e22;--ai-title-color:rgba(255,255,255,0.86);--ai-btn-color:rgb(255,255,255,0.9);--ai-speech-content:#1d1e22;}#post-ai.post-ai{background:var(--ai-post-bg);border-radius:12px;padding:10px 12px 11px;line-height:1.3;border:var(--ai-border);margin-top:10px;margin-bottom:6px;transition:all 0.3s;-webkit-transition:all 0.3s;-moz-transition:all 0.3s;-ms-transition:all 0.3s;-o-transition:all 0.3s;}#post-ai .ai-title{display:flex;color:var(--ai-title-color);border-radius:8px;align-items:center;padding:0 6px;position:relative;}#post-ai .ai-title i{font-weight:800;}#post-ai .ai-title-text{font-weight:bold;margin-left:8px;font-size:17px;}#post-ai .ai-tag{font-size:12px;background-color:var(--ai-tag-bg);color:var(--ai-btn-color);border-radius:4px;margin-left:auto;line-height:1;padding:4px 5px;border:var(--ai-border);}#post-ai .ai-explanation{margin-top:10px;padding:8px 12px;background:var(--ai-content-bg);border-radius:8px;border:var(--ai-content-border);font-size:15.5px;line-height:1.4;color:var(--ai-font-color);}#post-ai .ai-cursor{display:inline-block;width:7px;background:var(--ai-cursor);height:16px;margin-bottom:-2px;opacity:0.95;margin-left:3px;transition:all 0.3s;-webkit-transition:all 0.3s;-moz-transition:all 0.3s;-ms-transition:all 0.3s;-o-transition:all 0.3s;}#post-ai .ai-btn-box{font-size:15.5px;width:100%;display:flex;flex-direction:row;flex-wrap:wrap;}#post-ai .ai-btn-item{padding:5px 10px;margin:10px 16px 0px 5px;width:fit-content;line-height:1;background:var(--ai-btn-bg);border:var(--ai-border);color:var(--ai-btn-color);border-radius:6px 6px 6px 0;-webkit-border-radius:6px 6px 6px 0;-moz-border-radius:6px 6px 6px 0;-ms-border-radius:6px 6px 6px 0;-o-border-radius:6px 6px 6px 0;user-select:none;transition:all 0.3s;-webkit-transition:all 0.3s;-moz-transition:all 0.3s;-ms-transition:all 0.3s;-o-transition:all 0.3s;cursor:pointer;}#post-ai .ai-btn-item:hover{background:#49b0f5dc;}#post-ai .ai-recommend{display:flex;flex-direction:row;flex-wrap:wrap;}#post-ai .ai-recommend-item{width:50%;margin-top:2px;}#post-ai .ai-recommend-item a{border-bottom:2px solid #4c98f7;padding:0 .2em;color:#4c98f7;font-weight:700;text-decoration:none;transition:all 0.3s;-webkit-transition:all 0.3s;-moz-transition:all 0.3s;-ms-transition:all 0.3s;-o-transition:all 0.3s;}#post-ai .ai-recommend-item a:hover{background-color:#49b1f5;border-bottom:2px solid #49b1f5;color:#fff;border-radius:5px;}@media screen and (max-width:768px){#post-ai .ai-btn-box{justify-content:center;}}#post-ai .ai-title>svg{width:21px;height:21px;}#post-ai .ai-title>svg path{fill:var(--ai-font-color);}#post-ai .ai-Toggle{font-size:12px;border:var(--ai-border);background:var(--ai-btn-bg);color:var(--ai-btn-color);padding:3px 4px;border-radius:4px;margin-left:6px;cursor:pointer;-webkit-transition:.3s;-moz-transition:.3s;-o-transition:.3s;-ms-transition:.3s;transition:.3s;font-weight:bolder;pointer-events:none;opacity:0;}#post-ai .ai-Toggle:hover{background:#49b0f5dc;}#post-ai .ai-speech-box{width:21px;height:21px;background:var(--ai-font-color);margin-left:7px;border-radius:50%;display:flex;flex-direction:row;flex-wrap:wrap;align-content:center;justify-content:center;pointer-events:none;opacity:0;-webkit-transition:.3s;-moz-transition:.3s;-o-transition:.3s;-ms-transition:.3s;transition:.3s;cursor:pointer;}#post-ai .ai-speech-content{width:8px;background:var(--ai-speech-content);height:8px;border-radius:50%;-webkit-transition:.3s;-moz-transition:.3s;-o-transition:.3s;-ms-transition:.3s;transition:.3s;}#post-ai .ai-speech-box:hover .ai-speech-content{background:#49b0f5;}@keyframes ai_breathe{0%{transform:scale(0.9);-webkit-transform:scale(0.9);-moz-transform:scale(0.9);-ms-transform:scale(0.9);-o-transform:scale(0.9);}50%{transform:scale(1);-webkit-transform:scale(1);-moz-transform:scale(1);-ms-transform:scale(1);-o-transform:scale(1);}}`; 783 | AI_option.additional_css && (styleElement.textContent += AI_option.additional_css); 784 | document.head.appendChild(styleElement); 785 | } 786 | 787 | // 请求个性化推荐 788 | async function personalizedRecommend(){ 789 | completeGenerate = false; 790 | controller = new AbortController(); 791 | signal = controller.signal; 792 | const options = { 793 | signal, 794 | method: 'GET', 795 | headers: {'content-type': 'application/x-www-form-urlencoded'}, 796 | }; 797 | try{ 798 | const response = await fetch(`https://summary.tianli0.top/personalized_recommends?openid=${visitorId}`, options); 799 | completeGenerate = true; 800 | const data = await response.json(); 801 | return data; 802 | }catch{ 803 | startAI(`${interface.name}获取个性化推荐出错了,请稍后再试。`); 804 | completeGenerate = true; 805 | return null; 806 | } 807 | } 808 | 809 | ai_init(); 810 | } 811 | } 812 | // 兼容旧版本配置项 813 | if(typeof ai_option!=="undefined"){ 814 | console.log("正在使用旧版本配置方式,请前往项目仓库查看最新配置写法"); 815 | new ChucklePostAI(ai_option); 816 | } --------------------------------------------------------------------------------