@astro-minimax/ai 0.9.0 → 0.9.3

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 (251) hide show
  1. package/README.md +108 -18
  2. package/dist/cache/global-cache.d.ts +6 -2
  3. package/dist/cache/global-cache.d.ts.map +1 -1
  4. package/dist/cache/global-cache.js +24 -9
  5. package/dist/cache/index.d.ts +7 -6
  6. package/dist/cache/index.d.ts.map +1 -1
  7. package/dist/cache/index.js +12 -4
  8. package/dist/cache/injection-cache.d.ts +36 -0
  9. package/dist/cache/injection-cache.d.ts.map +1 -0
  10. package/dist/cache/injection-cache.js +90 -0
  11. package/dist/cache/kv-adapter.d.ts.map +1 -1
  12. package/dist/cache/kv-adapter.js +2 -1
  13. package/dist/cache/memory-adapter.d.ts.map +1 -1
  14. package/dist/cache/memory-adapter.js +2 -1
  15. package/dist/cache/response-cache.d.ts +10 -5
  16. package/dist/cache/response-cache.d.ts.map +1 -1
  17. package/dist/cache/response-cache.js +18 -6
  18. package/dist/components/AIChatContainer.d.ts +2 -2
  19. package/dist/components/AIChatContainer.d.ts.map +1 -1
  20. package/dist/components/AIChatContainer.js +8 -920
  21. package/dist/components/ChatInput.d.ts +15 -0
  22. package/dist/components/ChatInput.d.ts.map +1 -0
  23. package/dist/components/ChatInput.js +72 -0
  24. package/dist/components/ChatPanel.d.ts +1 -1
  25. package/dist/components/ChatPanel.d.ts.map +1 -1
  26. package/dist/components/ChatPanel.js +210 -672
  27. package/dist/components/CodeBlock.d.ts +31 -0
  28. package/dist/components/CodeBlock.d.ts.map +1 -0
  29. package/dist/components/CodeBlock.js +143 -0
  30. package/dist/components/MarkmapBlock.d.ts +4 -0
  31. package/dist/components/MarkmapBlock.d.ts.map +1 -0
  32. package/dist/components/MarkmapBlock.js +180 -0
  33. package/dist/components/MermaidBlock.d.ts +4 -0
  34. package/dist/components/MermaidBlock.d.ts.map +1 -0
  35. package/dist/components/MermaidBlock.js +193 -0
  36. package/dist/components/MessageBubble.d.ts +21 -0
  37. package/dist/components/MessageBubble.d.ts.map +1 -0
  38. package/dist/components/MessageBubble.js +233 -0
  39. package/dist/components/ReasoningBlock.d.ts +6 -0
  40. package/dist/components/ReasoningBlock.d.ts.map +1 -0
  41. package/dist/components/ReasoningBlock.js +11 -0
  42. package/dist/components/RichText.d.ts +41 -0
  43. package/dist/components/RichText.d.ts.map +1 -0
  44. package/dist/components/RichText.js +202 -0
  45. package/dist/components/VizShared.d.ts +57 -0
  46. package/dist/components/VizShared.d.ts.map +1 -0
  47. package/dist/components/VizShared.js +233 -0
  48. package/dist/components/tool-auto-continue.d.ts +5 -0
  49. package/dist/components/tool-auto-continue.d.ts.map +1 -0
  50. package/dist/components/tool-auto-continue.js +33 -0
  51. package/dist/constants.d.ts +61 -0
  52. package/dist/constants.d.ts.map +1 -0
  53. package/dist/constants.js +72 -0
  54. package/dist/data/index.d.ts +4 -3
  55. package/dist/data/index.d.ts.map +1 -1
  56. package/dist/data/index.js +4 -10
  57. package/dist/data/knowledge-types.d.ts +8 -0
  58. package/dist/data/knowledge-types.d.ts.map +1 -0
  59. package/dist/data/knowledge-types.js +14 -0
  60. package/dist/data/metadata-loader.d.ts +4 -28
  61. package/dist/data/metadata-loader.d.ts.map +1 -1
  62. package/dist/data/metadata-loader.js +11 -34
  63. package/dist/data/types.d.ts +17 -2
  64. package/dist/data/types.d.ts.map +1 -1
  65. package/dist/extensions/index.d.ts +5 -0
  66. package/dist/extensions/index.d.ts.map +1 -0
  67. package/dist/extensions/index.js +24 -0
  68. package/dist/extensions/injector.d.ts +14 -0
  69. package/dist/extensions/injector.d.ts.map +1 -0
  70. package/dist/extensions/injector.js +146 -0
  71. package/dist/extensions/loader.d.ts +5 -0
  72. package/dist/extensions/loader.d.ts.map +1 -0
  73. package/dist/extensions/loader.js +45 -0
  74. package/dist/extensions/registry.d.ts +4 -0
  75. package/dist/extensions/registry.d.ts.map +1 -0
  76. package/dist/extensions/registry.js +144 -0
  77. package/dist/extensions/types.d.ts +126 -0
  78. package/dist/extensions/types.d.ts.map +1 -0
  79. package/dist/extensions/types.js +0 -0
  80. package/dist/fact-registry/prompt-injector.d.ts +1 -1
  81. package/dist/fact-registry/prompt-injector.d.ts.map +1 -1
  82. package/dist/fact-registry/prompt-injector.js +2 -1
  83. package/dist/index.d.ts +3 -2
  84. package/dist/index.d.ts.map +1 -1
  85. package/dist/index.js +3 -2
  86. package/dist/intelligence/citation-guard.d.ts +2 -13
  87. package/dist/intelligence/citation-guard.d.ts.map +1 -1
  88. package/dist/intelligence/citation-guard.js +52 -23
  89. package/dist/intelligence/evidence-analysis.d.ts +24 -16
  90. package/dist/intelligence/evidence-analysis.d.ts.map +1 -1
  91. package/dist/intelligence/evidence-analysis.js +118 -20
  92. package/dist/intelligence/evidence-budget.d.ts +13 -0
  93. package/dist/intelligence/evidence-budget.d.ts.map +1 -0
  94. package/dist/intelligence/evidence-budget.js +49 -0
  95. package/dist/intelligence/index.d.ts +10 -4
  96. package/dist/intelligence/index.d.ts.map +1 -1
  97. package/dist/intelligence/index.js +27 -3
  98. package/dist/intelligence/keyword-extract.d.ts +1 -1
  99. package/dist/intelligence/keyword-extract.d.ts.map +1 -1
  100. package/dist/intelligence/keyword-extract.js +5 -9
  101. package/dist/intelligence/request-interpretation.d.ts +40 -0
  102. package/dist/intelligence/request-interpretation.d.ts.map +1 -0
  103. package/dist/intelligence/request-interpretation.js +71 -0
  104. package/dist/intelligence/response-templates.d.ts +1 -0
  105. package/dist/intelligence/response-templates.d.ts.map +1 -1
  106. package/dist/intelligence/response-templates.js +13 -0
  107. package/dist/prompt/dynamic-layer.d.ts +1 -5
  108. package/dist/prompt/dynamic-layer.d.ts.map +1 -1
  109. package/dist/prompt/dynamic-layer.js +145 -9
  110. package/dist/prompt/prompt-builder.d.ts +1 -1
  111. package/dist/prompt/prompt-builder.d.ts.map +1 -1
  112. package/dist/prompt/prompt-builder.js +5 -1
  113. package/dist/prompt/semi-static-layer.d.ts +1 -1
  114. package/dist/prompt/semi-static-layer.d.ts.map +1 -1
  115. package/dist/prompt/semi-static-layer.js +22 -12
  116. package/dist/prompt/static-layer.d.ts.map +1 -1
  117. package/dist/prompt/static-layer.js +37 -4
  118. package/dist/prompt/types.d.ts +9 -4
  119. package/dist/prompt/types.d.ts.map +1 -1
  120. package/dist/provider-manager/base.d.ts +5 -1
  121. package/dist/provider-manager/base.d.ts.map +1 -1
  122. package/dist/provider-manager/base.js +22 -2
  123. package/dist/provider-manager/config.d.ts.map +1 -1
  124. package/dist/provider-manager/config.js +3 -2
  125. package/dist/provider-manager/index.d.ts +1 -1
  126. package/dist/provider-manager/index.d.ts.map +1 -1
  127. package/dist/provider-manager/index.js +1 -2
  128. package/dist/provider-manager/manager.d.ts +10 -1
  129. package/dist/provider-manager/manager.d.ts.map +1 -1
  130. package/dist/provider-manager/manager.js +26 -10
  131. package/dist/provider-manager/openai.d.ts +2 -2
  132. package/dist/provider-manager/openai.d.ts.map +1 -1
  133. package/dist/provider-manager/openai.js +19 -4
  134. package/dist/provider-manager/types.d.ts +18 -38
  135. package/dist/provider-manager/types.d.ts.map +1 -1
  136. package/dist/provider-manager/workers.d.ts +2 -2
  137. package/dist/provider-manager/workers.d.ts.map +1 -1
  138. package/dist/provider-manager/workers.js +15 -4
  139. package/dist/query/followup.d.ts +7 -0
  140. package/dist/query/followup.d.ts.map +1 -0
  141. package/dist/query/followup.js +46 -0
  142. package/dist/query/intent.d.ts +6 -0
  143. package/dist/query/intent.d.ts.map +1 -0
  144. package/dist/query/intent.js +137 -0
  145. package/dist/query/types.d.ts +8 -0
  146. package/dist/query/types.d.ts.map +1 -0
  147. package/dist/query/types.js +0 -0
  148. package/dist/search/hybrid-search.d.ts +111 -0
  149. package/dist/search/hybrid-search.d.ts.map +1 -0
  150. package/dist/search/hybrid-search.js +326 -0
  151. package/dist/search/index.d.ts +11 -9
  152. package/dist/search/index.d.ts.map +1 -1
  153. package/dist/search/index.js +46 -10
  154. package/dist/search/scoring.d.ts +18 -0
  155. package/dist/search/scoring.d.ts.map +1 -0
  156. package/dist/search/{search-utils.js → scoring.js} +14 -27
  157. package/dist/search/search-api.d.ts +16 -1
  158. package/dist/search/search-api.d.ts.map +1 -1
  159. package/dist/search/search-api.js +118 -15
  160. package/dist/search/search-index.d.ts +2 -2
  161. package/dist/search/search-index.d.ts.map +1 -1
  162. package/dist/search/search-index.js +4 -2
  163. package/dist/search/session-cache.d.ts +4 -10
  164. package/dist/search/session-cache.d.ts.map +1 -1
  165. package/dist/search/session-cache.js +12 -45
  166. package/dist/search/types.d.ts +28 -0
  167. package/dist/search/types.d.ts.map +1 -1
  168. package/dist/search/vector-reranker.d.ts +3 -3
  169. package/dist/search/vector-reranker.d.ts.map +1 -1
  170. package/dist/search/vector-reranker.js +14 -2
  171. package/dist/server/chat-handler.d.ts +86 -1
  172. package/dist/server/chat-handler.d.ts.map +1 -1
  173. package/dist/server/chat-handler.js +835 -401
  174. package/dist/server/chat-message-utils.d.ts +6 -0
  175. package/dist/server/chat-message-utils.d.ts.map +1 -0
  176. package/dist/server/chat-message-utils.js +40 -0
  177. package/dist/server/chat-utils.d.ts +30 -0
  178. package/dist/server/chat-utils.d.ts.map +1 -0
  179. package/dist/server/chat-utils.js +88 -0
  180. package/dist/server/dev-server.js +238 -101
  181. package/dist/server/env-config.d.ts +22 -0
  182. package/dist/server/env-config.d.ts.map +1 -0
  183. package/dist/server/env-config.js +25 -0
  184. package/dist/server/errors.d.ts +1 -0
  185. package/dist/server/errors.d.ts.map +1 -1
  186. package/dist/server/errors.js +14 -7
  187. package/dist/server/index.d.ts +2 -4
  188. package/dist/server/index.d.ts.map +1 -1
  189. package/dist/server/index.js +4 -25
  190. package/dist/server/metadata-init.d.ts +10 -5
  191. package/dist/server/metadata-init.d.ts.map +1 -1
  192. package/dist/server/metadata-init.js +78 -34
  193. package/dist/server/notify.d.ts +12 -11
  194. package/dist/server/notify.d.ts.map +1 -1
  195. package/dist/server/notify.js +46 -48
  196. package/dist/server/prompt-runtime.d.ts +60 -0
  197. package/dist/server/prompt-runtime.d.ts.map +1 -0
  198. package/dist/server/prompt-runtime.js +284 -0
  199. package/dist/server/stream-helpers.d.ts +30 -16
  200. package/dist/server/stream-helpers.d.ts.map +1 -1
  201. package/dist/server/stream-helpers.js +152 -15
  202. package/dist/server/types.d.ts +47 -12
  203. package/dist/server/types.d.ts.map +1 -1
  204. package/dist/structured-output/generator.d.ts +6 -0
  205. package/dist/structured-output/generator.d.ts.map +1 -0
  206. package/dist/structured-output/generator.js +164 -0
  207. package/dist/structured-output/index.d.ts +4 -0
  208. package/dist/structured-output/index.d.ts.map +1 -0
  209. package/dist/structured-output/index.js +6 -0
  210. package/dist/structured-output/schemas/evidence.d.ts +88 -0
  211. package/dist/structured-output/schemas/evidence.d.ts.map +1 -0
  212. package/dist/structured-output/schemas/evidence.js +65 -0
  213. package/dist/structured-output/types.d.ts +69 -0
  214. package/dist/structured-output/types.d.ts.map +1 -0
  215. package/dist/structured-output/types.js +0 -0
  216. package/dist/tools/action-tools.d.ts +63 -0
  217. package/dist/tools/action-tools.d.ts.map +1 -0
  218. package/dist/tools/action-tools.js +158 -0
  219. package/dist/tools/index.d.ts +2 -0
  220. package/dist/tools/index.d.ts.map +1 -0
  221. package/dist/tools/index.js +30 -0
  222. package/dist/utils/i18n.d.ts +1 -1
  223. package/dist/utils/i18n.d.ts.map +1 -1
  224. package/dist/utils/i18n.js +1 -1
  225. package/dist/utils/logger.d.ts +11 -0
  226. package/dist/utils/logger.d.ts.map +1 -0
  227. package/dist/utils/logger.js +36 -0
  228. package/dist/utils/text.d.ts +11 -0
  229. package/dist/utils/text.d.ts.map +1 -0
  230. package/dist/utils/text.js +87 -0
  231. package/dist/utils/url.d.ts +19 -0
  232. package/dist/utils/url.d.ts.map +1 -0
  233. package/dist/utils/url.js +13 -0
  234. package/package.json +46 -12
  235. package/dist/intelligence/intent-detect.d.ts +0 -40
  236. package/dist/intelligence/intent-detect.d.ts.map +0 -1
  237. package/dist/intelligence/intent-detect.js +0 -93
  238. package/dist/providers/index.d.ts +0 -2
  239. package/dist/providers/index.d.ts.map +0 -1
  240. package/dist/providers/index.js +0 -5
  241. package/dist/search/search-utils.d.ts +0 -47
  242. package/dist/search/search-utils.d.ts.map +0 -1
  243. package/dist/stream/index.d.ts +0 -3
  244. package/dist/stream/index.d.ts.map +0 -1
  245. package/dist/stream/index.js +0 -8
  246. package/dist/stream/mock-stream.d.ts +0 -12
  247. package/dist/stream/mock-stream.d.ts.map +0 -1
  248. package/dist/stream/mock-stream.js +0 -26
  249. package/dist/stream/response.d.ts +0 -10
  250. package/dist/stream/response.d.ts.map +0 -1
  251. package/dist/stream/response.js +0 -21
@@ -1,139 +1,97 @@
1
1
  import {
2
2
  createUIMessageStream,
3
- createUIMessageStreamResponse,
4
- streamText,
5
- convertToModelMessages
3
+ createUIMessageStreamResponse
6
4
  } from "ai";
7
5
  import { t, getLang } from "../utils/i18n.js";
8
6
  import {
9
- getClientIP,
10
- checkRateLimit,
11
- rateLimitResponse,
12
- searchArticles,
13
- searchProjects,
14
- getSessionCacheKey,
15
- getCachedContext,
16
- setCachedContext,
17
- shouldReuseSearchContext,
18
7
  buildLocalSearchQuery,
19
8
  shouldRunKeywordExtraction,
20
9
  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,
10
+ rankArticlesByCategory,
11
+ applyBudgetToArticles,
12
+ shouldAppendCitations,
13
+ selectCitations,
14
+ formatCitationBlock,
15
+ resolveSearchInterpretation
16
+ } from "../intelligence/index.js";
17
+ import {
30
18
  mergeResults,
31
- getProviderManager,
19
+ searchArticles,
20
+ searchProjects,
21
+ getSessionCacheKey,
22
+ getCachedContext,
23
+ setCachedContext
24
+ } from "../search/index.js";
25
+ import {
26
+ extractCodeAnchors,
27
+ hasCodeAnchors,
28
+ normalizeText,
29
+ tokenize
30
+ } from "../utils/text.js";
31
+ import { getProviderManager } from "../provider-manager/index.js";
32
+ import {
32
33
  createCacheAdapter,
33
- detectPublicQuestion,
34
34
  getGlobalSearchCache,
35
- shouldAppendCitations,
36
- formatCitationBlock,
37
- selectCitations,
38
35
  setGlobalSearchCache,
39
36
  getGlobalCacheTTL,
40
37
  getResponseCache,
41
38
  setResponseCache,
42
39
  getResponseCacheConfig,
43
- rankArticlesByIntent,
44
- matchFactsToQuery,
45
- buildFactSection
46
- } from "../index.js";
40
+ detectPublicQuestion,
41
+ normalizePublicCacheQuery
42
+ } from "../cache/index.js";
43
+ import {
44
+ getClientIP,
45
+ checkRateLimit,
46
+ rateLimitResponse
47
+ } from "../middleware/index.js";
48
+ import {
49
+ getExtensionRegistry,
50
+ getSemanticFallback,
51
+ mergeSearchDocuments
52
+ } from "../extensions/index.js";
53
+ import { initializeExtensions, areExtensionsLoaded } from "./metadata-init.js";
47
54
  import { createChatStatusData } from "./types.js";
48
- import { errors, corsPreflightResponse } from "./errors.js";
49
- import { notifyAiChat } from "./notify.js";
55
+ import { errors, corsPreflightResponse, setCorsOrigin } from "./errors.js";
50
56
  import {
51
57
  writeSearchStatus,
52
58
  writeGeneratingStatus,
53
59
  writeSourceArticles,
54
- streamLLMResponse,
60
+ writeSourceSnippets,
61
+ streamAnswerWithFallback,
55
62
  streamMockFallback,
56
- streamCachedResponse
63
+ streamCachedResponse,
64
+ writeTextChunk,
65
+ writeFinish
57
66
  } 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
- }
67
+ import {
68
+ getMessageText,
69
+ filterValidMessages,
70
+ getLatestUserText
71
+ } from "./chat-message-utils.js";
72
+ import {
73
+ sendNotification,
74
+ getTimeoutConfig,
75
+ getHealthConfig
76
+ } from "./chat-utils.js";
77
+ import {
78
+ assemblePromptRuntime
79
+ } from "./prompt-runtime.js";
80
+ import { CHAT_HANDLER, RESPONSE } from "../constants.js";
81
+ import { createLogger, setLogLevel } from "../utils/logger.js";
82
+ import { getAllTools } from "../tools/index.js";
83
+ const log = createLogger("chat-handler");
131
84
  async function handleChatRequest(options) {
132
85
  const { env, request: req, waitUntil } = options;
86
+ if (env.AI_DEBUG) setLogLevel("debug");
87
+ if (env.CORS_ORIGIN) setCorsOrigin(env.CORS_ORIGIN);
133
88
  if (req.method === "OPTIONS") return corsPreflightResponse();
134
89
  if (req.method !== "POST") return errors.methodNotAllowed("zh");
135
90
  const ip = getClientIP(req);
136
- const rateCheck = checkRateLimit(ip, env);
91
+ const rateCheck = checkRateLimit(
92
+ ip,
93
+ env
94
+ );
137
95
  if (!rateCheck.allowed) return rateLimitResponse(rateCheck, "zh");
138
96
  let body;
139
97
  try {
@@ -143,116 +101,349 @@ async function handleChatRequest(options) {
143
101
  }
144
102
  const lang = getLang(body.lang ?? env.SITE_LANG);
145
103
  const context = body.context ?? { scope: "global" };
146
- const rawMessages = (body.messages ?? []).slice(-MAX_HISTORY_MESSAGES);
104
+ const rawMessages = (body.messages ?? []).slice(
105
+ -CHAT_HANDLER.MAX_HISTORY_MESSAGES
106
+ );
147
107
  if (!rawMessages.length) return errors.emptyMessage(lang);
148
108
  const messages = filterValidMessages(rawMessages);
149
109
  if (!messages.length) return errors.emptyMessage(lang);
150
110
  const latestMessage = messages[messages.length - 1];
151
- const latestText = getMessageText(latestMessage);
111
+ const latestText = latestMessage.role === "user" ? getMessageText(latestMessage) : getLatestUserText(messages);
152
112
  if (!latestText) return errors.emptyContent(lang);
153
- if (latestText.length > MAX_INPUT_LENGTH) return errors.inputTooLong(MAX_INPUT_LENGTH, lang);
113
+ if (latestText.length > CHAT_HANDLER.MAX_INPUT_LENGTH)
114
+ return errors.inputTooLong(CHAT_HANDLER.MAX_INPUT_LENGTH, lang);
115
+ const timeouts = getTimeoutConfig(env);
154
116
  const requestAbort = new AbortController();
155
- const requestTimer = setTimeout(() => requestAbort.abort(), REQUEST_TIMEOUT_MS);
117
+ const requestTimer = setTimeout(() => requestAbort.abort(), timeouts.request);
118
+ log.debug(
119
+ `Request: scope=${context.scope}, msg="${latestText.substring(0, 80)}", history=${messages.length}`
120
+ );
156
121
  try {
157
- return await runPipeline({ env, messages, latestText, context, req, requestAbort, lang, waitUntil });
122
+ return await runPipeline({
123
+ env,
124
+ messages,
125
+ latestText,
126
+ context,
127
+ req,
128
+ requestAbort,
129
+ lang,
130
+ waitUntil,
131
+ timeouts
132
+ });
158
133
  } catch (err) {
159
134
  if (requestAbort.signal.aborted) return errors.timeout(lang);
160
- console.error("[chat-handler] Unexpected error:", err);
135
+ log.error("Unexpected error:", err);
161
136
  return errors.internal(void 0, lang);
162
137
  } finally {
163
138
  clearTimeout(requestTimer);
164
139
  }
165
140
  }
166
- async function runPipeline(args) {
167
- const { env, messages, latestText, context, req, lang, waitUntil } = args;
141
+ async function initializeContext(args) {
142
+ const { env, messages, latestText, context, req, lang, timeouts } = args;
168
143
  const timing = { start: Date.now() };
169
144
  const cache = createCacheAdapter(env);
170
145
  const responseCacheConfig = getResponseCacheConfig(env);
146
+ const healthConfig = getHealthConfig(env);
171
147
  const manager = getProviderManager(env, {
172
148
  enableMockFallback: true,
173
- unhealthyThreshold: 3,
174
- healthRecoveryTTL: 6e4
149
+ unhealthyThreshold: healthConfig.unhealthyThreshold,
150
+ healthRecoveryTTL: healthConfig.recoveryTtl
175
151
  });
176
152
  const hasRealProvider = manager.hasProviders();
177
- const adapter = hasRealProvider ? await manager.getAvailableAdapter() : null;
153
+ const adapters = hasRealProvider ? await manager.getAvailableAdapters() : [];
154
+ const adapter = adapters[0] ?? null;
155
+ if (!areExtensionsLoaded()) {
156
+ await initializeExtensions();
157
+ }
158
+ const extensions = getExtensionRegistry().getLoadedExtensions();
178
159
  const articleSlug = context.scope === "article" && context.article?.slug ? context.article.slug : void 0;
179
160
  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
- }
161
+ const cacheKey = getSessionCacheKey(req);
162
+ return {
163
+ env,
164
+ messages,
165
+ latestText,
166
+ context,
167
+ lang,
168
+ timeouts,
169
+ timing,
170
+ cache,
171
+ responseCacheConfig,
172
+ adapters,
173
+ adapter,
174
+ hasRealProvider,
175
+ extensions,
176
+ articleSlug,
177
+ publicQuestion,
178
+ cacheKey
179
+ };
180
+ }
181
+ async function analyzeAndBuildPrompt(ctx, search) {
182
+ return assemblePromptRuntime({
183
+ env: ctx.env,
184
+ latestText: ctx.latestText,
185
+ context: ctx.context,
186
+ lang: ctx.lang,
187
+ evidenceAnalysisTimeout: ctx.timeouts.evidenceAnalysis,
188
+ timing: {
189
+ total: Date.now() - ctx.timing.start,
190
+ keywordExtraction: ctx.timing.keywordExtraction,
191
+ search: ctx.timing.search,
192
+ evidenceAnalysis: ctx.timing.evidenceAnalysis,
193
+ generation: ctx.timing.generation
194
+ },
195
+ adapter: ctx.adapter,
196
+ hasRealProvider: ctx.hasRealProvider,
197
+ extensions: ctx.extensions,
198
+ cacheKey: ctx.cacheKey,
199
+ searchQuery: search.searchQuery,
200
+ relatedArticles: search.relatedArticles,
201
+ relatedProjects: search.relatedProjects,
202
+ budget: search.budget,
203
+ answerMode: search.interpretation.answer.contract
204
+ });
205
+ }
206
+ function resolveSearchAnswerShaping(query) {
207
+ const { interpretation, budget } = resolveSearchInterpretation({
208
+ latestText: query
209
+ });
210
+ return { interpretation, budget };
211
+ }
212
+ function rankArticlesForQuery(query, articles) {
213
+ const { interpretation } = resolveSearchInterpretation({ latestText: query });
214
+ return rankArticlesByCategory(interpretation.topic.primary, articles);
215
+ }
216
+ const MIN_QUOTED_QUERY_LENGTH = 12;
217
+ const CURRENT_ARTICLE_SCORE_BOOST_RATIO = 0.12;
218
+ const CURRENT_ARTICLE_SCORE_CAP_RATIO = 1.08;
219
+ const CROSS_ARTICLE_INTENT_PATTERNS = [
220
+ /还有哪些/u,
221
+ /相关文章/u,
222
+ /类似/u,
223
+ /推荐/u,
224
+ /对比/u,
225
+ /比较/u,
226
+ /related/u,
227
+ /similar/u,
228
+ /compare/u,
229
+ /comparison/u,
230
+ /recommend/u,
231
+ /what else/u
232
+ ];
233
+ function extractQuotedCandidate(text) {
234
+ const trimmed = text.trim();
235
+ if (!trimmed) return "";
236
+ const matches = [...trimmed.matchAll(/["“”'‘’「」『』《》](.+?)["“”'‘’「」『』《》]/g)].map((match) => match[1]?.trim() ?? "").filter(Boolean).sort((a, b) => b.length - a.length);
237
+ return matches[0] ?? "";
238
+ }
239
+ function isLikelyQuotedArticleQuery(text) {
240
+ const quoted = extractQuotedCandidate(text);
241
+ if (!quoted) return false;
242
+ return normalizeText(quoted).length >= MIN_QUOTED_QUERY_LENGTH;
243
+ }
244
+ function isCrossArticleIntent(text) {
245
+ if (isLikelyQuotedArticleQuery(text)) {
246
+ return false;
247
+ }
248
+ const normalized = normalizeText(text);
249
+ if (!normalized) return false;
250
+ return CROSS_ARTICLE_INTENT_PATTERNS.some((pattern) => pattern.test(normalized));
251
+ }
252
+ function isCurrentArticle(article, articleSlug) {
253
+ return article.id === articleSlug || article.url?.includes(articleSlug);
254
+ }
255
+ function buildCurrentArticleFallback(context, articleSlug, tailScore) {
256
+ if (context?.scope !== "article" || !context.article) return null;
257
+ return {
258
+ id: articleSlug,
259
+ title: context.article.title,
260
+ url: `/posts/${articleSlug}`,
261
+ summary: context.article.summary ?? context.article.abstract,
262
+ keyPoints: context.article.keyPoints ?? [],
263
+ categories: context.article.categories ?? [],
264
+ dateTime: 0,
265
+ score: tailScore
266
+ };
267
+ }
268
+ function rerankArticlesForCurrentArticleQuote(query, articles, options = {}) {
269
+ const { articleSlug, context } = options;
270
+ if (!articleSlug || context?.scope !== "article") return articles;
271
+ if (!isLikelyQuotedArticleQuery(query)) return articles;
272
+ if (isCrossArticleIntent(query)) return articles;
273
+ const cloned = [...articles];
274
+ const currentIndex = cloned.findIndex(
275
+ (article) => isCurrentArticle(article, articleSlug)
276
+ );
277
+ if (currentIndex >= 0) {
278
+ const topScore = cloned[0]?.score ?? 0;
279
+ if (topScore <= 0) return cloned;
280
+ const current = cloned[currentIndex];
281
+ const boostedScore = Math.min(
282
+ (current.score ?? 0) + topScore * CURRENT_ARTICLE_SCORE_BOOST_RATIO,
283
+ topScore * CURRENT_ARTICLE_SCORE_CAP_RATIO
284
+ );
285
+ cloned[currentIndex] = { ...current, score: boostedScore };
286
+ return cloned.sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
287
+ }
288
+ const tailScore = Math.max((cloned[cloned.length - 1]?.score ?? 1) * 0.95, 0.01);
289
+ const fallback = buildCurrentArticleFallback(context, articleSlug, tailScore);
290
+ if (!fallback) return cloned;
291
+ return [...cloned, fallback];
292
+ }
293
+ function shapeArticlesForQuery(query, articles, options = {}) {
294
+ const { interpretation, budget } = resolveSearchInterpretation({
295
+ latestText: query
296
+ });
297
+ const boostedArticles = rerankArticlesForCurrentArticleQuote(
298
+ query,
299
+ articles,
300
+ options
301
+ );
302
+ const rankedArticles = hasCodeAnchors(query) ? rerankArticlesForCodeAnchors(query, boostedArticles) : rankArticlesByCategory(interpretation.topic.primary, boostedArticles);
303
+ return {
304
+ interpretation,
305
+ budget,
306
+ articles: applyBudgetToArticles(rankedArticles, budget)
307
+ };
308
+ }
309
+ function rerankArticlesForCodeAnchors(query, articles) {
310
+ const rawAnchors = extractCodeAnchors(query);
311
+ if (rawAnchors.length === 0 || articles.length <= 1) return articles;
312
+ return [...articles].sort((a, b) => {
313
+ const aAnchorScore = scoreArticleForCodeAnchors(a, rawAnchors);
314
+ const bAnchorScore = scoreArticleForCodeAnchors(b, rawAnchors);
315
+ return bAnchorScore - aAnchorScore || (b.score ?? 0) - (a.score ?? 0) || a.title.localeCompare(b.title);
316
+ });
317
+ }
318
+ function scoreArticleForCodeAnchors(article, rawAnchors) {
319
+ const title = article.title;
320
+ const summary = article.summary ?? "";
321
+ const keyPoints = article.keyPoints ?? [];
322
+ const chunks = article.chunks ?? [];
323
+ let score = 0;
324
+ for (const anchor of rawAnchors) {
325
+ const normalizedAnchor = normalizeText(anchor);
326
+ if (!normalizedAnchor) continue;
327
+ if (title.includes(anchor)) {
328
+ score += 10;
329
+ continue;
198
330
  }
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" } });
331
+ if (summary.includes(anchor)) {
332
+ score += 6;
333
+ continue;
334
+ }
335
+ if (keyPoints.some((point) => point.includes(anchor))) {
336
+ score += 5;
337
+ continue;
338
+ }
339
+ if (chunks.some((chunk) => chunk.heading.includes(anchor))) {
340
+ score += 8;
341
+ continue;
342
+ }
343
+ if (chunks.some((chunk) => chunk.content.includes(anchor))) {
344
+ score += 9;
345
+ continue;
346
+ }
347
+ const normalizedTitle = normalizeText(title);
348
+ const normalizedSummary = normalizeText(summary);
349
+ const normalizedKeyPoints = keyPoints.map((point) => normalizeText(point));
350
+ if (normalizedTitle.includes(normalizedAnchor)) {
351
+ score += 6;
352
+ } else if (normalizedSummary.includes(normalizedAnchor)) {
353
+ score += 4;
354
+ } else if (normalizedKeyPoints.some((point) => point.includes(normalizedAnchor))) {
355
+ score += 3;
356
+ } else if (chunks.some(
357
+ (chunk) => normalizeText(chunk.heading).includes(normalizedAnchor) || normalizeText(chunk.content).includes(normalizedAnchor)
358
+ )) {
359
+ score += 5;
243
360
  }
244
361
  }
245
- const cacheKey = getSessionCacheKey(req);
362
+ return score;
363
+ }
364
+ function shouldPersistAuthoritativeSources(sources) {
365
+ return sources.length > 0 && sources.every(
366
+ (source) => source.reason === "chunk" || source.reason === "evidence" || source.reason === "article-context"
367
+ );
368
+ }
369
+ function shapeCachedSearchForQuery(args) {
370
+ const shaped = shapeArticlesForQuery(args.query, args.articles);
371
+ return {
372
+ interpretation: shaped.interpretation,
373
+ budget: shaped.budget,
374
+ articles: shaped.articles,
375
+ projects: args.projects
376
+ };
377
+ }
378
+ function shouldPersistResponseCacheEntry(args) {
379
+ return args.enabled && args.success && args.responseText.length > 0 && shouldPersistAuthoritativeSources(args.sources);
380
+ }
381
+ function shouldUsePublicQuestionCaches(args) {
382
+ return Boolean(args.publicQuestion && (!args.publicQuestion.needsContext || args.articleSlug));
383
+ }
384
+ function buildPublicCacheContext(args) {
385
+ return {
386
+ articleSlug: args.articleSlug,
387
+ lang: args.lang,
388
+ queryKey: normalizePublicCacheQuery(args.latestText)
389
+ };
390
+ }
391
+ function shapePublicCacheBranch(args) {
392
+ return {
393
+ enabled: shouldUsePublicQuestionCaches({
394
+ publicQuestion: args.publicQuestion,
395
+ articleSlug: args.articleSlug
396
+ }),
397
+ context: buildPublicCacheContext({
398
+ articleSlug: args.articleSlug,
399
+ lang: args.lang,
400
+ latestText: args.latestText
401
+ })
402
+ };
403
+ }
404
+ async function retrieveContext(ctx, req) {
405
+ const {
406
+ messages,
407
+ latestText,
408
+ cache,
409
+ timeouts,
410
+ timing,
411
+ hasRealProvider,
412
+ adapter,
413
+ extensions,
414
+ publicQuestion,
415
+ articleSlug,
416
+ cacheKey,
417
+ lang
418
+ } = ctx;
246
419
  const now = Date.now();
247
420
  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 });
421
+ const userTurnCount = messages.filter(
422
+ (m) => m.role === "user"
423
+ ).length;
424
+ const initialSearchInterpretation = resolveSearchInterpretation({
425
+ latestText,
426
+ cachedContext,
427
+ userTurnCount,
428
+ now
429
+ });
430
+ const reuseContext = initialSearchInterpretation.interpretation.conversation.shouldReuseContext;
250
431
  let searchQuery = buildLocalSearchQuery(latestText) || latestText;
251
432
  let relatedArticles = reuseContext && cachedContext ? cachedContext.articles : [];
252
433
  let relatedProjects = reuseContext && cachedContext ? cachedContext.projects : [];
434
+ let { budget } = initialSearchInterpretation;
435
+ let { interpretation } = initialSearchInterpretation;
436
+ const semanticFallback = getSemanticFallback(latestText, extensions);
437
+ if (semanticFallback) {
438
+ searchQuery = semanticFallback.query;
439
+ }
253
440
  if (reuseContext && cachedContext && cacheKey) {
254
441
  searchQuery = cachedContext.query;
255
- await setCachedContext(cacheKey, { ...cachedContext, updatedAt: now }, cache);
442
+ await setCachedContext(
443
+ cacheKey,
444
+ { ...cachedContext, updatedAt: now },
445
+ cache
446
+ );
256
447
  } else {
257
448
  if (hasRealProvider && adapter) {
258
449
  const runKW = shouldRunKeywordExtraction({
@@ -263,7 +454,10 @@ async function runPipeline(args) {
263
454
  if (runKW) {
264
455
  const kwStart = Date.now();
265
456
  const abortCtrl = new AbortController();
266
- const timeoutId = setTimeout(() => abortCtrl.abort(), KEYWORD_EXTRACTION_TIMEOUT_MS);
457
+ const timeoutId = setTimeout(
458
+ () => abortCtrl.abort(),
459
+ timeouts.keywordExtraction
460
+ );
267
461
  try {
268
462
  const provider = adapter.getProvider();
269
463
  const kwResult = await extractSearchKeywords({
@@ -277,7 +471,9 @@ async function runPipeline(args) {
277
471
  searchQuery = kwResult.query;
278
472
  if (kwResult.primaryQuery && kwResult.primaryQuery !== searchQuery) {
279
473
  const searchStart = Date.now();
280
- const primary = searchArticles(kwResult.primaryQuery, { enableDeepContent: false });
474
+ const primary = searchArticles(kwResult.primaryQuery, {
475
+ enableDeepContent: false
476
+ });
281
477
  relatedArticles = mergeResults(
282
478
  searchArticles(searchQuery, { enableDeepContent: true }),
283
479
  primary
@@ -286,8 +482,12 @@ async function runPipeline(args) {
286
482
  timing.search = Date.now() - searchStart;
287
483
  }
288
484
  }
289
- } catch {
485
+ } catch (err) {
290
486
  timing.keywordExtraction = Date.now() - kwStart;
487
+ console.debug(
488
+ "[chat-handler] Keyword extraction failed, using local query:",
489
+ err.message
490
+ );
291
491
  } finally {
292
492
  clearTimeout(timeoutId);
293
493
  }
@@ -295,20 +495,31 @@ async function runPipeline(args) {
295
495
  }
296
496
  if (!relatedArticles.length) {
297
497
  const searchStart = Date.now();
298
- relatedArticles = searchArticles(searchQuery, { enableDeepContent: true });
498
+ relatedArticles = searchArticles(searchQuery, {
499
+ enableDeepContent: true
500
+ });
299
501
  relatedProjects = searchProjects(searchQuery);
300
502
  timing.search = Date.now() - searchStart;
301
503
  }
302
- relatedArticles = rankArticlesByIntent(latestText, relatedArticles);
303
504
  if (cacheKey) {
304
- await setCachedContext(cacheKey, {
305
- query: searchQuery,
306
- articles: relatedArticles,
307
- projects: relatedProjects,
308
- updatedAt: now
309
- }, cache);
505
+ await setCachedContext(
506
+ cacheKey,
507
+ {
508
+ query: searchQuery,
509
+ articles: relatedArticles,
510
+ projects: relatedProjects,
511
+ updatedAt: now
512
+ },
513
+ cache
514
+ );
310
515
  }
311
- if (publicQuestion && (!publicQuestion.needsContext || articleSlug)) {
516
+ const publicCacheBranch = shapePublicCacheBranch({
517
+ publicQuestion,
518
+ articleSlug,
519
+ lang,
520
+ latestText
521
+ });
522
+ if (publicCacheBranch.enabled && publicQuestion) {
312
523
  const globalTTL = getGlobalCacheTTL(publicQuestion.type);
313
524
  await setGlobalSearchCache(
314
525
  cache,
@@ -320,67 +531,223 @@ async function runPipeline(args) {
320
531
  updatedAt: now
321
532
  },
322
533
  globalTTL,
323
- { articleSlug, lang }
534
+ publicCacheBranch.context
324
535
  );
325
536
  }
326
537
  }
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
538
+ relatedArticles = mergeSearchDocuments(relatedArticles, extensions);
539
+ ({ interpretation, budget, articles: relatedArticles } = shapeArticlesForQuery(
540
+ latestText,
541
+ relatedArticles,
542
+ { articleSlug, context: ctx.context }
543
+ ));
544
+ log.debug(
545
+ `Search: query="${searchQuery}", articles=${relatedArticles.length}, projects=${relatedProjects.length}, mode=${interpretation.answer.contract}`
546
+ );
547
+ if (relatedArticles.length > 0) {
548
+ log.debug(
549
+ `Top articles: ${relatedArticles.slice(0, 3).map(
550
+ (a) => `"${a.title}" (chunks: ${a.chunks?.length ?? 0})`
551
+ ).join(", ")}`
552
+ );
553
+ }
554
+ return { searchQuery, relatedArticles, relatedProjects, budget, interpretation };
555
+ }
556
+ async function runPipeline(args) {
557
+ const ctx = await initializeContext(args);
558
+ const {
559
+ env,
560
+ messages,
561
+ latestText,
562
+ context,
563
+ lang,
564
+ timeouts,
565
+ timing,
566
+ cache,
567
+ responseCacheConfig,
568
+ adapters,
569
+ adapter,
570
+ hasRealProvider,
571
+ extensions,
572
+ articleSlug,
573
+ publicQuestion,
574
+ cacheKey
575
+ } = ctx;
576
+ const { waitUntil } = args;
577
+ const tools = getAllTools();
578
+ const publicCacheBranch = shapePublicCacheBranch({
579
+ publicQuestion,
580
+ articleSlug,
581
+ lang,
582
+ latestText
583
+ });
584
+ if (publicCacheBranch.enabled && publicQuestion) {
585
+ const globalCacheContext = publicCacheBranch.context;
586
+ if (responseCacheConfig.enabled) {
587
+ const cachedResponse = await getResponseCache(
588
+ cache,
589
+ publicQuestion.type,
590
+ globalCacheContext
591
+ );
592
+ if (cachedResponse) {
593
+ const notifyTiming = { total: Date.now() - timing.start };
594
+ sendNotification({
595
+ env,
596
+ messages,
597
+ responseText: cachedResponse.response,
598
+ relatedArticles: cachedResponse.articles,
599
+ timing: notifyTiming,
600
+ waitUntil
601
+ });
602
+ const stream2 = createUIMessageStream({
603
+ execute: async ({ writer }) => {
604
+ await streamCachedResponse(
605
+ writer,
606
+ cachedResponse,
607
+ responseCacheConfig,
608
+ lang
609
+ );
610
+ }
611
+ });
612
+ return createUIMessageStreamResponse({
613
+ stream: stream2,
614
+ headers: {
615
+ "Access-Control-Allow-Origin": "*",
616
+ "Cache-Control": "no-cache"
617
+ }
343
618
  });
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
619
  }
353
620
  }
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
621
+ const cachedSearch = await getGlobalSearchCache(
622
+ cache,
623
+ publicQuestion.type,
624
+ globalCacheContext
625
+ );
626
+ if (cachedSearch) {
627
+ const stream2 = createUIMessageStream({
628
+ execute: async ({ writer }) => {
629
+ const w = writer;
630
+ writeSearchStatus(
631
+ w,
632
+ cachedSearch.articles.length + cachedSearch.projects.length,
633
+ lang
634
+ );
635
+ if (cachedSearch.articles.length + cachedSearch.projects.length > 0) {
636
+ writeGeneratingStatus(w, lang);
637
+ }
638
+ let responseText = "";
639
+ if (adapter) {
640
+ const {
641
+ interpretation: cachedInterpretation,
642
+ budget: cachedBudget,
643
+ articles: shapedCachedArticles,
644
+ projects: shapedCachedProjects
645
+ } = shapeCachedSearchForQuery({
646
+ query: latestText,
647
+ articles: cachedSearch.articles,
648
+ projects: cachedSearch.projects
649
+ });
650
+ const promptRuntime = await assemblePromptRuntime({
651
+ env,
652
+ latestText,
653
+ context,
654
+ lang,
655
+ evidenceAnalysisTimeout: timeouts.evidenceAnalysis,
656
+ timing: {
657
+ total: Date.now() - timing.start,
658
+ keywordExtraction: timing.keywordExtraction,
659
+ search: timing.search,
660
+ evidenceAnalysis: timing.evidenceAnalysis,
661
+ generation: timing.generation
662
+ },
663
+ adapter,
664
+ hasRealProvider,
665
+ extensions,
666
+ cacheKey,
667
+ searchQuery: cachedSearch.query,
668
+ relatedArticles: shapedCachedArticles,
669
+ relatedProjects: shapedCachedProjects,
670
+ budget: cachedBudget,
671
+ answerMode: cachedInterpretation.answer.contract
672
+ });
673
+ const finalSources2 = buildFinalSources({
674
+ relatedArticles: shapedCachedArticles,
675
+ selectedSources: promptRuntime.selectedSources,
676
+ query: latestText,
677
+ lang,
678
+ max: RESPONSE.MAX_SOURCE_ARTICLES,
679
+ articleSlug,
680
+ context
681
+ });
682
+ writeSourceArticles(w, finalSources2);
683
+ writeSourceSnippets(w, finalSources2);
684
+ const llmResult = await streamAnswerWithFallback({
685
+ writer: w,
686
+ adapters: [adapter],
687
+ systemPrompt: promptRuntime.systemPrompt,
688
+ messages,
689
+ question: latestText,
690
+ lang,
691
+ tools
692
+ });
693
+ responseText = llmResult.responseText;
694
+ if (shouldPersistResponseCacheEntry({
695
+ enabled: responseCacheConfig.enabled,
696
+ success: llmResult.success,
697
+ responseText: llmResult.responseText,
698
+ sources: promptRuntime.selectedSources
699
+ })) {
700
+ const globalTTL = getGlobalCacheTTL(publicQuestion.type);
701
+ const responseCacheData = {
702
+ query: cachedSearch.query,
703
+ thinking: llmResult.reasoningText,
704
+ response: llmResult.responseText,
705
+ articles: shapedCachedArticles,
706
+ projects: shapedCachedProjects,
707
+ sources: promptRuntime.selectedSources,
708
+ lang,
709
+ model: adapter.model,
710
+ updatedAt: Date.now()
711
+ };
712
+ await setResponseCache(
713
+ cache,
714
+ publicQuestion.type,
715
+ responseCacheData,
716
+ globalTTL,
717
+ globalCacheContext
718
+ );
719
+ }
720
+ } else {
721
+ responseText = await streamMockFallback(w, latestText, lang);
722
+ }
723
+ }
724
+ });
725
+ return createUIMessageStreamResponse({
726
+ stream: stream2,
727
+ headers: {
728
+ "Access-Control-Allow-Origin": "*",
729
+ "Cache-Control": "no-cache"
730
+ }
731
+ });
382
732
  }
733
+ }
734
+ const search = await retrieveContext(ctx, args.req);
735
+ const { searchQuery, relatedArticles, relatedProjects, interpretation } = search;
736
+ const { systemPrompt, preflight, unknownRefusal, selectedSources } = await analyzeAndBuildPrompt(ctx, search);
737
+ const finalSources = buildFinalSources({
738
+ relatedArticles,
739
+ selectedSources,
740
+ query: latestText,
741
+ lang,
742
+ max: RESPONSE.MAX_SOURCE_ARTICLES,
743
+ articleSlug,
744
+ context
383
745
  });
746
+ log.debug(
747
+ `Final sources: ${finalSources.map(
748
+ (source) => `${source.title}[${source.reason}]${source.lang ? `:${source.lang}` : ""}`
749
+ ).join(", ")}`
750
+ );
384
751
  const stream = createUIMessageStream({
385
752
  execute: async ({ writer }) => {
386
753
  const articleCount = relatedArticles.length + relatedProjects.length;
@@ -390,36 +757,38 @@ ${articlePrompt}` : evidenceSection,
390
757
  messageMetadata: createChatStatusData({
391
758
  stage: "search",
392
759
  message: t("ai.status.found", lang, { count: articleCount }),
393
- progress: 40
760
+ progress: RESPONSE.SEARCH_PROGRESS
394
761
  })
395
762
  });
396
763
  }
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
- }
764
+ writeSourceArticles(writer, finalSources);
765
+ writeSourceSnippets(writer, finalSources);
408
766
  if (preflight) {
409
767
  writer.write({
410
768
  type: "message-metadata",
411
769
  messageMetadata: createChatStatusData({
412
770
  stage: "answer",
413
771
  message: t("ai.status.citation", lang),
414
- progress: 100,
772
+ progress: RESPONSE.COMPLETE_PROGRESS,
773
+ done: true
774
+ })
775
+ });
776
+ writeTextChunk(writer, preflight.text, "preflight");
777
+ writeFinish(writer);
778
+ return;
779
+ }
780
+ if (unknownRefusal) {
781
+ writer.write({
782
+ type: "message-metadata",
783
+ messageMetadata: createChatStatusData({
784
+ stage: "answer",
785
+ message: t("ai.status.generating", lang),
786
+ progress: RESPONSE.COMPLETE_PROGRESS,
415
787
  done: true
416
788
  })
417
789
  });
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" });
790
+ writeTextChunk(writer, unknownRefusal.text, "unknown");
791
+ writeFinish(writer);
423
792
  return;
424
793
  }
425
794
  writer.write({
@@ -427,141 +796,80 @@ ${articlePrompt}` : evidenceSection,
427
796
  messageMetadata: createChatStatusData({
428
797
  stage: "answer",
429
798
  message: t("ai.status.generating", lang),
430
- progress: 60
799
+ progress: RESPONSE.GENERATING_PROGRESS
431
800
  })
432
801
  });
433
- let streamSuccess = false;
434
802
  let responseText = "";
435
803
  let reasoningText;
436
804
  let tokenUsage;
437
805
  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;
806
+ const llmResult = await streamAnswerWithFallback({
807
+ writer,
808
+ adapters,
809
+ systemPrompt,
810
+ messages,
811
+ question: latestText,
812
+ lang,
813
+ temperature: CHAT_HANDLER.STREAMING_TEMPERATURE,
814
+ maxOutputTokens: CHAT_HANDLER.STREAMING_MAX_OUTPUT_TOKENS,
815
+ tools
816
+ });
817
+ timing.generation = llmResult.generationMs || Date.now() - generationStart;
818
+ responseText = llmResult.responseText;
819
+ reasoningText = llmResult.reasoningText;
820
+ tokenUsage = llmResult.tokenUsage ? {
821
+ total: llmResult.tokenUsage.total,
822
+ input: llmResult.tokenUsage.input,
823
+ output: llmResult.tokenUsage.output
824
+ } : void 0;
825
+ const usedAdapter = llmResult.adapter;
826
+ if (usedAdapter && llmResult.responseText.length > 0) {
827
+ log.debug(
828
+ `Provider success: adapter=${usedAdapter.id}, model=${usedAdapter.model}, responseLength=${responseText.length}, usage=${JSON.stringify(tokenUsage ?? null)}`
829
+ );
830
+ if (shouldAppendCitations(responseText, relatedArticles, relatedProjects)) {
831
+ const citations = selectCitations(
832
+ relatedArticles,
833
+ relatedProjects,
834
+ RESPONSE.MAX_SOURCE_ARTICLES,
835
+ RESPONSE.MAX_CITATIONS
836
+ );
837
+ if (citations.length > 0) {
838
+ const citationBlock = formatCitationBlock(citations, lang);
839
+ writeTextChunk(writer, citationBlock, "citation");
840
+ responseText += citationBlock;
540
841
  }
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);
842
+ }
843
+ if (publicCacheBranch.enabled && publicQuestion && shouldPersistResponseCacheEntry({
844
+ enabled: responseCacheConfig.enabled,
845
+ success: true,
846
+ responseText,
847
+ sources: selectedSources
848
+ })) {
849
+ const globalTTL = getGlobalCacheTTL(publicQuestion.type);
850
+ const responseCacheData = {
851
+ query: searchQuery,
852
+ thinking: reasoningText,
853
+ response: responseText,
854
+ articles: relatedArticles,
855
+ projects: relatedProjects,
856
+ sources: selectedSources,
857
+ lang,
858
+ model: usedAdapter.model,
859
+ updatedAt: Date.now()
860
+ };
861
+ await setResponseCache(
862
+ cache,
863
+ publicQuestion.type,
864
+ responseCacheData,
865
+ globalTTL,
866
+ publicCacheBranch.context
867
+ );
545
868
  }
546
869
  }
547
- if (!streamSuccess) {
548
- const { getMockResponse } = await import("../providers/mock.js");
549
- const mockText = getMockResponse(latestText, lang);
870
+ if (llmResult.usedMockFallback) {
550
871
  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" });
872
+ responseText = llmResult.responseText;
565
873
  }
566
874
  if (responseText) {
567
875
  const notifyTiming = {
@@ -571,17 +879,16 @@ ${articlePrompt}` : evidenceSection,
571
879
  evidenceAnalysis: timing.evidenceAnalysis,
572
880
  generation: timing.generation
573
881
  };
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
882
  sendNotification({
580
883
  env,
581
884
  messages,
582
885
  responseText,
583
- relatedArticles,
584
- model: notifyModel,
886
+ relatedArticles: finalSources,
887
+ model: usedAdapter ? {
888
+ name: usedAdapter.model,
889
+ provider: env.AI_PROVIDER || void 0,
890
+ apiHost: env.AI_BASE_URL || void 0
891
+ } : void 0,
585
892
  usage: tokenUsage,
586
893
  timing: notifyTiming,
587
894
  cacheKey,
@@ -598,6 +905,133 @@ ${articlePrompt}` : evidenceSection,
598
905
  }
599
906
  });
600
907
  }
908
+ function buildFinalSources(args) {
909
+ const { relatedArticles, selectedSources, query, lang, max, articleSlug, context } = args;
910
+ const normalizedQuery = normalizeText(query);
911
+ const fallbackSources = relatedArticles.map((article) => ({
912
+ title: article.title,
913
+ url: article.url,
914
+ lang: article.lang,
915
+ reason: "retrieval-fallback",
916
+ score: article.score
917
+ }));
918
+ const preferredPool = selectedSources.length > 0 ? selectedSources : fallbackSources;
919
+ const isCurrentArticleSource = (source) => {
920
+ if (!articleSlug) return false;
921
+ const url = source.url ?? "";
922
+ if (url.includes(`/posts/${articleSlug}`) || url.includes(articleSlug)) {
923
+ return true;
924
+ }
925
+ if (context?.article?.title && source.title === context.article.title) {
926
+ return true;
927
+ }
928
+ return false;
929
+ };
930
+ const shouldPrioritizeCurrentArticle = !!articleSlug && context?.scope === "article" && isLikelyQuotedArticleQuery(query) && !isCrossArticleIntent(query);
931
+ if (shouldPrioritizeCurrentArticle) {
932
+ const orderedCurrent = preferredPool.filter(isCurrentArticleSource);
933
+ const orderedOther = preferredPool.filter((source) => !isCurrentArticleSource(source));
934
+ const currentChunkSources = orderedCurrent.filter((source) => source.reason === "chunk");
935
+ const currentNonChunkSources = orderedCurrent.filter((source) => source.reason !== "chunk");
936
+ const dedupedPrioritized = [];
937
+ const seenPrioritized = /* @__PURE__ */ new Set();
938
+ for (const source of [
939
+ ...currentChunkSources,
940
+ ...currentNonChunkSources,
941
+ ...orderedOther
942
+ ]) {
943
+ const key = `${source.title}::${source.url ?? ""}::${source.chunkId ?? ""}`;
944
+ if (seenPrioritized.has(key)) continue;
945
+ seenPrioritized.add(key);
946
+ dedupedPrioritized.push(source);
947
+ if (dedupedPrioritized.length >= max) break;
948
+ }
949
+ return dedupedPrioritized;
950
+ }
951
+ const sameLang = preferredPool.filter((source) => source.lang === lang);
952
+ const anchorTerms = tokenize(query).filter((token) => token.length >= 2).sort((a, b) => b.length - a.length).slice(0, 3);
953
+ const sourceMatchesAnchor = (source) => {
954
+ if (!anchorTerms.length) return true;
955
+ const title = normalizeText(source.title);
956
+ return anchorTerms.some((term) => title.includes(term));
957
+ };
958
+ const sameLangAnchored = sameLang.filter(sourceMatchesAnchor);
959
+ const crossLangAnchored = preferredPool.filter(
960
+ (source) => source.lang !== lang && sourceMatchesAnchor(source)
961
+ );
962
+ const articleSummaryQuery = isArticleSummaryQuery(normalizedQuery);
963
+ const rankByTitleCloseness = (sources) => {
964
+ return [...sources].sort((a, b) => {
965
+ const aScore = computeTitleCloseness(normalizedQuery, a.title);
966
+ const bScore = computeTitleCloseness(normalizedQuery, b.title);
967
+ return bScore - aScore || (b.score ?? 0) - (a.score ?? 0);
968
+ });
969
+ };
970
+ const orderedSameLang = rankByTitleCloseness(
971
+ sameLangAnchored.length > 0 ? sameLangAnchored : sameLang
972
+ );
973
+ const orderedCrossLang = rankByTitleCloseness(
974
+ crossLangAnchored.length > 0 ? crossLangAnchored : preferredPool.filter((source) => source.lang !== lang)
975
+ );
976
+ let ordered = articleSummaryQuery ? orderedSameLang.length >= max ? orderedSameLang : [...orderedSameLang, ...orderedCrossLang] : [...orderedSameLang, ...orderedCrossLang];
977
+ const deduped = [];
978
+ const seen = /* @__PURE__ */ new Set();
979
+ for (const source of ordered) {
980
+ const key = `${source.title}::${source.url ?? ""}`;
981
+ if (seen.has(key)) continue;
982
+ seen.add(key);
983
+ deduped.push(source);
984
+ if (deduped.length >= max) break;
985
+ }
986
+ return deduped;
987
+ }
988
+ function isArticleSummaryQuery(normalizedQuery) {
989
+ return [
990
+ "\u8FD9\u7BC7\u6587\u7AE0",
991
+ "\u4E3B\u8981\u8BB2\u4E86\u4EC0\u4E48",
992
+ "\u8BB2\u4E86\u4EC0\u4E48",
993
+ "\u6587\u7AE0\u4E3B\u8981",
994
+ "\u6587\u7AE0\u8BB2\u4E86\u4EC0\u4E48",
995
+ "article summary",
996
+ "what does this article",
997
+ "what is this article about"
998
+ ].some((marker) => normalizedQuery.includes(marker));
999
+ }
1000
+ function computeTitleCloseness(normalizedQuery, title) {
1001
+ const normalizedTitle = normalizeText(title);
1002
+ const tokens = tokenize(normalizedQuery).filter((token) => token.length >= 2).sort((a, b) => b.length - a.length).slice(0, 5);
1003
+ let score = 0;
1004
+ for (const token of tokens) {
1005
+ if (normalizedTitle.includes(token)) {
1006
+ score += Math.max(1, Math.min(token.length, 6));
1007
+ }
1008
+ }
1009
+ if (normalizedQuery.includes("\u6280\u672F\u67B6\u6784") && normalizedTitle.includes("\u6280\u672F\u67B6\u6784")) {
1010
+ score += 8;
1011
+ }
1012
+ if (normalizedQuery.includes("\u6A21\u5757") && normalizedTitle.includes("\u6A21\u5757")) {
1013
+ score += 4;
1014
+ }
1015
+ if (normalizedQuery.includes("\u6587\u7AE0") && normalizedTitle.includes("\u6587\u7AE0")) {
1016
+ score += 2;
1017
+ }
1018
+ return score;
1019
+ }
601
1020
  export {
602
- handleChatRequest
1021
+ buildFinalSources,
1022
+ buildPublicCacheContext,
1023
+ extractQuotedCandidate,
1024
+ handleChatRequest,
1025
+ isCrossArticleIntent,
1026
+ isLikelyQuotedArticleQuery,
1027
+ rankArticlesForQuery,
1028
+ rerankArticlesForCodeAnchors,
1029
+ rerankArticlesForCurrentArticleQuote,
1030
+ resolveSearchAnswerShaping,
1031
+ shapeArticlesForQuery,
1032
+ shapeCachedSearchForQuery,
1033
+ shapePublicCacheBranch,
1034
+ shouldPersistAuthoritativeSources,
1035
+ shouldPersistResponseCacheEntry,
1036
+ shouldUsePublicQuestionCaches
603
1037
  };