@astro-minimax/ai 0.8.2 → 0.9.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.
Files changed (51) hide show
  1. package/dist/cache/global-cache.js +145 -0
  2. package/dist/cache/index.js +96 -0
  3. package/dist/cache/kv-adapter.js +99 -0
  4. package/dist/cache/memory-adapter.js +97 -0
  5. package/dist/cache/response-cache.js +87 -0
  6. package/dist/cache/types.js +8 -0
  7. package/dist/data/metadata-loader.js +48 -0
  8. package/dist/data/types.js +0 -0
  9. package/dist/fact-registry/fact-matcher.js +128 -0
  10. package/dist/fact-registry/prompt-injector.js +54 -0
  11. package/dist/fact-registry/registry.js +41 -0
  12. package/dist/fact-registry/types.js +0 -0
  13. package/dist/intelligence/citation-appender.js +63 -0
  14. package/dist/intelligence/citation-guard.js +108 -0
  15. package/dist/intelligence/evidence-analysis.js +79 -0
  16. package/dist/intelligence/intent-detect.js +93 -0
  17. package/dist/intelligence/keyword-extract.js +89 -0
  18. package/dist/intelligence/response-templates.js +117 -0
  19. package/dist/intelligence/types.js +0 -0
  20. package/dist/middleware/rate-limiter.js +110 -0
  21. package/dist/prompt/dynamic-layer.js +64 -0
  22. package/dist/prompt/prompt-builder.js +15 -0
  23. package/dist/prompt/semi-static-layer.js +28 -0
  24. package/dist/prompt/static-layer.js +153 -0
  25. package/dist/prompt/types.js +0 -0
  26. package/dist/provider-manager/base.js +53 -0
  27. package/dist/provider-manager/config.js +135 -0
  28. package/dist/provider-manager/index.js +19 -0
  29. package/dist/provider-manager/manager.js +122 -0
  30. package/dist/provider-manager/mock.js +77 -0
  31. package/dist/provider-manager/openai.js +106 -0
  32. package/dist/provider-manager/types.js +0 -0
  33. package/dist/provider-manager/workers.js +76 -0
  34. package/dist/providers/mock.js +227 -0
  35. package/dist/search/idf.js +24 -0
  36. package/dist/search/search-api.js +94 -0
  37. package/dist/search/search-index.js +32 -0
  38. package/dist/search/search-utils.js +81 -0
  39. package/dist/search/session-cache.js +96 -0
  40. package/dist/search/types.js +0 -0
  41. package/dist/search/vector-reranker.js +103 -0
  42. package/dist/server/chat-handler.js +603 -0
  43. package/dist/server/errors.js +46 -0
  44. package/dist/server/metadata-init.js +49 -0
  45. package/dist/server/notify.js +70 -0
  46. package/dist/server/stream-helpers.js +202 -0
  47. package/dist/server/types.js +16 -0
  48. package/dist/stream/mock-stream.js +26 -0
  49. package/dist/stream/response.js +21 -0
  50. package/dist/utils/i18n.js +154 -0
  51. package/package.json +3 -3
@@ -0,0 +1,122 @@
1
+ import { parseProviderConfigs, validateProviderConfig } from "./config.js";
2
+ import { OpenAIAdapter } from "./openai.js";
3
+ import { WorkersAIAdapter } from "./workers.js";
4
+ import { MockAdapter } from "./mock.js";
5
+ class ProviderManager {
6
+ providers = [];
7
+ mockAdapter;
8
+ options;
9
+ constructor(env, options) {
10
+ this.options = {
11
+ unhealthyThreshold: options?.unhealthyThreshold ?? 3,
12
+ healthRecoveryTTL: options?.healthRecoveryTTL ?? 6e4,
13
+ enableMockFallback: options?.enableMockFallback ?? true,
14
+ onProviderSwitch: options?.onProviderSwitch,
15
+ onStreamError: options?.onStreamError,
16
+ onHealthChange: options?.onHealthChange
17
+ };
18
+ this.mockAdapter = new MockAdapter();
19
+ this.initializeProviders(env);
20
+ }
21
+ initializeProviders(env) {
22
+ const configs = parseProviderConfigs(env);
23
+ for (const config of configs) {
24
+ if (config.enabled === false) continue;
25
+ const validationError = validateProviderConfig(config);
26
+ if (validationError) {
27
+ console.warn(`[ProviderManager] Skipping invalid config: ${validationError}`);
28
+ continue;
29
+ }
30
+ try {
31
+ const adapter = this.createAdapter(config, env);
32
+ if (adapter) {
33
+ this.providers.push(adapter);
34
+ }
35
+ } catch (error) {
36
+ console.warn(`[ProviderManager] Failed to create adapter for ${config.id}:`, error);
37
+ }
38
+ }
39
+ this.providers.sort((a, b) => b.weight - a.weight);
40
+ }
41
+ createAdapter(config, env) {
42
+ switch (config.type) {
43
+ case "openai":
44
+ return new OpenAIAdapter(config);
45
+ case "workers":
46
+ return new WorkersAIAdapter(config, env);
47
+ default:
48
+ return null;
49
+ }
50
+ }
51
+ async getAvailableProvider() {
52
+ for (const provider of this.providers) {
53
+ if (await provider.isAvailable()) {
54
+ return provider;
55
+ }
56
+ }
57
+ return null;
58
+ }
59
+ async streamText(options) {
60
+ let lastProviderId = null;
61
+ let lastError = null;
62
+ for (const provider of this.providers) {
63
+ const isAvailable = await provider.isAvailable();
64
+ if (!isAvailable) continue;
65
+ try {
66
+ const result = await provider.streamText(options);
67
+ provider.recordSuccess();
68
+ if (lastProviderId && lastProviderId !== provider.id) {
69
+ this.options.onProviderSwitch?.(lastProviderId, provider.id, "fallback success");
70
+ }
71
+ return result;
72
+ } catch (error) {
73
+ lastError = error instanceof Error ? error : new Error(String(error));
74
+ lastProviderId = provider.id;
75
+ provider.recordFailure(lastError);
76
+ this.options.onStreamError?.(provider.id, lastError);
77
+ if (!provider.getHealth().healthy) {
78
+ this.options.onHealthChange?.(provider.id, false);
79
+ }
80
+ }
81
+ }
82
+ if (this.options.enableMockFallback) {
83
+ this.options.onProviderSwitch?.(lastProviderId, "mock", "all providers failed");
84
+ return this.mockAdapter.streamText(options);
85
+ }
86
+ throw lastError || new Error("No providers available");
87
+ }
88
+ getProviderStatus() {
89
+ return this.providers.map((provider) => ({
90
+ id: provider.id,
91
+ type: provider.type,
92
+ weight: provider.weight,
93
+ enabled: true,
94
+ health: provider.getHealth(),
95
+ model: provider.model
96
+ }));
97
+ }
98
+ hasProviders() {
99
+ return this.providers.length > 0;
100
+ }
101
+ getProviderCount() {
102
+ return this.providers.length;
103
+ }
104
+ async getAvailableAdapter() {
105
+ return this.getAvailableProvider();
106
+ }
107
+ }
108
+ let managerInstance = null;
109
+ function getProviderManager(env, options) {
110
+ if (!managerInstance) {
111
+ managerInstance = new ProviderManager(env, options);
112
+ }
113
+ return managerInstance;
114
+ }
115
+ function resetProviderManager() {
116
+ managerInstance = null;
117
+ }
118
+ export {
119
+ ProviderManager,
120
+ getProviderManager,
121
+ resetProviderManager
122
+ };
@@ -0,0 +1,77 @@
1
+ import { BaseProviderAdapter } from "./base.js";
2
+ import { getMockResponse } from "../providers/mock.js";
3
+ const MOCK_WEIGHT = 0;
4
+ const CHAR_DELAY_MS = 15;
5
+ class MockAdapter extends BaseProviderAdapter {
6
+ id = "mock";
7
+ type = "mock";
8
+ weight = MOCK_WEIGHT;
9
+ model = "mock";
10
+ keywordModel = "mock";
11
+ evidenceModel = "mock";
12
+ timeout = 0;
13
+ constructor() {
14
+ super({ unhealthyThreshold: 999 });
15
+ }
16
+ async isAvailable() {
17
+ return true;
18
+ }
19
+ async streamText(options) {
20
+ const { userQuestion = "", lang = "zh" } = options;
21
+ const text = getMockResponse(userQuestion, lang);
22
+ const partId = `mock-${Date.now()}`;
23
+ const encoder = new TextEncoder();
24
+ const stream = new ReadableStream({
25
+ async start(controller) {
26
+ const write = (event) => controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}
27
+
28
+ `));
29
+ write({ type: "text-start", id: partId });
30
+ for (let i = 0; i < text.length; ) {
31
+ const chunkSize = Math.random() < 0.3 ? 3 : Math.random() < 0.5 ? 2 : 1;
32
+ const chunk = text.slice(i, i + chunkSize);
33
+ i += chunkSize;
34
+ controller.enqueue(
35
+ encoder.encode(
36
+ `data: ${JSON.stringify({ type: "text-delta", id: partId, delta: chunk })}
37
+
38
+ `
39
+ )
40
+ );
41
+ await new Promise((r) => setTimeout(r, CHAR_DELAY_MS + Math.random() * 20));
42
+ }
43
+ controller.enqueue(
44
+ encoder.encode(`data: ${JSON.stringify({ type: "text-end", id: partId })}
45
+
46
+ `)
47
+ );
48
+ controller.enqueue(
49
+ encoder.encode(
50
+ `data: ${JSON.stringify({ type: "finish", finishReason: "stop" })}
51
+
52
+ `
53
+ )
54
+ );
55
+ controller.close();
56
+ }
57
+ });
58
+ const response = new Response(stream, {
59
+ headers: {
60
+ "Content-Type": "text/event-stream; charset=utf-8",
61
+ "Cache-Control": "no-cache, no-store",
62
+ "Access-Control-Allow-Origin": "*"
63
+ }
64
+ });
65
+ return {
66
+ toUIMessageStreamResponse: () => response,
67
+ providerId: this.id,
68
+ isMock: true
69
+ };
70
+ }
71
+ getProvider() {
72
+ throw new Error("Mock provider does not support chatModel interface");
73
+ }
74
+ }
75
+ export {
76
+ MockAdapter
77
+ };
@@ -0,0 +1,106 @@
1
+ import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
2
+ import { streamText, convertToModelMessages } from "ai";
3
+ import { BaseProviderAdapter } from "./base.js";
4
+ let proxyInitialized = false;
5
+ async function setupGlobalProxy() {
6
+ if (proxyInitialized) return;
7
+ if (typeof process === "undefined" || !process.env) {
8
+ return;
9
+ }
10
+ const proxyUrl = process.env.https_proxy || process.env.HTTPS_PROXY || process.env.http_proxy || process.env.HTTP_PROXY;
11
+ if (!proxyUrl) {
12
+ return;
13
+ }
14
+ try {
15
+ const undici = await import("undici");
16
+ if (typeof undici.setGlobalDispatcher !== "function" || typeof undici.ProxyAgent !== "function") {
17
+ console.log("[OpenAIAdapter] undici APIs not available, skipping proxy setup (likely Edge Runtime)");
18
+ return;
19
+ }
20
+ undici.setGlobalDispatcher(new undici.ProxyAgent(proxyUrl));
21
+ console.log("[OpenAIAdapter] Global proxy dispatcher set:", proxyUrl);
22
+ proxyInitialized = true;
23
+ } catch (e) {
24
+ console.log("[OpenAIAdapter] Proxy setup skipped:", e instanceof Error ? e.message : String(e));
25
+ }
26
+ }
27
+ let proxySetupPromise = null;
28
+ function ensureProxySetup() {
29
+ if (!proxySetupPromise) {
30
+ proxySetupPromise = setupGlobalProxy();
31
+ }
32
+ return proxySetupPromise;
33
+ }
34
+ class OpenAIAdapter extends BaseProviderAdapter {
35
+ id;
36
+ type = "openai";
37
+ weight;
38
+ model;
39
+ keywordModel;
40
+ evidenceModel;
41
+ timeout;
42
+ provider;
43
+ config;
44
+ constructor(config) {
45
+ super({
46
+ unhealthyThreshold: config.maxRetries ? config.maxRetries + 2 : 3
47
+ });
48
+ this.id = config.id;
49
+ this.weight = config.weight ?? 100;
50
+ this.model = config.model;
51
+ this.keywordModel = config.keywordModel ?? config.model;
52
+ this.evidenceModel = config.evidenceModel ?? this.keywordModel;
53
+ this.timeout = config.timeout ?? 3e4;
54
+ this.config = config;
55
+ this.provider = createOpenAICompatible({
56
+ name: `openai-${config.id}`,
57
+ baseURL: config.baseURL,
58
+ apiKey: config.apiKey,
59
+ includeUsage: true
60
+ });
61
+ ensureProxySetup().catch(() => {
62
+ });
63
+ }
64
+ async streamText(options) {
65
+ await ensureProxySetup();
66
+ const { system, messages, temperature = 0.7, maxOutputTokens, topP, abortSignal, onError } = options;
67
+ const abortController = new AbortController();
68
+ const timeoutId = setTimeout(() => abortController.abort(), this.timeout);
69
+ if (abortSignal) {
70
+ abortSignal.addEventListener("abort", () => abortController.abort());
71
+ }
72
+ try {
73
+ const result = streamText({
74
+ model: this.provider.chatModel(this.model),
75
+ system,
76
+ messages: await convertToModelMessages(messages),
77
+ temperature,
78
+ maxOutputTokens,
79
+ topP,
80
+ abortSignal: abortController.signal,
81
+ onError: ({ error }) => {
82
+ onError?.(error instanceof Error ? error : new Error(String(error)));
83
+ }
84
+ });
85
+ const streamResult = {
86
+ toUIMessageStreamResponse: (responseOptions) => result.toUIMessageStreamResponse(responseOptions),
87
+ providerId: this.id,
88
+ isMock: false
89
+ };
90
+ clearTimeout(timeoutId);
91
+ return streamResult;
92
+ } catch (error) {
93
+ clearTimeout(timeoutId);
94
+ throw error;
95
+ }
96
+ }
97
+ getConfig() {
98
+ return { ...this.config };
99
+ }
100
+ getProvider() {
101
+ return this.provider;
102
+ }
103
+ }
104
+ export {
105
+ OpenAIAdapter
106
+ };
File without changes
@@ -0,0 +1,76 @@
1
+ import { createWorkersAI } from "workers-ai-provider";
2
+ import { streamText, convertToModelMessages } from "ai";
3
+ import { BaseProviderAdapter } from "./base.js";
4
+ class WorkersAIAdapter extends BaseProviderAdapter {
5
+ id;
6
+ type = "workers";
7
+ weight;
8
+ model;
9
+ keywordModel;
10
+ evidenceModel;
11
+ timeout;
12
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
13
+ provider;
14
+ config;
15
+ constructor(config, env) {
16
+ super({
17
+ unhealthyThreshold: config.maxRetries ? config.maxRetries + 2 : 3
18
+ });
19
+ this.id = config.id;
20
+ this.weight = config.weight ?? 90;
21
+ this.model = config.model;
22
+ this.keywordModel = config.keywordModel ?? config.model;
23
+ this.evidenceModel = config.evidenceModel ?? this.keywordModel;
24
+ this.timeout = config.timeout ?? 3e4;
25
+ this.config = config;
26
+ const binding = env[config.bindingName];
27
+ if (!binding) {
28
+ throw new Error(`Workers AI binding '${config.bindingName}' not found in environment`);
29
+ }
30
+ this.provider = createWorkersAI({ binding });
31
+ }
32
+ async streamText(options) {
33
+ const { system, messages, temperature = 0.7, maxOutputTokens, topP, abortSignal, onError } = options;
34
+ const abortController = new AbortController();
35
+ const timeoutId = setTimeout(() => abortController.abort(), this.timeout);
36
+ if (abortSignal) {
37
+ abortSignal.addEventListener("abort", () => abortController.abort());
38
+ }
39
+ try {
40
+ const model = this.provider(this.model, { safePrompt: true });
41
+ const result = streamText({
42
+ model,
43
+ system,
44
+ messages: await convertToModelMessages(messages),
45
+ temperature,
46
+ maxOutputTokens,
47
+ topP,
48
+ abortSignal: abortController.signal,
49
+ onError: ({ error }) => {
50
+ onError?.(error instanceof Error ? error : new Error(String(error)));
51
+ }
52
+ });
53
+ const streamResult = {
54
+ toUIMessageStreamResponse: (responseOptions) => result.toUIMessageStreamResponse(responseOptions),
55
+ providerId: this.id,
56
+ isMock: false
57
+ };
58
+ clearTimeout(timeoutId);
59
+ return streamResult;
60
+ } catch (error) {
61
+ clearTimeout(timeoutId);
62
+ throw error;
63
+ }
64
+ }
65
+ getConfig() {
66
+ return { ...this.config };
67
+ }
68
+ getProvider() {
69
+ return {
70
+ chatModel: (modelId) => this.provider(modelId, { safePrompt: true })
71
+ };
72
+ }
73
+ }
74
+ export {
75
+ WorkersAIAdapter
76
+ };
@@ -0,0 +1,227 @@
1
+ const MOCK_RESPONSES = [
2
+ {
3
+ patterns: [/astro/i, /框架/],
4
+ zh: `Astro \u662F\u4E00\u4E2A\u73B0\u4EE3\u5316\u7684\u9759\u6001\u7AD9\u70B9\u751F\u6210\u5668\uFF0C\u6838\u5FC3\u4F18\u52BF\u662F"\u5C9B\u5C7F\u67B6\u6784"\u2014\u2014\u9ED8\u8BA4\u96F6 JS\uFF0C\u53EA\u5728\u4EA4\u4E92\u7EC4\u4EF6\u4E0A\u52A0\u8F7D\u811A\u672C\u3002\u672C\u535A\u5BA2\u57FA\u4E8E Astro \u6784\u5EFA\u3002
5
+
6
+ \u63A8\u8350\u9605\u8BFB\uFF1A
7
+ - [\u5FEB\u901F\u4E0A\u624B\uFF1A\u4E24\u79CD\u96C6\u6210\u65B9\u5F0F](/zh/posts/getting-started) \u2014 \u4E86\u89E3\u5982\u4F55\u642D\u5EFA astro-minimax \u535A\u5BA2
8
+ - [\u5982\u4F55\u914D\u7F6E\u4E3B\u9898](/zh/posts/how-to-configure-astro-minimax-theme) \u2014 \u81EA\u5B9A\u4E49\u4F60\u7684\u535A\u5BA2\u5916\u89C2
9
+
10
+ \u5916\u90E8\u8D44\u6E90\uFF1A
11
+ - [Astro \u5B98\u65B9\u6587\u6863](https://docs.astro.build) \u2014 \u6DF1\u5165\u5B66\u4E60 Astro \u6846\u67B6
12
+ - [Astro \u4E3B\u9898\u5E02\u573A](https://astro.build/themes/) \u2014 \u53D1\u73B0\u66F4\u591A Astro \u4E3B\u9898`,
13
+ en: `Astro is a modern static site generator with an "Islands Architecture" \u2014 zero JS by default, loading scripts only for interactive components. This blog is built with Astro.
14
+
15
+ Recommended reading:
16
+ - [Getting Started: Two Integration Methods](/en/posts/getting-started) \u2014 Learn how to set up an astro-minimax blog
17
+ - [How to Configure the Theme](/en/posts/how-to-configure-astro-minimax-theme) \u2014 Customize your blog
18
+
19
+ External resources:
20
+ - [Astro Documentation](https://docs.astro.build) \u2014 Learn Astro in depth
21
+ - [Astro Themes](https://astro.build/themes/) \u2014 Discover more themes`
22
+ },
23
+ {
24
+ patterns: [/推荐|文章|看什么|读什么|recommend/i],
25
+ zh: `\u4EE5\u4E0B\u662F\u4E00\u4E9B\u70ED\u95E8\u6587\u7AE0\u63A8\u8350\uFF1A
26
+
27
+ **\u5165\u95E8\u7CFB\u5217\uFF1A**
28
+ - [\u5FEB\u901F\u4E0A\u624B\uFF1A\u4E24\u79CD\u96C6\u6210\u65B9\u5F0F](/zh/posts/getting-started) \u2014 \u642D\u5EFA\u4F60\u7684\u7B2C\u4E00\u4E2A\u535A\u5BA2
29
+ - [\u5982\u4F55\u6DFB\u52A0\u65B0\u6587\u7AE0](/zh/posts/adding-new-post) \u2014 \u5185\u5BB9\u521B\u4F5C\u6307\u5357
30
+ - [\u9884\u5B9A\u4E49\u914D\u8272\u65B9\u6848](/zh/posts/predefined-color-schemes) \u2014 \u9009\u4E00\u4E2A\u4F60\u559C\u6B22\u7684\u4E3B\u9898\u8272
31
+
32
+ **\u6280\u672F\u6DF1\u5EA6\uFF1A**
33
+ - [\u5982\u4F55\u5728\u535A\u5BA2\u4E2D\u4F7F\u7528 LaTeX \u516C\u5F0F](/zh/posts/how-to-add-latex-equations-in-blog-posts) \u2014 \u6570\u5B66\u516C\u5F0F\u652F\u6301
34
+ - [\u52A8\u6001 OG \u56FE\u7247\u751F\u6210](/zh/posts/dynamic-og-images) \u2014 \u81EA\u52A8\u751F\u6210\u793E\u4EA4\u5206\u4EAB\u56FE
35
+
36
+ \u4F60\u5BF9\u54EA\u4E2A\u65B9\u5411\u7684\u5185\u5BB9\u66F4\u611F\u5174\u8DA3\uFF1F\u6211\u53EF\u4EE5\u505A\u66F4\u7CBE\u51C6\u7684\u63A8\u8350\u3002`,
37
+ en: `Here are some recommended articles:
38
+
39
+ **Getting Started:**
40
+ - [Getting Started: Two Integration Methods](/en/posts/getting-started) \u2014 Build your first blog
41
+ - [Adding New Posts](/en/posts/adding-new-post) \u2014 Content creation guide
42
+ - [Predefined Color Schemes](/en/posts/predefined-color-schemes) \u2014 Pick your favorite theme color
43
+
44
+ **Technical Deep Dives:**
45
+ - [LaTeX Equations in Blog Posts](/en/posts/how-to-add-latex-equations-in-blog-posts) \u2014 Math formula support
46
+ - [Dynamic OG Images](/en/posts/dynamic-og-images) \u2014 Auto-generate social share images
47
+
48
+ What direction interests you more? I can provide more specific recommendations.`
49
+ },
50
+ {
51
+ patterns: [/博客|blog|功能|feature/i],
52
+ zh: `\u8FD9\u4E2A\u535A\u5BA2\u57FA\u4E8E **astro-minimax** \u4E3B\u9898\uFF0C\u529F\u80FD\u4E30\u5BCC\uFF1A
53
+
54
+ \u6838\u5FC3\u529F\u80FD\uFF1AMarkdown/MDX\u3001\u4EE3\u7801\u9AD8\u4EAE\u3001[\u6570\u5B66\u516C\u5F0F(KaTeX)](/zh/posts/how-to-add-latex-equations-in-blog-posts)\u3001[Mermaid \u56FE\u8868](/zh/posts/mermaid-diagrams)\u3001\u6807\u7B7E\u5206\u7C7B\u3001\u5168\u6587\u641C\u7D22(Pagefind)\u3001[Waline \u8BC4\u8BBA](https://waline.js.org)\u3001\u6DF1\u8272\u6A21\u5F0F\u3002
55
+
56
+ \u4E86\u89E3\u66F4\u591A\uFF1A
57
+ - [\u914D\u7F6E\u6307\u5357](/zh/posts/how-to-configure-astro-minimax-theme) \u2014 \u5B8C\u6574\u914D\u7F6E\u9009\u9879
58
+ - [Markdown \u6269\u5C55\u8BED\u6CD5](/zh/posts/markdown-extended) \u2014 \u6240\u6709\u652F\u6301\u7684\u8BED\u6CD5\u7279\u6027
59
+
60
+ \u5F00\u6E90\u5730\u5740\uFF1A[souloss/astro-minimax](https://github.com/souloss/astro-minimax)`,
61
+ en: `This blog uses the **astro-minimax** theme with rich features:
62
+
63
+ 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.
64
+
65
+ Learn more:
66
+ - [Configuration Guide](/en/posts/how-to-configure-astro-minimax-theme) \u2014 Full config options
67
+ - [Extended Markdown](/en/posts/markdown-extended) \u2014 All supported syntax features
68
+
69
+ Open source: [souloss/astro-minimax](https://github.com/souloss/astro-minimax)`
70
+ },
71
+ {
72
+ patterns: [/主题|theme|暗色|dark|颜色|color|配色/i],
73
+ zh: `\u535A\u5BA2\u652F\u6301\u4EAE\u8272\u548C\u6697\u8272\u4E3B\u9898\uFF0C\u53F3\u4E0B\u89D2\u6309\u94AE\u5373\u53EF\u5207\u6362\uFF0C\u4E5F\u4F1A\u81EA\u52A8\u68C0\u6D4B\u7CFB\u7EDF\u504F\u597D\u3002
74
+
75
+ \u914D\u8272\u65B9\u6848\u53EF\u4EE5\u5728\u914D\u7F6E\u4E2D\u81EA\u5B9A\u4E49\uFF0C\u76EE\u524D\u63D0\u4F9B\u591A\u79CD\u9884\u8BBE\uFF1A
76
+ - [\u9884\u5B9A\u4E49\u914D\u8272\u65B9\u6848](/zh/posts/predefined-color-schemes) \u2014 \u67E5\u770B\u6240\u6709\u53EF\u7528\u914D\u8272
77
+ - [\u4E3B\u9898\u914D\u7F6E\u6307\u5357](/zh/posts/how-to-configure-astro-minimax-theme) \u2014 \u521B\u5EFA\u4F60\u81EA\u5DF1\u7684\u914D\u8272
78
+
79
+ \u53C2\u8003 [Tailwind CSS \u8C03\u8272\u677F](https://tailwindcss.com/docs/customizing-colors) \u83B7\u53D6\u7075\u611F\u3002`,
80
+ en: `The blog supports light and dark themes \u2014 toggle with the bottom-right button or auto-detect system preference.
81
+
82
+ Color schemes are customizable:
83
+ - [Predefined Color Schemes](/en/posts/predefined-color-schemes) \u2014 See all available schemes
84
+ - [Theme Configuration Guide](/en/posts/how-to-configure-astro-minimax-theme) \u2014 Create your own
85
+
86
+ Check [Tailwind CSS Color Palette](https://tailwindcss.com/docs/customizing-colors) for inspiration.`
87
+ },
88
+ {
89
+ patterns: [/搭建|部署|deploy|build|install|安装|搭/i],
90
+ zh: `\u642D\u5EFA\u7C7B\u4F3C\u7684\u535A\u5BA2\u975E\u5E38\u7B80\u5355\uFF01\u6709\u4E24\u79CD\u65B9\u5F0F\uFF1A
91
+
92
+ 1. **GitHub \u6A21\u677F**\uFF08\u63A8\u8350\u65B0\u624B\uFF09\u2014 \u4E00\u952E Fork\uFF0C\u5F00\u7BB1\u5373\u7528
93
+ 2. **NPM \u5305\u96C6\u6210** \u2014 \u9002\u5408\u5185\u5BB9\u4E0E\u7CFB\u7EDF\u5206\u79BB\u7684\u8FDB\u9636\u7528\u6CD5
94
+
95
+ \u8BE6\u7EC6\u6B65\u9AA4\u8BF7\u770B [\u5FEB\u901F\u4E0A\u624B](/zh/posts/getting-started)\u3002
96
+
97
+ \u90E8\u7F72\u63A8\u8350 [Cloudflare Pages](https://pages.cloudflare.com)\uFF08\u514D\u8D39\u3001\u5168\u7403 CDN\uFF09\uFF0C\u4E5F\u652F\u6301 [Vercel](https://vercel.com) \u548C [Netlify](https://netlify.com)\u3002`,
98
+ en: `Setting up a similar blog is easy! Two methods:
99
+
100
+ 1. **GitHub Template** (recommended for beginners) \u2014 One-click fork, ready to use
101
+ 2. **NPM Package Integration** \u2014 For advanced content/system separation
102
+
103
+ See [Getting Started](/en/posts/getting-started) for detailed steps.
104
+
105
+ Deploy with [Cloudflare Pages](https://pages.cloudflare.com) (free, global CDN), or [Vercel](https://vercel.com) / [Netlify](https://netlify.com).`
106
+ },
107
+ {
108
+ patterns: [/rust/i],
109
+ zh: `\u535A\u5BA2\u4E2D\u6709\u4E00\u7CFB\u5217 Rust \u6587\u7AE0\uFF1A
110
+ - [Rust \u5165\u95E8\u4ECB\u7ECD](/zh/posts/rust-series-01-introduction) \u2014 \u8BED\u8A00\u57FA\u7840
111
+ - [\u6240\u6709\u6743\u7CFB\u7EDF](/zh/posts/rust-series-02-ownership) \u2014 Rust \u6838\u5FC3\u6982\u5FF5
112
+ - [\u9519\u8BEF\u5904\u7406](/zh/posts/rust-series-03-error-handling) \u2014 Result \u548C Option
113
+ - [\u5E76\u53D1\u7F16\u7A0B](/zh/posts/rust-series-04-concurrency) \u2014 \u5B89\u5168\u7684\u591A\u7EBF\u7A0B
114
+
115
+ \u5916\u90E8\u5B66\u4E60\u8D44\u6E90\uFF1A
116
+ - [The Rust Book](https://doc.rust-lang.org/book/) \u2014 \u5B98\u65B9\u6559\u7A0B
117
+ - [Rust by Example](https://doc.rust-lang.org/rust-by-example/) \u2014 \u5B9E\u4F8B\u5B66\u4E60`,
118
+ en: `The blog has a Rust series:
119
+ - [Rust Introduction](/en/posts/rust-series-01-introduction) \u2014 Language basics
120
+ - [Ownership System](/en/posts/rust-series-02-ownership) \u2014 Core Rust concept
121
+ - [Error Handling](/en/posts/rust-series-03-error-handling) \u2014 Result and Option
122
+ - [Concurrency](/en/posts/rust-series-04-concurrency) \u2014 Safe multithreading
123
+
124
+ External resources:
125
+ - [The Rust Book](https://doc.rust-lang.org/book/) \u2014 Official tutorial
126
+ - [Rust by Example](https://doc.rust-lang.org/rust-by-example/) \u2014 Learn by examples`
127
+ },
128
+ {
129
+ patterns: [/ai|人工智能|助手|assistant|chat/i],
130
+ zh: `\u6211\u662F\u8FD9\u4E2A\u535A\u5BA2\u7684 AI \u52A9\u624B\uFF01\u5F53\u524D\u8FD0\u884C\u5728 Demo \u6A21\u5F0F\uFF0C\u53EF\u4EE5\uFF1A
131
+ - \u6839\u636E\u4F60\u7684\u95EE\u9898\u63A8\u8350\u76F8\u5173\u535A\u5BA2\u6587\u7AE0
132
+ - \u63A8\u8350\u6709\u7528\u7684\u5916\u90E8\u5B66\u4E60\u8D44\u6E90
133
+ - \u89E3\u7B54\u5173\u4E8E\u535A\u5BA2\u6280\u672F\u6808\u7684\u95EE\u9898
134
+
135
+ \u542F\u7528\u5B8C\u6574 AI \u529F\u80FD\uFF08RAG \u641C\u7D22\u589E\u5F3A\uFF09\u9700\u8981\u914D\u7F6E \`AI_BASE_URL\` \u548C \`AI_API_KEY\` \u73AF\u5883\u53D8\u91CF\u3002
136
+
137
+ \u8BD5\u8BD5\u95EE\u6211\uFF1A"\u6709\u54EA\u4E9B\u6587\u7AE0\u63A8\u8350\uFF1F" \u6216 "\u600E\u4E48\u642D\u5EFA\u7C7B\u4F3C\u7684\u535A\u5BA2\uFF1F"`,
138
+ en: `I'm the blog AI assistant! Currently in Demo mode, I can:
139
+ - Recommend relevant blog articles based on your questions
140
+ - Suggest useful external learning resources
141
+ - Answer questions about the blog's tech stack
142
+
143
+ For full AI features (RAG search enhancement), configure \`AI_BASE_URL\` and \`AI_API_KEY\` environment variables.
144
+
145
+ Try asking: "Recommend some articles?" or "How to build a similar blog?"`
146
+ },
147
+ {
148
+ patterns: [/搜索|search|pagefind/i],
149
+ zh: `\u535A\u5BA2\u96C6\u6210\u4E86 [Pagefind](https://pagefind.app) \u5168\u6587\u641C\u7D22\u5F15\u64CE\uFF0C\u6784\u5EFA\u65F6\u81EA\u52A8\u7D22\u5F15\u3002\u70B9\u51FB\u9875\u9762\u9876\u90E8\u641C\u7D22\u56FE\u6807\u5373\u53EF\u4F7F\u7528\u3002
150
+
151
+ \u4E86\u89E3\u66F4\u591A\u641C\u7D22\u529F\u80FD\uFF1A
152
+ - [Pagefind \u5B98\u65B9\u6587\u6863](https://pagefind.app/docs/) \u2014 \u5B8C\u6574\u914D\u7F6E\u6307\u5357
153
+ - \u641C\u7D22\u652F\u6301\u4E2D\u6587\u548C\u82F1\u6587\u5185\u5BB9`,
154
+ 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.
155
+
156
+ Learn more:
157
+ - [Pagefind Documentation](https://pagefind.app/docs/) \u2014 Complete configuration guide
158
+ - Search supports both Chinese and English content`
159
+ },
160
+ {
161
+ patterns: [/markdown|mdx|语法|syntax|公式|latex|mermaid|图表/i],
162
+ zh: `\u535A\u5BA2\u652F\u6301\u4E30\u5BCC\u7684\u5185\u5BB9\u8BED\u6CD5\uFF1A
163
+
164
+ - [Markdown \u57FA\u7840\u8BED\u6CD5](/zh/posts/markdown-basics) \u2014 \u6807\u9898\u3001\u5217\u8868\u3001\u8868\u683C\u7B49
165
+ - [Markdown \u6269\u5C55\u8BED\u6CD5](/zh/posts/markdown-extended) \u2014 \u811A\u6CE8\u3001\u9AD8\u4EAE\u3001\u6298\u53E0\u7B49
166
+ - [LaTeX \u6570\u5B66\u516C\u5F0F](/zh/posts/how-to-add-latex-equations-in-blog-posts) \u2014 KaTeX \u6E32\u67D3
167
+ - [Mermaid \u56FE\u8868](/zh/posts/mermaid-diagrams) \u2014 \u6D41\u7A0B\u56FE\u3001\u65F6\u5E8F\u56FE
168
+ - [Markmap \u601D\u7EF4\u5BFC\u56FE](/zh/posts/markmap-mindmaps) \u2014 \u4EA4\u4E92\u5F0F\u601D\u7EF4\u5BFC\u56FE
169
+
170
+ \u5916\u90E8\u53C2\u8003\uFF1A[GitHub Flavored Markdown](https://github.github.com/gfm/)`,
171
+ en: `The blog supports rich content syntax:
172
+
173
+ - [Markdown Basics](/en/posts/markdown-basics) \u2014 Headings, lists, tables
174
+ - [Extended Markdown](/en/posts/markdown-extended) \u2014 Footnotes, highlights, collapsible
175
+ - [LaTeX Equations](/en/posts/how-to-add-latex-equations-in-blog-posts) \u2014 KaTeX rendering
176
+ - [Mermaid Diagrams](/en/posts/mermaid-diagrams) \u2014 Flowcharts, sequence diagrams
177
+ - [Markmap Mind Maps](/en/posts/markmap-mindmaps) \u2014 Interactive mind maps
178
+
179
+ Reference: [GitHub Flavored Markdown](https://github.github.com/gfm/)`
180
+ }
181
+ ];
182
+ const FALLBACK = {
183
+ zh: `\u611F\u8C22\u63D0\u95EE\uFF01\u6211\u76EE\u524D\u5728 Demo \u6A21\u5F0F\u4E0B\uFF0C\u53EF\u4EE5\u63A8\u8350\u535A\u5BA2\u6587\u7AE0\u548C\u5916\u90E8\u8D44\u6E90\u3002
184
+
185
+ \u8BD5\u8BD5\u8FD9\u4E9B\u8BDD\u9898\uFF1A
186
+ - "\u6709\u54EA\u4E9B\u6587\u7AE0\u63A8\u8350\uFF1F"
187
+ - "Astro \u6846\u67B6\u662F\u4EC0\u4E48\uFF1F"
188
+ - "\u600E\u4E48\u642D\u5EFA\u7C7B\u4F3C\u7684\u535A\u5BA2\uFF1F"
189
+ - "\u652F\u6301\u54EA\u4E9B Markdown \u8BED\u6CD5\uFF1F"`,
190
+ en: `Thanks for asking! I'm in Demo mode and can recommend blog articles and external resources.
191
+
192
+ Try these topics:
193
+ - "Recommend some articles?"
194
+ - "What is Astro?"
195
+ - "How to build a similar blog?"
196
+ - "What Markdown syntax is supported?"`
197
+ };
198
+ function getMockResponse(question, lang = "zh") {
199
+ const q = question.toLowerCase();
200
+ const isZh = lang !== "en";
201
+ for (const { patterns, zh, en } of MOCK_RESPONSES) {
202
+ if (patterns.some((p) => p.test(q))) {
203
+ return isZh ? zh : en;
204
+ }
205
+ }
206
+ return isZh ? FALLBACK.zh : FALLBACK.en;
207
+ }
208
+ function createMockStream(text) {
209
+ let index = 0;
210
+ return new ReadableStream({
211
+ async pull(controller) {
212
+ if (index >= text.length) {
213
+ controller.close();
214
+ return;
215
+ }
216
+ const chunkSize = Math.random() < 0.3 ? 2 : 1;
217
+ const chunk = text.slice(index, index + chunkSize);
218
+ index += chunkSize;
219
+ controller.enqueue(chunk);
220
+ await new Promise((resolve) => setTimeout(resolve, 12 + Math.random() * 23));
221
+ }
222
+ });
223
+ }
224
+ export {
225
+ createMockStream,
226
+ getMockResponse
227
+ };
@@ -0,0 +1,24 @@
1
+ function buildIDFMap(documents) {
2
+ const N = documents.length;
3
+ if (N === 0) return { weights: /* @__PURE__ */ new Map(), docCount: 0 };
4
+ const df = /* @__PURE__ */ new Map();
5
+ for (const doc of documents) {
6
+ const uniqueTokens = new Set(doc.tokens);
7
+ for (const token of uniqueTokens) {
8
+ df.set(token, (df.get(token) || 0) + 1);
9
+ }
10
+ }
11
+ const weights = /* @__PURE__ */ new Map();
12
+ for (const [term, count] of df) {
13
+ weights.set(term, Math.log(N / (count + 1)) + 1);
14
+ }
15
+ return { weights, docCount: N };
16
+ }
17
+ function getIDFWeight(idfMap, token) {
18
+ if (!idfMap) return 1;
19
+ return idfMap.weights.get(token) ?? Math.log(idfMap.docCount + 1) + 1;
20
+ }
21
+ export {
22
+ buildIDFMap,
23
+ getIDFWeight
24
+ };