@astro-minimax/ai 0.5.0 → 0.7.1

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 (47) hide show
  1. package/dist/intelligence/citation-appender.d.ts +19 -0
  2. package/dist/intelligence/citation-appender.d.ts.map +1 -0
  3. package/dist/intelligence/citation-appender.js +65 -0
  4. package/dist/intelligence/citation-guard.d.ts +7 -0
  5. package/dist/intelligence/citation-guard.d.ts.map +1 -1
  6. package/dist/intelligence/citation-guard.js +56 -13
  7. package/dist/intelligence/index.d.ts +6 -2
  8. package/dist/intelligence/index.d.ts.map +1 -1
  9. package/dist/intelligence/index.js +3 -2
  10. package/dist/intelligence/intent-detect.d.ts +12 -1
  11. package/dist/intelligence/intent-detect.d.ts.map +1 -1
  12. package/dist/intelligence/intent-detect.js +67 -0
  13. package/dist/intelligence/response-templates.d.ts +16 -0
  14. package/dist/intelligence/response-templates.d.ts.map +1 -0
  15. package/dist/intelligence/response-templates.js +116 -0
  16. package/dist/prompt/dynamic-layer.d.ts.map +1 -1
  17. package/dist/prompt/dynamic-layer.js +30 -7
  18. package/dist/prompt/prompt-builder.d.ts +0 -8
  19. package/dist/prompt/prompt-builder.d.ts.map +1 -1
  20. package/dist/prompt/prompt-builder.js +2 -9
  21. package/dist/prompt/semi-static-layer.d.ts +0 -4
  22. package/dist/prompt/semi-static-layer.d.ts.map +1 -1
  23. package/dist/prompt/semi-static-layer.js +7 -10
  24. package/dist/prompt/static-layer.d.ts.map +1 -1
  25. package/dist/prompt/static-layer.js +75 -3
  26. package/dist/prompt/types.d.ts +2 -0
  27. package/dist/prompt/types.d.ts.map +1 -1
  28. package/dist/search/search-api.d.ts.map +1 -1
  29. package/dist/search/search-api.js +2 -0
  30. package/dist/search/types.d.ts +2 -0
  31. package/dist/search/types.d.ts.map +1 -1
  32. package/dist/server/chat-handler.d.ts.map +1 -1
  33. package/dist/server/chat-handler.js +75 -241
  34. package/dist/server/index.d.ts +1 -0
  35. package/dist/server/index.d.ts.map +1 -1
  36. package/dist/server/index.js +1 -0
  37. package/dist/server/notify.d.ts.map +1 -1
  38. package/dist/server/notify.js +13 -1
  39. package/dist/server/stream-helpers.d.ts +45 -0
  40. package/dist/server/stream-helpers.d.ts.map +1 -0
  41. package/dist/server/stream-helpers.js +197 -0
  42. package/dist/server/types.d.ts +1 -0
  43. package/dist/server/types.d.ts.map +1 -1
  44. package/dist/utils/i18n.d.ts +1 -1
  45. package/dist/utils/i18n.d.ts.map +1 -1
  46. package/dist/utils/i18n.js +16 -0
  47. package/package.json +2 -2
@@ -0,0 +1,19 @@
1
+ import type { ArticleContext, ProjectContext } from '../search/types.js';
2
+ export interface CitationAppenderConfig {
3
+ articles: ArticleContext[];
4
+ projects: ProjectContext[];
5
+ lang: string;
6
+ maxCitations: number;
7
+ minScore: number;
8
+ }
9
+ interface CitationCandidate {
10
+ title: string;
11
+ url: string;
12
+ score: number;
13
+ }
14
+ export declare function selectCitations(articles: ArticleContext[], projects: ProjectContext[], maxCitations: number, minScore: number): CitationCandidate[];
15
+ export declare function formatCitationBlock(citations: CitationCandidate[], lang: string): string;
16
+ export declare function createCitationAppenderTransform(config: CitationAppenderConfig): (stream: ReadableStream<string>) => ReadableStream<string>;
17
+ export declare function shouldAppendCitations(response: string, articles: ArticleContext[], projects: ProjectContext[]): boolean;
18
+ export {};
19
+ //# sourceMappingURL=citation-appender.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"citation-appender.d.ts","sourceRoot":"","sources":["../../src/intelligence/citation-appender.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAGzE,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,cAAc,EAAE,CAAC;IAC3B,QAAQ,EAAE,cAAc,EAAE,CAAC;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,UAAU,iBAAiB;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;CACf;AAQD,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,cAAc,EAAE,EAC1B,QAAQ,EAAE,cAAc,EAAE,EAC1B,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,GACf,iBAAiB,EAAE,CAarB;AAED,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,iBAAiB,EAAE,EAC9B,IAAI,EAAE,MAAM,GACX,MAAM,CAYR;AAED,wBAAgB,+BAA+B,CAC7C,MAAM,EAAE,sBAAsB,GAC7B,CAAC,MAAM,EAAE,cAAc,CAAC,MAAM,CAAC,KAAK,cAAc,CAAC,MAAM,CAAC,CAkC5D;AAED,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,cAAc,EAAE,EAC1B,QAAQ,EAAE,cAAc,EAAE,GACzB,OAAO,CAQT"}
@@ -0,0 +1,65 @@
1
+ function hasExistingCitations(text, validUrls) {
2
+ const linkPattern = /\[([^\]]+)\]\(([^)]+)\)/g;
3
+ const matches = [...text.matchAll(linkPattern)];
4
+ return matches.some(m => validUrls.has(m[2]));
5
+ }
6
+ export function selectCitations(articles, projects, maxCitations, minScore) {
7
+ const candidates = [
8
+ ...articles
9
+ .filter(a => (a.score ?? 0) >= minScore)
10
+ .map(a => ({ title: a.title, url: a.url, score: a.score ?? 0 })),
11
+ ...projects
12
+ .filter(p => (p.score ?? 0) >= minScore)
13
+ .map(p => ({ title: p.name, url: p.url, score: p.score ?? 0 })),
14
+ ];
15
+ return candidates
16
+ .sort((a, b) => b.score - a.score)
17
+ .slice(0, maxCitations);
18
+ }
19
+ export function formatCitationBlock(citations, lang) {
20
+ if (citations.length === 0)
21
+ return '';
22
+ const heading = lang === 'zh' ? '延伸阅读' : 'Further Reading';
23
+ const lines = [
24
+ '',
25
+ `**${heading}:**`,
26
+ ...citations.map(c => `- [${c.title}](${c.url})`),
27
+ ];
28
+ return lines.join('\n');
29
+ }
30
+ export function createCitationAppenderTransform(config) {
31
+ const { articles, projects, lang, maxCitations = 3, minScore = 5 } = config;
32
+ const validUrls = new Set([
33
+ ...articles.map(a => a.url),
34
+ ...projects.map(p => p.url),
35
+ ]);
36
+ return (stream) => {
37
+ let fullText = '';
38
+ const transform = new TransformStream({
39
+ transform(chunk, controller) {
40
+ fullText += chunk;
41
+ controller.enqueue(chunk);
42
+ },
43
+ flush(controller) {
44
+ if (hasExistingCitations(fullText, validUrls)) {
45
+ return;
46
+ }
47
+ const citations = selectCitations(articles, projects, maxCitations, minScore);
48
+ if (citations.length === 0) {
49
+ return;
50
+ }
51
+ const citationBlock = formatCitationBlock(citations, lang);
52
+ controller.enqueue(citationBlock);
53
+ },
54
+ });
55
+ return stream.pipeThrough(transform);
56
+ };
57
+ }
58
+ export function shouldAppendCitations(response, articles, projects) {
59
+ const validUrls = new Set([
60
+ ...articles.map(a => a.url),
61
+ ...projects.map(p => p.url),
62
+ ]);
63
+ return !hasExistingCitations(response, validUrls) &&
64
+ [...articles, ...projects].some(item => (item.score ?? 0) >= 5);
65
+ }
@@ -1,5 +1,11 @@
1
1
  import type { ArticleContext, ProjectContext } from '../search/types.js';
2
2
  import type { CitationGuardPreflight, CitationGuardAction } from './types.js';
3
+ export type AnswerMode = 'fact' | 'count' | 'list' | 'opinion' | 'recommendation' | 'unknown' | 'general';
4
+ /**
5
+ * Resolves the expected answer mode from the user query.
6
+ * Helps the system decide how to structure the response.
7
+ */
8
+ export declare function resolveAnswerMode(query: string): AnswerMode;
3
9
  /**
4
10
  * Pre-flight check: if the user is asking about something that can be
5
11
  * answered directly from the available context without an LLM, return it.
@@ -9,6 +15,7 @@ export declare function getCitationGuardPreflight(params: {
9
15
  userQuery: string;
10
16
  articles: ArticleContext[];
11
17
  projects: ProjectContext[];
18
+ lang?: string;
12
19
  }): CitationGuardPreflight | null;
13
20
  /**
14
21
  * Creates a transform stream that monitors the AI output for hallucinated references.
@@ -1 +1 @@
1
- {"version":3,"file":"citation-guard.d.ts","sourceRoot":"","sources":["../../src/intelligence/citation-guard.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzE,OAAO,KAAK,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAE9E;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE;IAChD,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,cAAc,EAAE,CAAC;IAC3B,QAAQ,EAAE,cAAc,EAAE,CAAC;CAC5B,GAAG,sBAAsB,GAAG,IAAI,CA0BhC;AAED;;;GAGG;AACH,wBAAgB,4BAA4B,CAAC,MAAM,EAAE;IACnD,QAAQ,EAAE,cAAc,EAAE,CAAC;IAC3B,QAAQ,EAAE,cAAc,EAAE,CAAC;IAC3B,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE;QAAE,OAAO,EAAE,mBAAmB,EAAE,CAAA;KAAE,KAAK,IAAI,CAAC;CAClE,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,MAAM,CAAC,KAAK,cAAc,CAAC,MAAM,CAAC,CAuD7D"}
1
+ {"version":3,"file":"citation-guard.d.ts","sourceRoot":"","sources":["../../src/intelligence/citation-guard.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzE,OAAO,KAAK,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAS9E,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,GAAG,gBAAgB,GAAG,SAAS,GAAG,SAAS,CAAC;AAgB1G;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,CAS3D;AAoBD;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE;IAChD,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,cAAc,EAAE,CAAC;IAC3B,QAAQ,EAAE,cAAc,EAAE,CAAC;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,GAAG,sBAAsB,GAAG,IAAI,CAuBhC;AAED;;;GAGG;AACH,wBAAgB,4BAA4B,CAAC,MAAM,EAAE;IACnD,QAAQ,EAAE,cAAc,EAAE,CAAC;IAC3B,QAAQ,EAAE,cAAc,EAAE,CAAC;IAC3B,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE;QAAE,OAAO,EAAE,mBAAmB,EAAE,CAAA;KAAE,KAAK,IAAI,CAAC;CAClE,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,MAAM,CAAC,KAAK,cAAc,CAAC,MAAM,CAAC,CAuD7D"}
@@ -1,28 +1,71 @@
1
+ import { PRIVACY_REFUSAL_TEMPLATES, NO_ARTICLE_TEMPLATES, ARTICLE_COUNT_TEMPLATES, pickTemplate, pickTemplateWithVars, } from './response-templates.js';
2
+ const PRIVACY_PATTERNS = [
3
+ { regex: /(住址|地址|住在哪|address|where.*live)/iu, key: 'address' },
4
+ { regex: /(收入|工资|薪资|salary|income|earn)/iu, key: 'income' },
5
+ { regex: /(家人|妻子|丈夫|孩子|父母|family|wife|husband|children|parent)/iu, key: 'family' },
6
+ { regex: /(电话|手机号|phone|mobile)/iu, key: 'phone' },
7
+ { regex: /(身份证|id\s*card|passport)/iu, key: 'id' },
8
+ { regex: /(年龄|多大了|几岁|how old|age)/iu, key: 'age' },
9
+ ];
10
+ /**
11
+ * Resolves the expected answer mode from the user query.
12
+ * Helps the system decide how to structure the response.
13
+ */
14
+ export function resolveAnswerMode(query) {
15
+ const q = query.toLowerCase();
16
+ if (/几次|多少|几篇|数量|count|how many/u.test(q))
17
+ return 'count';
18
+ if (/哪些|哪几个|列表|列举|list|what are/u.test(q))
19
+ return 'list';
20
+ if (/怎么看|怎么想|看法|观点|opinion|think about/u.test(q))
21
+ return 'opinion';
22
+ if (/推荐|建议|suggest|recommend/u.test(q))
23
+ return 'recommendation';
24
+ if (/是什么|什么是|介绍|解释|what is|explain/u.test(q))
25
+ return 'fact';
26
+ if (/有没有|是否|是不是|真的吗|does|is there/u.test(q))
27
+ return 'fact';
28
+ return 'general';
29
+ }
30
+ /**
31
+ * Checks if the query is asking for sensitive personal information.
32
+ * Returns a privacy refusal if matched.
33
+ */
34
+ function checkPrivacyRefusal(query, lang) {
35
+ for (const pattern of PRIVACY_PATTERNS) {
36
+ if (pattern.regex.test(query)) {
37
+ const templates = PRIVACY_REFUSAL_TEMPLATES[pattern.key];
38
+ const text = templates ? pickTemplate(templates, lang) : '';
39
+ return {
40
+ text,
41
+ actions: ['preflight_reject'],
42
+ };
43
+ }
44
+ }
45
+ return null;
46
+ }
1
47
  /**
2
48
  * Pre-flight check: if the user is asking about something that can be
3
49
  * answered directly from the available context without an LLM, return it.
4
50
  * This prevents hallucination for specific factual queries.
5
51
  */
6
52
  export function getCitationGuardPreflight(params) {
7
- const { userQuery, articles, projects } = params;
53
+ const { userQuery, articles, projects, lang = 'zh' } = params;
8
54
  const q = userQuery.toLowerCase();
9
- // Detect queries asking for article counts or lists
10
- if (/有几篇|有多少篇|文章数量|总共.*文章/.test(q)) {
55
+ const privacyRefusal = checkPrivacyRefusal(userQuery, lang);
56
+ if (privacyRefusal)
57
+ return privacyRefusal;
58
+ if (/有几篇|有多少篇|文章数量|总共.*文章|how many.*article/u.test(q)) {
11
59
  const total = articles.length;
12
60
  if (total > 0) {
13
- return {
14
- text: `根据我检索到的信息,当前共找到 ${total} 篇相关文章。`,
15
- actions: ['preflight_reject'],
16
- };
61
+ const text = pickTemplateWithVars(ARTICLE_COUNT_TEMPLATES, lang, { count: total });
62
+ return { text, actions: ['preflight_reject'] };
17
63
  }
18
64
  }
19
- // Detect queries about specific article existence that we can verify
20
- if (/有没有|是否有|有.*文章|写过.*吗/.test(q)) {
65
+ if (/有没有|是否有|有.*文章|写过.*吗|is there|any.*article/u.test(q)) {
21
66
  if (articles.length === 0 && projects.length === 0) {
22
- return {
23
- text: '根据博客内容搜索,目前没有找到与这个主题直接相关的文章。你可以尝试用其他关键词搜索,或者问我其他问题。',
24
- actions: ['preflight_reject'],
25
- };
67
+ const text = pickTemplate(NO_ARTICLE_TEMPLATES, lang);
68
+ return { text, actions: ['preflight_reject'] };
26
69
  }
27
70
  }
28
71
  return null;
@@ -1,6 +1,10 @@
1
- export { isLikelyFollowUp, hasNewSignificantTokens, hasQueryOverlap, shouldReuseSearchContext, buildLocalSearchQuery, } from './intent-detect.js';
1
+ export { isLikelyFollowUp, hasNewSignificantTokens, hasQueryOverlap, shouldReuseSearchContext, buildLocalSearchQuery, classifyIntent, rankArticlesByIntent, } from './intent-detect.js';
2
+ export type { IntentCategory } from './intent-detect.js';
2
3
  export { shouldRunKeywordExtraction, extractSearchKeywords, KEYWORD_EXTRACTION_TIMEOUT_MS, } from './keyword-extract.js';
3
4
  export { shouldSkipAnalysis, analyzeRetrievedEvidence, buildEvidenceSection, EVIDENCE_ANALYSIS_TIMEOUT_MS, EVIDENCE_ANALYSIS_MAX_TOKENS, } from './evidence-analysis.js';
4
- export { getCitationGuardPreflight, createCitationGuardTransform, } from './citation-guard.js';
5
+ export { getCitationGuardPreflight, createCitationGuardTransform, resolveAnswerMode, } from './citation-guard.js';
6
+ export type { AnswerMode } from './citation-guard.js';
7
+ export { createCitationAppenderTransform, shouldAppendCitations, selectCitations, formatCitationBlock, } from './citation-appender.js';
8
+ export type { CitationAppenderConfig } from './citation-appender.js';
5
9
  export type { QueryComplexity, KeywordExtractionResult, TokenUsageStats, EvidenceAnalysisResult, CitationGuardPreflight, CitationGuardAction, } from './types.js';
6
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/intelligence/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,uBAAuB,EACvB,eAAe,EACf,wBAAwB,EACxB,qBAAqB,GACtB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,0BAA0B,EAC1B,qBAAqB,EACrB,6BAA6B,GAC9B,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EACL,kBAAkB,EAClB,wBAAwB,EACxB,oBAAoB,EACpB,4BAA4B,EAC5B,4BAA4B,GAC7B,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EACL,yBAAyB,EACzB,4BAA4B,GAC7B,MAAM,qBAAqB,CAAC;AAE7B,YAAY,EACV,eAAe,EACf,uBAAuB,EACvB,eAAe,EACf,sBAAsB,EACtB,sBAAsB,EACtB,mBAAmB,GACpB,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/intelligence/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,uBAAuB,EACvB,eAAe,EACf,wBAAwB,EACxB,qBAAqB,EACrB,cAAc,EACd,oBAAoB,GACrB,MAAM,oBAAoB,CAAC;AAE5B,YAAY,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEzD,OAAO,EACL,0BAA0B,EAC1B,qBAAqB,EACrB,6BAA6B,GAC9B,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EACL,kBAAkB,EAClB,wBAAwB,EACxB,oBAAoB,EACpB,4BAA4B,EAC5B,4BAA4B,GAC7B,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EACL,yBAAyB,EACzB,4BAA4B,EAC5B,iBAAiB,GAClB,MAAM,qBAAqB,CAAC;AAE7B,YAAY,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAEtD,OAAO,EACL,+BAA+B,EAC/B,qBAAqB,EACrB,eAAe,EACf,mBAAmB,GACpB,MAAM,wBAAwB,CAAC;AAEhC,YAAY,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAErE,YAAY,EACV,eAAe,EACf,uBAAuB,EACvB,eAAe,EACf,sBAAsB,EACtB,sBAAsB,EACtB,mBAAmB,GACpB,MAAM,YAAY,CAAC"}
@@ -1,4 +1,5 @@
1
- export { isLikelyFollowUp, hasNewSignificantTokens, hasQueryOverlap, shouldReuseSearchContext, buildLocalSearchQuery, } from './intent-detect.js';
1
+ export { isLikelyFollowUp, hasNewSignificantTokens, hasQueryOverlap, shouldReuseSearchContext, buildLocalSearchQuery, classifyIntent, rankArticlesByIntent, } from './intent-detect.js';
2
2
  export { shouldRunKeywordExtraction, extractSearchKeywords, KEYWORD_EXTRACTION_TIMEOUT_MS, } from './keyword-extract.js';
3
3
  export { shouldSkipAnalysis, analyzeRetrievedEvidence, buildEvidenceSection, EVIDENCE_ANALYSIS_TIMEOUT_MS, EVIDENCE_ANALYSIS_MAX_TOKENS, } from './evidence-analysis.js';
4
- export { getCitationGuardPreflight, createCitationGuardTransform, } from './citation-guard.js';
4
+ export { getCitationGuardPreflight, createCitationGuardTransform, resolveAnswerMode, } from './citation-guard.js';
5
+ export { createCitationAppenderTransform, shouldAppendCitations, selectCitations, formatCitationBlock, } from './citation-appender.js';
@@ -1,4 +1,15 @@
1
- import type { CachedSearchContext } from '../search/types.js';
1
+ import type { CachedSearchContext, ArticleContext } from '../search/types.js';
2
+ export type IntentCategory = 'setup' | 'config' | 'content' | 'feature' | 'deployment' | 'troubleshooting' | 'general';
3
+ /**
4
+ * Classifies the user query into an intent category.
5
+ * Used to adjust search relevance scoring.
6
+ */
7
+ export declare function classifyIntent(query: string): IntentCategory;
8
+ /**
9
+ * Re-ranks articles by intent relevance with weighted multi-dimension scoring.
10
+ * Scoring: title(+3) / categories(+2) / summary(+2) / keyPoints(+1) / recency(+1)
11
+ */
12
+ export declare function rankArticlesByIntent(query: string, articles: ArticleContext[]): ArticleContext[];
2
13
  /**
3
14
  * Determines if the latest message is likely a follow-up to the previous context.
4
15
  * Uses heuristics: message length, punctuation, word count.
@@ -1 +1 @@
1
- {"version":3,"file":"intent-detect.d.ts","sourceRoot":"","sources":["../../src/intelligence/intent-detect.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAK9D;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAUzD;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,YAAY,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAK1F;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,YAAY,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAKlF;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE;IAC/C,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,mBAAmB,GAAG,SAAS,CAAC;IAC/C,aAAa,EAAE,MAAM,CAAC;IACtB,GAAG,EAAE,MAAM,CAAC;CACb,GAAG,OAAO,CASV;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAEhE"}
1
+ {"version":3,"file":"intent-detect.d.ts","sourceRoot":"","sources":["../../src/intelligence/intent-detect.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAO9E,MAAM,MAAM,cAAc,GACtB,OAAO,GACP,QAAQ,GACR,SAAS,GACT,SAAS,GACT,YAAY,GACZ,iBAAiB,GACjB,SAAS,CAAC;AAYd;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,CAY5D;AAiBD;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,cAAc,EAAE,GACzB,cAAc,EAAE,CAsBlB;AAID;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAUzD;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,YAAY,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAK1F;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,YAAY,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAKlF;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE;IAC/C,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,mBAAmB,GAAG,SAAS,CAAC;IAC/C,aAAa,EAAE,MAAM,CAAC;IACtB,GAAG,EAAE,MAAM,CAAC;CACb,GAAG,OAAO,CASV;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAEhE"}
@@ -1,6 +1,73 @@
1
1
  import { tokenize, normalizeText } from '../search/search-utils.js';
2
2
  import { SESSION_CACHE_TTL_MS } from '../search/session-cache.js';
3
3
  const MAX_FOLLOW_UP_LENGTH = 48;
4
+ const INTENT_KEYWORDS = {
5
+ setup: ['搭建', '创建', '安装', 'install', 'setup', 'create', 'init', 'scaffold', '新建', '开始'],
6
+ config: ['配置', '设置', 'config', 'settings', '环境变量', '.env', 'wrangler', 'tsconfig', '主题色', '颜色'],
7
+ content: ['文章', '博客', '写作', 'markdown', 'mdx', '标签', '分类', '摘要', '封面', '翻译'],
8
+ feature: ['功能', '特性', 'feature', '支持', 'AI', 'RAG', '搜索', '评论', 'RSS', '暗色', '深色'],
9
+ deployment: ['部署', 'deploy', 'cloudflare', 'vercel', 'netlify', 'build', '构建', 'CI', 'CD'],
10
+ troubleshooting: ['报错', '错误', 'error', 'bug', '问题', '不工作', '失败', 'fail', '修复', 'fix'],
11
+ general: [],
12
+ };
13
+ /**
14
+ * Classifies the user query into an intent category.
15
+ * Used to adjust search relevance scoring.
16
+ */
17
+ export function classifyIntent(query) {
18
+ const q = query.toLowerCase();
19
+ const scores = {};
20
+ for (const [intent, keywords] of Object.entries(INTENT_KEYWORDS)) {
21
+ if (intent === 'general')
22
+ continue;
23
+ const score = keywords.reduce((acc, kw) => acc + (q.includes(kw.toLowerCase()) ? 1 : 0), 0);
24
+ if (score > 0)
25
+ scores[intent] = score;
26
+ }
27
+ const sorted = Object.entries(scores).sort((a, b) => b[1] - a[1]);
28
+ return sorted[0]?.[0] || 'general';
29
+ }
30
+ /**
31
+ * Re-ranks articles by intent relevance.
32
+ * Boosts articles whose title/categories/keyPoints match the detected intent.
33
+ */
34
+ function countKeywordHits(text, keywords) {
35
+ if (!text)
36
+ return 0;
37
+ const lower = text.toLowerCase();
38
+ return keywords.reduce((hits, kw) => hits + (lower.includes(kw.toLowerCase()) ? 1 : 0), 0);
39
+ }
40
+ function isRecent(dateTime) {
41
+ if (!dateTime || !Number.isFinite(dateTime))
42
+ return false;
43
+ return Date.now() - dateTime <= 365 * 24 * 60 * 60 * 1000;
44
+ }
45
+ /**
46
+ * Re-ranks articles by intent relevance with weighted multi-dimension scoring.
47
+ * Scoring: title(+3) / categories(+2) / summary(+2) / keyPoints(+1) / recency(+1)
48
+ */
49
+ export function rankArticlesByIntent(query, articles) {
50
+ const intent = classifyIntent(query);
51
+ if (intent === 'general' || articles.length <= 1)
52
+ return articles;
53
+ const keywords = INTENT_KEYWORDS[intent];
54
+ if (!keywords.length)
55
+ return articles;
56
+ const scored = articles.map((article, index) => {
57
+ const titleHit = countKeywordHits(article.title, keywords) > 0 ? 3 : 0;
58
+ const categoryHit = (article.categories ?? []).some(c => countKeywordHits(c, keywords) > 0) ? 2 : 0;
59
+ const summaryHit = countKeywordHits(article.summary, keywords) > 0 ? 2 : 0;
60
+ const keyPointHit = article.keyPoints.some(kp => countKeywordHits(kp, keywords) > 0) ? 1 : 0;
61
+ const recentHit = isRecent(article.dateTime) ? 1 : 0;
62
+ return { article, index, score: titleHit + categoryHit + summaryHit + keyPointHit + recentHit };
63
+ });
64
+ const maxScore = Math.max(...scored.map(s => s.score), 0);
65
+ if (maxScore === 0)
66
+ return articles;
67
+ scored.sort((a, b) => b.score - a.score || a.index - b.index);
68
+ return scored.map(s => s.article);
69
+ }
70
+ // ── Follow-up Detection ──────────────────────────────────────
4
71
  /**
5
72
  * Determines if the latest message is likely a follow-up to the previous context.
6
73
  * Uses heuristics: message length, punctuation, word count.
@@ -0,0 +1,16 @@
1
+ export interface ResponseTemplate {
2
+ zh: string[];
3
+ en: string[];
4
+ }
5
+ export declare const PRIVACY_REFUSAL_TEMPLATES: Record<string, ResponseTemplate>;
6
+ export declare const NO_ARTICLE_TEMPLATES: ResponseTemplate;
7
+ export declare const ARTICLE_COUNT_TEMPLATES: ResponseTemplate;
8
+ /**
9
+ * Randomly selects a template from the available options.
10
+ */
11
+ export declare function pickTemplate(templates: ResponseTemplate, lang: string): string;
12
+ /**
13
+ * Picks a template and interpolates variables.
14
+ */
15
+ export declare function pickTemplateWithVars(templates: ResponseTemplate, lang: string, vars: Record<string, string | number>): string;
16
+ //# sourceMappingURL=response-templates.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"response-templates.d.ts","sourceRoot":"","sources":["../../src/intelligence/response-templates.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,EAAE,CAAC;IACb,EAAE,EAAE,MAAM,EAAE,CAAC;CACd;AAED,eAAO,MAAM,yBAAyB,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAyEtE,CAAC;AAEF,eAAO,MAAM,oBAAoB,EAAE,gBAWlC,CAAC;AAEF,eAAO,MAAM,uBAAuB,EAAE,gBAWrC,CAAC;AAEF;;GAEG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAI9E;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,gBAAgB,EAC3B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,GACpC,MAAM,CAMR"}
@@ -0,0 +1,116 @@
1
+ export const PRIVACY_REFUSAL_TEMPLATES = {
2
+ address: {
3
+ zh: [
4
+ '具体住址是私人信息,未在博客中公开。',
5
+ '关于住址信息,博客中没有相关内容。',
6
+ '这个信息涉及隐私,博主没有在博客中分享。',
7
+ ],
8
+ en: [
9
+ 'Address is private and not disclosed on the blog.',
10
+ 'The blogger has not shared address information publicly.',
11
+ 'This is private information that is not available on the blog.',
12
+ ],
13
+ },
14
+ income: {
15
+ zh: [
16
+ '收入信息未在博客中公开。',
17
+ '关于收入,博客中没有相关内容。',
18
+ '这个信息属于隐私范畴,博主没有公开。',
19
+ ],
20
+ en: [
21
+ 'Income information is not disclosed on the blog.',
22
+ 'The blogger has not shared income details publicly.',
23
+ 'This is private financial information not available on the blog.',
24
+ ],
25
+ },
26
+ family: {
27
+ zh: [
28
+ '家人信息未在博客中公开。',
29
+ '关于家人,博客中没有详细介绍。',
30
+ '这属于私人生活范畴,博主选择不公开。',
31
+ ],
32
+ en: [
33
+ 'Family information is not disclosed on the blog.',
34
+ 'The blogger keeps family matters private.',
35
+ 'Details about family members are not shared publicly.',
36
+ ],
37
+ },
38
+ phone: {
39
+ zh: [
40
+ '联系电话未在博客中公开。',
41
+ '博主的联系方式没有在博客中分享。',
42
+ '电话号码属于隐私信息,无法提供。',
43
+ ],
44
+ en: [
45
+ 'Phone number is not disclosed on the blog.',
46
+ 'Contact details are not shared publicly on the blog.',
47
+ 'Phone numbers are private information not available here.',
48
+ ],
49
+ },
50
+ id: {
51
+ zh: [
52
+ '身份证件信息未在博客中公开。',
53
+ '这属于敏感个人信息,博主没有公开。',
54
+ '身份证件信息受保护,不在博客内容中。',
55
+ ],
56
+ en: [
57
+ 'ID information is not disclosed on the blog.',
58
+ 'Identity document details are private and not shared.',
59
+ 'This is sensitive personal information not available publicly.',
60
+ ],
61
+ },
62
+ age: {
63
+ zh: [
64
+ '年龄信息未在博客中公开。',
65
+ '关于年龄,博客中没有明确提及。',
66
+ '这个信息博主没有在博客中分享。',
67
+ ],
68
+ en: [
69
+ 'Age information is not disclosed on the blog.',
70
+ 'The blogger has not shared age details publicly.',
71
+ 'Age is not mentioned in the blog content.',
72
+ ],
73
+ },
74
+ };
75
+ export const NO_ARTICLE_TEMPLATES = {
76
+ zh: [
77
+ '根据博客内容搜索,目前没有找到与这个主题直接相关的文章。你可以尝试用其他关键词搜索,或者问我其他问题。',
78
+ '我在博客中没有找到相关的内容。试试换个方式提问,或者浏览其他话题。',
79
+ '抱歉,博客里暂时没有涉及这个话题的文章。你可以问我其他问题,我尽力帮你找答案。',
80
+ ],
81
+ en: [
82
+ 'No articles directly related to this topic were found. Try different keywords or ask another question.',
83
+ 'I could not find relevant content in the blog. Try rephrasing your question or exploring other topics.',
84
+ 'Sorry, there are no articles on this topic in the blog. Feel free to ask about something else.',
85
+ ],
86
+ };
87
+ export const ARTICLE_COUNT_TEMPLATES = {
88
+ zh: [
89
+ '根据我检索到的信息,当前共找到 {count} 篇相关文章。',
90
+ '搜索结果显示,有 {count} 篇文章与你的问题相关。',
91
+ '我找到了 {count} 篇可能对你有帮助的文章。',
92
+ ],
93
+ en: [
94
+ 'Based on my search, I found {count} related articles.',
95
+ 'The search returned {count} articles that may be relevant.',
96
+ 'I discovered {count} articles related to your query.',
97
+ ],
98
+ };
99
+ /**
100
+ * Randomly selects a template from the available options.
101
+ */
102
+ export function pickTemplate(templates, lang) {
103
+ const options = lang === 'en' ? templates.en : templates.zh;
104
+ const index = Math.floor(Math.random() * options.length);
105
+ return options[index];
106
+ }
107
+ /**
108
+ * Picks a template and interpolates variables.
109
+ */
110
+ export function pickTemplateWithVars(templates, lang, vars) {
111
+ let text = pickTemplate(templates, lang);
112
+ for (const [key, value] of Object.entries(vars)) {
113
+ text = text.replace(new RegExp(`\\{${key}\\}`, 'g'), String(value));
114
+ }
115
+ return text;
116
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-layer.d.ts","sourceRoot":"","sources":["../../src/prompt/dynamic-layer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAErD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM,CAwCpE"}
1
+ {"version":3,"file":"dynamic-layer.d.ts","sourceRoot":"","sources":["../../src/prompt/dynamic-layer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAwBrD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM,CA0CpE"}
@@ -1,31 +1,54 @@
1
+ import { getLang } from '../utils/i18n.js';
2
+ const LABELS = {
3
+ zh: {
4
+ relatedContent: '与当前问题相关的内容',
5
+ relatedArticles: '相关文章',
6
+ relatedProjects: '相关项目',
7
+ summary: '摘要',
8
+ keyPoints: '要点',
9
+ excerpt: '内容节选',
10
+ instruction: (query) => `基于以上内容回答用户关于「${query}」的问题。如果以上内容与问题不相关,如实告知并提供力所能及的帮助。`,
11
+ },
12
+ en: {
13
+ relatedContent: 'Content related to the current question',
14
+ relatedArticles: 'Related Articles',
15
+ relatedProjects: 'Related Projects',
16
+ summary: 'Summary',
17
+ keyPoints: 'Key points',
18
+ excerpt: 'Excerpt',
19
+ instruction: (query) => `Answer the user's question about "${query}" based on the content above. If the above content is not relevant, say so honestly and provide whatever help you can.`,
20
+ },
21
+ };
1
22
  /**
2
23
  * Dynamic layer: per-request search results and evidence analysis.
3
24
  * Built fresh on every chat request.
4
25
  */
5
26
  export function buildDynamicLayer(config) {
6
27
  const { userQuery, articles, projects, evidenceSection } = config;
28
+ const lang = getLang(config.lang);
29
+ const l = LABELS[lang];
7
30
  if (!articles.length && !projects.length)
8
31
  return '';
9
32
  const lines = [];
10
- lines.push('## 与当前问题相关的内容');
33
+ lines.push(`## ${l.relatedContent}`);
11
34
  if (articles.length) {
12
35
  lines.push('');
13
- lines.push('### 相关文章');
36
+ lines.push(`### ${l.relatedArticles}`);
14
37
  for (const article of articles.slice(0, 8)) {
15
38
  lines.push(`**[${article.title}](${article.url})**`);
16
39
  if (article.summary)
17
- lines.push(`摘要:${article.summary.slice(0, 120)}`);
40
+ lines.push(`${l.summary}:${article.summary.slice(0, 120)}`);
18
41
  if (article.keyPoints.length) {
19
- lines.push(`要点:${article.keyPoints.slice(0, 3).join(';')}`);
42
+ lines.push(`${l.keyPoints}:${article.keyPoints.slice(0, 3).join(';')}`);
20
43
  }
21
44
  if (article.fullContent) {
22
- lines.push(`内容节选:${article.fullContent.slice(0, 600)}`);
45
+ lines.push(`${l.excerpt}:${article.fullContent.slice(0, 600)}`);
23
46
  }
24
47
  lines.push('');
25
48
  }
26
49
  }
27
50
  if (projects.length) {
28
- lines.push('### 相关项目');
51
+ lines.push(`### ${l.relatedProjects}`);
29
52
  for (const project of projects.slice(0, 4)) {
30
53
  lines.push(`- **[${project.name}](${project.url})**:${project.description.slice(0, 100)}`);
31
54
  }
@@ -35,6 +58,6 @@ export function buildDynamicLayer(config) {
35
58
  lines.push(evidenceSection);
36
59
  }
37
60
  lines.push(`---`);
38
- lines.push(`基于以上内容回答用户关于「${userQuery.slice(0, 50)}」的问题。如果以上内容与问题不相关,如实告知并提供力所能及的帮助。`);
61
+ lines.push(l.instruction(userQuery.slice(0, 50)));
39
62
  return lines.join('\n');
40
63
  }
@@ -1,11 +1,3 @@
1
1
  import type { PromptBuildConfig } from './types.js';
2
- /**
3
- * Assembles the three-layer system prompt.
4
- *
5
- * Structure:
6
- * 1. Static layer — Author identity, role, behavior constraints (rarely changes)
7
- * 2. Semi-static layer — Blog metadata loaded at startup (changes on rebuild)
8
- * 3. Dynamic layer — Per-request search results + evidence analysis
9
- */
10
2
  export declare function buildSystemPrompt(config: PromptBuildConfig): string;
11
3
  //# sourceMappingURL=prompt-builder.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"prompt-builder.d.ts","sourceRoot":"","sources":["../../src/prompt/prompt-builder.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEpD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM,CAQnE"}
1
+ {"version":3,"file":"prompt-builder.d.ts","sourceRoot":"","sources":["../../src/prompt/prompt-builder.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEpD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM,CAUnE"}
@@ -1,18 +1,11 @@
1
1
  import { buildStaticLayer } from './static-layer.js';
2
2
  import { buildSemiStaticLayer } from './semi-static-layer.js';
3
3
  import { buildDynamicLayer } from './dynamic-layer.js';
4
- /**
5
- * Assembles the three-layer system prompt.
6
- *
7
- * Structure:
8
- * 1. Static layer — Author identity, role, behavior constraints (rarely changes)
9
- * 2. Semi-static layer — Blog metadata loaded at startup (changes on rebuild)
10
- * 3. Dynamic layer — Per-request search results + evidence analysis
11
- */
12
4
  export function buildSystemPrompt(config) {
5
+ const lang = config.static.lang || config.dynamic.lang;
13
6
  const layers = [
14
7
  buildStaticLayer(config.static),
15
- buildSemiStaticLayer(config.semiStatic),
8
+ buildSemiStaticLayer({ ...config.semiStatic, lang }),
16
9
  buildDynamicLayer(config.dynamic),
17
10
  ].filter(Boolean);
18
11
  return layers.join('\n\n');
@@ -1,7 +1,3 @@
1
1
  import type { SemiStaticLayerConfig } from './types.js';
2
- /**
3
- * Semi-static layer: blog metadata loaded at build/startup time.
4
- * This changes when the blog is rebuilt, not per-request.
5
- */
6
2
  export declare function buildSemiStaticLayer(config: SemiStaticLayerConfig): string;
7
3
  //# sourceMappingURL=semi-static-layer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"semi-static-layer.d.ts","sourceRoot":"","sources":["../../src/prompt/semi-static-layer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAExD;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,qBAAqB,GAAG,MAAM,CA+B1E"}
1
+ {"version":3,"file":"semi-static-layer.d.ts","sourceRoot":"","sources":["../../src/prompt/semi-static-layer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAGxD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,qBAAqB,GAAG,MAAM,CA+B1E"}
@@ -1,28 +1,25 @@
1
- /**
2
- * Semi-static layer: blog metadata loaded at build/startup time.
3
- * This changes when the blog is rebuilt, not per-request.
4
- */
1
+ import { t, getLang } from '../utils/i18n.js';
5
2
  export function buildSemiStaticLayer(config) {
6
- const { authorContext } = config;
3
+ const { authorContext, lang: configLang } = config;
7
4
  if (!authorContext)
8
5
  return '';
6
+ const lang = getLang(configLang);
9
7
  const lines = [];
10
8
  const { posts } = authorContext;
11
9
  if (!posts.length)
12
10
  return '';
13
- // Blog overview
14
11
  const totalPosts = posts.length;
15
12
  const categories = [...new Set(posts.map(p => p.category).filter(Boolean))];
16
13
  const recentPosts = posts
17
14
  .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
18
15
  .slice(0, 10);
19
- lines.push('## 博客概况');
20
- lines.push(`- 共有 ${totalPosts} 篇文章`);
16
+ lines.push('## ' + t('ai.semiStatic.blogOverview', lang));
17
+ lines.push('- ' + t('ai.semiStatic.totalPosts', lang, { count: totalPosts }));
21
18
  if (categories.length) {
22
- lines.push(`- 主要分类:${categories.slice(0, 8).join('、')}`);
19
+ lines.push('- ' + t('ai.semiStatic.mainCategories', lang, { categories: categories.slice(0, 8).join(lang === 'zh' ? '、' : ', ') }));
23
20
  }
24
21
  lines.push('');
25
- lines.push('## 最新文章');
22
+ lines.push('## ' + t('ai.semiStatic.latestArticles', lang));
26
23
  for (const post of recentPosts) {
27
24
  const date = post.date ? new Date(post.date).toISOString().slice(0, 10) : '';
28
25
  const summary = post.summary ? ` — ${post.summary.slice(0, 60)}` : '';
@@ -1 +1 @@
1
- {"version":3,"file":"static-layer.d.ts","sourceRoot":"","sources":["../../src/prompt/static-layer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAkEpD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM,CAyBlE"}
1
+ {"version":3,"file":"static-layer.d.ts","sourceRoot":"","sources":["../../src/prompt/static-layer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAkIpD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM,CAqClE"}