@astro-minimax/ai 0.8.3 → 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.
- package/dist/cache/global-cache.js +145 -0
- package/dist/cache/index.js +96 -0
- package/dist/cache/kv-adapter.js +99 -0
- package/dist/cache/memory-adapter.js +97 -0
- package/dist/cache/response-cache.js +87 -0
- package/dist/cache/types.js +8 -0
- package/dist/data/metadata-loader.js +48 -0
- package/dist/data/types.js +0 -0
- package/dist/fact-registry/fact-matcher.js +128 -0
- package/dist/fact-registry/prompt-injector.js +54 -0
- package/dist/fact-registry/registry.js +41 -0
- package/dist/fact-registry/types.js +0 -0
- package/dist/intelligence/citation-appender.js +63 -0
- package/dist/intelligence/citation-guard.js +108 -0
- package/dist/intelligence/evidence-analysis.js +79 -0
- package/dist/intelligence/intent-detect.js +93 -0
- package/dist/intelligence/keyword-extract.js +89 -0
- package/dist/intelligence/response-templates.js +117 -0
- package/dist/intelligence/types.js +0 -0
- package/dist/middleware/rate-limiter.js +110 -0
- package/dist/prompt/dynamic-layer.js +64 -0
- package/dist/prompt/prompt-builder.js +15 -0
- package/dist/prompt/semi-static-layer.js +28 -0
- package/dist/prompt/static-layer.js +153 -0
- package/dist/prompt/types.js +0 -0
- package/dist/provider-manager/base.js +53 -0
- package/dist/provider-manager/config.js +135 -0
- package/dist/provider-manager/index.js +19 -0
- package/dist/provider-manager/manager.js +122 -0
- package/dist/provider-manager/mock.js +77 -0
- package/dist/provider-manager/openai.js +106 -0
- package/dist/provider-manager/types.js +0 -0
- package/dist/provider-manager/workers.js +76 -0
- package/dist/providers/mock.js +227 -0
- package/dist/search/idf.js +24 -0
- package/dist/search/search-api.js +94 -0
- package/dist/search/search-index.js +32 -0
- package/dist/search/search-utils.js +81 -0
- package/dist/search/session-cache.js +96 -0
- package/dist/search/types.js +0 -0
- package/dist/search/vector-reranker.js +103 -0
- package/dist/server/chat-handler.js +603 -0
- package/dist/server/errors.js +46 -0
- package/dist/server/metadata-init.js +49 -0
- package/dist/server/notify.js +70 -0
- package/dist/server/stream-helpers.js +202 -0
- package/dist/server/types.js +16 -0
- package/dist/stream/mock-stream.js +26 -0
- package/dist/stream/response.js +21 -0
- package/dist/utils/i18n.js +154 -0
- 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
|
+
};
|