前言
偶然间看到Cloudflare的ai search,不需要自己配置向量化模型和问答模型就可以完成ai知识库配置,只需导入内容即可,遂尝试构建自己的ai知识库,并接入博客作为ai增强搜索以及ai知识库问答助手
知识库AI助手接入
搭建
在cloudflare中AI -->AI搜索中创建项目,数据来源可选R2存储桶和网站,先搭建知识库,所以这里选R2存储桶作为数据来源

我习惯使用Obsidian作为笔记工具,所以使用Obsidian和Remotely-Save插件将本地笔记同步到R2存储桶,然后下一步给AI网关和API等授权,cloudflare会自动处理创建网关等操作,并在ai搜索项目首次创建时进行一次索引构建,后续会定期同步数据源更新索引,也可以手动同步。
接入
在项目设置中启用Public URL,完成自定义样式设置,在src/layouts/Layout.astro文件约159行位置插入如下代码,替换<hash>和自定义的样式设置
1<!-- 在布局文件末尾,</body> 之前添加 -->2<script type="module" src="https://<hash>.search.ai.cloudflare.com/assets/v0.0.25/search-snippet.es.js"></script>3
4<chat-bubble-snippet5api-url="https://<hash>.search.ai.cloudflare.com/"6placeholder="Search..."7theme="light"8hide-branding="true">9</chat-bubble-snippet>10
11<style is:global>12/* 可选:自定义样式,作用全局 */13chat-bubble-snippet {14--search-snippet-primary-color: #74a7d8;15--search-snippet-primary-hover: #92c6f7;16--search-snippet-focus-ring: #74a7d8;17--search-snippet-surface: #ffffff;18--search-snippet-hover-background: #ffffff;19--search-snippet-border-color: #f0f9ff;20}21</style>效果如下:

AI搜索接入
搭建
流程同上,这里作为网页内容搜索,所以数据来源选择网站,设置完sitemap后启用Public URL
接入
编辑src/components/Search.svelte,这里给出我用ai生成的代码,仅供参考,替换<hash>和外观配置,可能存在一些小bug,请自行修改优化:
1<script lang="ts">2import I18nKey from "@i18n/i18nKey";3import { i18n } from "@i18n/translation";4import Icon from "@iconify/svelte";5import { url } from "@utils/url-utils.ts";6import { onMount, tick } from "svelte";7import type { SearchResult } from "@/global";8
9let keywordDesktop = "";10let keywordMobile = "";11let result: SearchResult[] = [];12let isSearching = false;13let pagefindLoaded = false;368 collapsed lines
14let initialized = false;15let showAISearch = false; // 新增:控制AI搜索显示状态16let aiSearchContainer: HTMLElement; // 新增:AI搜索容器引用17
18const fakeResult: SearchResult[] = [19 {20 url: url("/"),21 meta: {22 title: "This Is a Fake Search Result",23 },24 excerpt:25 "Because the search cannot work in the <mark>dev</mark> environment.",26 },27 {28 url: url("/"),29 meta: {30 title: "If You Want to Test the Search",31 },32 excerpt: "Try running <mark>npm build && npm preview</mark> instead.",33 },34];35
36// 新增:点击外部关闭AI搜索框37const handleClickOutside = (event: MouseEvent) => {38 if (!showAISearch || !aiSearchContainer) return;39
40 const target = event.target as HTMLElement;41 const searchBar = document.getElementById("search-bar");42 const aiButton = searchBar?.querySelector('button[aria-label="AI Search"]');43
44 // 检查点击是否在AI搜索容器、搜索框或AI按钮内部45 const isClickInside =46 aiSearchContainer.contains(target) ||47 searchBar?.contains(target) ||48 aiButton?.contains(target);49
50 if (!isClickInside) {51 showAISearch = false;52 }53};54
55// 新增:切换AI搜索显示56const toggleAISearch = async (event?: MouseEvent) => {57 // 阻止事件冒泡,避免立即触发外部点击检测58 event?.stopPropagation();59
60 showAISearch = !showAISearch;61
62 if (showAISearch) {63 // 等待DOM更新,确保容器已渲染64 await tick();65
66 // 添加全局点击监听器67 setTimeout(() => {68 document.addEventListener('click', handleClickOutside);69 }, 0);70
71 // 如果开启了AI搜索,加载Cloudflare AI搜索库72 if (!document.querySelector('script[src*="search.ai.cloudflare.com"]')) {73 const script = document.createElement('script');74 script.type = 'module';75 script.src = 'https://<hash>.search.ai.cloudflare.com/assets/v0.0.25/search-snippet.es.js';76 document.head.appendChild(script);77
78 // 添加样式79 const style = document.createElement('style');80 style.textContent = `81 search-bar-snippet {82 --search-snippet-primary-color: #74a7d8;83 --search-snippet-primary-hover: #92c6f7;84 --search-snippet-focus-ring: #74a8d8;85 }86 `;87 document.head.appendChild(style);88 }89 } else {90 // 移除全局点击监听器91 document.removeEventListener('click', handleClickOutside);92 }93};94
95// 新增:组件卸载时清理事件监听器96const cleanup = () => {97 document.removeEventListener('click', handleClickOutside);98};99
100const togglePanel = () => {101 const panel = document.getElementById("search-panel");102 panel?.classList.toggle("float-panel-closed");103};104
105const setPanelVisibility = (show: boolean, isDesktop: boolean): void => {106 const panel = document.getElementById("search-panel");107 if (!panel || !isDesktop) return;108 if (show) {109 panel.classList.remove("float-panel-closed");110 } else {111 panel.classList.add("float-panel-closed");112 }113};114
115const search = async (keyword: string, isDesktop: boolean): Promise<void> => {116 if (!keyword) {117 setPanelVisibility(false, isDesktop);118 result = [];119 return;120 }121
122 if (!initialized) {123 return;124 }125
126 isSearching = true;127 try {128 let searchResults: SearchResult[] = [];129 if (import.meta.env.PROD && pagefindLoaded && window.pagefind) {130 const response = await window.pagefind.search(keyword);131 searchResults = await Promise.all(132 response.results.map((item) => item.data()),133 );134 } else if (import.meta.env.DEV) {135 searchResults = fakeResult;136 } else {137 searchResults = [];138 console.error("Pagefind is not available in production environment.");139 }140
141 result = searchResults;142 setPanelVisibility(result.length > 0, isDesktop);143 } catch (error) {144 console.error("Search error:", error);145 result = [];146 setPanelVisibility(false, isDesktop);147 } finally {148 isSearching = false;149 }150};151
152onMount(() => {153 const initializeSearch = () => {154 initialized = true;155 pagefindLoaded =156 typeof window !== "undefined" &&157 !!window.pagefind &&158 typeof window.pagefind.search === "function";159 console.log("Pagefind status on init:", pagefindLoaded);160
161 if (keywordDesktop) search(keywordDesktop, true);162 if (keywordMobile) search(keywordMobile, false);163 };164
165 if (import.meta.env.DEV) {166 console.log(167 "Pagefind is not available in development mode. Using mock data.",168 );169 initializeSearch();170 } else {171 document.addEventListener("pagefindready", () => {172 console.log("Pagefind ready event received.");173 initializeSearch();174 });175 document.addEventListener("pagefindloaderror", () => {176 console.warn(177 "Pagefind load error event received. Search functionality will be limited.",178 );179 initializeSearch(); // Initialize with pagefindLoaded as false180 });181
182 // Fallback in case events are not caught or pagefind is already loaded by the time this script runs183 setTimeout(() => {184 if (!initialized) {185 console.log("Fallback: Initializing search after timeout.");186 initializeSearch();187 }188 }, 2000); // Adjust timeout as needed189 }190
191 // 返回清理函数192 return cleanup;193});194
195$: if (initialized && keywordDesktop) {196 (async () => {197 await search(keywordDesktop, true);198 })();199}200
201$: if (initialized && keywordMobile) {202 (async () => {203 await search(keywordMobile, false);204 })();205}206
207// 新增:当AI搜索关闭时,移除事件监听器208$: if (!showAISearch) {209 document.removeEventListener('click', handleClickOutside);210}211</script>212
213<!-- 桌面版搜索容器 -->214<div class="hidden lg:flex flex-col relative">215 <!-- search bar for desktop view -->216 <div id="search-bar" class="flex transition-all items-center h-11 mr-2 rounded-lg217 bg-black/[0.04] hover:bg-black/[0.06] focus-within:bg-black/[0.06]218 dark:bg-white/5 dark:hover:bg-white/10 dark:focus-within:bg-white/10219 ">220 <Icon icon="material-symbols:search" class="absolute text-[1.25rem] pointer-events-none ml-3 transition my-auto text-black/30 dark:text-white/30"></Icon>221 <input placeholder="{i18n(I18nKey.search)}" bind:value={keywordDesktop} on:focus={() => search(keywordDesktop, true)}222 class="transition-all pl-10 text-sm bg-transparent outline-0223 h-full w-40 active:w-60 focus:w-60 text-black/50 dark:text-white/50"224 >225
226 <!-- 新增:AI搜索按钮 -->227 <button228 on:click={toggleAISearch}229 aria-label="AI Search"230 class="ml-2 p-2 rounded-lg transition-colors duration-200 hover:bg-black/[0.08] dark:hover:bg-white/10"231 style:background-color={showAISearch ? '#92c6f7' : 'transparent'}232 >233 <Icon234 icon="material-symbols:smart-toy-outline"235 class="text-[1.25rem] transition-colors {showAISearch ? 'text-white' : 'text-black/50 dark:text-white/50'}"236 ></Icon>237 </button>238 </div>239
240 <!-- AI搜索栏(桌面版,在搜索框下方弹出) -->241 {#if showAISearch}242 <div243 bind:this={aiSearchContainer}244 class="absolute top-full left-0 right-0 mt-2 z-50"245 role="dialog"246 tabindex="0"247 aria-modal="true"248 on:click|stopPropagation249 on:keydown={(e) => {250 if (e.key === "Escape") {251 showAISearch = false;252 }253 }}254 >255 <search-bar-snippet placeholder="使用AI搜索..." api-url="https://<hash>.search.ai.cloudflare.com/" hide-branding="true"></search-bar-snippet>256 </div>257 {/if}258</div>259
260<!-- toggle btn for phone/tablet view -->261<button on:click={togglePanel} aria-label="Search Panel" id="search-switch"262 class="btn-plain scale-animation lg:!hidden rounded-lg w-11 h-11 active:scale-90">263 <Icon icon="material-symbols:search" class="text-[1.25rem]"></Icon>264</button>265
266<!-- search panel -->267<div id="search-panel" class="float-panel float-panel-closed search-panel absolute md:w-[30rem]268top-20 left-4 md:left-[unset] right-4 shadow-2xl rounded-2xl p-2">269 <!-- search bar inside panel for phone/tablet -->270 <div id="search-bar-inside" class="flex relative lg:hidden transition-all items-center h-11 rounded-xl271 bg-black/[0.04] hover:bg-black/[0.06] focus-within:bg-black/[0.06]272 dark:bg-white/5 dark:hover:bg-white/10 dark:focus-within:bg-white/10273 ">274 <Icon icon="material-symbols:search" class="absolute text-[1.25rem] pointer-events-none ml-3 transition my-auto text-black/30 dark:text-white/30"></Icon>275 <input placeholder="Search" bind:value={keywordMobile}276 class="pl-10 absolute inset-0 text-sm bg-transparent outline-0277 focus:w-60 text-black/50 dark:text-white/50"278 >279
280 <!-- 新增:移动端AI搜索按钮 -->281 <button282 on:click={toggleAISearch}283 aria-label="AI Search"284 class="absolute right-2 p-2 rounded-lg transition-colors duration-200 hover:bg-black/[0.08] dark:hover:bg-white/10"285 style:background-color={showAISearch ? '#92c6f7' : 'transparent'}286 >287 <Icon288 icon="material-symbols:smart-toy-outline"289 class="text-[1.25rem] transition-colors {showAISearch ? 'text-white' : 'text-black/50 dark:text-white/50'}"290 ></Icon>291 </button>292 </div>293
294 <!-- AI搜索栏(移动端,在搜索框下方) -->295 {#if showAISearch}296 <div297 class="lg:hidden mt-2 mb-4 relative z-10"298 role="dialog"299 aria-label="AI Search"300 tabindex="-1"301 on:click|stopPropagation302 on:keydown={(e) => {303 if (e.key === "Escape") {304 showAISearch = false;305 }306 }}307 >308 <!-- 包裹容器,确保AI搜索组件能够正常展开 -->309 <div class="ai-search-container-mobile">310 <search-bar-snippet placeholder="使用AI搜索..." api-url="https://<hash>.search.ai.cloudflare.com/" hide-branding="true"></search-bar-snippet>311 </div>312 </div>313 {/if}314
315 <!-- search results -->316 {#each result as item}317 <a href={item.url}318 class="transition first-of-type:mt-2 lg:first-of-type:mt-0 group block319 rounded-xl text-lg px-3 py-2 hover:bg-[var(--btn-plain-bg-hover)] active:bg-[var(--btn-plain-bg-active)]">320 <div class="transition text-90 inline-flex font-bold group-hover:text-[var(--primary)]">321 {item.meta.title}<Icon icon="fa6-solid:chevron-right" class="transition text-[0.75rem] translate-x-1 my-auto text-[var(--primary)]"></Icon>322 </div>323 <div class="transition text-sm text-50">324 {@html item.excerpt}325 </div>326 </a>327 {/each}328</div>329
330<style>331 input:focus {332 outline: 0;333 }334 .search-panel {335 max-height: calc(100vh - 100px);336 overflow-y: auto;337 }338
339 /* Cloudflare AI搜索组件样式 */340 search-bar-snippet {341 --search-snippet-primary-color: #74a7d8;342 --search-snippet-primary-hover: #92c6f7;343 --search-snippet-focus-ring: #74a8d8;344 width: 100%;345 border-radius: 0.5rem;346 box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);347 }348
349 /* 移动端AI搜索容器样式 */350 .ai-search-container-mobile {351 position: relative;352 width: 100%;353 min-height: 60px; /* 确保有最小高度 */354 }355
356 /* 确保AI搜索组件在移动端能够正常展开 */357 .ai-search-container-mobile search-bar-snippet {358 position: static !important; /* 覆盖可能的绝对定位 */359 max-height: none !important; /* 移除高度限制 */360 overflow: visible !important; /* 确保内容可见 */361 }362
363 /* 针对Cloudflare AI搜索组件的特定样式 */364 .ai-search-container-mobile ::ng-deep search-bar-snippet,365 .ai-search-container-mobile ::slotted(search-bar-snippet) {366 position: static !important;367 max-height: none !important;368 overflow: visible !important;369 }370
371 /* 确保搜索结果列表能够正常显示 */372 .search-panel {373 overflow: visible; /* 允许内容溢出 */374 }375
376 /* 当AI搜索激活时,调整搜索面板样式 */377 .search-panel:has(.ai-search-container-mobile search-bar-snippet[expanded]) {378 overflow-y: auto; /* 恢复滚动 */379 max-height: 80vh; /* 增加最大高度 */380 }381</style>效果如下:

完结撒花
cf的AI搜索省去了自己配置嵌入模型和前端查询响应模型的繁琐步骤,并且每天提供10k神经元额度,这个评价必须给到夯。目前模型响应速度和国内访问速度都还不错,写完笔记点点Remotely-Save就能推送到知识库😋
题外话
AI发展真的太快了,从最初chatgpt发布,只觉得这玩意儿能聊天还挺好玩,受限于没有境外信用卡和网络环境,对gpt的使用仅限于网页聊天。到deepseek发布,第一次知道ai可以通过api接入到自己的程序里面,那时候deepseek每个月还有10元的免费额度,低廉的调用成本带来了巨量的使用量,可能还有竞争对手的攻击,让其api接口一度瘫痪,甚至连官网的reasoner模型都基本无法响应。后来国内各厂商也是紧追其后,阿里、字节等都发布了自己的模型。
从高中到大学短短几年,见证了ai从娱乐聊天机器人,变成了实打实的生产力工具,自己对ai的依赖也是越来越重。初学逆向时,我经常把整段的代码贴给ai,可往往一个花指令就能骗过ai,到现在比赛,配好agent,配好ida-mcp和wsl,接个powerful的模型api,ai接管了从代码分析到动态调试、接收报错继续修正的所有流程,除了烧token外几乎不需要额外介入。很难想象,赛场上隐藏在屏幕之下和你对线的,可能不是碳基生物。
openclaw发布后,总能刷到“付费上门安装龙虾”或是“付费卸载龙虾”一类的推文,且不论是博人眼球亦或是确有此事,ai对日常生活渗透之快是不可置否的,使用技术的门槛降低也许是好事,但对于我而言,对ai的依赖总是无声无息的消磨着我学习的欲望,盯着电脑花几个小时调试程序,不如问ai来的快,甚至容易产生”这就是我的水平”的错觉,希望自己能时刻保持清醒,继续学习🫡