@astro-minimax/ai 0.8.3 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cache/global-cache.js +145 -0
- package/dist/cache/index.js +96 -0
- package/dist/cache/kv-adapter.js +99 -0
- package/dist/cache/memory-adapter.js +97 -0
- package/dist/cache/response-cache.js +87 -0
- package/dist/cache/types.js +8 -0
- package/dist/data/metadata-loader.js +48 -0
- package/dist/data/types.js +0 -0
- package/dist/fact-registry/fact-matcher.js +128 -0
- package/dist/fact-registry/prompt-injector.js +54 -0
- package/dist/fact-registry/registry.js +41 -0
- package/dist/fact-registry/types.js +0 -0
- package/dist/intelligence/citation-appender.js +63 -0
- package/dist/intelligence/citation-guard.js +108 -0
- package/dist/intelligence/evidence-analysis.js +79 -0
- package/dist/intelligence/intent-detect.js +93 -0
- package/dist/intelligence/keyword-extract.js +89 -0
- package/dist/intelligence/response-templates.js +117 -0
- package/dist/intelligence/types.js +0 -0
- package/dist/middleware/rate-limiter.js +110 -0
- package/dist/prompt/dynamic-layer.js +64 -0
- package/dist/prompt/prompt-builder.js +15 -0
- package/dist/prompt/semi-static-layer.js +28 -0
- package/dist/prompt/static-layer.js +153 -0
- package/dist/prompt/types.js +0 -0
- package/dist/provider-manager/base.js +53 -0
- package/dist/provider-manager/config.js +135 -0
- package/dist/provider-manager/index.js +19 -0
- package/dist/provider-manager/manager.js +122 -0
- package/dist/provider-manager/mock.js +77 -0
- package/dist/provider-manager/openai.js +106 -0
- package/dist/provider-manager/types.js +0 -0
- package/dist/provider-manager/workers.js +76 -0
- package/dist/providers/mock.js +227 -0
- package/dist/search/idf.js +24 -0
- package/dist/search/search-api.js +94 -0
- package/dist/search/search-index.js +32 -0
- package/dist/search/search-utils.js +81 -0
- package/dist/search/session-cache.js +96 -0
- package/dist/search/types.js +0 -0
- package/dist/search/vector-reranker.js +103 -0
- package/dist/server/chat-handler.js +603 -0
- package/dist/server/errors.js +46 -0
- package/dist/server/metadata-init.js +49 -0
- package/dist/server/notify.js +70 -0
- package/dist/server/stream-helpers.js +202 -0
- package/dist/server/types.js +16 -0
- package/dist/stream/mock-stream.js +26 -0
- package/dist/stream/response.js +21 -0
- package/dist/utils/i18n.js +154 -0
- package/package.json +3 -3
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
const PUBLIC_QUESTION_PATTERNS = [
|
|
2
|
+
{
|
|
3
|
+
type: "tech",
|
|
4
|
+
keywords: ["\u6280\u672F\u6808", "\u6280\u672F", "\u6846\u67B6", "\u7528\u4E86\u4EC0\u4E48", "built with", "tech stack", "framework"],
|
|
5
|
+
patterns: [
|
|
6
|
+
/这个博客用了什么/,
|
|
7
|
+
/博客.*技术栈/,
|
|
8
|
+
/用了什么技术/,
|
|
9
|
+
/what.*tech.*stack/,
|
|
10
|
+
/built with/,
|
|
11
|
+
/用什么框架/
|
|
12
|
+
],
|
|
13
|
+
ttl: 86400,
|
|
14
|
+
needsContext: false
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
type: "recommend",
|
|
18
|
+
keywords: ["\u63A8\u8350", "\u6587\u7AE0\u63A8\u8350", "\u597D\u6587", "recommend", "suggest"],
|
|
19
|
+
patterns: [
|
|
20
|
+
/推荐.*文章/,
|
|
21
|
+
/有哪些推荐/,
|
|
22
|
+
/文章推荐/,
|
|
23
|
+
/recommend.*article/,
|
|
24
|
+
/any.*recommend/
|
|
25
|
+
],
|
|
26
|
+
ttl: 1800,
|
|
27
|
+
needsContext: false
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
type: "build",
|
|
31
|
+
keywords: ["\u642D\u5EFA", "\u90E8\u7F72", "\u600E\u4E48\u5EFA", "how to build", "deploy", "setup"],
|
|
32
|
+
patterns: [
|
|
33
|
+
/怎么搭建/,
|
|
34
|
+
/如何搭建/,
|
|
35
|
+
/怎么部署/,
|
|
36
|
+
/how to build/,
|
|
37
|
+
/how to deploy/,
|
|
38
|
+
/搭建.*博客/
|
|
39
|
+
],
|
|
40
|
+
ttl: 86400,
|
|
41
|
+
needsContext: false
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
type: "summary",
|
|
45
|
+
keywords: ["\u603B\u7ED3", "\u6982\u62EC", "\u6458\u8981", "summarize", "summary", "tl;dr"],
|
|
46
|
+
patterns: [
|
|
47
|
+
/总结(一下|这篇文章)/,
|
|
48
|
+
/概括(一下|这篇文章)/,
|
|
49
|
+
/文章摘要/,
|
|
50
|
+
/summarize/,
|
|
51
|
+
/summary/,
|
|
52
|
+
/主要讲了什么/
|
|
53
|
+
],
|
|
54
|
+
ttl: 14400,
|
|
55
|
+
needsContext: true
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
type: "author",
|
|
59
|
+
keywords: ["\u4F5C\u8005", "\u535A\u4E3B", "\u8C01", "author", "who"],
|
|
60
|
+
patterns: [
|
|
61
|
+
/作者是谁/,
|
|
62
|
+
/博主是谁/,
|
|
63
|
+
/关于作者/,
|
|
64
|
+
/who.*author/,
|
|
65
|
+
/about author/
|
|
66
|
+
],
|
|
67
|
+
ttl: 86400,
|
|
68
|
+
needsContext: false
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
type: "about",
|
|
72
|
+
keywords: ["\u5173\u4E8E", "\u4ECB\u7ECD", "about", "intro"],
|
|
73
|
+
patterns: [
|
|
74
|
+
/关于.*博客/,
|
|
75
|
+
/博客介绍/,
|
|
76
|
+
/about.*blog/,
|
|
77
|
+
/介绍一下/
|
|
78
|
+
],
|
|
79
|
+
ttl: 86400,
|
|
80
|
+
needsContext: false
|
|
81
|
+
}
|
|
82
|
+
];
|
|
83
|
+
function detectPublicQuestion(query) {
|
|
84
|
+
const normalized = normalizeQuery(query);
|
|
85
|
+
let bestMatch = null;
|
|
86
|
+
let bestScore = 0;
|
|
87
|
+
for (const pattern of PUBLIC_QUESTION_PATTERNS) {
|
|
88
|
+
let score = 0;
|
|
89
|
+
for (const keyword of pattern.keywords) {
|
|
90
|
+
if (normalized.includes(keyword.toLowerCase())) {
|
|
91
|
+
score += 1;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
for (const regex of pattern.patterns) {
|
|
95
|
+
if (regex.test(normalized)) {
|
|
96
|
+
score += 3;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (score > bestScore && score >= 2) {
|
|
100
|
+
bestScore = score;
|
|
101
|
+
const confidence = Math.min(score / 5, 1);
|
|
102
|
+
bestMatch = {
|
|
103
|
+
type: pattern.type,
|
|
104
|
+
confidence,
|
|
105
|
+
ttl: pattern.ttl,
|
|
106
|
+
needsContext: pattern.needsContext
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return bestMatch;
|
|
111
|
+
}
|
|
112
|
+
function buildGlobalCacheKey(type, context) {
|
|
113
|
+
const parts = ["global", type];
|
|
114
|
+
if (context?.articleSlug) {
|
|
115
|
+
parts.push(context.articleSlug);
|
|
116
|
+
}
|
|
117
|
+
if (context?.lang) {
|
|
118
|
+
parts.push(context.lang);
|
|
119
|
+
}
|
|
120
|
+
return parts.join(":");
|
|
121
|
+
}
|
|
122
|
+
function normalizeQuery(query) {
|
|
123
|
+
return query.toLowerCase().replace(/[??!!。,,.]/g, "").replace(/\s+/g, " ").trim();
|
|
124
|
+
}
|
|
125
|
+
async function getGlobalSearchCache(cache, type, context) {
|
|
126
|
+
const key = buildGlobalCacheKey(type, context);
|
|
127
|
+
const entry = await cache.get(key);
|
|
128
|
+
return entry?.value ?? null;
|
|
129
|
+
}
|
|
130
|
+
async function setGlobalSearchCache(cache, type, data, ttl, context) {
|
|
131
|
+
const key = buildGlobalCacheKey(type, context);
|
|
132
|
+
await cache.set(key, data, { ttl });
|
|
133
|
+
}
|
|
134
|
+
function getGlobalCacheTTL(type) {
|
|
135
|
+
const pattern = PUBLIC_QUESTION_PATTERNS.find((p) => p.type === type);
|
|
136
|
+
return pattern?.ttl ?? 3600;
|
|
137
|
+
}
|
|
138
|
+
export {
|
|
139
|
+
PUBLIC_QUESTION_PATTERNS,
|
|
140
|
+
buildGlobalCacheKey,
|
|
141
|
+
detectPublicQuestion,
|
|
142
|
+
getGlobalCacheTTL,
|
|
143
|
+
getGlobalSearchCache,
|
|
144
|
+
setGlobalSearchCache
|
|
145
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MemoryCacheAdapter
|
|
3
|
+
} from "./memory-adapter.js";
|
|
4
|
+
import {
|
|
5
|
+
KVCacheAdapter
|
|
6
|
+
} from "./kv-adapter.js";
|
|
7
|
+
import {
|
|
8
|
+
createCacheKeyBuilder
|
|
9
|
+
} from "./types.js";
|
|
10
|
+
import {
|
|
11
|
+
detectPublicQuestion,
|
|
12
|
+
buildGlobalCacheKey,
|
|
13
|
+
getGlobalSearchCache,
|
|
14
|
+
setGlobalSearchCache,
|
|
15
|
+
getGlobalCacheTTL,
|
|
16
|
+
PUBLIC_QUESTION_PATTERNS
|
|
17
|
+
} from "./global-cache.js";
|
|
18
|
+
import {
|
|
19
|
+
getResponseCache,
|
|
20
|
+
setResponseCache,
|
|
21
|
+
deleteResponseCache,
|
|
22
|
+
getResponseCacheConfig,
|
|
23
|
+
buildResponseCacheKey,
|
|
24
|
+
createResponsePlaybackGenerator,
|
|
25
|
+
DEFAULT_RESPONSE_CACHE_CONFIG
|
|
26
|
+
} from "./response-cache.js";
|
|
27
|
+
import { MemoryCacheAdapter as MemoryCacheAdapter2 } from "./memory-adapter.js";
|
|
28
|
+
import { KVCacheAdapter as KVCacheAdapter2 } from "./kv-adapter.js";
|
|
29
|
+
const DEFAULT_TTL = 600;
|
|
30
|
+
const DEFAULT_MAX_ENTRIES = 400;
|
|
31
|
+
let globalMemoryCache = null;
|
|
32
|
+
function getGlobalMemoryCache(ttl, maxEntries) {
|
|
33
|
+
if (!globalMemoryCache) {
|
|
34
|
+
globalMemoryCache = new MemoryCacheAdapter2({ defaultTtl: ttl, maxEntries });
|
|
35
|
+
}
|
|
36
|
+
return globalMemoryCache;
|
|
37
|
+
}
|
|
38
|
+
function createCacheAdapter(env, config) {
|
|
39
|
+
const ttl = config?.defaultTtl ?? parseTtl(env.CACHE_TTL) ?? DEFAULT_TTL;
|
|
40
|
+
const maxEntries = config?.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
41
|
+
if (isCacheDisabled(env)) {
|
|
42
|
+
return getGlobalMemoryCache(ttl, maxEntries);
|
|
43
|
+
}
|
|
44
|
+
const kvBinding = getKVBinding(env);
|
|
45
|
+
if (kvBinding) {
|
|
46
|
+
return new KVCacheAdapter2(kvBinding, { defaultTtl: ttl });
|
|
47
|
+
}
|
|
48
|
+
return getGlobalMemoryCache(ttl, maxEntries);
|
|
49
|
+
}
|
|
50
|
+
function isCacheDisabled(env) {
|
|
51
|
+
const val = env.CACHE_DISABLED;
|
|
52
|
+
if (val === true || val === "true" || val === "1") {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
function getKVBinding(env) {
|
|
58
|
+
const customBinding = env.CACHE_KV_BINDING;
|
|
59
|
+
if (customBinding && typeof customBinding === "string") {
|
|
60
|
+
const binding = env[customBinding];
|
|
61
|
+
return isKVNamespace(binding) ? binding : null;
|
|
62
|
+
}
|
|
63
|
+
if (env.CACHE_KV && isKVNamespace(env.CACHE_KV)) {
|
|
64
|
+
return env.CACHE_KV;
|
|
65
|
+
}
|
|
66
|
+
const defaultBinding = env["CACHE_KV"];
|
|
67
|
+
return isKVNamespace(defaultBinding) ? defaultBinding : null;
|
|
68
|
+
}
|
|
69
|
+
function isKVNamespace(value) {
|
|
70
|
+
return typeof value === "object" && value !== null && "get" in value && "put" in value && "delete" in value;
|
|
71
|
+
}
|
|
72
|
+
function parseTtl(value) {
|
|
73
|
+
if (value === void 0) return void 0;
|
|
74
|
+
if (typeof value === "number") return value;
|
|
75
|
+
const parsed = parseInt(value, 10);
|
|
76
|
+
return isNaN(parsed) ? void 0 : parsed;
|
|
77
|
+
}
|
|
78
|
+
export {
|
|
79
|
+
DEFAULT_RESPONSE_CACHE_CONFIG,
|
|
80
|
+
KVCacheAdapter,
|
|
81
|
+
MemoryCacheAdapter,
|
|
82
|
+
PUBLIC_QUESTION_PATTERNS,
|
|
83
|
+
buildGlobalCacheKey,
|
|
84
|
+
buildResponseCacheKey,
|
|
85
|
+
createCacheAdapter,
|
|
86
|
+
createCacheKeyBuilder,
|
|
87
|
+
createResponsePlaybackGenerator,
|
|
88
|
+
deleteResponseCache,
|
|
89
|
+
detectPublicQuestion,
|
|
90
|
+
getGlobalCacheTTL,
|
|
91
|
+
getGlobalSearchCache,
|
|
92
|
+
getResponseCache,
|
|
93
|
+
getResponseCacheConfig,
|
|
94
|
+
setGlobalSearchCache,
|
|
95
|
+
setResponseCache
|
|
96
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
const DEFAULT_TTL_SECONDS = 600;
|
|
2
|
+
const MIN_TTL_SECONDS = 60;
|
|
3
|
+
class KVCacheAdapter {
|
|
4
|
+
name = "cloudflare-kv";
|
|
5
|
+
kv;
|
|
6
|
+
defaultTtl;
|
|
7
|
+
prefix;
|
|
8
|
+
constructor(kv, options) {
|
|
9
|
+
this.kv = kv;
|
|
10
|
+
this.defaultTtl = options?.defaultTtl ?? DEFAULT_TTL_SECONDS;
|
|
11
|
+
this.prefix = options?.prefix ?? "";
|
|
12
|
+
}
|
|
13
|
+
buildKey(key) {
|
|
14
|
+
return this.prefix ? `${this.prefix}:${key}` : key;
|
|
15
|
+
}
|
|
16
|
+
async get(key, options) {
|
|
17
|
+
try {
|
|
18
|
+
const fullKey = this.buildKey(key);
|
|
19
|
+
const result = await this.kv.getWithMetadata(fullKey, "json");
|
|
20
|
+
if (!result.value) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
const storedEntry = result.value;
|
|
24
|
+
if (!storedEntry.value || !storedEntry.metadata) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
value: storedEntry.value,
|
|
29
|
+
metadata: storedEntry.metadata
|
|
30
|
+
};
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (!options?.silent) {
|
|
33
|
+
console.error("[KVCacheAdapter] Get error:", error);
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async set(key, value, options) {
|
|
39
|
+
try {
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
const ttl = options?.ttl ?? this.defaultTtl;
|
|
42
|
+
const entry = {
|
|
43
|
+
value,
|
|
44
|
+
metadata: {
|
|
45
|
+
createdAt: now,
|
|
46
|
+
updatedAt: now,
|
|
47
|
+
custom: options?.metadata
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
const fullKey = this.buildKey(key);
|
|
51
|
+
const effectiveTtl = Math.max(ttl, MIN_TTL_SECONDS);
|
|
52
|
+
await this.kv.put(fullKey, JSON.stringify(entry), {
|
|
53
|
+
expirationTtl: effectiveTtl
|
|
54
|
+
});
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error("[KVCacheAdapter] Set error:", error);
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async delete(key) {
|
|
61
|
+
try {
|
|
62
|
+
const fullKey = this.buildKey(key);
|
|
63
|
+
const existing = await this.kv.get(fullKey);
|
|
64
|
+
if (existing === null) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
await this.kv.delete(fullKey);
|
|
68
|
+
return true;
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.error("[KVCacheAdapter] Delete error:", error);
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async clear() {
|
|
75
|
+
console.warn("[KVCacheAdapter] Clear not supported for KV namespace");
|
|
76
|
+
}
|
|
77
|
+
async has(key) {
|
|
78
|
+
try {
|
|
79
|
+
const fullKey = this.buildKey(key);
|
|
80
|
+
const value = await this.kv.get(fullKey);
|
|
81
|
+
return value !== null;
|
|
82
|
+
} catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async isAvailable() {
|
|
87
|
+
try {
|
|
88
|
+
await this.kv.get("__health_check__");
|
|
89
|
+
return true;
|
|
90
|
+
} catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
dispose() {
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
export {
|
|
98
|
+
KVCacheAdapter
|
|
99
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
const DEFAULT_MAX_ENTRIES = 400;
|
|
2
|
+
const DEFAULT_TTL_SECONDS = 600;
|
|
3
|
+
class MemoryCacheAdapter {
|
|
4
|
+
name = "memory";
|
|
5
|
+
cache = /* @__PURE__ */ new Map();
|
|
6
|
+
maxEntries;
|
|
7
|
+
defaultTtl;
|
|
8
|
+
cleanupTimer;
|
|
9
|
+
constructor(options) {
|
|
10
|
+
this.maxEntries = options?.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
11
|
+
this.defaultTtl = options?.defaultTtl ?? DEFAULT_TTL_SECONDS;
|
|
12
|
+
if (options?.cleanupInterval) {
|
|
13
|
+
this.cleanupTimer = setInterval(
|
|
14
|
+
() => this.cleanup(),
|
|
15
|
+
options.cleanupInterval * 1e3
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
async get(key, _options) {
|
|
20
|
+
const entry = this.cache.get(key);
|
|
21
|
+
if (!entry) return null;
|
|
22
|
+
if (entry.expiresAt && Date.now() > entry.expiresAt) {
|
|
23
|
+
this.cache.delete(key);
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
this.cache.delete(key);
|
|
27
|
+
this.cache.set(key, entry);
|
|
28
|
+
return {
|
|
29
|
+
value: entry.value,
|
|
30
|
+
metadata: entry.metadata
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
async set(key, value, options) {
|
|
34
|
+
const now = Date.now();
|
|
35
|
+
const ttl = options?.ttl ?? this.defaultTtl;
|
|
36
|
+
const entry = {
|
|
37
|
+
value,
|
|
38
|
+
metadata: {
|
|
39
|
+
createdAt: now,
|
|
40
|
+
updatedAt: now,
|
|
41
|
+
custom: options?.metadata
|
|
42
|
+
},
|
|
43
|
+
expiresAt: ttl ? now + ttl * 1e3 : void 0
|
|
44
|
+
};
|
|
45
|
+
this.cache.delete(key);
|
|
46
|
+
this.cache.set(key, entry);
|
|
47
|
+
if (this.cache.size > this.maxEntries) {
|
|
48
|
+
this.evictLRU();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async delete(key) {
|
|
52
|
+
return this.cache.delete(key);
|
|
53
|
+
}
|
|
54
|
+
async clear() {
|
|
55
|
+
this.cache.clear();
|
|
56
|
+
}
|
|
57
|
+
async has(key) {
|
|
58
|
+
const entry = this.cache.get(key);
|
|
59
|
+
if (!entry) return false;
|
|
60
|
+
if (entry.expiresAt && Date.now() > entry.expiresAt) {
|
|
61
|
+
this.cache.delete(key);
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
async isAvailable() {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
cleanup() {
|
|
70
|
+
const now = Date.now();
|
|
71
|
+
for (const [key, entry] of this.cache) {
|
|
72
|
+
if (entry.expiresAt && now > entry.expiresAt) {
|
|
73
|
+
this.cache.delete(key);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
evictLRU() {
|
|
78
|
+
const overflow = this.cache.size - this.maxEntries;
|
|
79
|
+
if (overflow <= 0) return;
|
|
80
|
+
const keys = this.cache.keys();
|
|
81
|
+
for (let i = 0; i < overflow; i++) {
|
|
82
|
+
const next = keys.next();
|
|
83
|
+
if (next.done) break;
|
|
84
|
+
this.cache.delete(next.value);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
dispose() {
|
|
88
|
+
if (this.cleanupTimer) {
|
|
89
|
+
clearInterval(this.cleanupTimer);
|
|
90
|
+
this.cleanupTimer = void 0;
|
|
91
|
+
}
|
|
92
|
+
this.cache.clear();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
export {
|
|
96
|
+
MemoryCacheAdapter
|
|
97
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const DEFAULT_RESPONSE_CACHE_CONFIG = {
|
|
2
|
+
enabled: false,
|
|
3
|
+
defaultTtl: 3600,
|
|
4
|
+
playbackDelayMs: 20,
|
|
5
|
+
chunkSize: 15,
|
|
6
|
+
thinkingPlaybackDelayMs: 5
|
|
7
|
+
};
|
|
8
|
+
function getResponseCacheConfig(env) {
|
|
9
|
+
const parseBool = (val) => {
|
|
10
|
+
if (val === void 0) return false;
|
|
11
|
+
if (typeof val === "boolean") return val;
|
|
12
|
+
if (typeof val === "string") return val === "true" || val === "1";
|
|
13
|
+
return false;
|
|
14
|
+
};
|
|
15
|
+
const parseNum = (val, defaultVal) => {
|
|
16
|
+
if (val === void 0) return defaultVal;
|
|
17
|
+
if (typeof val === "number") return val;
|
|
18
|
+
if (typeof val === "string") {
|
|
19
|
+
const num = parseInt(val, 10);
|
|
20
|
+
return isNaN(num) ? defaultVal : num;
|
|
21
|
+
}
|
|
22
|
+
return defaultVal;
|
|
23
|
+
};
|
|
24
|
+
return {
|
|
25
|
+
enabled: parseBool(env.AI_RESPONSE_CACHE_ENABLED),
|
|
26
|
+
defaultTtl: parseNum(env.AI_RESPONSE_CACHE_TTL, 3600),
|
|
27
|
+
playbackDelayMs: parseNum(env.AI_RESPONSE_CACHE_PLAYBACK_DELAY, 20),
|
|
28
|
+
chunkSize: parseNum(env.AI_RESPONSE_CACHE_CHUNK_SIZE, 15),
|
|
29
|
+
thinkingPlaybackDelayMs: parseNum(env.AI_RESPONSE_CACHE_THINKING_DELAY, 5)
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
const RESPONSE_CACHE_PREFIX = "response";
|
|
33
|
+
function buildResponseCacheKey(type, context) {
|
|
34
|
+
const parts = [RESPONSE_CACHE_PREFIX, type];
|
|
35
|
+
if (context?.articleSlug) parts.push(context.articleSlug);
|
|
36
|
+
if (context?.lang) parts.push(context.lang);
|
|
37
|
+
return parts.join(":");
|
|
38
|
+
}
|
|
39
|
+
async function getResponseCache(cache, type, context) {
|
|
40
|
+
const key = buildResponseCacheKey(type, context);
|
|
41
|
+
const entry = await cache.get(key);
|
|
42
|
+
return entry?.value ?? null;
|
|
43
|
+
}
|
|
44
|
+
async function setResponseCache(cache, type, data, ttl, context) {
|
|
45
|
+
const key = buildResponseCacheKey(type, context);
|
|
46
|
+
await cache.set(key, data, { ttl });
|
|
47
|
+
}
|
|
48
|
+
async function deleteResponseCache(cache, type, context) {
|
|
49
|
+
const key = buildResponseCacheKey(type, context);
|
|
50
|
+
return cache.delete(key);
|
|
51
|
+
}
|
|
52
|
+
function createResponsePlaybackGenerator(cached, config) {
|
|
53
|
+
const { playbackDelayMs, chunkSize, thinkingPlaybackDelayMs } = config;
|
|
54
|
+
return (async function* () {
|
|
55
|
+
if (cached.thinking) {
|
|
56
|
+
const thinkingChunks = Math.ceil(cached.thinking.length / chunkSize);
|
|
57
|
+
for (let i = 0; i < thinkingChunks; i++) {
|
|
58
|
+
const chunk = cached.thinking.slice(i * chunkSize, (i + 1) * chunkSize);
|
|
59
|
+
if (chunk) {
|
|
60
|
+
yield { type: "thinking", text: chunk };
|
|
61
|
+
if (i < thinkingChunks - 1 && thinkingPlaybackDelayMs > 0) {
|
|
62
|
+
await new Promise((resolve) => setTimeout(resolve, thinkingPlaybackDelayMs));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const responseChunks = Math.ceil(cached.response.length / chunkSize);
|
|
68
|
+
for (let i = 0; i < responseChunks; i++) {
|
|
69
|
+
const chunk = cached.response.slice(i * chunkSize, (i + 1) * chunkSize);
|
|
70
|
+
if (chunk) {
|
|
71
|
+
yield { type: "response", text: chunk };
|
|
72
|
+
if (i < responseChunks - 1 && playbackDelayMs > 0) {
|
|
73
|
+
await new Promise((resolve) => setTimeout(resolve, playbackDelayMs));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
})();
|
|
78
|
+
}
|
|
79
|
+
export {
|
|
80
|
+
DEFAULT_RESPONSE_CACHE_CONFIG,
|
|
81
|
+
buildResponseCacheKey,
|
|
82
|
+
createResponsePlaybackGenerator,
|
|
83
|
+
deleteResponseCache,
|
|
84
|
+
getResponseCache,
|
|
85
|
+
getResponseCacheConfig,
|
|
86
|
+
setResponseCache
|
|
87
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
const createCacheKeyBuilder = (prefix = "chat") => ({
|
|
2
|
+
session: (sessionId) => `${prefix}:sid:${sessionId}`,
|
|
3
|
+
article: (sessionId, slug) => `${prefix}:sid:${sessionId}:article:${slug}`,
|
|
4
|
+
custom: (...parts) => `${prefix}:${parts.join(":")}`
|
|
5
|
+
});
|
|
6
|
+
export {
|
|
7
|
+
createCacheKeyBuilder
|
|
8
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { loadFactRegistry as loadFactRegistryCache } from "../fact-registry/registry.js";
|
|
2
|
+
import { loadVectorIndex as loadVectorIndexCache } from "../search/vector-reranker.js";
|
|
3
|
+
let cachedMetadata = null;
|
|
4
|
+
function preloadMetadata(data) {
|
|
5
|
+
cachedMetadata = {
|
|
6
|
+
summaries: data.summaries ?? null,
|
|
7
|
+
authorContext: data.authorContext ?? null,
|
|
8
|
+
voiceProfile: data.voiceProfile ?? null,
|
|
9
|
+
factRegistry: data.factRegistry ?? null,
|
|
10
|
+
vectorIndex: data.vectorIndex ?? null
|
|
11
|
+
};
|
|
12
|
+
if (cachedMetadata.factRegistry) {
|
|
13
|
+
loadFactRegistryCache(cachedMetadata.factRegistry);
|
|
14
|
+
}
|
|
15
|
+
if (cachedMetadata.vectorIndex) {
|
|
16
|
+
loadVectorIndexCache(cachedMetadata.vectorIndex);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function clearMetadataCache() {
|
|
20
|
+
cachedMetadata = null;
|
|
21
|
+
loadFactRegistryCache(null);
|
|
22
|
+
loadVectorIndexCache(null);
|
|
23
|
+
}
|
|
24
|
+
function getMetadata() {
|
|
25
|
+
return cachedMetadata ?? { summaries: null, authorContext: null, voiceProfile: null, factRegistry: null, vectorIndex: null };
|
|
26
|
+
}
|
|
27
|
+
function getArticleSummary(slug) {
|
|
28
|
+
return cachedMetadata?.summaries?.articles[slug]?.data;
|
|
29
|
+
}
|
|
30
|
+
function getAllSummaries() {
|
|
31
|
+
const articles = cachedMetadata?.summaries?.articles ?? {};
|
|
32
|
+
return Object.entries(articles).map(([slug, entry]) => ({ slug, ...entry.data }));
|
|
33
|
+
}
|
|
34
|
+
function getAuthorContext() {
|
|
35
|
+
return cachedMetadata?.authorContext ?? null;
|
|
36
|
+
}
|
|
37
|
+
function getVoiceProfile() {
|
|
38
|
+
return cachedMetadata?.voiceProfile ?? null;
|
|
39
|
+
}
|
|
40
|
+
export {
|
|
41
|
+
clearMetadataCache,
|
|
42
|
+
getAllSummaries,
|
|
43
|
+
getArticleSummary,
|
|
44
|
+
getAuthorContext,
|
|
45
|
+
getMetadata,
|
|
46
|
+
getVoiceProfile,
|
|
47
|
+
preloadMetadata
|
|
48
|
+
};
|
|
File without changes
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { queryFacts } from "./registry.js";
|
|
2
|
+
const CATEGORY_KEYWORDS = {
|
|
3
|
+
author: [
|
|
4
|
+
"\u4F5C\u8005",
|
|
5
|
+
"\u535A\u4E3B",
|
|
6
|
+
"\u8C01",
|
|
7
|
+
"\u5173\u4E8E\u6211",
|
|
8
|
+
"\u81EA\u6211\u4ECB\u7ECD",
|
|
9
|
+
"\u4E2A\u4EBA",
|
|
10
|
+
"author",
|
|
11
|
+
"who",
|
|
12
|
+
"about me",
|
|
13
|
+
"introduce"
|
|
14
|
+
],
|
|
15
|
+
blog: [
|
|
16
|
+
"\u535A\u5BA2",
|
|
17
|
+
"\u6587\u7AE0",
|
|
18
|
+
"\u591A\u5C11",
|
|
19
|
+
"\u6570\u91CF",
|
|
20
|
+
"\u7EDF\u8BA1",
|
|
21
|
+
"\u603B\u5171",
|
|
22
|
+
"\u5206\u7C7B",
|
|
23
|
+
"\u6807\u7B7E",
|
|
24
|
+
"\u8BED\u8A00",
|
|
25
|
+
"blog",
|
|
26
|
+
"post",
|
|
27
|
+
"how many",
|
|
28
|
+
"count",
|
|
29
|
+
"statistic",
|
|
30
|
+
"category",
|
|
31
|
+
"tag"
|
|
32
|
+
],
|
|
33
|
+
content: [
|
|
34
|
+
"\u5199\u8FC7",
|
|
35
|
+
"\u63D0\u5230",
|
|
36
|
+
"\u8BA8\u8BBA",
|
|
37
|
+
"\u89C2\u70B9",
|
|
38
|
+
"\u4E3B\u9898",
|
|
39
|
+
"\u8BDD\u9898",
|
|
40
|
+
"\u6DB5\u76D6",
|
|
41
|
+
"\u9886\u57DF",
|
|
42
|
+
"wrote",
|
|
43
|
+
"mention",
|
|
44
|
+
"discuss",
|
|
45
|
+
"topic",
|
|
46
|
+
"cover",
|
|
47
|
+
"area",
|
|
48
|
+
"opinion"
|
|
49
|
+
],
|
|
50
|
+
project: [
|
|
51
|
+
"\u9879\u76EE",
|
|
52
|
+
"\u5F00\u6E90",
|
|
53
|
+
"\u4ED3\u5E93",
|
|
54
|
+
"\u5DE5\u5177",
|
|
55
|
+
"\u4EA7\u54C1",
|
|
56
|
+
"project",
|
|
57
|
+
"open source",
|
|
58
|
+
"repo",
|
|
59
|
+
"github",
|
|
60
|
+
"tool",
|
|
61
|
+
"product"
|
|
62
|
+
],
|
|
63
|
+
tech: [
|
|
64
|
+
"\u6280\u672F",
|
|
65
|
+
"\u6280\u672F\u6808",
|
|
66
|
+
"\u6846\u67B6",
|
|
67
|
+
"\u5E93",
|
|
68
|
+
"\u7F16\u7A0B\u8BED\u8A00",
|
|
69
|
+
"\u524D\u7AEF",
|
|
70
|
+
"\u540E\u7AEF",
|
|
71
|
+
"tech",
|
|
72
|
+
"stack",
|
|
73
|
+
"framework",
|
|
74
|
+
"library",
|
|
75
|
+
"language",
|
|
76
|
+
"frontend",
|
|
77
|
+
"backend"
|
|
78
|
+
]
|
|
79
|
+
};
|
|
80
|
+
function detectRelevantCategories(query) {
|
|
81
|
+
const q = query.toLowerCase();
|
|
82
|
+
const matched = [];
|
|
83
|
+
for (const [category, keywords] of Object.entries(CATEGORY_KEYWORDS)) {
|
|
84
|
+
if (keywords.some((kw) => q.includes(kw))) {
|
|
85
|
+
matched.push(category);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return matched;
|
|
89
|
+
}
|
|
90
|
+
function extractQueryTags(query) {
|
|
91
|
+
const tokens = query.match(
|
|
92
|
+
/[A-Za-z][A-Za-z0-9.+#-]{1,}|[\u4e00-\u9fa5]{2,6}/g
|
|
93
|
+
);
|
|
94
|
+
return tokens?.map((t) => t.toLowerCase()) ?? [];
|
|
95
|
+
}
|
|
96
|
+
function matchFactsToQuery(query, lang, maxFacts = 15) {
|
|
97
|
+
const categories = detectRelevantCategories(query);
|
|
98
|
+
const queryTags = extractQueryTags(query);
|
|
99
|
+
const coreFacts = queryFacts({
|
|
100
|
+
minConfidence: 0.95,
|
|
101
|
+
lang,
|
|
102
|
+
limit: 5
|
|
103
|
+
});
|
|
104
|
+
const categoryFacts = categories.length > 0 ? queryFacts({
|
|
105
|
+
categories,
|
|
106
|
+
minConfidence: 0.7,
|
|
107
|
+
lang,
|
|
108
|
+
limit: 10
|
|
109
|
+
}) : [];
|
|
110
|
+
const tagFacts = queryTags.length > 0 ? queryFacts({
|
|
111
|
+
tags: queryTags,
|
|
112
|
+
minConfidence: 0.6,
|
|
113
|
+
lang,
|
|
114
|
+
limit: 5
|
|
115
|
+
}) : [];
|
|
116
|
+
const seen = /* @__PURE__ */ new Set();
|
|
117
|
+
const result = [];
|
|
118
|
+
for (const fact of [...categoryFacts, ...tagFacts, ...coreFacts]) {
|
|
119
|
+
if (!seen.has(fact.id)) {
|
|
120
|
+
seen.add(fact.id);
|
|
121
|
+
result.push(fact);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return result.slice(0, maxFacts);
|
|
125
|
+
}
|
|
126
|
+
export {
|
|
127
|
+
matchFactsToQuery
|
|
128
|
+
};
|