@astro-minimax/ai 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +223 -0
- package/dist/cache/global-cache.d.ts +31 -0
- package/dist/cache/global-cache.d.ts.map +1 -0
- package/dist/cache/global-cache.js +141 -0
- package/dist/cache/index.d.ts +8 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +62 -0
- package/dist/cache/kv-adapter.d.ts +21 -0
- package/dist/cache/kv-adapter.d.ts.map +1 -0
- package/dist/cache/kv-adapter.js +102 -0
- package/dist/cache/memory-adapter.d.ts +24 -0
- package/dist/cache/memory-adapter.d.ts.map +1 -0
- package/dist/cache/memory-adapter.js +95 -0
- package/dist/cache/response-cache.d.ts +45 -0
- package/dist/cache/response-cache.d.ts.map +1 -0
- package/dist/cache/response-cache.js +85 -0
- package/dist/cache/types.d.ts +118 -0
- package/dist/cache/types.d.ts.map +1 -0
- package/dist/cache/types.js +16 -0
- package/dist/data/index.d.ts +3 -0
- package/dist/data/index.d.ts.map +1 -0
- package/dist/data/index.js +1 -0
- package/dist/data/metadata-loader.d.ts +37 -0
- package/dist/data/metadata-loader.d.ts.map +1 -0
- package/dist/data/metadata-loader.js +54 -0
- package/dist/data/types.d.ts +51 -0
- package/dist/data/types.d.ts.map +1 -0
- package/dist/data/types.js +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/intelligence/citation-guard.d.ts +24 -0
- package/dist/intelligence/citation-guard.d.ts.map +1 -0
- package/dist/intelligence/citation-guard.js +82 -0
- package/dist/intelligence/evidence-analysis.d.ts +29 -0
- package/dist/intelligence/evidence-analysis.d.ts.map +1 -0
- package/dist/intelligence/evidence-analysis.js +88 -0
- package/dist/intelligence/index.d.ts +6 -0
- package/dist/intelligence/index.d.ts.map +1 -0
- package/dist/intelligence/index.js +4 -0
- package/dist/intelligence/intent-detect.d.ts +29 -0
- package/dist/intelligence/intent-detect.d.ts.map +1 -0
- package/dist/intelligence/intent-detect.js +64 -0
- package/dist/intelligence/keyword-extract.d.ts +31 -0
- package/dist/intelligence/keyword-extract.d.ts.map +1 -0
- package/dist/intelligence/keyword-extract.js +114 -0
- package/dist/intelligence/types.d.ts +27 -0
- package/dist/intelligence/types.d.ts.map +1 -0
- package/dist/intelligence/types.js +1 -0
- package/dist/middleware/index.d.ts +3 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +1 -0
- package/dist/middleware/rate-limiter.d.ts +26 -0
- package/dist/middleware/rate-limiter.d.ts.map +1 -0
- package/dist/middleware/rate-limiter.js +129 -0
- package/dist/prompt/dynamic-layer.d.ts +7 -0
- package/dist/prompt/dynamic-layer.d.ts.map +1 -0
- package/dist/prompt/dynamic-layer.js +40 -0
- package/dist/prompt/index.d.ts +6 -0
- package/dist/prompt/index.d.ts.map +1 -0
- package/dist/prompt/index.js +4 -0
- package/dist/prompt/prompt-builder.d.ts +11 -0
- package/dist/prompt/prompt-builder.d.ts.map +1 -0
- package/dist/prompt/prompt-builder.js +19 -0
- package/dist/prompt/semi-static-layer.d.ts +7 -0
- package/dist/prompt/semi-static-layer.d.ts.map +1 -0
- package/dist/prompt/semi-static-layer.js +32 -0
- package/dist/prompt/static-layer.d.ts +3 -0
- package/dist/prompt/static-layer.d.ts.map +1 -0
- package/dist/prompt/static-layer.js +78 -0
- package/dist/prompt/types.d.ts +25 -0
- package/dist/prompt/types.d.ts.map +1 -0
- package/dist/prompt/types.js +1 -0
- package/dist/provider-manager/base.d.ts +26 -0
- package/dist/provider-manager/base.d.ts.map +1 -0
- package/dist/provider-manager/base.js +47 -0
- package/dist/provider-manager/config.d.ts +7 -0
- package/dist/provider-manager/config.d.ts.map +1 -0
- package/dist/provider-manager/config.js +134 -0
- package/dist/provider-manager/index.d.ts +8 -0
- package/dist/provider-manager/index.d.ts.map +1 -0
- package/dist/provider-manager/index.js +6 -0
- package/dist/provider-manager/manager.d.ts +18 -0
- package/dist/provider-manager/manager.d.ts.map +1 -0
- package/dist/provider-manager/manager.js +121 -0
- package/dist/provider-manager/mock.d.ts +18 -0
- package/dist/provider-manager/mock.d.ts.map +1 -0
- package/dist/provider-manager/mock.js +56 -0
- package/dist/provider-manager/openai.d.ts +20 -0
- package/dist/provider-manager/openai.d.ts.map +1 -0
- package/dist/provider-manager/openai.js +83 -0
- package/dist/provider-manager/types.d.ts +217 -0
- package/dist/provider-manager/types.d.ts.map +1 -0
- package/dist/provider-manager/types.js +6 -0
- package/dist/provider-manager/workers.d.ts +20 -0
- package/dist/provider-manager/workers.d.ts.map +1 -0
- package/dist/provider-manager/workers.js +74 -0
- package/dist/providers/index.d.ts +2 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +1 -0
- package/dist/providers/mock.d.ts +14 -0
- package/dist/providers/mock.d.ts.map +1 -0
- package/dist/providers/mock.js +234 -0
- package/dist/search/index.d.ts +5 -0
- package/dist/search/index.d.ts.map +1 -0
- package/dist/search/index.js +3 -0
- package/dist/search/search-api.d.ts +28 -0
- package/dist/search/search-api.d.ts.map +1 -0
- package/dist/search/search-api.js +110 -0
- package/dist/search/search-index.d.ts +6 -0
- package/dist/search/search-index.d.ts.map +1 -0
- package/dist/search/search-index.js +22 -0
- package/dist/search/search-utils.d.ts +43 -0
- package/dist/search/search-utils.d.ts.map +1 -0
- package/dist/search/search-utils.js +114 -0
- package/dist/search/session-cache.d.ts +19 -0
- package/dist/search/session-cache.d.ts.map +1 -0
- package/dist/search/session-cache.js +92 -0
- package/dist/search/types.d.ts +41 -0
- package/dist/search/types.d.ts.map +1 -0
- package/dist/search/types.js +1 -0
- package/dist/server/chat-handler.d.ts +3 -0
- package/dist/server/chat-handler.d.ts.map +1 -0
- package/dist/server/chat-handler.js +750 -0
- package/dist/server/dev-server.d.ts +18 -0
- package/dist/server/dev-server.d.ts.map +1 -0
- package/dist/server/dev-server.js +294 -0
- package/dist/server/errors.d.ts +17 -0
- package/dist/server/errors.d.ts.map +1 -0
- package/dist/server/errors.js +41 -0
- package/dist/server/index.d.ts +8 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +5 -0
- package/dist/server/metadata-init.d.ts +11 -0
- package/dist/server/metadata-init.d.ts.map +1 -0
- package/dist/server/metadata-init.js +45 -0
- package/dist/server/notify.d.ts +25 -0
- package/dist/server/notify.d.ts.map +1 -0
- package/dist/server/notify.js +62 -0
- package/dist/server/types.d.ts +56 -0
- package/dist/server/types.d.ts.map +1 -0
- package/dist/server/types.js +13 -0
- package/dist/stream/index.d.ts +3 -0
- package/dist/stream/index.d.ts.map +1 -0
- package/dist/stream/index.js +2 -0
- package/dist/stream/mock-stream.d.ts +12 -0
- package/dist/stream/mock-stream.d.ts.map +1 -0
- package/dist/stream/mock-stream.js +27 -0
- package/dist/stream/response.d.ts +10 -0
- package/dist/stream/response.d.ts.map +1 -0
- package/dist/stream/response.js +22 -0
- package/dist/utils/i18n.d.ts +18 -0
- package/dist/utils/i18n.d.ts.map +1 -0
- package/dist/utils/i18n.js +148 -0
- package/package.json +93 -0
- package/src/components/AIChatContainer.tsx +30 -0
- package/src/components/AIChatWidget.astro +30 -0
- package/src/components/ChatPanel.tsx +865 -0
- package/src/styles/source.css +2 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { getMockResponse, createMockStream } from './mock.js';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock provider for development and testing.
|
|
3
|
+
* Returns predefined responses with article/link recommendations.
|
|
4
|
+
* Responses use Markdown links which the ChatPanel renders as clickable elements.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Returns a mock response with Markdown links for article/external link recommendations.
|
|
8
|
+
*/
|
|
9
|
+
export declare function getMockResponse(question: string, lang?: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* Creates a ReadableStream that simulates character-by-character streaming.
|
|
12
|
+
*/
|
|
13
|
+
export declare function createMockStream(text: string): ReadableStream<string>;
|
|
14
|
+
//# sourceMappingURL=mock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mock.d.ts","sourceRoot":"","sources":["../../src/providers/mock.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAyMH;;GAEG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,SAAO,GAAG,MAAM,CAWrE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,CAAC,MAAM,CAAC,CAerE"}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock provider for development and testing.
|
|
3
|
+
* Returns predefined responses with article/link recommendations.
|
|
4
|
+
* Responses use Markdown links which the ChatPanel renders as clickable elements.
|
|
5
|
+
*/
|
|
6
|
+
const MOCK_RESPONSES = [
|
|
7
|
+
{
|
|
8
|
+
patterns: [/astro/i, /框架/],
|
|
9
|
+
zh: `Astro 是一个现代化的静态站点生成器,核心优势是"岛屿架构"——默认零 JS,只在交互组件上加载脚本。本博客基于 Astro 构建。
|
|
10
|
+
|
|
11
|
+
推荐阅读:
|
|
12
|
+
- [快速上手:两种集成方式](/zh/posts/getting-started) — 了解如何搭建 astro-minimax 博客
|
|
13
|
+
- [如何配置主题](/zh/posts/how-to-configure-astro-minimax-theme) — 自定义你的博客外观
|
|
14
|
+
|
|
15
|
+
外部资源:
|
|
16
|
+
- [Astro 官方文档](https://docs.astro.build) — 深入学习 Astro 框架
|
|
17
|
+
- [Astro 主题市场](https://astro.build/themes/) — 发现更多 Astro 主题`,
|
|
18
|
+
en: `Astro is a modern static site generator with an "Islands Architecture" — zero JS by default, loading scripts only for interactive components. This blog is built with Astro.
|
|
19
|
+
|
|
20
|
+
Recommended reading:
|
|
21
|
+
- [Getting Started: Two Integration Methods](/en/posts/getting-started) — Learn how to set up an astro-minimax blog
|
|
22
|
+
- [How to Configure the Theme](/en/posts/how-to-configure-astro-minimax-theme) — Customize your blog
|
|
23
|
+
|
|
24
|
+
External resources:
|
|
25
|
+
- [Astro Documentation](https://docs.astro.build) — Learn Astro in depth
|
|
26
|
+
- [Astro Themes](https://astro.build/themes/) — Discover more themes`,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
patterns: [/推荐|文章|看什么|读什么|recommend/i],
|
|
30
|
+
zh: `以下是一些热门文章推荐:
|
|
31
|
+
|
|
32
|
+
**入门系列:**
|
|
33
|
+
- [快速上手:两种集成方式](/zh/posts/getting-started) — 搭建你的第一个博客
|
|
34
|
+
- [如何添加新文章](/zh/posts/adding-new-post) — 内容创作指南
|
|
35
|
+
- [预定义配色方案](/zh/posts/predefined-color-schemes) — 选一个你喜欢的主题色
|
|
36
|
+
|
|
37
|
+
**技术深度:**
|
|
38
|
+
- [如何在博客中使用 LaTeX 公式](/zh/posts/how-to-add-latex-equations-in-blog-posts) — 数学公式支持
|
|
39
|
+
- [动态 OG 图片生成](/zh/posts/dynamic-og-images) — 自动生成社交分享图
|
|
40
|
+
|
|
41
|
+
你对哪个方向的内容更感兴趣?我可以做更精准的推荐。`,
|
|
42
|
+
en: `Here are some recommended articles:
|
|
43
|
+
|
|
44
|
+
**Getting Started:**
|
|
45
|
+
- [Getting Started: Two Integration Methods](/en/posts/getting-started) — Build your first blog
|
|
46
|
+
- [Adding New Posts](/en/posts/adding-new-post) — Content creation guide
|
|
47
|
+
- [Predefined Color Schemes](/en/posts/predefined-color-schemes) — Pick your favorite theme color
|
|
48
|
+
|
|
49
|
+
**Technical Deep Dives:**
|
|
50
|
+
- [LaTeX Equations in Blog Posts](/en/posts/how-to-add-latex-equations-in-blog-posts) — Math formula support
|
|
51
|
+
- [Dynamic OG Images](/en/posts/dynamic-og-images) — Auto-generate social share images
|
|
52
|
+
|
|
53
|
+
What direction interests you more? I can provide more specific recommendations.`,
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
patterns: [/博客|blog|功能|feature/i],
|
|
57
|
+
zh: `这个博客基于 **astro-minimax** 主题,功能丰富:
|
|
58
|
+
|
|
59
|
+
核心功能:Markdown/MDX、代码高亮、[数学公式(KaTeX)](/zh/posts/how-to-add-latex-equations-in-blog-posts)、[Mermaid 图表](/zh/posts/mermaid-diagrams)、标签分类、全文搜索(Pagefind)、[Waline 评论](https://waline.js.org)、深色模式。
|
|
60
|
+
|
|
61
|
+
了解更多:
|
|
62
|
+
- [配置指南](/zh/posts/how-to-configure-astro-minimax-theme) — 完整配置选项
|
|
63
|
+
- [Markdown 扩展语法](/zh/posts/markdown-extended) — 所有支持的语法特性
|
|
64
|
+
|
|
65
|
+
开源地址:[souloss/astro-minimax](https://github.com/souloss/astro-minimax)`,
|
|
66
|
+
en: `This blog uses the **astro-minimax** theme with rich features:
|
|
67
|
+
|
|
68
|
+
Core features: Markdown/MDX, syntax highlighting, [math equations (KaTeX)](/en/posts/how-to-add-latex-equations-in-blog-posts), [Mermaid diagrams](/en/posts/mermaid-diagrams), tags & categories, full-text search (Pagefind), [Waline comments](https://waline.js.org), dark mode.
|
|
69
|
+
|
|
70
|
+
Learn more:
|
|
71
|
+
- [Configuration Guide](/en/posts/how-to-configure-astro-minimax-theme) — Full config options
|
|
72
|
+
- [Extended Markdown](/en/posts/markdown-extended) — All supported syntax features
|
|
73
|
+
|
|
74
|
+
Open source: [souloss/astro-minimax](https://github.com/souloss/astro-minimax)`,
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
patterns: [/主题|theme|暗色|dark|颜色|color|配色/i],
|
|
78
|
+
zh: `博客支持亮色和暗色主题,右下角按钮即可切换,也会自动检测系统偏好。
|
|
79
|
+
|
|
80
|
+
配色方案可以在配置中自定义,目前提供多种预设:
|
|
81
|
+
- [预定义配色方案](/zh/posts/predefined-color-schemes) — 查看所有可用配色
|
|
82
|
+
- [自定义主题色](/zh/posts/customizing-astro-minimax-theme-color-schemes) — 创建你自己的配色
|
|
83
|
+
|
|
84
|
+
参考 [Tailwind CSS 调色板](https://tailwindcss.com/docs/customizing-colors) 获取灵感。`,
|
|
85
|
+
en: `The blog supports light and dark themes — toggle with the bottom-right button or auto-detect system preference.
|
|
86
|
+
|
|
87
|
+
Color schemes are customizable:
|
|
88
|
+
- [Predefined Color Schemes](/en/posts/predefined-color-schemes) — See all available schemes
|
|
89
|
+
- [Custom Theme Colors](/en/posts/customizing-astro-minimax-theme-color-schemes) — Create your own
|
|
90
|
+
|
|
91
|
+
Check [Tailwind CSS Color Palette](https://tailwindcss.com/docs/customizing-colors) for inspiration.`,
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
patterns: [/搭建|部署|deploy|build|install|安装|搭/i],
|
|
95
|
+
zh: `搭建类似的博客非常简单!有两种方式:
|
|
96
|
+
|
|
97
|
+
1. **GitHub 模板**(推荐新手)— 一键 Fork,开箱即用
|
|
98
|
+
2. **NPM 包集成** — 适合内容与系统分离的进阶用法
|
|
99
|
+
|
|
100
|
+
详细步骤请看 [快速上手](/zh/posts/getting-started)。
|
|
101
|
+
|
|
102
|
+
部署推荐 [Cloudflare Pages](https://pages.cloudflare.com)(免费、全球 CDN),也支持 [Vercel](https://vercel.com) 和 [Netlify](https://netlify.com)。`,
|
|
103
|
+
en: `Setting up a similar blog is easy! Two methods:
|
|
104
|
+
|
|
105
|
+
1. **GitHub Template** (recommended for beginners) — One-click fork, ready to use
|
|
106
|
+
2. **NPM Package Integration** — For advanced content/system separation
|
|
107
|
+
|
|
108
|
+
See [Getting Started](/en/posts/getting-started) for detailed steps.
|
|
109
|
+
|
|
110
|
+
Deploy with [Cloudflare Pages](https://pages.cloudflare.com) (free, global CDN), or [Vercel](https://vercel.com) / [Netlify](https://netlify.com).`,
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
patterns: [/rust/i],
|
|
114
|
+
zh: `博客中有一系列 Rust 文章:
|
|
115
|
+
- [Rust 入门介绍](/zh/posts/rust-series-01-introduction) — 语言基础
|
|
116
|
+
- [所有权系统](/zh/posts/rust-series-02-ownership) — Rust 核心概念
|
|
117
|
+
- [错误处理](/zh/posts/rust-series-03-error-handling) — Result 和 Option
|
|
118
|
+
- [并发编程](/zh/posts/rust-series-04-concurrency) — 安全的多线程
|
|
119
|
+
|
|
120
|
+
外部学习资源:
|
|
121
|
+
- [The Rust Book](https://doc.rust-lang.org/book/) — 官方教程
|
|
122
|
+
- [Rust by Example](https://doc.rust-lang.org/rust-by-example/) — 实例学习`,
|
|
123
|
+
en: `The blog has a Rust series:
|
|
124
|
+
- [Rust Introduction](/en/posts/rust-series-01-introduction) — Language basics
|
|
125
|
+
- [Ownership System](/en/posts/rust-series-02-ownership) — Core Rust concept
|
|
126
|
+
- [Error Handling](/en/posts/rust-series-03-error-handling) — Result and Option
|
|
127
|
+
- [Concurrency](/en/posts/rust-series-04-concurrency) — Safe multithreading
|
|
128
|
+
|
|
129
|
+
External resources:
|
|
130
|
+
- [The Rust Book](https://doc.rust-lang.org/book/) — Official tutorial
|
|
131
|
+
- [Rust by Example](https://doc.rust-lang.org/rust-by-example/) — Learn by examples`,
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
patterns: [/ai|人工智能|助手|assistant|chat/i],
|
|
135
|
+
zh: `我是这个博客的 AI 助手!当前运行在 Demo 模式,可以:
|
|
136
|
+
- 根据你的问题推荐相关博客文章
|
|
137
|
+
- 推荐有用的外部学习资源
|
|
138
|
+
- 解答关于博客技术栈的问题
|
|
139
|
+
|
|
140
|
+
启用完整 AI 功能(RAG 搜索增强)需要配置 \`AI_BASE_URL\` 和 \`AI_API_KEY\` 环境变量。
|
|
141
|
+
|
|
142
|
+
试试问我:"有哪些文章推荐?" 或 "怎么搭建类似的博客?"`,
|
|
143
|
+
en: `I'm the blog AI assistant! Currently in Demo mode, I can:
|
|
144
|
+
- Recommend relevant blog articles based on your questions
|
|
145
|
+
- Suggest useful external learning resources
|
|
146
|
+
- Answer questions about the blog's tech stack
|
|
147
|
+
|
|
148
|
+
For full AI features (RAG search enhancement), configure \`AI_BASE_URL\` and \`AI_API_KEY\` environment variables.
|
|
149
|
+
|
|
150
|
+
Try asking: "Recommend some articles?" or "How to build a similar blog?"`,
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
patterns: [/搜索|search|pagefind/i],
|
|
154
|
+
zh: `博客集成了 [Pagefind](https://pagefind.app) 全文搜索引擎,构建时自动索引。点击页面顶部搜索图标即可使用。
|
|
155
|
+
|
|
156
|
+
了解更多搜索功能:
|
|
157
|
+
- [Pagefind 官方文档](https://pagefind.app/docs/) — 完整配置指南
|
|
158
|
+
- 搜索支持中文和英文内容`,
|
|
159
|
+
en: `The blog integrates [Pagefind](https://pagefind.app) for full-text search, auto-indexed at build time. Click the search icon at the top to use it.
|
|
160
|
+
|
|
161
|
+
Learn more:
|
|
162
|
+
- [Pagefind Documentation](https://pagefind.app/docs/) — Complete configuration guide
|
|
163
|
+
- Search supports both Chinese and English content`,
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
patterns: [/markdown|mdx|语法|syntax|公式|latex|mermaid|图表/i],
|
|
167
|
+
zh: `博客支持丰富的内容语法:
|
|
168
|
+
|
|
169
|
+
- [Markdown 基础语法](/zh/posts/markdown-basics) — 标题、列表、表格等
|
|
170
|
+
- [Markdown 扩展语法](/zh/posts/markdown-extended) — 脚注、高亮、折叠等
|
|
171
|
+
- [LaTeX 数学公式](/zh/posts/how-to-add-latex-equations-in-blog-posts) — KaTeX 渲染
|
|
172
|
+
- [Mermaid 图表](/zh/posts/mermaid-diagrams) — 流程图、时序图
|
|
173
|
+
- [Markmap 思维导图](/zh/posts/markmap-mindmaps) — 交互式思维导图
|
|
174
|
+
|
|
175
|
+
外部参考:[GitHub Flavored Markdown](https://github.github.com/gfm/)`,
|
|
176
|
+
en: `The blog supports rich content syntax:
|
|
177
|
+
|
|
178
|
+
- [Markdown Basics](/en/posts/markdown-basics) — Headings, lists, tables
|
|
179
|
+
- [Extended Markdown](/en/posts/markdown-extended) — Footnotes, highlights, collapsible
|
|
180
|
+
- [LaTeX Equations](/en/posts/how-to-add-latex-equations-in-blog-posts) — KaTeX rendering
|
|
181
|
+
- [Mermaid Diagrams](/en/posts/mermaid-diagrams) — Flowcharts, sequence diagrams
|
|
182
|
+
- [Markmap Mind Maps](/en/posts/markmap-mindmaps) — Interactive mind maps
|
|
183
|
+
|
|
184
|
+
Reference: [GitHub Flavored Markdown](https://github.github.com/gfm/)`,
|
|
185
|
+
},
|
|
186
|
+
];
|
|
187
|
+
const FALLBACK = {
|
|
188
|
+
zh: `感谢提问!我目前在 Demo 模式下,可以推荐博客文章和外部资源。
|
|
189
|
+
|
|
190
|
+
试试这些话题:
|
|
191
|
+
- "有哪些文章推荐?"
|
|
192
|
+
- "Astro 框架是什么?"
|
|
193
|
+
- "怎么搭建类似的博客?"
|
|
194
|
+
- "支持哪些 Markdown 语法?"`,
|
|
195
|
+
en: `Thanks for asking! I'm in Demo mode and can recommend blog articles and external resources.
|
|
196
|
+
|
|
197
|
+
Try these topics:
|
|
198
|
+
- "Recommend some articles?"
|
|
199
|
+
- "What is Astro?"
|
|
200
|
+
- "How to build a similar blog?"
|
|
201
|
+
- "What Markdown syntax is supported?"`,
|
|
202
|
+
};
|
|
203
|
+
/**
|
|
204
|
+
* Returns a mock response with Markdown links for article/external link recommendations.
|
|
205
|
+
*/
|
|
206
|
+
export function getMockResponse(question, lang = 'zh') {
|
|
207
|
+
const q = question.toLowerCase();
|
|
208
|
+
const isZh = lang !== 'en';
|
|
209
|
+
for (const { patterns, zh, en } of MOCK_RESPONSES) {
|
|
210
|
+
if (patterns.some(p => p.test(q))) {
|
|
211
|
+
return isZh ? zh : en;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return isZh ? FALLBACK.zh : FALLBACK.en;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Creates a ReadableStream that simulates character-by-character streaming.
|
|
218
|
+
*/
|
|
219
|
+
export function createMockStream(text) {
|
|
220
|
+
let index = 0;
|
|
221
|
+
return new ReadableStream({
|
|
222
|
+
async pull(controller) {
|
|
223
|
+
if (index >= text.length) {
|
|
224
|
+
controller.close();
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const chunkSize = Math.random() < 0.3 ? 2 : 1;
|
|
228
|
+
const chunk = text.slice(index, index + chunkSize);
|
|
229
|
+
index += chunkSize;
|
|
230
|
+
controller.enqueue(chunk);
|
|
231
|
+
await new Promise(resolve => setTimeout(resolve, 12 + Math.random() * 23));
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { initArticleIndex, initProjectIndex, searchArticles, searchProjects, mergeResults } from './search-api.js';
|
|
2
|
+
export { getSessionCacheKey, getCachedContext, setCachedContext, deleteCachedContext, setCacheAdapter, getCacheAdapter, cleanupCache, SESSION_CACHE_TTL_SECONDS, SESSION_CACHE_TTL_MS, getCachedContextSync, setCachedContextSync, cleanupCacheLegacy, } from './session-cache.js';
|
|
3
|
+
export { normalizeText, tokenize, scoreDocument } from './search-utils.js';
|
|
4
|
+
export type { SearchDocument, ArticleContext, ProjectContext, CachedSearchContext, SearchResult } from './types.js';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/search/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,cAAc,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AACnH,OAAO,EACL,kBAAkB,EAClB,gBAAgB,EAChB,gBAAgB,EAChB,mBAAmB,EACnB,eAAe,EACf,eAAe,EACf,YAAY,EACZ,yBAAyB,EACzB,oBAAoB,EACpB,oBAAoB,EACpB,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAC3E,YAAY,EAAE,cAAc,EAAE,cAAc,EAAE,cAAc,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { initArticleIndex, initProjectIndex, searchArticles, searchProjects, mergeResults } from './search-api.js';
|
|
2
|
+
export { getSessionCacheKey, getCachedContext, setCachedContext, deleteCachedContext, setCacheAdapter, getCacheAdapter, cleanupCache, SESSION_CACHE_TTL_SECONDS, SESSION_CACHE_TTL_MS, getCachedContextSync, setCachedContextSync, cleanupCacheLegacy, } from './session-cache.js';
|
|
3
|
+
export { normalizeText, tokenize, scoreDocument } from './search-utils.js';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { SearchDocument, ArticleContext, ProjectContext } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Initializes the article search index from the provided documents.
|
|
4
|
+
* Should be called once at startup (e.g., when the edge function initializes).
|
|
5
|
+
*/
|
|
6
|
+
export declare function initArticleIndex(documents: SearchDocument[]): void;
|
|
7
|
+
export declare function initProjectIndex(documents: SearchDocument[]): void;
|
|
8
|
+
/**
|
|
9
|
+
* Searches for articles related to the query.
|
|
10
|
+
* Returns enriched ArticleContext objects ready for prompt injection.
|
|
11
|
+
*/
|
|
12
|
+
export declare function searchArticles(query: string, options?: {
|
|
13
|
+
enableDeepContent?: boolean;
|
|
14
|
+
siteUrl?: string;
|
|
15
|
+
}): ArticleContext[];
|
|
16
|
+
/**
|
|
17
|
+
* Searches for projects related to the query.
|
|
18
|
+
*/
|
|
19
|
+
export declare function searchProjects(query: string, options?: {
|
|
20
|
+
siteUrl?: string;
|
|
21
|
+
}): ProjectContext[];
|
|
22
|
+
/**
|
|
23
|
+
* Merges two result arrays by URL, preferring items from the primary array.
|
|
24
|
+
*/
|
|
25
|
+
export declare function mergeResults<T extends {
|
|
26
|
+
url: string;
|
|
27
|
+
}>(primary: T[], secondary: T[]): T[];
|
|
28
|
+
//# sourceMappingURL=search-api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search-api.d.ts","sourceRoot":"","sources":["../../src/search/search-api.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAiC,cAAc,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAYhH;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,cAAc,EAAE,GAAG,IAAI,CAElE;AAED,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,cAAc,EAAE,GAAG,IAAI,CAElE;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,EACb,OAAO,GAAE;IAAE,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAO,GAC9D,cAAc,EAAE,CAqClB;AAED;;GAEG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,EACb,OAAO,GAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAA;CAAO,GACjC,cAAc,EAAE,CAelB;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,EAAE,OAAO,EAAE,CAAC,EAAE,EAAE,SAAS,EAAE,CAAC,EAAE,GAAG,CAAC,EAAE,CAUzF"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { scoreDocument, filterLowRelevance, tokenize, pickAnchorTerms, normalizeText } from './search-utils.js';
|
|
2
|
+
import { buildSearchIndex } from './search-index.js';
|
|
3
|
+
// Lazy-initialized, cached indexes
|
|
4
|
+
let articleIndex = null;
|
|
5
|
+
let projectIndex = null;
|
|
6
|
+
const ARTICLE_LIMIT = 10;
|
|
7
|
+
const ARTICLE_LIMIT_BROAD = 20;
|
|
8
|
+
const PROJECT_LIMIT = 5;
|
|
9
|
+
const DEEP_CONTENT_SCORE_THRESHOLD = 8;
|
|
10
|
+
const DEEP_CONTENT_MAX_LENGTH = 1500;
|
|
11
|
+
/**
|
|
12
|
+
* Initializes the article search index from the provided documents.
|
|
13
|
+
* Should be called once at startup (e.g., when the edge function initializes).
|
|
14
|
+
*/
|
|
15
|
+
export function initArticleIndex(documents) {
|
|
16
|
+
articleIndex = buildSearchIndex(documents);
|
|
17
|
+
}
|
|
18
|
+
export function initProjectIndex(documents) {
|
|
19
|
+
projectIndex = buildSearchIndex(documents);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Searches for articles related to the query.
|
|
23
|
+
* Returns enriched ArticleContext objects ready for prompt injection.
|
|
24
|
+
*/
|
|
25
|
+
export function searchArticles(query, options = {}) {
|
|
26
|
+
if (!query.trim() || !articleIndex)
|
|
27
|
+
return [];
|
|
28
|
+
const tokens = tokenize(query);
|
|
29
|
+
if (!tokens.length)
|
|
30
|
+
return [];
|
|
31
|
+
const limit = tokens.length <= 2 ? ARTICLE_LIMIT_BROAD : ARTICLE_LIMIT;
|
|
32
|
+
const rawResults = scoreDocs(articleIndex, tokens, limit * 2);
|
|
33
|
+
const filtered = applyAnchorFilter(rawResults, query, tokens);
|
|
34
|
+
const deduplicated = filterLowRelevance(filtered.length > 0 ? filtered : rawResults);
|
|
35
|
+
const results = deduplicated.slice(0, limit);
|
|
36
|
+
const topScore = results[0]?.score ?? 0;
|
|
37
|
+
const secondScore = results[1]?.score ?? 0;
|
|
38
|
+
const isDeepHit = options.enableDeepContent &&
|
|
39
|
+
topScore >= DEEP_CONTENT_SCORE_THRESHOLD &&
|
|
40
|
+
topScore > secondScore * 1.5;
|
|
41
|
+
return results.map((result, index) => {
|
|
42
|
+
const baseUrl = options.siteUrl ?? '';
|
|
43
|
+
const url = result.url.startsWith('http') ? result.url : `${baseUrl}${result.url}`;
|
|
44
|
+
const fullContent = isDeepHit && index === 0 && result.content
|
|
45
|
+
? result.content.slice(0, DEEP_CONTENT_MAX_LENGTH)
|
|
46
|
+
: undefined;
|
|
47
|
+
return {
|
|
48
|
+
title: result.title,
|
|
49
|
+
url,
|
|
50
|
+
summary: result.summary ?? result.excerpt,
|
|
51
|
+
keyPoints: result.keyPoints,
|
|
52
|
+
categories: result.categories,
|
|
53
|
+
dateTime: result.dateTime,
|
|
54
|
+
fullContent,
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Searches for projects related to the query.
|
|
60
|
+
*/
|
|
61
|
+
export function searchProjects(query, options = {}) {
|
|
62
|
+
if (!query.trim() || !projectIndex)
|
|
63
|
+
return [];
|
|
64
|
+
const tokens = tokenize(query);
|
|
65
|
+
if (!tokens.length)
|
|
66
|
+
return [];
|
|
67
|
+
const rawResults = scoreDocs(projectIndex, tokens, PROJECT_LIMIT * 2);
|
|
68
|
+
if (!rawResults.length)
|
|
69
|
+
return [];
|
|
70
|
+
const baseUrl = options.siteUrl ?? '';
|
|
71
|
+
return rawResults.slice(0, PROJECT_LIMIT).map(r => ({
|
|
72
|
+
name: r.title,
|
|
73
|
+
url: r.url.startsWith('http') ? r.url : `${baseUrl}${r.url}`,
|
|
74
|
+
description: r.excerpt || r.content.slice(0, 200),
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Merges two result arrays by URL, preferring items from the primary array.
|
|
79
|
+
*/
|
|
80
|
+
export function mergeResults(primary, secondary) {
|
|
81
|
+
const seen = new Set(primary.map(i => i.url));
|
|
82
|
+
const merged = [...primary];
|
|
83
|
+
for (const item of secondary) {
|
|
84
|
+
if (!seen.has(item.url)) {
|
|
85
|
+
seen.add(item.url);
|
|
86
|
+
merged.push(item);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return merged;
|
|
90
|
+
}
|
|
91
|
+
// ---- Internals ----
|
|
92
|
+
function scoreDocs(index, tokens, limit) {
|
|
93
|
+
return index
|
|
94
|
+
.map(doc => ({ ...doc, score: scoreDocument(tokens, doc) }))
|
|
95
|
+
.filter(doc => doc.score > 0)
|
|
96
|
+
.sort((a, b) => b.score - a.score)
|
|
97
|
+
.slice(0, limit);
|
|
98
|
+
}
|
|
99
|
+
function applyAnchorFilter(results, query, tokens) {
|
|
100
|
+
if (tokens.length > 2)
|
|
101
|
+
return results;
|
|
102
|
+
const anchorTerms = pickAnchorTerms(query, results, 2, 2);
|
|
103
|
+
if (!anchorTerms.length)
|
|
104
|
+
return results;
|
|
105
|
+
const strict = results.filter(r => {
|
|
106
|
+
const text = normalizeText([r.title, ...r.keyPoints, ...r.categories].join(' '));
|
|
107
|
+
return anchorTerms.some(term => text.includes(term));
|
|
108
|
+
});
|
|
109
|
+
return strict.length > 0 ? strict : results;
|
|
110
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { SearchDocument, IndexedDocument } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Builds an in-memory inverted index from a list of documents.
|
|
4
|
+
*/
|
|
5
|
+
export declare function buildSearchIndex(documents: SearchDocument[]): IndexedDocument[];
|
|
6
|
+
//# sourceMappingURL=search-index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search-index.d.ts","sourceRoot":"","sources":["../../src/search/search-index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElE;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,cAAc,EAAE,GAAG,eAAe,EAAE,CAK/E"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { normalizeText } from './search-utils.js';
|
|
2
|
+
/**
|
|
3
|
+
* Builds an in-memory inverted index from a list of documents.
|
|
4
|
+
*/
|
|
5
|
+
export function buildSearchIndex(documents) {
|
|
6
|
+
return documents.map(doc => ({
|
|
7
|
+
...doc,
|
|
8
|
+
tokens: buildDocumentTokens(doc),
|
|
9
|
+
}));
|
|
10
|
+
}
|
|
11
|
+
function buildDocumentTokens(doc) {
|
|
12
|
+
const parts = [
|
|
13
|
+
doc.title,
|
|
14
|
+
doc.excerpt,
|
|
15
|
+
doc.content.slice(0, 1000),
|
|
16
|
+
...doc.keyPoints,
|
|
17
|
+
...doc.categories,
|
|
18
|
+
...doc.tags,
|
|
19
|
+
doc.summary ?? '',
|
|
20
|
+
];
|
|
21
|
+
return [...new Set(parts.map(normalizeText).join(' ').split(/\s+/).filter(Boolean))];
|
|
22
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text normalization and tokenization utilities for search.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Normalizes text for search: lowercase, remove punctuation, normalize whitespace.
|
|
6
|
+
*/
|
|
7
|
+
export declare function normalizeText(text: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* Splits a query into normalized tokens (handles both Chinese and Latin text).
|
|
10
|
+
*/
|
|
11
|
+
export declare function tokenize(text: string): string[];
|
|
12
|
+
/**
|
|
13
|
+
* Removes tokens that are substrings of longer tokens (avoids redundant matching).
|
|
14
|
+
*/
|
|
15
|
+
export declare function dedupeByContainment(terms: string[]): string[];
|
|
16
|
+
/**
|
|
17
|
+
* Computes a relevance score for a document against a set of query tokens.
|
|
18
|
+
* Title matches score higher than content matches.
|
|
19
|
+
*/
|
|
20
|
+
export declare function scoreDocument(tokens: string[], doc: {
|
|
21
|
+
title: string;
|
|
22
|
+
content: string;
|
|
23
|
+
excerpt: string;
|
|
24
|
+
keyPoints: string[];
|
|
25
|
+
categories: string[];
|
|
26
|
+
tags: string[];
|
|
27
|
+
}): number;
|
|
28
|
+
/**
|
|
29
|
+
* Filters out low-relevance results relative to the top score.
|
|
30
|
+
*/
|
|
31
|
+
export declare function filterLowRelevance<T extends {
|
|
32
|
+
score: number;
|
|
33
|
+
}>(results: T[], relativeThreshold?: number, minAbsoluteScore?: number): T[];
|
|
34
|
+
/**
|
|
35
|
+
* Selects the best "anchor terms" from the query — terms that are specific
|
|
36
|
+
* enough to be meaningful but appear in enough results to be useful.
|
|
37
|
+
*/
|
|
38
|
+
export declare function pickAnchorTerms(query: string, candidates: Array<{
|
|
39
|
+
title: string;
|
|
40
|
+
keyPoints: string[];
|
|
41
|
+
categories: string[];
|
|
42
|
+
}>, maxTerms?: number, minTermLength?: number): string[];
|
|
43
|
+
//# sourceMappingURL=search-utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search-utils.d.ts","sourceRoot":"","sources":["../../src/search/search-utils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;GAEG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAMlD;AAED;;GAEG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAK/C;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAS7D;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,EAAE,CAAC;IAAC,UAAU,EAAE,MAAM,EAAE,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,MAAM,CA2B3K;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,EAC5D,OAAO,EAAE,CAAC,EAAE,EACZ,iBAAiB,SAAO,EACxB,gBAAgB,SAAI,GACnB,CAAC,EAAE,CAML;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,EACb,UAAU,EAAE,KAAK,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,EAAE,CAAC;IAAC,UAAU,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,EAC/E,QAAQ,SAAI,EACZ,aAAa,SAAI,GAChB,MAAM,EAAE,CAuBV"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text normalization and tokenization utilities for search.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Normalizes text for search: lowercase, remove punctuation, normalize whitespace.
|
|
6
|
+
*/
|
|
7
|
+
export function normalizeText(text) {
|
|
8
|
+
return text
|
|
9
|
+
.toLowerCase()
|
|
10
|
+
.replace(/[^\u4e00-\u9fa5\w\s]/g, ' ')
|
|
11
|
+
.replace(/\s+/g, ' ')
|
|
12
|
+
.trim();
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Splits a query into normalized tokens (handles both Chinese and Latin text).
|
|
16
|
+
*/
|
|
17
|
+
export function tokenize(text) {
|
|
18
|
+
const normalized = normalizeText(text);
|
|
19
|
+
// Split on whitespace for multi-word queries
|
|
20
|
+
const parts = normalized.split(/\s+/).filter(Boolean);
|
|
21
|
+
return dedupeByContainment(parts);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Removes tokens that are substrings of longer tokens (avoids redundant matching).
|
|
25
|
+
*/
|
|
26
|
+
export function dedupeByContainment(terms) {
|
|
27
|
+
const unique = [...new Set(terms)];
|
|
28
|
+
const kept = [];
|
|
29
|
+
for (const term of unique.sort((a, b) => b.length - a.length)) {
|
|
30
|
+
if (!kept.some(existing => existing.includes(term))) {
|
|
31
|
+
kept.push(term);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return kept;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Computes a relevance score for a document against a set of query tokens.
|
|
38
|
+
* Title matches score higher than content matches.
|
|
39
|
+
*/
|
|
40
|
+
export function scoreDocument(tokens, doc) {
|
|
41
|
+
if (!tokens.length)
|
|
42
|
+
return 0;
|
|
43
|
+
let score = 0;
|
|
44
|
+
const title = normalizeText(doc.title);
|
|
45
|
+
const excerpt = normalizeText(doc.excerpt);
|
|
46
|
+
const keyPointsText = normalizeText(doc.keyPoints.join(' '));
|
|
47
|
+
const categoriesText = normalizeText(doc.categories.join(' '));
|
|
48
|
+
const tagsText = normalizeText(doc.tags.join(' '));
|
|
49
|
+
const contentSample = normalizeText(doc.content.slice(0, 500));
|
|
50
|
+
for (const token of tokens) {
|
|
51
|
+
if (!token)
|
|
52
|
+
continue;
|
|
53
|
+
// Title: highest weight
|
|
54
|
+
if (title.includes(token))
|
|
55
|
+
score += 8;
|
|
56
|
+
// KeyPoints: high weight
|
|
57
|
+
if (keyPointsText.includes(token))
|
|
58
|
+
score += 5;
|
|
59
|
+
// Categories/tags: medium weight
|
|
60
|
+
if (categoriesText.includes(token))
|
|
61
|
+
score += 4;
|
|
62
|
+
if (tagsText.includes(token))
|
|
63
|
+
score += 3;
|
|
64
|
+
// Excerpt: medium weight
|
|
65
|
+
if (excerpt.includes(token))
|
|
66
|
+
score += 3;
|
|
67
|
+
// Content sample: low weight
|
|
68
|
+
if (contentSample.includes(token))
|
|
69
|
+
score += 1;
|
|
70
|
+
}
|
|
71
|
+
return score;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Filters out low-relevance results relative to the top score.
|
|
75
|
+
*/
|
|
76
|
+
export function filterLowRelevance(results, relativeThreshold = 0.35, minAbsoluteScore = 2) {
|
|
77
|
+
if (results.length <= 3)
|
|
78
|
+
return results;
|
|
79
|
+
const topScore = results[0]?.score ?? 0;
|
|
80
|
+
if (topScore <= 0)
|
|
81
|
+
return results;
|
|
82
|
+
const threshold = Math.max(minAbsoluteScore, topScore * relativeThreshold);
|
|
83
|
+
return results.filter((item, index) => index < 3 || item.score >= threshold);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Selects the best "anchor terms" from the query — terms that are specific
|
|
87
|
+
* enough to be meaningful but appear in enough results to be useful.
|
|
88
|
+
*/
|
|
89
|
+
export function pickAnchorTerms(query, candidates, maxTerms = 2, minTermLength = 2) {
|
|
90
|
+
const terms = tokenize(query).filter(t => t.length >= minTermLength);
|
|
91
|
+
if (terms.length <= maxTerms)
|
|
92
|
+
return terms.slice(0, maxTerms);
|
|
93
|
+
if (!candidates.length)
|
|
94
|
+
return terms.slice(0, maxTerms);
|
|
95
|
+
const scored = terms.map(term => {
|
|
96
|
+
let hitCount = 0;
|
|
97
|
+
for (const c of candidates) {
|
|
98
|
+
const text = normalizeText([c.title, ...c.keyPoints, ...c.categories].join(' '));
|
|
99
|
+
if (text.includes(term))
|
|
100
|
+
hitCount++;
|
|
101
|
+
}
|
|
102
|
+
if (hitCount <= 0)
|
|
103
|
+
return { term, score: Number.NEGATIVE_INFINITY };
|
|
104
|
+
const coverage = hitCount / candidates.length;
|
|
105
|
+
const specificity = 1 - coverage;
|
|
106
|
+
const lengthScore = Math.min(term.length, 8) / 8;
|
|
107
|
+
return { term, score: specificity * 2 + lengthScore };
|
|
108
|
+
});
|
|
109
|
+
return scored
|
|
110
|
+
.filter(s => Number.isFinite(s.score))
|
|
111
|
+
.sort((a, b) => b.score - a.score)
|
|
112
|
+
.map(s => s.term)
|
|
113
|
+
.slice(0, maxTerms);
|
|
114
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { CacheAdapter } from '../cache/types.js';
|
|
2
|
+
import type { CachedSearchContext } from './types.js';
|
|
3
|
+
export { type CachedSearchContext, type ArticleContext, type ProjectContext } from './types.js';
|
|
4
|
+
export declare const SESSION_CACHE_TTL_SECONDS = 600;
|
|
5
|
+
export declare const SESSION_CACHE_TTL_MS: number;
|
|
6
|
+
export declare function getSessionCacheKey(req: Request): string | null;
|
|
7
|
+
export declare function setCacheAdapter(cache: CacheAdapter): void;
|
|
8
|
+
export declare function getCacheAdapter(): CacheAdapter;
|
|
9
|
+
export declare function getCachedContext(key: string, cache?: CacheAdapter): Promise<CachedSearchContext | undefined>;
|
|
10
|
+
export declare function setCachedContext(key: string, ctx: CachedSearchContext, cache?: CacheAdapter): Promise<void>;
|
|
11
|
+
export declare function deleteCachedContext(key: string, cache?: CacheAdapter): Promise<boolean>;
|
|
12
|
+
export declare function cleanupCache(_now: number): void;
|
|
13
|
+
/** @deprecated Use getCachedContext instead */
|
|
14
|
+
export declare function getCachedContextSync(key: string): CachedSearchContext | undefined;
|
|
15
|
+
/** @deprecated Use setCachedContext instead */
|
|
16
|
+
export declare function setCachedContextSync(key: string, ctx: CachedSearchContext): void;
|
|
17
|
+
/** @deprecated Use cleanupCache instead (no-op) */
|
|
18
|
+
export declare function cleanupCacheLegacy(now: number): void;
|
|
19
|
+
//# sourceMappingURL=session-cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-cache.d.ts","sourceRoot":"","sources":["../../src/search/session-cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,KAAK,EAAE,mBAAmB,EAAkC,MAAM,YAAY,CAAC;AAGtF,OAAO,EAAE,KAAK,mBAAmB,EAAE,KAAK,cAAc,EAAE,KAAK,cAAc,EAAE,MAAM,YAAY,CAAC;AAGhG,eAAO,MAAM,yBAAyB,MAAM,CAAC;AAC7C,eAAO,MAAM,oBAAoB,QAAmC,CAAC;AAcrE,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAM9D;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,CAEzD;AAED,wBAAgB,eAAe,IAAI,YAAY,CAE9C;AAED,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,MAAM,EACX,KAAK,CAAC,EAAE,YAAY,GACnB,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC,CAI1C;AAED,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,mBAAmB,EACxB,KAAK,CAAC,EAAE,YAAY,GACnB,OAAO,CAAC,IAAI,CAAC,CAGf;AAED,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,MAAM,EACX,KAAK,CAAC,EAAE,YAAY,GACnB,OAAO,CAAC,OAAO,CAAC,CAGlB;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAG/C;AAQD,+CAA+C;AAC/C,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,mBAAmB,GAAG,SAAS,CASjF;AAED,+CAA+C;AAC/C,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,mBAAmB,GAAG,IAAI,CAYhF;AAED,mDAAmD;AACnD,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAepD"}
|