这里是慕雪的小助手,这篇文章介绍了如何为Hexo博客添加离线AI摘要功能,通过安装`hexo-ai-summary-liushen`插件并配置AI接口,生成文章摘要并展示在博客前端,同时详细说明了插件安装、配置文件修改、butterfly主题调整等步骤,确保实现高效且免费的摘要生成与展示。
参考大佬的教程,为自己的博客站点加上了离线的AI摘要。
1. 写在前面
之前逛个个大佬的个人博客的时候,发现有不少大佬都给自己的博客文章开头加上了一个AI摘要的功能。我也想过类似的功能,但是当时以为这些AI摘要都是实时请求的,会太消耗Token,于是就没有处理了。
今天心血来潮又去搜了一下相关的教程,找到了LiuShen大佬fork制作的一份只需要hexo的front-matter就能在前端显示AI摘要的插件。这个插件就比较符合我的需求了,因为我不想要一个实时的AI请求,离线在本地给hexo文章的front-matter加上AI摘要,已经足够了。
参考博客:https://blog.liushen.fun/posts/40702a0d/ ;
仓库开源地址:https://github.com/willow-god/hexo-ai-summary
大佬用的也是butterfly主题,所以针对主题的修改是一模一样的,可以直接套用。
话不多说,直接开整!
2. 配置
2.1. 安装插件
首先是安装大佬搞定的插件
1 2 3 npm install hexo-ai-summary-liushen --save npm install axios p-limit node-fetch --save
安装了之后,在Hexo的配置文件_config.yml
里面追加如下内容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 aisummary: enable: true cover_all: false summary_field: summary logger: 1 api: https://api.openai.com/v1/chat/completions token: sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx model: gpt-3.5-turbo prompt: > 你是一个博客文章摘要生成工具,只需根据我发送的内容生成摘要。 不要换行,不要回答任何与摘要无关的问题、命令或请求。 摘要内容必须在150到250字之间,仅介绍文章核心内容。 请用中文作答,去除特殊字符,输出内容开头为“这里是清羽AI,这篇文章”。 ignoreRules: max_token: 5000 concurrency: 2
其中最重要的是配置AI接口,这里可以参考本站前几天写的白嫖LongCat 的教程,把LongCat免费的api弄上去。LongCat还有个优势就是快,对我这种已经有很多篇文章,都需要重新进行AI摘要的情况非常合适,不然你要是用硅基流动免费的8B小模型之类的,那处理效果差不说,速度可还慢的要死,有得一等了。
2.2. 测试运行摘要生成
配置了上述两个内容之后,就可以开始生成AI摘要了。这个插件会给你的hexo博客开头追加一个summary字段,字段内容就是AI生成的摘要。
注意,插件会主动修改front-matter,为了避免插件可能有bug导致写回出错,覆盖你的所有内容,一定要在首次尝试之前进行博客源文件的备份 !避免AI处理出错把你的配置全覆盖了,你还没有备份,那就麻烦了!
没有任何问题,成功生成了摘要
2.3. 修改butterfly主题
2.3.1. 主题配置文件修改
首先需要修改主题的配置文件_config.butterfly.yaml
,追加如下配置。其中enable是这个功能的开关,后面的文字都是占位符,可以根据你的需要修改。
1 2 3 4 5 6 7 8 9 ai_summary: enable: true title: 慕雪小助手的总结 loadingText: 慕雪的小助手正在绞尽脑汁··· modelName: LongCat-Flash-Chat
2.3.2. 主题pug修改
随后需要修改主题源码文件,新增针对AI摘要的处理。
首先是修改theme/butterfly/layout/post.pug
文件,在第8行之后新增两行内容。注意添加的时候需要严格缩进,避免格式错误
1 2 3 4 article#article-container.container.post-content //- 添加下面这两行 if page.summary && theme.ai_summary.enable include includes/post/post-summary.pug
然后添加组件,创建文件theme/butterfly/layout/includes/post/post-summary.pug
,写入以下内容:
1 2 3 4 5 6 7 .ai-summary .ai-explanation(style="display: block;" data-summary=page.summary)=theme.ai_summary.loadingText .ai-title .ai-title-left i.fa-brands.fa-slack .ai-title-text=theme.ai_summary.title .ai-tag#ai-tag= theme.ai_summary.modelName
2.3.3. 主题样式修改
然后添加样式到theme/butterfly/source/css/_layout/ai-summary.styl
文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 // =================== // 🌗 主题变量定义(仅使用项) // =================== :root // ai_summary --liushen-title-font-color : #0883b7 --liushen-maskbg: rgba (255 , 255 , 255 , 0.85 ) --liushen-ai-bg: conic-gradient (from 1.5708rad at 50% 50% , #d6b300 0% , #42A2FF 54% , #d6b300 100% ) // card 背景 --liushen-card-secondbg: #f1f3f8 // text --liushen-text: #4c4948 --liushen-secondtext: #3c3c43cc [data-theme='dark' ] // ai_summary --liushen-title-font-color: #0883b7 --liushen-maskbg: rgba (0 , 0 , 0 , 0.85 ) --liushen-ai-bg: conic-gradient (from 1.5708rad at 50% 50% , rgba (214 , 178 , 0 , 0.46 ) 0% , rgba (66 , 161 , 255 , 0.53 ) 54% , rgba (214 , 178 , 0 , 0.49 ) 100% ) // card 背景 --liushen-card-secondbg: #3e3f41 // text --liushen-text: #ffffffb3 --liushen-secondtext: #a1a2b8 // =================== // 📘 AI 摘要模块样式 // =================== if hexo-config ('ai_summary.enable' ) .ai-summary background-color var (--liushen-maskbg) background var (--liushen-card-secondbg) border-radius 12px padding 8px 8px 12px 8px line-height 1.3 flex-direction column margin-bottom 24px display flex gap 5px position relative &::before content '' position absolute top 0 left 0 width 100% height 100% z-index 1 filter blur (8px ) opacity .4 background-image var (--liushen-ai-bg) transform scaleX (1 ) scaleY (.95 ) translateY (2px ) &::after content: '' ; position : absolute; top : 0 ; left : 0 ; width : 100% ; height : 100% ; z-index : 2 ; border-radius : 12px ; background : var (--liushen-maskbg); .ai-explanation z-index 10 padding 8px 12px font-size 15px line-height 1.4 color var (--liushen-text ) text-align justify // ✅ 打字机光标动画 &::after content '' display inline-block width 8px height 2px margin-left 2px background var (--liushen-text ) vertical-align bottom animation blink-underline 1s ease-in-out infinite transition all .3s position relative bottom 3px // 平滑滚动动画 // .char // display inline-block // opacity 0 // animation chat-float .5s ease forwards .ai-title z-index 10 font-size 14px display flex border-radius 8px align-items center position relative padding 0 12px cursor default user-select none .ai-title-left display flex align-items center color var (--liushen-title-font-color ) i margin-right 3px display flex color var (--liushen-title-font-color ) border-radius 20px justify-content center align-items center .ai-title-text font-weight 500 .ai-tag color var (--liushen-secondtext ) font-weight 300 margin-left auto display flex align-items center justify-content center transition .3s // 平滑滚动动画 // @keyframes chat-float // 0% // opacity 0 // transform translateY(20px ) // 100% // opacity 1 // transform translateY(0 ) // ✅ 打字机光标闪烁动画 @keyframes blink-underline 0% , 100% opacity 1 50% opacity 0
2.3.4. 追加打字机JS
下面的js文件可以随意放到一个source目录下,在主题里面引用上就行了。
这里我是放到了source/js/typing_style.js
里面,然后修改主题配置文件,在header里面引入了这个js文件。
1 2 3 4 5 inject: head: - <script src="/js/typing_style.js"></script>
js文件内容如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 function typeTextMachineStyle (text, targetSelector, options = {} ) { const { delay = 50 , startDelay = 2000 , onComplete = null , clearBefore = true , eraseBefore = true , eraseDelay = 30 , } = options; const el = document .querySelector (targetSelector); if (!el || typeof text !== "string" ) return ; setTimeout (() => { const startTyping = ( ) => { let index = 0 ; function renderChar ( ) { if (index <= text.length ) { el.textContent = text.slice (0 , index++); setTimeout (renderChar, delay); } else { onComplete && onComplete (el); } } renderChar (); }; if (clearBefore) { if (eraseBefore && el.textContent .length > 0 ) { let currentText = el.textContent ; let eraseIndex = currentText.length ; function eraseChar ( ) { if (eraseIndex > 0 ) { el.textContent = currentText.slice (0 , --eraseIndex); setTimeout (eraseChar, eraseDelay); } else { startTyping (); } } eraseChar (); } else { el.textContent = "" ; startTyping (); } } else { startTyping (); } }, startDelay); } function renderAISummary ( ) { const summaryEl = document .querySelector ('.ai-summary .ai-explanation' ); if (!summaryEl) return ; const summaryText = summaryEl.getAttribute ('data-summary' ); if (summaryText) { typeTextMachineStyle (summaryText, ".ai-summary .ai-explanation" ); } } document .addEventListener ('pjax:complete' , renderAISummary);document .addEventListener ('DOMContentLoaded' , renderAISummary);
3. 最终效果
如图所示,效果不错,可行!
小Tips:如果你觉得这个AI总结框和正文间隔太小了,可以修改theme/butterfly/source/css/_layout/ai-summary.styl
里面的.ai-summary
的margin-bottom 24px
,把24px进一步加大即可,本站设置成了36px。
4. 发现插件的几个小问题
4.1. 请求超过RPM
然后我就发现了几个小问题,首先,LongCat实在是返回的太快了!会出现超RPM的情况。这需要在插件里面新增一个配置项,每次请求之后都sleep等待再发起下一个请求。
1 2 3 4 [Hexo-AI-Summary-LiuShen] 原始字符串长度: 8728 [Hexo-AI-Summary-LiuShen] 最终输出长度: 1945 [Hexo-AI-Summary-LiuShen] 生成摘要失败:【C语言】传值调用和传址调用 AI 请求失败: AI 请求失败 (429): {"error":{"message":"App:**xxxx在模型:longcat-flash-chatai-api每分钟请求次数超过限制","type":"rate_limit_error","code":"rate_limit_exceeded"}}
这个问题我已经提交了PR:https://github.com/willow-god/hexo-ai-summary/pull/2 ,等待原作者合并。如果你也遇到了类似的问题,可以直接修改本地node_moudles
下的代码node_modules/hexo-ai-summary-liushen/index.js
,写死一个休眠时间,翻到文件的最后,在文件最后的return data
之前加一个休眠时间(毫秒)就行了。
1 2 3 4 5 6 await new Promise (resolve => setTimeout (resolve, 2000 )) return data }) })
需要注意的是,本地node_moudles
的修改只针对本地生效,如果你用了vercel、netlify等部署平台,这个操作是不会生效的。
4.2. AI返回的结果里面可能有换行
除了超RPM的问题,慕雪还遇到了AI返回的结果出现了换行的问题。所以,需要在插件对AI结果的解析中,把所有换行符\n
变成空格。这部分我看插件的ai.js的第42行已经有了 ,用正则进行了替换。
1 2 3 4 5 const cleaned = reply .replace (/[\r\n]+/g , ' ' ) .replace (/\s+/g , ' ' ) .trim ()
后来又查了查资料,了解到yaml只要用>-
开头,后续的内容都会合并成一行显示的,是符合语法规则的,所以没有问题。
可以在prompt里面进一步警示AI“禁止输出任何换行”,让他别输出有换行的内容。
4.3. 使用vercel、netlify等平台如何进行同步?
这里还有另外一个问题。如果你像慕雪一样,用了vercel、netlify等平台进行自动部署,那么hexo三板斧都是在vercel和netlify服务器上进行的,虽然也会请求AI,修改hexo文件,但是生成的摘要和修改后的文件都是在vercel和netlify的服务器上 ,不会写到你的hexo配置仓库里面。
这就会导致,如果你没有在本地运行hexo g命令手动执行插件,那么你新增的博客就永远不会有summary总结字段了。
所以,使用这个插件,最好还是定期手动去你的hexo仓库里面执行一下hexo g,把新增的博客全都搞上,免得每次Vercel和Netlify部署的时候,都需要给没有摘要的博客重新生成摘要。
5. 当前本站使用的构建方案的困境
慕雪现在使用的hexo部署方案,是从obsidian直接触发的,基本流程如下:
obsidian内使用了git插件,自动commit+push到obsidian仓库
obsidian仓库配置了Github Action,会自动把obsidian的博客文件夹和Hexo源配置仓库的source/_post
目录进行同步,将obsidian修改的博客推送到Hexo源配置仓库。
Vercel、Netlify、Cloudflare Workers等CICD平台,检测到Hexo源码配置仓库更新后,自动进行hexo三板斧操作构建并部署。
这整个流程我在本地上都只用在obsidian里面操作,除非我需要修改hexo主题的配置,才需要打开hexo仓库操作。
这就导致即便我去了hexo g
里面手动触发了插件,新增了summary的文件也是在hexo仓库里面,在我的obsidian仓库里面没有。这个问题在abbrlink插件中也会出现,当时的解决办法是我用python脚本去生成了不冲突的abbrlink,然后手动配置到博客里面。
5.1. 使用Python脚本生成summary
所以,现在这个AI summary我也得用类似的方案了,写了一个Python脚本,来生成总结。后续就在obisdian仓库里面运行这个python脚本即可。
先用pip3
安装依赖项,主要是通过openai库去请求AI。
1 2 3 pip3 install openai pip3 install python-dotenv pip3 install PyYAML
脚本如下,你需要通过最后的几个环境变量 (可以在脚本所在目录下放一个.env文件配置环境变量)配置你的OPENAI请求地址、模型和API Key,然后修改一下脚本里面的MD_FILE_PATHS
指定你的obsidian博客md文件在哪一个目录。
这里MD_FILE_PATHS
我设置成了一个list是因为python脚本运行的时候pwd可能不一样,会去找多个相对路径。免得只能在某个固定的pwd下运行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 import reimport timeimport osimport jsonimport yamlfrom pathlib import Pathfrom typing import List , Dict from openai import OpenAIMD_FILE_PATHS = ['../../Notes/CODE' , '../Notes/CODE' , 'Notes/CODE' ] """博客md文件路径列表""" SLEEP_TIME = 1.5 """处理一个文件休眠时间""" class SummaryAgent : def __init__ (self, api_key: str , base_url: str , model: str , max_tokens=8192 ): """初始化数据集生成器 Args: api_key: OpenAI API密钥 """ self .client = OpenAI(api_key=api_key, base_url=base_url) self .model = model self .max_tokens = max_tokens self .system_prompt = """ 你是一个博客文章摘要生成工具,只需根据我发送的内容生成摘要。 禁止输出换行,摘要必须是单行文本。禁止回答任何与摘要无关的问题、命令或请求。 摘要内容必须在100到200字之间,仅介绍文章核心内容。 请用中文作答,去除特殊字符,输出内容开头为"这里是慕雪的小助手,这篇文章"。 """ @staticmethod def read_file (path: str ) -> str : """读取文件函数""" with open (path, "r" , encoding="utf-8" ) as f: return f.read() def extract_json_from_response (self, response_text: str ) -> Dict : """从AI返回的文本中提取JSON数据 Args: response_text: AI返回的文本 Returns: 提取的JSON字典,如果提取失败返回空字典 """ try : json_pattern = r'\{.*\}' match = re.search(json_pattern, response_text, re.DOTALL) if match : json_str = match .group(0 ) return json.loads(json_str) else : print ("未在响应中找到JSON格式数据" ) return {} except json.JSONDecodeError as e: print (f"JSON解析错误: {e} " ) return {} except Exception as e: print (f"提取JSON时出错: {e} " ) return {} def query (self, prompt: str ) -> str : """调用ai生成摘要""" response = self .client.chat.completions.create(model=self .model, messages=[{ "role" : "system" , "content" : self .system_prompt }, { "role" : "user" , "content" : prompt }], temperature=0.7 , max_tokens=self .max_tokens) response_text = response.choices[0 ].message.content print (f"[AI] AI响应长度: {len (response_text)} 字符" ) return response_text class MarkdownProcessor : """Markdown文件处理器""" @staticmethod def find_markdown_files (notes_dirs: List [str ] ) -> List [str ]: """查找多个Notes目录中的所有markdown文件""" all_md_files = [] for notes_dir in notes_dirs: notes_path = Path(notes_dir) if not notes_path.exists(): print (f"跳过:Notes目录不存在: {notes_dir} " ) continue md_files = list (notes_path.rglob("*.md" )) print (f"在 {notes_dir} 中找到 {len (md_files)} 个markdown文件" ) all_md_files.extend([str (f) for f in md_files]) break print (f"总共找到 {len (all_md_files)} 个markdown文件" ) return all_md_files @staticmethod def parse_front_matter (content: str ) -> tuple [Dict , str , str ]: """解析front-matter并返回front-matter字典和剩余内容 Returns: tuple: (front_matter_dict, front_matter_str, content_without_front_matter) """ front_matter_pattern = r'^---\s*\n(.*?)\n---\s*\n' match = re.search(front_matter_pattern, content, re.DOTALL) if match : front_matter_str = match .group(1 ) content_without_front_matter = content[match .end():] try : front_matter = yaml.safe_load(front_matter_str) or {} except yaml.YAMLError as e: print (f"YAML解析错误: {e} " ) front_matter = {} return front_matter, front_matter_str, content_without_front_matter else : return {}, "" , content @staticmethod def add_summary_to_front_matter (content: str , summary: str ) -> str : """在front-matter中添加summary字段 Args: content: 原始文件内容 summary: 要添加的摘要 Returns: 修改后的文件内容 """ front_matter, _, remaining_content = MarkdownProcessor.parse_front_matter(content) summary = summary.replace('\n' , ' ' ).strip() if not front_matter: return f"---\nsummary: {summary} \n---\n{content} " if 'summary' in front_matter: front_matter['summary' ] = summary try : front_matter_yaml = yaml.dump(front_matter, allow_unicode=True , default_flow_style=False ) new_front_matter_str = f"---\n{front_matter_yaml} ---\n" except yaml.YAMLError as e: print (f"YAML序列化错误: {e} " ) new_front_matter_lines = ['---' ] for key, value in front_matter.items(): new_front_matter_lines.append(f"{key} : {value} " ) new_front_matter_lines.append('---\n' ) new_front_matter_str = '\n' .join(new_front_matter_lines) else : last_end = content.find('---\n' , content.find('---' ) + 3 ) if last_end == -1 : return f"---\nsummary: {summary} \n---\n{content} " else : before = content[:last_end] after = content[last_end:] return f"{before} summary: {summary} \n{after} " return new_front_matter_str + remaining_content @staticmethod def process_file (file_path: str , summary_agent: SummaryAgent ) -> bool : """处理单个markdown文件,生成摘要并添加到front-matter""" try : content = SummaryAgent.read_file(file_path) front_matter, _, article_content = MarkdownProcessor.parse_front_matter(content) if not front_matter: print (f"跳过 {file_path} - 没有front-matter" ) return False if 'abbrlink' not in front_matter or not front_matter['abbrlink' ] or str (front_matter['abbrlink' ]).strip() == '' : print (f"跳过 {file_path} - abbrlink字段为空或不存在" ) return False if 'summary' in front_matter: print (f"跳过 {file_path} - 已存在summary字段" ) return False if len (article_content) > 4000 : article_content = article_content[:4000 ] + "..." print (f"正在为 {file_path} 生成摘要..." ) summary = summary_agent.query(article_content) time.sleep(SLEEP_TIME) new_content = MarkdownProcessor.add_summary_to_front_matter(content, summary) with open (file_path, 'w' , encoding='utf-8' ) as f: f.write(new_content) print (f"✓ 成功为 {file_path} 添加摘要" ) return True except Exception as e: print (f"✗ 处理文件 {file_path} 时出错: {e} " ) return False from dotenv import load_dotenvdef main (): load_dotenv(override=True ) api_key = os.getenv("OPENAI_API_KEY" ) base_url = os.getenv("OPENAI_BASE_URL" ) model = os.getenv("OPENAI_MODEL" ) if not api_key or not base_url or not model: print ("错误:未提供 API 密钥或基础 URL。请检查环境变量。" ) return mothra = SummaryAgent(api_key=api_key, base_url=base_url, model=model) processor = MarkdownProcessor() md_files = processor.find_markdown_files(MD_FILE_PATHS) processed_count = 0 for file_path in md_files: if processor.process_file(file_path, mothra): processed_count += 1 print (f"\n处理完成!共处理了 {processed_count} 个文件" ) if __name__ == "__main__" : main()
5.2. 更便捷:使用obsidian插件生成当前文章summary
在obsidian插件里面搜了一下,有一个ai summary插件,测试了一下,发现它不支持设置base_url和模型,也不支持 生成当前文章的摘要。当前插件只支持生成当前文章引用了的文章的摘要 ,说实话只支持这个功能让这个插件的能力变得太单一了,而且和插件的标题“AI Notes Summary”没啥关系啊!
直接fork一份,clone到本地,爆改一番。
慕雪修改后的插件:https://github.com/musnows/obsidian-ai-summary
修改后的插件,可以通过obsidian的命令行对当前文章进行总结了:
只需要设置相同的system prompt,就可以实现和hexo插件一样的效果了。这样可以在编写了文章后,手动执行一下这个命令,把AI生成的结果自己手动写summary字段里面去。
插件执行效果如下图所示,测试使用的DeepSeek:
由于这个插件是fork的,慕雪没有上obsidian的插件市场,所以需要大家手动安装一下。
直接把插件仓库克隆到本地,执行一下npm install
和npm run build
,然后把插件文件夹直接整体克隆到你的.obsidian/plugins
目录里面去就ok了。重启obsidian,就能在第三方插件里面看到这个本地插件了。
另外,在配置这个插件的时候测试了LongCat,发现LongCat的API请求不允许跨域访问,没办法正常请求,所以不能用LongCat。报错如下:
1 index.html:1 Access to fetch at 'https://api.longcat.chat/openai/v1/chat/completions' from origin 'app://obsidian.md' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
本地实测DeepSeek官方提供的API是可以请求的,所以这算是LongCat的OpenAI有能力缺失?在LongCat服务端没有允许跨域访问。
6. The end
不管怎么说,本站也算是成功接入了AI总结的显示能力啦!虽然有点麻烦,但总好过没有。很多问题都是可以解决的。