@astro-minimax/ai 0.7.4 → 0.8.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 (80) hide show
  1. package/dist/components/AIChatContainer.d.ts +9 -0
  2. package/dist/components/AIChatContainer.d.ts.map +1 -0
  3. package/dist/components/AIChatContainer.js +936 -0
  4. package/{src → dist}/components/AIChatWidget.astro +1 -1
  5. package/dist/components/ChatPanel.d.ts +19 -0
  6. package/dist/components/ChatPanel.d.ts.map +1 -0
  7. package/dist/components/ChatPanel.js +914 -0
  8. package/dist/data/index.js +18 -1
  9. package/dist/fact-registry/index.js +16 -3
  10. package/dist/index.js +11 -30
  11. package/dist/intelligence/evidence-analysis.d.ts.map +1 -1
  12. package/dist/intelligence/index.js +56 -5
  13. package/dist/intelligence/keyword-extract.d.ts.map +1 -1
  14. package/dist/middleware/index.js +10 -1
  15. package/dist/prompt/index.js +10 -4
  16. package/dist/provider-manager/base.d.ts +1 -0
  17. package/dist/provider-manager/base.d.ts.map +1 -1
  18. package/dist/provider-manager/types.d.ts +1 -0
  19. package/dist/provider-manager/types.d.ts.map +1 -1
  20. package/dist/providers/index.js +5 -1
  21. package/dist/search/index.js +48 -6
  22. package/dist/server/dev-server.js +236 -259
  23. package/dist/server/index.js +39 -6
  24. package/dist/stream/index.js +8 -2
  25. package/package.json +16 -10
  26. package/dist/cache/global-cache.js +0 -141
  27. package/dist/cache/index.js +0 -62
  28. package/dist/cache/kv-adapter.js +0 -102
  29. package/dist/cache/memory-adapter.js +0 -95
  30. package/dist/cache/response-cache.js +0 -85
  31. package/dist/cache/types.js +0 -16
  32. package/dist/data/metadata-loader.js +0 -66
  33. package/dist/data/types.js +0 -1
  34. package/dist/fact-registry/fact-matcher.js +0 -94
  35. package/dist/fact-registry/prompt-injector.js +0 -57
  36. package/dist/fact-registry/registry.js +0 -38
  37. package/dist/fact-registry/types.js +0 -5
  38. package/dist/intelligence/citation-appender.js +0 -65
  39. package/dist/intelligence/citation-guard.js +0 -125
  40. package/dist/intelligence/evidence-analysis.js +0 -88
  41. package/dist/intelligence/intent-detect.js +0 -131
  42. package/dist/intelligence/keyword-extract.js +0 -114
  43. package/dist/intelligence/response-templates.js +0 -116
  44. package/dist/intelligence/types.js +0 -1
  45. package/dist/middleware/rate-limiter.js +0 -129
  46. package/dist/prompt/dynamic-layer.js +0 -67
  47. package/dist/prompt/prompt-builder.js +0 -12
  48. package/dist/prompt/semi-static-layer.js +0 -29
  49. package/dist/prompt/static-layer.js +0 -150
  50. package/dist/prompt/types.js +0 -1
  51. package/dist/provider-manager/base.js +0 -47
  52. package/dist/provider-manager/config.js +0 -134
  53. package/dist/provider-manager/index.js +0 -6
  54. package/dist/provider-manager/manager.js +0 -121
  55. package/dist/provider-manager/mock.js +0 -56
  56. package/dist/provider-manager/openai.js +0 -112
  57. package/dist/provider-manager/types.js +0 -6
  58. package/dist/provider-manager/workers.js +0 -74
  59. package/dist/providers/mock.js +0 -234
  60. package/dist/search/idf.js +0 -31
  61. package/dist/search/search-api.js +0 -119
  62. package/dist/search/search-index.js +0 -35
  63. package/dist/search/search-utils.js +0 -122
  64. package/dist/search/session-cache.js +0 -92
  65. package/dist/search/types.js +0 -1
  66. package/dist/search/vector-reranker.js +0 -135
  67. package/dist/server/chat-handler.js +0 -590
  68. package/dist/server/errors.js +0 -41
  69. package/dist/server/metadata-init.js +0 -47
  70. package/dist/server/notify.js +0 -74
  71. package/dist/server/stream-helpers.js +0 -197
  72. package/dist/server/types.js +0 -13
  73. package/dist/stream/mock-stream.js +0 -27
  74. package/dist/stream/response.js +0 -22
  75. package/dist/utils/i18n.js +0 -164
  76. package/src/components/AIChatContainer.tsx +0 -31
  77. package/src/components/ChatPanel.tsx +0 -866
  78. package/src/providers/mock.ts +0 -240
  79. package/src/server/types.ts +0 -89
  80. package/src/utils/i18n.ts +0 -238
@@ -1,590 +0,0 @@
1
- import { createUIMessageStream, createUIMessageStreamResponse, streamText, convertToModelMessages, } from 'ai';
2
- import { t, getLang } from '../utils/i18n.js';
3
- import { getClientIP, checkRateLimit, rateLimitResponse, searchArticles, searchProjects, getSessionCacheKey, getCachedContext, setCachedContext, shouldReuseSearchContext, buildLocalSearchQuery, shouldRunKeywordExtraction, extractSearchKeywords, KEYWORD_EXTRACTION_TIMEOUT_MS, shouldSkipAnalysis, analyzeRetrievedEvidence, buildEvidenceSection, EVIDENCE_ANALYSIS_TIMEOUT_MS, getCitationGuardPreflight, buildSystemPrompt, getAuthorContext, getVoiceProfile, mergeResults, getProviderManager, createCacheAdapter, detectPublicQuestion, getGlobalSearchCache, shouldAppendCitations, formatCitationBlock, selectCitations, setGlobalSearchCache, getGlobalCacheTTL, getResponseCache, setResponseCache, getResponseCacheConfig, rankArticlesByIntent, matchFactsToQuery, buildFactSection, } from '../index.js';
4
- import { createChatStatusData } from './types.js';
5
- import { errors, corsPreflightResponse } from './errors.js';
6
- import { notifyAiChat } from './notify.js';
7
- import { writeSearchStatus, writeGeneratingStatus, writeSourceArticles, streamLLMResponse, streamMockFallback, streamCachedResponse, } from './stream-helpers.js';
8
- const MAX_HISTORY_MESSAGES = 20;
9
- const MAX_INPUT_LENGTH = 500;
10
- const REQUEST_TIMEOUT_MS = 45_000;
11
- function sendNotification(args) {
12
- const { env, messages, responseText, relatedArticles, model, usage, timing, cacheKey, waitUntil } = args;
13
- const sessionId = cacheKey || `dev-${Date.now().toString(36)}`;
14
- const notifyArticles = relatedArticles.slice(0, 5).map(a => ({
15
- title: a.title,
16
- url: a.url,
17
- }));
18
- const notifyPromise = notifyAiChat({
19
- env,
20
- sessionId,
21
- messages,
22
- aiResponse: responseText,
23
- referencedArticles: notifyArticles,
24
- model,
25
- usage,
26
- timing,
27
- });
28
- if (waitUntil) {
29
- waitUntil(notifyPromise);
30
- }
31
- else {
32
- void notifyPromise;
33
- }
34
- }
35
- // ── Message Helpers ───────────────────────────────────────────
36
- function getMessageText(message) {
37
- if (Array.isArray(message.parts)) {
38
- return message.parts
39
- .filter((p) => p.type === 'text')
40
- .map(p => p.text)
41
- .join('');
42
- }
43
- return '';
44
- }
45
- function hasContent(message) {
46
- const text = getMessageText(message);
47
- if (text.trim())
48
- return true;
49
- if (Array.isArray(message.parts)) {
50
- return message.parts.some(p => p.type !== 'text');
51
- }
52
- return false;
53
- }
54
- function filterValidMessages(messages) {
55
- const filtered = [];
56
- let lastRole = null;
57
- for (const msg of messages) {
58
- if (!hasContent(msg))
59
- continue;
60
- if (msg.role === lastRole)
61
- continue;
62
- filtered.push(msg);
63
- lastRole = msg.role;
64
- }
65
- if (filtered.length > 0 && filtered[filtered.length - 1].role !== 'user') {
66
- filtered.pop();
67
- }
68
- return filtered;
69
- }
70
- // ── Article Context Prompt Enhancement ────────────────────────
71
- function buildArticleContextPrompt(context) {
72
- if (context.scope !== 'article' || !context.article)
73
- return '';
74
- const a = context.article;
75
- const parts = [
76
- '\n[当前阅读文章]',
77
- `用户正在阅读:《${a.title}》`,
78
- ];
79
- if (a.summary)
80
- parts.push(`摘要:${a.summary}`);
81
- if (a.abstract)
82
- parts.push(`详细概要:${a.abstract}`);
83
- if (a.keyPoints?.length)
84
- parts.push(`核心要点:${a.keyPoints.join(';')}`);
85
- if (a.categories?.length)
86
- parts.push(`分类:${a.categories.join('、')}`);
87
- parts.push('', '你正在陪用户阅读这篇文章。优先围绕这篇文章的内容回答问题。', '当用户的问题与当前文章相关时,引用文章中的具体内容。', '当用户想要延伸时,推荐相关的博客文章。');
88
- return parts.join('\n');
89
- }
90
- // ── Main Handler ──────────────────────────────────────────────
91
- export async function handleChatRequest(options) {
92
- const { env, request: req, waitUntil } = options;
93
- if (req.method === 'OPTIONS')
94
- return corsPreflightResponse();
95
- if (req.method !== 'POST')
96
- return errors.methodNotAllowed('zh');
97
- const ip = getClientIP(req);
98
- const rateCheck = checkRateLimit(ip, env);
99
- if (!rateCheck.allowed)
100
- return rateLimitResponse(rateCheck, 'zh');
101
- let body;
102
- try {
103
- body = await req.json();
104
- }
105
- catch {
106
- return errors.invalidRequest(t('ai.error.format', 'zh'));
107
- }
108
- const lang = getLang(body.lang ?? env.SITE_LANG);
109
- const context = body.context ?? { scope: 'global' };
110
- const rawMessages = (body.messages ?? []).slice(-MAX_HISTORY_MESSAGES);
111
- if (!rawMessages.length)
112
- return errors.emptyMessage(lang);
113
- const messages = filterValidMessages(rawMessages);
114
- if (!messages.length)
115
- return errors.emptyMessage(lang);
116
- const latestMessage = messages[messages.length - 1];
117
- const latestText = getMessageText(latestMessage);
118
- if (!latestText)
119
- return errors.emptyContent(lang);
120
- if (latestText.length > MAX_INPUT_LENGTH)
121
- return errors.inputTooLong(MAX_INPUT_LENGTH, lang);
122
- const requestAbort = new AbortController();
123
- const requestTimer = setTimeout(() => requestAbort.abort(), REQUEST_TIMEOUT_MS);
124
- try {
125
- return await runPipeline({ env, messages, latestText, context, req, requestAbort, lang, waitUntil });
126
- }
127
- catch (err) {
128
- if (requestAbort.signal.aborted)
129
- return errors.timeout(lang);
130
- console.error('[chat-handler] Unexpected error:', err);
131
- return errors.internal(undefined, lang);
132
- }
133
- finally {
134
- clearTimeout(requestTimer);
135
- }
136
- }
137
- async function runPipeline(args) {
138
- const { env, messages, latestText, context, req, lang, waitUntil } = args;
139
- const timing = { start: Date.now() };
140
- const cache = createCacheAdapter(env);
141
- const responseCacheConfig = getResponseCacheConfig(env);
142
- const manager = getProviderManager(env, {
143
- enableMockFallback: true,
144
- unhealthyThreshold: 3,
145
- healthRecoveryTTL: 60_000,
146
- });
147
- const hasRealProvider = manager.hasProviders();
148
- const adapter = hasRealProvider ? await manager.getAvailableAdapter() : null;
149
- // ── Global Cache Check for Public Questions ─────────────────────────
150
- const articleSlug = context.scope === 'article' && context.article?.slug
151
- ? context.article.slug
152
- : undefined;
153
- const publicQuestion = detectPublicQuestion(latestText);
154
- let globalCacheHit = false;
155
- let globalCacheType;
156
- if (publicQuestion && (!publicQuestion.needsContext || articleSlug)) {
157
- const globalCacheContext = { articleSlug, lang };
158
- // Check response cache first if enabled
159
- if (responseCacheConfig.enabled) {
160
- const cachedResponse = await getResponseCache(cache, publicQuestion.type, globalCacheContext);
161
- if (cachedResponse) {
162
- globalCacheHit = true;
163
- globalCacheType = publicQuestion.type;
164
- const notifyTiming = { total: Date.now() - timing.start };
165
- sendNotification({ env, messages, responseText: cachedResponse.response, relatedArticles: cachedResponse.articles, timing: notifyTiming, waitUntil });
166
- const stream = createUIMessageStream({
167
- execute: async ({ writer }) => {
168
- await streamCachedResponse(writer, cachedResponse, responseCacheConfig, lang);
169
- },
170
- });
171
- return createUIMessageStreamResponse({ stream, headers: { 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' } });
172
- }
173
- }
174
- // Check search context cache
175
- const cachedSearch = await getGlobalSearchCache(cache, publicQuestion.type, globalCacheContext);
176
- if (cachedSearch) {
177
- globalCacheHit = true;
178
- globalCacheType = publicQuestion.type;
179
- const stream = createUIMessageStream({
180
- execute: async ({ writer }) => {
181
- const w = writer;
182
- writeSearchStatus(w, cachedSearch.articles.length + cachedSearch.projects.length, lang);
183
- if (cachedSearch.articles.length + cachedSearch.projects.length > 0) {
184
- writeGeneratingStatus(w, lang);
185
- }
186
- writeSourceArticles(w, cachedSearch.articles);
187
- let responseText = '';
188
- if (adapter) {
189
- const articlePrompt = buildArticleContextPrompt(context);
190
- const matchedFacts = matchFactsToQuery(cachedSearch.query, lang);
191
- const factPromptSection = buildFactSection(matchedFacts, lang);
192
- const systemPrompt = buildSystemPrompt({
193
- static: { authorName: env.SITE_AUTHOR || '博主', siteUrl: env.SITE_URL || '', lang },
194
- semiStatic: { authorContext: getAuthorContext(), voiceProfile: getVoiceProfile() },
195
- dynamic: { userQuery: cachedSearch.query, articles: cachedSearch.articles, projects: cachedSearch.projects, evidenceSection: articlePrompt, factSection: factPromptSection },
196
- });
197
- const llmResult = await streamLLMResponse({ writer: w, adapter, systemPrompt, messages, lang });
198
- responseText = llmResult.responseText;
199
- if (responseCacheConfig.enabled && llmResult.success && llmResult.responseText.length > 0) {
200
- const globalTTL = getGlobalCacheTTL(publicQuestion.type);
201
- const responseCacheData = {
202
- query: cachedSearch.query, thinking: llmResult.reasoningText, response: llmResult.responseText,
203
- articles: cachedSearch.articles, projects: cachedSearch.projects, lang, model: adapter.model, updatedAt: Date.now(),
204
- };
205
- await setResponseCache(cache, publicQuestion.type, responseCacheData, globalTTL, globalCacheContext);
206
- }
207
- }
208
- else {
209
- responseText = await streamMockFallback(w, latestText, lang);
210
- }
211
- },
212
- });
213
- return createUIMessageStreamResponse({ stream, headers: { 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' } });
214
- }
215
- }
216
- // ── Search / Retrieval ──────────────────────────────────────
217
- const cacheKey = getSessionCacheKey(req);
218
- const now = Date.now();
219
- const cachedContext = cacheKey ? await getCachedContext(cacheKey, cache) : undefined;
220
- const userTurnCount = messages.filter((m) => m.role === 'user').length;
221
- const reuseContext = shouldReuseSearchContext({ latestText, cachedContext, userTurnCount, now });
222
- let searchQuery = buildLocalSearchQuery(latestText) || latestText;
223
- let relatedArticles = reuseContext && cachedContext ? cachedContext.articles : [];
224
- let relatedProjects = reuseContext && cachedContext ? cachedContext.projects : [];
225
- if (reuseContext && cachedContext && cacheKey) {
226
- searchQuery = cachedContext.query;
227
- await setCachedContext(cacheKey, { ...cachedContext, updatedAt: now }, cache);
228
- }
229
- else {
230
- if (hasRealProvider && adapter) {
231
- const runKW = shouldRunKeywordExtraction({
232
- messageCount: messages.length,
233
- localQuery: searchQuery,
234
- latestText,
235
- });
236
- if (runKW) {
237
- const kwStart = Date.now();
238
- const abortCtrl = new AbortController();
239
- const timeoutId = setTimeout(() => abortCtrl.abort(), KEYWORD_EXTRACTION_TIMEOUT_MS);
240
- try {
241
- const provider = adapter.getProvider();
242
- const kwResult = await extractSearchKeywords({
243
- messages: messages,
244
- provider,
245
- model: adapter.keywordModel,
246
- abortSignal: abortCtrl.signal,
247
- });
248
- timing.keywordExtraction = Date.now() - kwStart;
249
- if (kwResult.query && !kwResult.usedFallback) {
250
- searchQuery = kwResult.query;
251
- if (kwResult.primaryQuery && kwResult.primaryQuery !== searchQuery) {
252
- const searchStart = Date.now();
253
- const primary = searchArticles(kwResult.primaryQuery, { enableDeepContent: false });
254
- relatedArticles = mergeResults(searchArticles(searchQuery, { enableDeepContent: true }), primary);
255
- relatedProjects = searchProjects(searchQuery);
256
- timing.search = Date.now() - searchStart;
257
- }
258
- }
259
- }
260
- catch {
261
- timing.keywordExtraction = Date.now() - kwStart;
262
- }
263
- finally {
264
- clearTimeout(timeoutId);
265
- }
266
- }
267
- }
268
- if (!relatedArticles.length) {
269
- const searchStart = Date.now();
270
- relatedArticles = searchArticles(searchQuery, { enableDeepContent: true });
271
- relatedProjects = searchProjects(searchQuery);
272
- timing.search = Date.now() - searchStart;
273
- }
274
- relatedArticles = rankArticlesByIntent(latestText, relatedArticles);
275
- if (cacheKey) {
276
- await setCachedContext(cacheKey, {
277
- query: searchQuery,
278
- articles: relatedArticles,
279
- projects: relatedProjects,
280
- updatedAt: now,
281
- }, cache);
282
- }
283
- if (publicQuestion && (!publicQuestion.needsContext || articleSlug)) {
284
- const globalTTL = getGlobalCacheTTL(publicQuestion.type);
285
- await setGlobalSearchCache(cache, publicQuestion.type, {
286
- query: searchQuery,
287
- articles: relatedArticles,
288
- projects: relatedProjects,
289
- updatedAt: now,
290
- }, globalTTL, { articleSlug, lang });
291
- }
292
- }
293
- // ── Evidence Analysis (optional) ────────────────────────────
294
- let evidenceSection = '';
295
- if (hasRealProvider && adapter) {
296
- const skipEvidence = shouldSkipAnalysis(latestText, relatedArticles.length, 'moderate');
297
- if (!skipEvidence) {
298
- const evidenceStart = Date.now();
299
- const abortCtrl = new AbortController();
300
- const timeoutId = setTimeout(() => abortCtrl.abort(), EVIDENCE_ANALYSIS_TIMEOUT_MS);
301
- try {
302
- const provider = adapter.getProvider();
303
- const evidenceResult = await analyzeRetrievedEvidence({
304
- userQuery: latestText,
305
- articles: relatedArticles,
306
- projects: relatedProjects,
307
- provider,
308
- model: adapter.evidenceModel,
309
- abortSignal: abortCtrl.signal,
310
- });
311
- if (evidenceResult.analysis) {
312
- evidenceSection = buildEvidenceSection(evidenceResult.analysis);
313
- }
314
- timing.evidenceAnalysis = Date.now() - evidenceStart;
315
- }
316
- catch {
317
- timing.evidenceAnalysis = Date.now() - evidenceStart;
318
- }
319
- finally {
320
- clearTimeout(timeoutId);
321
- }
322
- }
323
- }
324
- // ── Citation Guard ──────────────────────────────────────────
325
- const preflight = getCitationGuardPreflight({
326
- userQuery: latestText,
327
- articles: relatedArticles,
328
- projects: relatedProjects,
329
- lang,
330
- });
331
- // ── Fact Registry ───────────────────────────────────────────
332
- const matchedFacts = matchFactsToQuery(latestText, lang);
333
- const factPromptSection = buildFactSection(matchedFacts, lang);
334
- // ── Build System Prompt ─────────────────────────────────────
335
- const articlePrompt = buildArticleContextPrompt(context);
336
- const systemPrompt = buildSystemPrompt({
337
- static: {
338
- authorName: env.SITE_AUTHOR || '博主',
339
- siteUrl: env.SITE_URL || '',
340
- lang,
341
- },
342
- semiStatic: {
343
- authorContext: getAuthorContext(),
344
- voiceProfile: getVoiceProfile(),
345
- },
346
- dynamic: {
347
- userQuery: searchQuery,
348
- articles: relatedArticles,
349
- projects: relatedProjects,
350
- evidenceSection: articlePrompt
351
- ? `${evidenceSection}\n${articlePrompt}`
352
- : evidenceSection,
353
- factSection: factPromptSection,
354
- lang,
355
- },
356
- });
357
- // ── Stream Response via createUIMessageStream ───────────────
358
- const stream = createUIMessageStream({
359
- execute: async ({ writer }) => {
360
- const articleCount = relatedArticles.length + relatedProjects.length;
361
- // Push status: search results
362
- if (articleCount > 0) {
363
- writer.write({
364
- type: 'message-metadata',
365
- messageMetadata: createChatStatusData({
366
- stage: 'search',
367
- message: t('ai.status.found', lang, { count: articleCount }),
368
- progress: 40,
369
- }),
370
- });
371
- }
372
- // Push source parts for top related articles
373
- for (const article of relatedArticles.slice(0, 3)) {
374
- try {
375
- writer.write({
376
- type: 'source-url',
377
- sourceId: `source-${article.title}`,
378
- url: article.url ?? '#',
379
- title: article.title,
380
- });
381
- }
382
- catch {
383
- // source writing is best-effort
384
- }
385
- }
386
- // Citation guard preflight: return canned response without calling LLM
387
- if (preflight) {
388
- writer.write({
389
- type: 'message-metadata',
390
- messageMetadata: createChatStatusData({
391
- stage: 'answer',
392
- message: t('ai.status.citation', lang),
393
- progress: 100,
394
- done: true,
395
- }),
396
- });
397
- const partId = `preflight-${Date.now()}`;
398
- writer.write({ type: 'text-start', id: partId });
399
- writer.write({ type: 'text-delta', id: partId, delta: preflight.text });
400
- writer.write({ type: 'text-end', id: partId });
401
- writer.write({ type: 'finish', finishReason: 'stop' });
402
- return;
403
- }
404
- // Push status: generating
405
- writer.write({
406
- type: 'message-metadata',
407
- messageMetadata: createChatStatusData({
408
- stage: 'answer',
409
- message: t('ai.status.generating', lang),
410
- progress: 60,
411
- }),
412
- });
413
- // Try real provider with stream-level error detection
414
- let streamSuccess = false;
415
- let responseText = '';
416
- let reasoningText;
417
- let tokenUsage;
418
- const generationStart = Date.now();
419
- if (adapter) {
420
- try {
421
- const provider = adapter.getProvider();
422
- const result = streamText({
423
- model: provider.chatModel(adapter.model),
424
- system: systemPrompt,
425
- messages: await convertToModelMessages(messages),
426
- temperature: 0.3,
427
- maxOutputTokens: 2500,
428
- onError: ({ error }) => {
429
- console.error('[chat-handler] streamText error:', error);
430
- },
431
- });
432
- let hasTextOutput = false;
433
- const errors = [];
434
- writer.merge(result.toUIMessageStream({ sendFinish: false }));
435
- await result.consumeStream({
436
- onError: (error) => {
437
- errors.push(error instanceof Error ? error : new Error(String(error)));
438
- },
439
- });
440
- const text = await result.text;
441
- const reasoningPromise = result.reasoning;
442
- const usagePromise = result.usage;
443
- if (reasoningPromise) {
444
- try {
445
- const reasoningOutput = await Promise.resolve(reasoningPromise);
446
- reasoningText = typeof reasoningOutput === 'string' ? reasoningOutput :
447
- (Array.isArray(reasoningOutput) ? reasoningOutput.map((r) => {
448
- if (typeof r === 'object' && r !== null && 'text' in r)
449
- return r.text;
450
- return String(r);
451
- }).join('') : undefined);
452
- }
453
- catch { }
454
- }
455
- if (usagePromise) {
456
- try {
457
- const usage = await Promise.resolve(usagePromise);
458
- const inputTokens = usage.inputTokens ?? 0;
459
- const outputTokens = usage.outputTokens ?? 0;
460
- tokenUsage = {
461
- total: usage.totalTokens ?? inputTokens + outputTokens,
462
- input: inputTokens,
463
- output: outputTokens,
464
- };
465
- }
466
- catch { }
467
- }
468
- timing.generation = Date.now() - generationStart;
469
- responseText = text;
470
- hasTextOutput = text.length > 0;
471
- if (hasTextOutput && errors.length === 0) {
472
- adapter.recordSuccess();
473
- if (shouldAppendCitations(responseText, relatedArticles, relatedProjects)) {
474
- const citations = selectCitations(relatedArticles, relatedProjects, 3, 5);
475
- if (citations.length > 0) {
476
- const citationBlock = formatCitationBlock(citations, lang);
477
- const citationId = `citation-${Date.now()}`;
478
- writer.write({ type: 'text-start', id: citationId });
479
- writer.write({ type: 'text-delta', id: citationId, delta: citationBlock });
480
- writer.write({ type: 'text-end', id: citationId });
481
- responseText += citationBlock;
482
- }
483
- }
484
- writer.write({ type: 'finish', finishReason: 'stop' });
485
- streamSuccess = true;
486
- // Save to response cache if enabled and public question
487
- if (responseCacheConfig.enabled && publicQuestion && (!publicQuestion.needsContext || articleSlug)) {
488
- const globalTTL = getGlobalCacheTTL(publicQuestion.type);
489
- const responseCacheData = {
490
- query: searchQuery,
491
- thinking: reasoningText,
492
- response: text,
493
- articles: relatedArticles,
494
- projects: relatedProjects,
495
- lang,
496
- model: adapter.model,
497
- updatedAt: Date.now(),
498
- };
499
- await setResponseCache(cache, publicQuestion.type, responseCacheData, globalTTL, { articleSlug, lang });
500
- }
501
- }
502
- else if (errors.length > 0) {
503
- adapter.recordFailure(errors[0]);
504
- console.error('[chat-handler] Stream error:', errors[0].message);
505
- const errorId = `error-${Date.now()}`;
506
- writer.write({ type: 'text-start', id: errorId });
507
- writer.write({
508
- type: 'text-delta',
509
- id: errorId,
510
- delta: t('ai.error.generic', lang)
511
- });
512
- writer.write({ type: 'text-end', id: errorId });
513
- writer.write({ type: 'finish', finishReason: 'error' });
514
- streamSuccess = true;
515
- }
516
- else if (!hasTextOutput) {
517
- const noOutputId = `no-output-${Date.now()}`;
518
- writer.write({ type: 'text-start', id: noOutputId });
519
- writer.write({ type: 'text-delta', id: noOutputId, delta: t('ai.error.noOutput', lang) });
520
- writer.write({ type: 'text-end', id: noOutputId });
521
- writer.write({ type: 'finish', finishReason: 'stop' });
522
- streamSuccess = true;
523
- }
524
- else {
525
- writer.write({ type: 'finish', finishReason: 'stop' });
526
- streamSuccess = true;
527
- }
528
- }
529
- catch (err) {
530
- timing.generation = Date.now() - generationStart;
531
- adapter.recordFailure(err instanceof Error ? err : new Error(String(err)));
532
- console.error('[chat-handler] Provider threw:', err.message);
533
- }
534
- }
535
- // Fallback to mock if real provider didn't produce output
536
- if (!streamSuccess) {
537
- const { getMockResponse } = await import('../providers/mock.js');
538
- const mockText = getMockResponse(latestText, lang);
539
- timing.generation = Date.now() - generationStart;
540
- responseText = mockText;
541
- writer.write({
542
- type: 'message-metadata',
543
- messageMetadata: createChatStatusData({
544
- stage: 'answer',
545
- message: t('ai.status.fallback', lang),
546
- progress: 80,
547
- }),
548
- });
549
- const fallbackId = `fallback-${Date.now()}`;
550
- writer.write({ type: 'text-start', id: fallbackId });
551
- writer.write({ type: 'text-delta', id: fallbackId, delta: mockText });
552
- writer.write({ type: 'text-end', id: fallbackId });
553
- writer.write({ type: 'finish', finishReason: 'stop' });
554
- }
555
- // Send notification (fire and forget)
556
- if (responseText) {
557
- const notifyTiming = {
558
- total: Date.now() - timing.start,
559
- keywordExtraction: timing.keywordExtraction,
560
- search: timing.search,
561
- evidenceAnalysis: timing.evidenceAnalysis,
562
- generation: timing.generation,
563
- };
564
- const notifyModel = adapter ? {
565
- name: adapter.model,
566
- provider: env.AI_PROVIDER || undefined,
567
- apiHost: env.AI_BASE_URL || undefined,
568
- } : undefined;
569
- sendNotification({
570
- env,
571
- messages,
572
- responseText,
573
- relatedArticles,
574
- model: notifyModel,
575
- usage: tokenUsage,
576
- timing: notifyTiming,
577
- cacheKey,
578
- waitUntil,
579
- });
580
- }
581
- },
582
- });
583
- return createUIMessageStreamResponse({
584
- stream,
585
- headers: {
586
- 'Access-Control-Allow-Origin': '*',
587
- 'Cache-Control': 'no-cache',
588
- },
589
- });
590
- }
@@ -1,41 +0,0 @@
1
- import { t } from '../utils/i18n.js';
2
- const CORS_HEADERS = {
3
- 'Content-Type': 'application/json',
4
- 'Access-Control-Allow-Origin': '*',
5
- };
6
- export 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
- export const errors = {
23
- methodNotAllowed: (lang) => chatError('METHOD_NOT_ALLOWED', lang === 'en' ? 'Method not allowed' : '方法不允许', 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
- export 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
- }
@@ -1,47 +0,0 @@
1
- import { preloadMetadata, getAuthorContext, getAllSummaries, initArticleIndex, initProjectIndex, } from '../index.js';
2
- let initialized = false;
3
- /**
4
- * Initializes AI metadata: loads summaries, author context, voice profile,
5
- * and builds search indices. Safe to call multiple times (idempotent).
6
- */
7
- export function initializeMetadata(config, env) {
8
- if (initialized)
9
- return;
10
- initialized = true;
11
- preloadMetadata({
12
- summaries: config.summaries,
13
- authorContext: config.authorContext,
14
- voiceProfile: config.voiceProfile,
15
- factRegistry: (config.factRegistry ?? null),
16
- vectorIndex: (config.vectorIndex ?? null),
17
- });
18
- const authorCtx = getAuthorContext();
19
- const allSummaries = getAllSummaries();
20
- const summaryMap = new Map(allSummaries.map(s => [s.slug, s]));
21
- const siteUrl = config.siteUrl ?? env?.SITE_URL ?? '';
22
- const articleDocs = (authorCtx?.posts ?? []).map((post) => {
23
- const summary = summaryMap.get(post.id);
24
- const baseUrl = post.url?.startsWith('http') ? '' : siteUrl;
25
- return {
26
- id: post.id,
27
- title: post.title,
28
- url: post.url ? `${baseUrl}${post.url}` : `${siteUrl}/${post.id}`,
29
- excerpt: post.summary || summary?.summary || '',
30
- content: [...(post.keyPoints ?? []), ...(summary?.keyPoints ?? [])].join(' '),
31
- categories: [post.category].filter(Boolean),
32
- tags: post.tags ?? [],
33
- keyPoints: [...(post.keyPoints ?? []), ...(summary?.keyPoints ?? [])],
34
- dateTime: post.date ? new Date(post.date).getTime() : 0,
35
- lang: post.lang,
36
- summary: summary?.summary,
37
- };
38
- });
39
- initArticleIndex(articleDocs);
40
- initProjectIndex([]);
41
- }
42
- /**
43
- * Resets initialization state (for testing).
44
- */
45
- export function resetMetadataInit() {
46
- initialized = false;
47
- }