@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.
- 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,603 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createUIMessageStream,
|
|
3
|
+
createUIMessageStreamResponse,
|
|
4
|
+
streamText,
|
|
5
|
+
convertToModelMessages
|
|
6
|
+
} from "ai";
|
|
7
|
+
import { t, getLang } from "../utils/i18n.js";
|
|
8
|
+
import {
|
|
9
|
+
getClientIP,
|
|
10
|
+
checkRateLimit,
|
|
11
|
+
rateLimitResponse,
|
|
12
|
+
searchArticles,
|
|
13
|
+
searchProjects,
|
|
14
|
+
getSessionCacheKey,
|
|
15
|
+
getCachedContext,
|
|
16
|
+
setCachedContext,
|
|
17
|
+
shouldReuseSearchContext,
|
|
18
|
+
buildLocalSearchQuery,
|
|
19
|
+
shouldRunKeywordExtraction,
|
|
20
|
+
extractSearchKeywords,
|
|
21
|
+
KEYWORD_EXTRACTION_TIMEOUT_MS,
|
|
22
|
+
shouldSkipAnalysis,
|
|
23
|
+
analyzeRetrievedEvidence,
|
|
24
|
+
buildEvidenceSection,
|
|
25
|
+
EVIDENCE_ANALYSIS_TIMEOUT_MS,
|
|
26
|
+
getCitationGuardPreflight,
|
|
27
|
+
buildSystemPrompt,
|
|
28
|
+
getAuthorContext,
|
|
29
|
+
getVoiceProfile,
|
|
30
|
+
mergeResults,
|
|
31
|
+
getProviderManager,
|
|
32
|
+
createCacheAdapter,
|
|
33
|
+
detectPublicQuestion,
|
|
34
|
+
getGlobalSearchCache,
|
|
35
|
+
shouldAppendCitations,
|
|
36
|
+
formatCitationBlock,
|
|
37
|
+
selectCitations,
|
|
38
|
+
setGlobalSearchCache,
|
|
39
|
+
getGlobalCacheTTL,
|
|
40
|
+
getResponseCache,
|
|
41
|
+
setResponseCache,
|
|
42
|
+
getResponseCacheConfig,
|
|
43
|
+
rankArticlesByIntent,
|
|
44
|
+
matchFactsToQuery,
|
|
45
|
+
buildFactSection
|
|
46
|
+
} from "../index.js";
|
|
47
|
+
import { createChatStatusData } from "./types.js";
|
|
48
|
+
import { errors, corsPreflightResponse } from "./errors.js";
|
|
49
|
+
import { notifyAiChat } from "./notify.js";
|
|
50
|
+
import {
|
|
51
|
+
writeSearchStatus,
|
|
52
|
+
writeGeneratingStatus,
|
|
53
|
+
writeSourceArticles,
|
|
54
|
+
streamLLMResponse,
|
|
55
|
+
streamMockFallback,
|
|
56
|
+
streamCachedResponse
|
|
57
|
+
} from "./stream-helpers.js";
|
|
58
|
+
const MAX_HISTORY_MESSAGES = 20;
|
|
59
|
+
const MAX_INPUT_LENGTH = 500;
|
|
60
|
+
const REQUEST_TIMEOUT_MS = 45e3;
|
|
61
|
+
function sendNotification(args) {
|
|
62
|
+
const { env, messages, responseText, relatedArticles, model, usage, timing, cacheKey, waitUntil } = args;
|
|
63
|
+
const sessionId = cacheKey || `dev-${Date.now().toString(36)}`;
|
|
64
|
+
const notifyArticles = relatedArticles.slice(0, 5).map((a) => ({
|
|
65
|
+
title: a.title,
|
|
66
|
+
url: a.url
|
|
67
|
+
}));
|
|
68
|
+
const notifyPromise = notifyAiChat({
|
|
69
|
+
env,
|
|
70
|
+
sessionId,
|
|
71
|
+
messages,
|
|
72
|
+
aiResponse: responseText,
|
|
73
|
+
referencedArticles: notifyArticles,
|
|
74
|
+
model,
|
|
75
|
+
usage,
|
|
76
|
+
timing
|
|
77
|
+
});
|
|
78
|
+
if (waitUntil) {
|
|
79
|
+
waitUntil(notifyPromise);
|
|
80
|
+
} else {
|
|
81
|
+
void notifyPromise;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function getMessageText(message) {
|
|
85
|
+
if (Array.isArray(message.parts)) {
|
|
86
|
+
return message.parts.filter((p) => p.type === "text").map((p) => p.text).join("");
|
|
87
|
+
}
|
|
88
|
+
return "";
|
|
89
|
+
}
|
|
90
|
+
function hasContent(message) {
|
|
91
|
+
const text = getMessageText(message);
|
|
92
|
+
if (text.trim()) return true;
|
|
93
|
+
if (Array.isArray(message.parts)) {
|
|
94
|
+
return message.parts.some((p) => p.type !== "text");
|
|
95
|
+
}
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
function filterValidMessages(messages) {
|
|
99
|
+
const filtered = [];
|
|
100
|
+
let lastRole = null;
|
|
101
|
+
for (const msg of messages) {
|
|
102
|
+
if (!hasContent(msg)) continue;
|
|
103
|
+
if (msg.role === lastRole) continue;
|
|
104
|
+
filtered.push(msg);
|
|
105
|
+
lastRole = msg.role;
|
|
106
|
+
}
|
|
107
|
+
if (filtered.length > 0 && filtered[filtered.length - 1].role !== "user") {
|
|
108
|
+
filtered.pop();
|
|
109
|
+
}
|
|
110
|
+
return filtered;
|
|
111
|
+
}
|
|
112
|
+
function buildArticleContextPrompt(context) {
|
|
113
|
+
if (context.scope !== "article" || !context.article) return "";
|
|
114
|
+
const a = context.article;
|
|
115
|
+
const parts = [
|
|
116
|
+
"\n[\u5F53\u524D\u9605\u8BFB\u6587\u7AE0]",
|
|
117
|
+
`\u7528\u6237\u6B63\u5728\u9605\u8BFB\uFF1A\u300A${a.title}\u300B`
|
|
118
|
+
];
|
|
119
|
+
if (a.summary) parts.push(`\u6458\u8981\uFF1A${a.summary}`);
|
|
120
|
+
if (a.abstract) parts.push(`\u8BE6\u7EC6\u6982\u8981\uFF1A${a.abstract}`);
|
|
121
|
+
if (a.keyPoints?.length) parts.push(`\u6838\u5FC3\u8981\u70B9\uFF1A${a.keyPoints.join("\uFF1B")}`);
|
|
122
|
+
if (a.categories?.length) parts.push(`\u5206\u7C7B\uFF1A${a.categories.join("\u3001")}`);
|
|
123
|
+
parts.push(
|
|
124
|
+
"",
|
|
125
|
+
"\u4F60\u6B63\u5728\u966A\u7528\u6237\u9605\u8BFB\u8FD9\u7BC7\u6587\u7AE0\u3002\u4F18\u5148\u56F4\u7ED5\u8FD9\u7BC7\u6587\u7AE0\u7684\u5185\u5BB9\u56DE\u7B54\u95EE\u9898\u3002",
|
|
126
|
+
"\u5F53\u7528\u6237\u7684\u95EE\u9898\u4E0E\u5F53\u524D\u6587\u7AE0\u76F8\u5173\u65F6\uFF0C\u5F15\u7528\u6587\u7AE0\u4E2D\u7684\u5177\u4F53\u5185\u5BB9\u3002",
|
|
127
|
+
"\u5F53\u7528\u6237\u60F3\u8981\u5EF6\u4F38\u65F6\uFF0C\u63A8\u8350\u76F8\u5173\u7684\u535A\u5BA2\u6587\u7AE0\u3002"
|
|
128
|
+
);
|
|
129
|
+
return parts.join("\n");
|
|
130
|
+
}
|
|
131
|
+
async function handleChatRequest(options) {
|
|
132
|
+
const { env, request: req, waitUntil } = options;
|
|
133
|
+
if (req.method === "OPTIONS") return corsPreflightResponse();
|
|
134
|
+
if (req.method !== "POST") return errors.methodNotAllowed("zh");
|
|
135
|
+
const ip = getClientIP(req);
|
|
136
|
+
const rateCheck = checkRateLimit(ip, env);
|
|
137
|
+
if (!rateCheck.allowed) return rateLimitResponse(rateCheck, "zh");
|
|
138
|
+
let body;
|
|
139
|
+
try {
|
|
140
|
+
body = await req.json();
|
|
141
|
+
} catch {
|
|
142
|
+
return errors.invalidRequest(t("ai.error.format", "zh"));
|
|
143
|
+
}
|
|
144
|
+
const lang = getLang(body.lang ?? env.SITE_LANG);
|
|
145
|
+
const context = body.context ?? { scope: "global" };
|
|
146
|
+
const rawMessages = (body.messages ?? []).slice(-MAX_HISTORY_MESSAGES);
|
|
147
|
+
if (!rawMessages.length) return errors.emptyMessage(lang);
|
|
148
|
+
const messages = filterValidMessages(rawMessages);
|
|
149
|
+
if (!messages.length) return errors.emptyMessage(lang);
|
|
150
|
+
const latestMessage = messages[messages.length - 1];
|
|
151
|
+
const latestText = getMessageText(latestMessage);
|
|
152
|
+
if (!latestText) return errors.emptyContent(lang);
|
|
153
|
+
if (latestText.length > MAX_INPUT_LENGTH) return errors.inputTooLong(MAX_INPUT_LENGTH, lang);
|
|
154
|
+
const requestAbort = new AbortController();
|
|
155
|
+
const requestTimer = setTimeout(() => requestAbort.abort(), REQUEST_TIMEOUT_MS);
|
|
156
|
+
try {
|
|
157
|
+
return await runPipeline({ env, messages, latestText, context, req, requestAbort, lang, waitUntil });
|
|
158
|
+
} catch (err) {
|
|
159
|
+
if (requestAbort.signal.aborted) return errors.timeout(lang);
|
|
160
|
+
console.error("[chat-handler] Unexpected error:", err);
|
|
161
|
+
return errors.internal(void 0, lang);
|
|
162
|
+
} finally {
|
|
163
|
+
clearTimeout(requestTimer);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
async function runPipeline(args) {
|
|
167
|
+
const { env, messages, latestText, context, req, lang, waitUntil } = args;
|
|
168
|
+
const timing = { start: Date.now() };
|
|
169
|
+
const cache = createCacheAdapter(env);
|
|
170
|
+
const responseCacheConfig = getResponseCacheConfig(env);
|
|
171
|
+
const manager = getProviderManager(env, {
|
|
172
|
+
enableMockFallback: true,
|
|
173
|
+
unhealthyThreshold: 3,
|
|
174
|
+
healthRecoveryTTL: 6e4
|
|
175
|
+
});
|
|
176
|
+
const hasRealProvider = manager.hasProviders();
|
|
177
|
+
const adapter = hasRealProvider ? await manager.getAvailableAdapter() : null;
|
|
178
|
+
const articleSlug = context.scope === "article" && context.article?.slug ? context.article.slug : void 0;
|
|
179
|
+
const publicQuestion = detectPublicQuestion(latestText);
|
|
180
|
+
let globalCacheHit = false;
|
|
181
|
+
let globalCacheType;
|
|
182
|
+
if (publicQuestion && (!publicQuestion.needsContext || articleSlug)) {
|
|
183
|
+
const globalCacheContext = { articleSlug, lang };
|
|
184
|
+
if (responseCacheConfig.enabled) {
|
|
185
|
+
const cachedResponse = await getResponseCache(cache, publicQuestion.type, globalCacheContext);
|
|
186
|
+
if (cachedResponse) {
|
|
187
|
+
globalCacheHit = true;
|
|
188
|
+
globalCacheType = publicQuestion.type;
|
|
189
|
+
const notifyTiming = { total: Date.now() - timing.start };
|
|
190
|
+
sendNotification({ env, messages, responseText: cachedResponse.response, relatedArticles: cachedResponse.articles, timing: notifyTiming, waitUntil });
|
|
191
|
+
const stream2 = createUIMessageStream({
|
|
192
|
+
execute: async ({ writer }) => {
|
|
193
|
+
await streamCachedResponse(writer, cachedResponse, responseCacheConfig, lang);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
return createUIMessageStreamResponse({ stream: stream2, headers: { "Access-Control-Allow-Origin": "*", "Cache-Control": "no-cache" } });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const cachedSearch = await getGlobalSearchCache(cache, publicQuestion.type, globalCacheContext);
|
|
200
|
+
if (cachedSearch) {
|
|
201
|
+
globalCacheHit = true;
|
|
202
|
+
globalCacheType = publicQuestion.type;
|
|
203
|
+
const stream2 = createUIMessageStream({
|
|
204
|
+
execute: async ({ writer }) => {
|
|
205
|
+
const w = writer;
|
|
206
|
+
writeSearchStatus(w, cachedSearch.articles.length + cachedSearch.projects.length, lang);
|
|
207
|
+
if (cachedSearch.articles.length + cachedSearch.projects.length > 0) {
|
|
208
|
+
writeGeneratingStatus(w, lang);
|
|
209
|
+
}
|
|
210
|
+
writeSourceArticles(w, cachedSearch.articles);
|
|
211
|
+
let responseText = "";
|
|
212
|
+
if (adapter) {
|
|
213
|
+
const articlePrompt2 = buildArticleContextPrompt(context);
|
|
214
|
+
const matchedFacts2 = matchFactsToQuery(cachedSearch.query, lang);
|
|
215
|
+
const factPromptSection2 = buildFactSection(matchedFacts2, lang);
|
|
216
|
+
const systemPrompt2 = buildSystemPrompt({
|
|
217
|
+
static: { authorName: env.SITE_AUTHOR || "\u535A\u4E3B", siteUrl: env.SITE_URL || "", lang },
|
|
218
|
+
semiStatic: { authorContext: getAuthorContext(), voiceProfile: getVoiceProfile() },
|
|
219
|
+
dynamic: { userQuery: cachedSearch.query, articles: cachedSearch.articles, projects: cachedSearch.projects, evidenceSection: articlePrompt2, factSection: factPromptSection2 }
|
|
220
|
+
});
|
|
221
|
+
const llmResult = await streamLLMResponse({ writer: w, adapter, systemPrompt: systemPrompt2, messages, lang });
|
|
222
|
+
responseText = llmResult.responseText;
|
|
223
|
+
if (responseCacheConfig.enabled && llmResult.success && llmResult.responseText.length > 0) {
|
|
224
|
+
const globalTTL = getGlobalCacheTTL(publicQuestion.type);
|
|
225
|
+
const responseCacheData = {
|
|
226
|
+
query: cachedSearch.query,
|
|
227
|
+
thinking: llmResult.reasoningText,
|
|
228
|
+
response: llmResult.responseText,
|
|
229
|
+
articles: cachedSearch.articles,
|
|
230
|
+
projects: cachedSearch.projects,
|
|
231
|
+
lang,
|
|
232
|
+
model: adapter.model,
|
|
233
|
+
updatedAt: Date.now()
|
|
234
|
+
};
|
|
235
|
+
await setResponseCache(cache, publicQuestion.type, responseCacheData, globalTTL, globalCacheContext);
|
|
236
|
+
}
|
|
237
|
+
} else {
|
|
238
|
+
responseText = await streamMockFallback(w, latestText, lang);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
return createUIMessageStreamResponse({ stream: stream2, headers: { "Access-Control-Allow-Origin": "*", "Cache-Control": "no-cache" } });
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const cacheKey = getSessionCacheKey(req);
|
|
246
|
+
const now = Date.now();
|
|
247
|
+
const cachedContext = cacheKey ? await getCachedContext(cacheKey, cache) : void 0;
|
|
248
|
+
const userTurnCount = messages.filter((m) => m.role === "user").length;
|
|
249
|
+
const reuseContext = shouldReuseSearchContext({ latestText, cachedContext, userTurnCount, now });
|
|
250
|
+
let searchQuery = buildLocalSearchQuery(latestText) || latestText;
|
|
251
|
+
let relatedArticles = reuseContext && cachedContext ? cachedContext.articles : [];
|
|
252
|
+
let relatedProjects = reuseContext && cachedContext ? cachedContext.projects : [];
|
|
253
|
+
if (reuseContext && cachedContext && cacheKey) {
|
|
254
|
+
searchQuery = cachedContext.query;
|
|
255
|
+
await setCachedContext(cacheKey, { ...cachedContext, updatedAt: now }, cache);
|
|
256
|
+
} else {
|
|
257
|
+
if (hasRealProvider && adapter) {
|
|
258
|
+
const runKW = shouldRunKeywordExtraction({
|
|
259
|
+
messageCount: messages.length,
|
|
260
|
+
localQuery: searchQuery,
|
|
261
|
+
latestText
|
|
262
|
+
});
|
|
263
|
+
if (runKW) {
|
|
264
|
+
const kwStart = Date.now();
|
|
265
|
+
const abortCtrl = new AbortController();
|
|
266
|
+
const timeoutId = setTimeout(() => abortCtrl.abort(), KEYWORD_EXTRACTION_TIMEOUT_MS);
|
|
267
|
+
try {
|
|
268
|
+
const provider = adapter.getProvider();
|
|
269
|
+
const kwResult = await extractSearchKeywords({
|
|
270
|
+
messages,
|
|
271
|
+
provider,
|
|
272
|
+
model: adapter.keywordModel,
|
|
273
|
+
abortSignal: abortCtrl.signal
|
|
274
|
+
});
|
|
275
|
+
timing.keywordExtraction = Date.now() - kwStart;
|
|
276
|
+
if (kwResult.query && !kwResult.usedFallback) {
|
|
277
|
+
searchQuery = kwResult.query;
|
|
278
|
+
if (kwResult.primaryQuery && kwResult.primaryQuery !== searchQuery) {
|
|
279
|
+
const searchStart = Date.now();
|
|
280
|
+
const primary = searchArticles(kwResult.primaryQuery, { enableDeepContent: false });
|
|
281
|
+
relatedArticles = mergeResults(
|
|
282
|
+
searchArticles(searchQuery, { enableDeepContent: true }),
|
|
283
|
+
primary
|
|
284
|
+
);
|
|
285
|
+
relatedProjects = searchProjects(searchQuery);
|
|
286
|
+
timing.search = Date.now() - searchStart;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
} catch {
|
|
290
|
+
timing.keywordExtraction = Date.now() - kwStart;
|
|
291
|
+
} finally {
|
|
292
|
+
clearTimeout(timeoutId);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (!relatedArticles.length) {
|
|
297
|
+
const searchStart = Date.now();
|
|
298
|
+
relatedArticles = searchArticles(searchQuery, { enableDeepContent: true });
|
|
299
|
+
relatedProjects = searchProjects(searchQuery);
|
|
300
|
+
timing.search = Date.now() - searchStart;
|
|
301
|
+
}
|
|
302
|
+
relatedArticles = rankArticlesByIntent(latestText, relatedArticles);
|
|
303
|
+
if (cacheKey) {
|
|
304
|
+
await setCachedContext(cacheKey, {
|
|
305
|
+
query: searchQuery,
|
|
306
|
+
articles: relatedArticles,
|
|
307
|
+
projects: relatedProjects,
|
|
308
|
+
updatedAt: now
|
|
309
|
+
}, cache);
|
|
310
|
+
}
|
|
311
|
+
if (publicQuestion && (!publicQuestion.needsContext || articleSlug)) {
|
|
312
|
+
const globalTTL = getGlobalCacheTTL(publicQuestion.type);
|
|
313
|
+
await setGlobalSearchCache(
|
|
314
|
+
cache,
|
|
315
|
+
publicQuestion.type,
|
|
316
|
+
{
|
|
317
|
+
query: searchQuery,
|
|
318
|
+
articles: relatedArticles,
|
|
319
|
+
projects: relatedProjects,
|
|
320
|
+
updatedAt: now
|
|
321
|
+
},
|
|
322
|
+
globalTTL,
|
|
323
|
+
{ articleSlug, lang }
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
let evidenceSection = "";
|
|
328
|
+
if (hasRealProvider && adapter) {
|
|
329
|
+
const skipEvidence = shouldSkipAnalysis(latestText, relatedArticles.length, "moderate");
|
|
330
|
+
if (!skipEvidence) {
|
|
331
|
+
const evidenceStart = Date.now();
|
|
332
|
+
const abortCtrl = new AbortController();
|
|
333
|
+
const timeoutId = setTimeout(() => abortCtrl.abort(), EVIDENCE_ANALYSIS_TIMEOUT_MS);
|
|
334
|
+
try {
|
|
335
|
+
const provider = adapter.getProvider();
|
|
336
|
+
const evidenceResult = await analyzeRetrievedEvidence({
|
|
337
|
+
userQuery: latestText,
|
|
338
|
+
articles: relatedArticles,
|
|
339
|
+
projects: relatedProjects,
|
|
340
|
+
provider,
|
|
341
|
+
model: adapter.evidenceModel,
|
|
342
|
+
abortSignal: abortCtrl.signal
|
|
343
|
+
});
|
|
344
|
+
if (evidenceResult.analysis) {
|
|
345
|
+
evidenceSection = buildEvidenceSection(evidenceResult.analysis);
|
|
346
|
+
}
|
|
347
|
+
timing.evidenceAnalysis = Date.now() - evidenceStart;
|
|
348
|
+
} catch {
|
|
349
|
+
timing.evidenceAnalysis = Date.now() - evidenceStart;
|
|
350
|
+
} finally {
|
|
351
|
+
clearTimeout(timeoutId);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
const preflight = getCitationGuardPreflight({
|
|
356
|
+
userQuery: latestText,
|
|
357
|
+
articles: relatedArticles,
|
|
358
|
+
projects: relatedProjects,
|
|
359
|
+
lang
|
|
360
|
+
});
|
|
361
|
+
const matchedFacts = matchFactsToQuery(latestText, lang);
|
|
362
|
+
const factPromptSection = buildFactSection(matchedFacts, lang);
|
|
363
|
+
const articlePrompt = buildArticleContextPrompt(context);
|
|
364
|
+
const systemPrompt = buildSystemPrompt({
|
|
365
|
+
static: {
|
|
366
|
+
authorName: env.SITE_AUTHOR || "\u535A\u4E3B",
|
|
367
|
+
siteUrl: env.SITE_URL || "",
|
|
368
|
+
lang
|
|
369
|
+
},
|
|
370
|
+
semiStatic: {
|
|
371
|
+
authorContext: getAuthorContext(),
|
|
372
|
+
voiceProfile: getVoiceProfile()
|
|
373
|
+
},
|
|
374
|
+
dynamic: {
|
|
375
|
+
userQuery: searchQuery,
|
|
376
|
+
articles: relatedArticles,
|
|
377
|
+
projects: relatedProjects,
|
|
378
|
+
evidenceSection: articlePrompt ? `${evidenceSection}
|
|
379
|
+
${articlePrompt}` : evidenceSection,
|
|
380
|
+
factSection: factPromptSection,
|
|
381
|
+
lang
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
const stream = createUIMessageStream({
|
|
385
|
+
execute: async ({ writer }) => {
|
|
386
|
+
const articleCount = relatedArticles.length + relatedProjects.length;
|
|
387
|
+
if (articleCount > 0) {
|
|
388
|
+
writer.write({
|
|
389
|
+
type: "message-metadata",
|
|
390
|
+
messageMetadata: createChatStatusData({
|
|
391
|
+
stage: "search",
|
|
392
|
+
message: t("ai.status.found", lang, { count: articleCount }),
|
|
393
|
+
progress: 40
|
|
394
|
+
})
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
for (const article of relatedArticles.slice(0, 3)) {
|
|
398
|
+
try {
|
|
399
|
+
writer.write({
|
|
400
|
+
type: "source-url",
|
|
401
|
+
sourceId: `source-${article.title}`,
|
|
402
|
+
url: article.url ?? "#",
|
|
403
|
+
title: article.title
|
|
404
|
+
});
|
|
405
|
+
} catch {
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (preflight) {
|
|
409
|
+
writer.write({
|
|
410
|
+
type: "message-metadata",
|
|
411
|
+
messageMetadata: createChatStatusData({
|
|
412
|
+
stage: "answer",
|
|
413
|
+
message: t("ai.status.citation", lang),
|
|
414
|
+
progress: 100,
|
|
415
|
+
done: true
|
|
416
|
+
})
|
|
417
|
+
});
|
|
418
|
+
const partId = `preflight-${Date.now()}`;
|
|
419
|
+
writer.write({ type: "text-start", id: partId });
|
|
420
|
+
writer.write({ type: "text-delta", id: partId, delta: preflight.text });
|
|
421
|
+
writer.write({ type: "text-end", id: partId });
|
|
422
|
+
writer.write({ type: "finish", finishReason: "stop" });
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
writer.write({
|
|
426
|
+
type: "message-metadata",
|
|
427
|
+
messageMetadata: createChatStatusData({
|
|
428
|
+
stage: "answer",
|
|
429
|
+
message: t("ai.status.generating", lang),
|
|
430
|
+
progress: 60
|
|
431
|
+
})
|
|
432
|
+
});
|
|
433
|
+
let streamSuccess = false;
|
|
434
|
+
let responseText = "";
|
|
435
|
+
let reasoningText;
|
|
436
|
+
let tokenUsage;
|
|
437
|
+
const generationStart = Date.now();
|
|
438
|
+
if (adapter) {
|
|
439
|
+
try {
|
|
440
|
+
const provider = adapter.getProvider();
|
|
441
|
+
const result = streamText({
|
|
442
|
+
model: provider.chatModel(adapter.model),
|
|
443
|
+
system: systemPrompt,
|
|
444
|
+
messages: await convertToModelMessages(messages),
|
|
445
|
+
temperature: 0.3,
|
|
446
|
+
maxOutputTokens: 2500,
|
|
447
|
+
onError: ({ error }) => {
|
|
448
|
+
console.error("[chat-handler] streamText error:", error);
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
let hasTextOutput = false;
|
|
452
|
+
const errors2 = [];
|
|
453
|
+
writer.merge(result.toUIMessageStream({ sendFinish: false }));
|
|
454
|
+
await result.consumeStream({
|
|
455
|
+
onError: (error) => {
|
|
456
|
+
errors2.push(error instanceof Error ? error : new Error(String(error)));
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
const text = await result.text;
|
|
460
|
+
const reasoningPromise = result.reasoning;
|
|
461
|
+
const usagePromise = result.usage;
|
|
462
|
+
if (reasoningPromise) {
|
|
463
|
+
try {
|
|
464
|
+
const reasoningOutput = await Promise.resolve(reasoningPromise);
|
|
465
|
+
reasoningText = typeof reasoningOutput === "string" ? reasoningOutput : Array.isArray(reasoningOutput) ? reasoningOutput.map((r) => {
|
|
466
|
+
if (typeof r === "object" && r !== null && "text" in r) return r.text;
|
|
467
|
+
return String(r);
|
|
468
|
+
}).join("") : void 0;
|
|
469
|
+
} catch {
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
if (usagePromise) {
|
|
473
|
+
try {
|
|
474
|
+
const usage = await Promise.resolve(usagePromise);
|
|
475
|
+
const inputTokens = usage.inputTokens ?? 0;
|
|
476
|
+
const outputTokens = usage.outputTokens ?? 0;
|
|
477
|
+
tokenUsage = {
|
|
478
|
+
total: usage.totalTokens ?? inputTokens + outputTokens,
|
|
479
|
+
input: inputTokens,
|
|
480
|
+
output: outputTokens
|
|
481
|
+
};
|
|
482
|
+
} catch {
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
timing.generation = Date.now() - generationStart;
|
|
486
|
+
responseText = text;
|
|
487
|
+
hasTextOutput = text.length > 0;
|
|
488
|
+
if (hasTextOutput && errors2.length === 0) {
|
|
489
|
+
adapter.recordSuccess();
|
|
490
|
+
if (shouldAppendCitations(responseText, relatedArticles, relatedProjects)) {
|
|
491
|
+
const citations = selectCitations(relatedArticles, relatedProjects, 3, 5);
|
|
492
|
+
if (citations.length > 0) {
|
|
493
|
+
const citationBlock = formatCitationBlock(citations, lang);
|
|
494
|
+
const citationId = `citation-${Date.now()}`;
|
|
495
|
+
writer.write({ type: "text-start", id: citationId });
|
|
496
|
+
writer.write({ type: "text-delta", id: citationId, delta: citationBlock });
|
|
497
|
+
writer.write({ type: "text-end", id: citationId });
|
|
498
|
+
responseText += citationBlock;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
writer.write({ type: "finish", finishReason: "stop" });
|
|
502
|
+
streamSuccess = true;
|
|
503
|
+
if (responseCacheConfig.enabled && publicQuestion && (!publicQuestion.needsContext || articleSlug)) {
|
|
504
|
+
const globalTTL = getGlobalCacheTTL(publicQuestion.type);
|
|
505
|
+
const responseCacheData = {
|
|
506
|
+
query: searchQuery,
|
|
507
|
+
thinking: reasoningText,
|
|
508
|
+
response: text,
|
|
509
|
+
articles: relatedArticles,
|
|
510
|
+
projects: relatedProjects,
|
|
511
|
+
lang,
|
|
512
|
+
model: adapter.model,
|
|
513
|
+
updatedAt: Date.now()
|
|
514
|
+
};
|
|
515
|
+
await setResponseCache(cache, publicQuestion.type, responseCacheData, globalTTL, { articleSlug, lang });
|
|
516
|
+
}
|
|
517
|
+
} else if (errors2.length > 0) {
|
|
518
|
+
adapter.recordFailure(errors2[0]);
|
|
519
|
+
console.error("[chat-handler] Stream error:", errors2[0].message);
|
|
520
|
+
const errorId = `error-${Date.now()}`;
|
|
521
|
+
writer.write({ type: "text-start", id: errorId });
|
|
522
|
+
writer.write({
|
|
523
|
+
type: "text-delta",
|
|
524
|
+
id: errorId,
|
|
525
|
+
delta: t("ai.error.generic", lang)
|
|
526
|
+
});
|
|
527
|
+
writer.write({ type: "text-end", id: errorId });
|
|
528
|
+
writer.write({ type: "finish", finishReason: "error" });
|
|
529
|
+
streamSuccess = true;
|
|
530
|
+
} else if (!hasTextOutput) {
|
|
531
|
+
const noOutputId = `no-output-${Date.now()}`;
|
|
532
|
+
writer.write({ type: "text-start", id: noOutputId });
|
|
533
|
+
writer.write({ type: "text-delta", id: noOutputId, delta: t("ai.error.noOutput", lang) });
|
|
534
|
+
writer.write({ type: "text-end", id: noOutputId });
|
|
535
|
+
writer.write({ type: "finish", finishReason: "stop" });
|
|
536
|
+
streamSuccess = true;
|
|
537
|
+
} else {
|
|
538
|
+
writer.write({ type: "finish", finishReason: "stop" });
|
|
539
|
+
streamSuccess = true;
|
|
540
|
+
}
|
|
541
|
+
} catch (err) {
|
|
542
|
+
timing.generation = Date.now() - generationStart;
|
|
543
|
+
adapter.recordFailure(err instanceof Error ? err : new Error(String(err)));
|
|
544
|
+
console.error("[chat-handler] Provider threw:", err.message);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
if (!streamSuccess) {
|
|
548
|
+
const { getMockResponse } = await import("../providers/mock.js");
|
|
549
|
+
const mockText = getMockResponse(latestText, lang);
|
|
550
|
+
timing.generation = Date.now() - generationStart;
|
|
551
|
+
responseText = mockText;
|
|
552
|
+
writer.write({
|
|
553
|
+
type: "message-metadata",
|
|
554
|
+
messageMetadata: createChatStatusData({
|
|
555
|
+
stage: "answer",
|
|
556
|
+
message: t("ai.status.fallback", lang),
|
|
557
|
+
progress: 80
|
|
558
|
+
})
|
|
559
|
+
});
|
|
560
|
+
const fallbackId = `fallback-${Date.now()}`;
|
|
561
|
+
writer.write({ type: "text-start", id: fallbackId });
|
|
562
|
+
writer.write({ type: "text-delta", id: fallbackId, delta: mockText });
|
|
563
|
+
writer.write({ type: "text-end", id: fallbackId });
|
|
564
|
+
writer.write({ type: "finish", finishReason: "stop" });
|
|
565
|
+
}
|
|
566
|
+
if (responseText) {
|
|
567
|
+
const notifyTiming = {
|
|
568
|
+
total: Date.now() - timing.start,
|
|
569
|
+
keywordExtraction: timing.keywordExtraction,
|
|
570
|
+
search: timing.search,
|
|
571
|
+
evidenceAnalysis: timing.evidenceAnalysis,
|
|
572
|
+
generation: timing.generation
|
|
573
|
+
};
|
|
574
|
+
const notifyModel = adapter ? {
|
|
575
|
+
name: adapter.model,
|
|
576
|
+
provider: env.AI_PROVIDER || void 0,
|
|
577
|
+
apiHost: env.AI_BASE_URL || void 0
|
|
578
|
+
} : void 0;
|
|
579
|
+
sendNotification({
|
|
580
|
+
env,
|
|
581
|
+
messages,
|
|
582
|
+
responseText,
|
|
583
|
+
relatedArticles,
|
|
584
|
+
model: notifyModel,
|
|
585
|
+
usage: tokenUsage,
|
|
586
|
+
timing: notifyTiming,
|
|
587
|
+
cacheKey,
|
|
588
|
+
waitUntil
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
return createUIMessageStreamResponse({
|
|
594
|
+
stream,
|
|
595
|
+
headers: {
|
|
596
|
+
"Access-Control-Allow-Origin": "*",
|
|
597
|
+
"Cache-Control": "no-cache"
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
export {
|
|
602
|
+
handleChatRequest
|
|
603
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { t } from "../utils/i18n.js";
|
|
2
|
+
const CORS_HEADERS = {
|
|
3
|
+
"Content-Type": "application/json",
|
|
4
|
+
"Access-Control-Allow-Origin": "*"
|
|
5
|
+
};
|
|
6
|
+
function chatError(code, error, status, options) {
|
|
7
|
+
const body = {
|
|
8
|
+
error,
|
|
9
|
+
code,
|
|
10
|
+
retryable: options?.retryable ?? false,
|
|
11
|
+
retryAfter: options?.retryAfter
|
|
12
|
+
};
|
|
13
|
+
const headers = { ...CORS_HEADERS };
|
|
14
|
+
if (options?.retryAfter) {
|
|
15
|
+
headers["Retry-After"] = String(options.retryAfter);
|
|
16
|
+
}
|
|
17
|
+
return new Response(JSON.stringify(body), { status, headers });
|
|
18
|
+
}
|
|
19
|
+
function te(key, lang, vars) {
|
|
20
|
+
return t(key, lang ?? "zh", vars);
|
|
21
|
+
}
|
|
22
|
+
const errors = {
|
|
23
|
+
methodNotAllowed: (lang) => chatError("METHOD_NOT_ALLOWED", lang === "en" ? "Method not allowed" : "\u65B9\u6CD5\u4E0D\u5141\u8BB8", 405),
|
|
24
|
+
invalidRequest: (detail, lang) => chatError("INVALID_REQUEST", detail ?? te("ai.error.format", lang), 400),
|
|
25
|
+
emptyMessage: (lang) => chatError("INVALID_REQUEST", te("ai.error.emptyMessage", lang), 400),
|
|
26
|
+
emptyContent: (lang) => chatError("INVALID_REQUEST", te("ai.error.emptyContent", lang), 400),
|
|
27
|
+
inputTooLong: (max, lang) => chatError("INPUT_TOO_LONG", te("ai.error.inputTooLong", lang, { max }), 400),
|
|
28
|
+
rateLimited: (retryAfter, lang) => chatError("RATE_LIMITED", te("ai.error.rateLimit", lang), 429, { retryable: true, retryAfter: retryAfter ?? 10 }),
|
|
29
|
+
timeout: (lang) => chatError("TIMEOUT", te("ai.error.timeout", lang), 504, { retryable: true }),
|
|
30
|
+
providerUnavailable: (lang) => chatError("PROVIDER_UNAVAILABLE", te("ai.error.unavailable", lang), 503, { retryable: true, retryAfter: 30 }),
|
|
31
|
+
internal: (detail, lang) => chatError("INTERNAL_ERROR", detail ?? te("ai.error.generic", lang), 500, { retryable: true, retryAfter: 5 })
|
|
32
|
+
};
|
|
33
|
+
function corsPreflightResponse() {
|
|
34
|
+
return new Response(null, {
|
|
35
|
+
headers: {
|
|
36
|
+
"Access-Control-Allow-Origin": "*",
|
|
37
|
+
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
|
38
|
+
"Access-Control-Allow-Headers": "Content-Type, x-session-id"
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
export {
|
|
43
|
+
chatError,
|
|
44
|
+
corsPreflightResponse,
|
|
45
|
+
errors
|
|
46
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import {
|
|
2
|
+
preloadMetadata,
|
|
3
|
+
getAuthorContext,
|
|
4
|
+
getAllSummaries,
|
|
5
|
+
initArticleIndex,
|
|
6
|
+
initProjectIndex
|
|
7
|
+
} from "../index.js";
|
|
8
|
+
let initialized = false;
|
|
9
|
+
function initializeMetadata(config, env) {
|
|
10
|
+
if (initialized) return;
|
|
11
|
+
initialized = true;
|
|
12
|
+
preloadMetadata({
|
|
13
|
+
summaries: config.summaries,
|
|
14
|
+
authorContext: config.authorContext,
|
|
15
|
+
voiceProfile: config.voiceProfile,
|
|
16
|
+
factRegistry: config.factRegistry ?? null,
|
|
17
|
+
vectorIndex: config.vectorIndex ?? null
|
|
18
|
+
});
|
|
19
|
+
const authorCtx = getAuthorContext();
|
|
20
|
+
const allSummaries = getAllSummaries();
|
|
21
|
+
const summaryMap = new Map(allSummaries.map((s) => [s.slug, s]));
|
|
22
|
+
const siteUrl = config.siteUrl ?? env?.SITE_URL ?? "";
|
|
23
|
+
const articleDocs = (authorCtx?.posts ?? []).map((post) => {
|
|
24
|
+
const summary = summaryMap.get(post.id);
|
|
25
|
+
const baseUrl = post.url?.startsWith("http") ? "" : siteUrl;
|
|
26
|
+
return {
|
|
27
|
+
id: post.id,
|
|
28
|
+
title: post.title,
|
|
29
|
+
url: post.url ? `${baseUrl}${post.url}` : `${siteUrl}/${post.id}`,
|
|
30
|
+
excerpt: post.summary || summary?.summary || "",
|
|
31
|
+
content: [...post.keyPoints ?? [], ...summary?.keyPoints ?? []].join(" "),
|
|
32
|
+
categories: [post.category].filter(Boolean),
|
|
33
|
+
tags: post.tags ?? [],
|
|
34
|
+
keyPoints: [...post.keyPoints ?? [], ...summary?.keyPoints ?? []],
|
|
35
|
+
dateTime: post.date ? new Date(post.date).getTime() : 0,
|
|
36
|
+
lang: post.lang,
|
|
37
|
+
summary: summary?.summary
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
initArticleIndex(articleDocs);
|
|
41
|
+
initProjectIndex([]);
|
|
42
|
+
}
|
|
43
|
+
function resetMetadataInit() {
|
|
44
|
+
initialized = false;
|
|
45
|
+
}
|
|
46
|
+
export {
|
|
47
|
+
initializeMetadata,
|
|
48
|
+
resetMetadataInit
|
|
49
|
+
};
|