simple-dynamsoft-mcp 7.7.0 → 7.7.2
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/data/metadata/data-manifest.json +24 -24
- package/data/metadata/dynamsoft_sdks.json +3 -3
- package/package.json +2 -2
- package/scripts/prebuild-rag-index.mjs +45 -9
- package/scripts/seed-rag-cache.sh +42 -0
- package/src/rag/config.js +3 -0
- package/src/rag/index.js +4 -1
- package/src/rag/providers.js +76 -74
- package/src/rag/vector-cache.js +105 -47
- package/src/server/resource-index.js +5 -1
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
"branch": "main",
|
|
29
29
|
"owner": "dynamsoft-docs",
|
|
30
30
|
"repo": "barcode-reader-docs-server",
|
|
31
|
-
"commit": "
|
|
32
|
-
"archiveUrl": "https://codeload.github.com/dynamsoft-docs/barcode-reader-docs-server/zip/
|
|
31
|
+
"commit": "f82b5b92fb039064be3d355f9aeaacf0947b7096",
|
|
32
|
+
"archiveUrl": "https://codeload.github.com/dynamsoft-docs/barcode-reader-docs-server/zip/f82b5b92fb039064be3d355f9aeaacf0947b7096"
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
"name": "data/documentation/capture-vision-docs",
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
"branch": "main",
|
|
39
39
|
"owner": "dynamsoft-docs",
|
|
40
40
|
"repo": "capture-vision-docs",
|
|
41
|
-
"commit": "
|
|
42
|
-
"archiveUrl": "https://codeload.github.com/dynamsoft-docs/capture-vision-docs/zip/
|
|
41
|
+
"commit": "428c0080975aee70f15e09deda09ca548a4f9a93",
|
|
42
|
+
"archiveUrl": "https://codeload.github.com/dynamsoft-docs/capture-vision-docs/zip/428c0080975aee70f15e09deda09ca548a4f9a93"
|
|
43
43
|
},
|
|
44
44
|
{
|
|
45
45
|
"name": "data/documentation/capture-vision-docs-js",
|
|
@@ -68,8 +68,8 @@
|
|
|
68
68
|
"branch": "main",
|
|
69
69
|
"owner": "dynamsoft-docs",
|
|
70
70
|
"repo": "capture-vision-docs-server",
|
|
71
|
-
"commit": "
|
|
72
|
-
"archiveUrl": "https://codeload.github.com/dynamsoft-docs/capture-vision-docs-server/zip/
|
|
71
|
+
"commit": "4138672377f9ac99380bc985d28fbbe457c31c2b",
|
|
72
|
+
"archiveUrl": "https://codeload.github.com/dynamsoft-docs/capture-vision-docs-server/zip/4138672377f9ac99380bc985d28fbbe457c31c2b"
|
|
73
73
|
},
|
|
74
74
|
{
|
|
75
75
|
"name": "data/documentation/document-viewer-docs",
|
|
@@ -108,8 +108,8 @@
|
|
|
108
108
|
"branch": "master",
|
|
109
109
|
"owner": "dynamsoft-docs",
|
|
110
110
|
"repo": "web-twain-docs",
|
|
111
|
-
"commit": "
|
|
112
|
-
"archiveUrl": "https://codeload.github.com/dynamsoft-docs/web-twain-docs/zip/
|
|
111
|
+
"commit": "256fe47f8b83c79cbf8f23034fe77eab65a3fc7b",
|
|
112
|
+
"archiveUrl": "https://codeload.github.com/dynamsoft-docs/web-twain-docs/zip/256fe47f8b83c79cbf8f23034fe77eab65a3fc7b"
|
|
113
113
|
},
|
|
114
114
|
{
|
|
115
115
|
"name": "data/samples/document-scanner-javascript",
|
|
@@ -148,8 +148,8 @@
|
|
|
148
148
|
"branch": "main",
|
|
149
149
|
"owner": "Dynamsoft",
|
|
150
150
|
"repo": "barcode-reader-c-cpp-samples",
|
|
151
|
-
"commit": "
|
|
152
|
-
"archiveUrl": "https://codeload.github.com/Dynamsoft/barcode-reader-c-cpp-samples/zip/
|
|
151
|
+
"commit": "96e6242b43253a930b1edf6bbe5b0dbcadf1bb5a",
|
|
152
|
+
"archiveUrl": "https://codeload.github.com/Dynamsoft/barcode-reader-c-cpp-samples/zip/96e6242b43253a930b1edf6bbe5b0dbcadf1bb5a"
|
|
153
153
|
},
|
|
154
154
|
{
|
|
155
155
|
"name": "data/samples/dynamsoft-barcode-reader-dotnet",
|
|
@@ -158,8 +158,8 @@
|
|
|
158
158
|
"branch": "main",
|
|
159
159
|
"owner": "Dynamsoft",
|
|
160
160
|
"repo": "barcode-reader-dotnet-samples",
|
|
161
|
-
"commit": "
|
|
162
|
-
"archiveUrl": "https://codeload.github.com/Dynamsoft/barcode-reader-dotnet-samples/zip/
|
|
161
|
+
"commit": "49e205da121046998d8633c80ff2e31d3d3594f3",
|
|
162
|
+
"archiveUrl": "https://codeload.github.com/Dynamsoft/barcode-reader-dotnet-samples/zip/49e205da121046998d8633c80ff2e31d3d3594f3"
|
|
163
163
|
},
|
|
164
164
|
{
|
|
165
165
|
"name": "data/samples/dynamsoft-barcode-reader-flutter",
|
|
@@ -178,8 +178,8 @@
|
|
|
178
178
|
"branch": "main",
|
|
179
179
|
"owner": "Dynamsoft",
|
|
180
180
|
"repo": "barcode-reader-java-samples",
|
|
181
|
-
"commit": "
|
|
182
|
-
"archiveUrl": "https://codeload.github.com/Dynamsoft/barcode-reader-java-samples/zip/
|
|
181
|
+
"commit": "fbadec4609803fb3ec542b20b630adb99481cfe2",
|
|
182
|
+
"archiveUrl": "https://codeload.github.com/Dynamsoft/barcode-reader-java-samples/zip/fbadec4609803fb3ec542b20b630adb99481cfe2"
|
|
183
183
|
},
|
|
184
184
|
{
|
|
185
185
|
"name": "data/samples/dynamsoft-barcode-reader-maui",
|
|
@@ -208,8 +208,8 @@
|
|
|
208
208
|
"branch": "master",
|
|
209
209
|
"owner": "Dynamsoft",
|
|
210
210
|
"repo": "barcode-reader-python-samples",
|
|
211
|
-
"commit": "
|
|
212
|
-
"archiveUrl": "https://codeload.github.com/Dynamsoft/barcode-reader-python-samples/zip/
|
|
211
|
+
"commit": "bd63acaaf5d8b427aee7d3d1c8b316dc3021b9f5",
|
|
212
|
+
"archiveUrl": "https://codeload.github.com/Dynamsoft/barcode-reader-python-samples/zip/bd63acaaf5d8b427aee7d3d1c8b316dc3021b9f5"
|
|
213
213
|
},
|
|
214
214
|
{
|
|
215
215
|
"name": "data/samples/dynamsoft-barcode-reader-react-native",
|
|
@@ -228,8 +228,8 @@
|
|
|
228
228
|
"branch": "main",
|
|
229
229
|
"owner": "Dynamsoft",
|
|
230
230
|
"repo": "capture-vision-cpp-samples",
|
|
231
|
-
"commit": "
|
|
232
|
-
"archiveUrl": "https://codeload.github.com/Dynamsoft/capture-vision-cpp-samples/zip/
|
|
231
|
+
"commit": "cfd0fc6f4fee070e188955dd0cccfab39ff2c02d",
|
|
232
|
+
"archiveUrl": "https://codeload.github.com/Dynamsoft/capture-vision-cpp-samples/zip/cfd0fc6f4fee070e188955dd0cccfab39ff2c02d"
|
|
233
233
|
},
|
|
234
234
|
{
|
|
235
235
|
"name": "data/samples/dynamsoft-capture-vision-dotnet",
|
|
@@ -238,8 +238,8 @@
|
|
|
238
238
|
"branch": "main",
|
|
239
239
|
"owner": "Dynamsoft",
|
|
240
240
|
"repo": "capture-vision-dotnet-samples",
|
|
241
|
-
"commit": "
|
|
242
|
-
"archiveUrl": "https://codeload.github.com/Dynamsoft/capture-vision-dotnet-samples/zip/
|
|
241
|
+
"commit": "f3fe6aecc7c3c628f4241fe3a52ed448680c9d64",
|
|
242
|
+
"archiveUrl": "https://codeload.github.com/Dynamsoft/capture-vision-dotnet-samples/zip/f3fe6aecc7c3c628f4241fe3a52ed448680c9d64"
|
|
243
243
|
},
|
|
244
244
|
{
|
|
245
245
|
"name": "data/samples/dynamsoft-capture-vision-flutter",
|
|
@@ -258,8 +258,8 @@
|
|
|
258
258
|
"branch": "main",
|
|
259
259
|
"owner": "Dynamsoft",
|
|
260
260
|
"repo": "capture-vision-java-samples",
|
|
261
|
-
"commit": "
|
|
262
|
-
"archiveUrl": "https://codeload.github.com/Dynamsoft/capture-vision-java-samples/zip/
|
|
261
|
+
"commit": "83660139a8d32b0b319b6bc0b213b827437ddc43",
|
|
262
|
+
"archiveUrl": "https://codeload.github.com/Dynamsoft/capture-vision-java-samples/zip/83660139a8d32b0b319b6bc0b213b827437ddc43"
|
|
263
263
|
},
|
|
264
264
|
{
|
|
265
265
|
"name": "data/samples/dynamsoft-capture-vision-maui",
|
|
@@ -298,8 +298,8 @@
|
|
|
298
298
|
"branch": "main",
|
|
299
299
|
"owner": "Dynamsoft",
|
|
300
300
|
"repo": "capture-vision-python-samples",
|
|
301
|
-
"commit": "
|
|
302
|
-
"archiveUrl": "https://codeload.github.com/Dynamsoft/capture-vision-python-samples/zip/
|
|
301
|
+
"commit": "6f350f5d68e3ddcd90041bf46cac5541677f04b5",
|
|
302
|
+
"archiveUrl": "https://codeload.github.com/Dynamsoft/capture-vision-python-samples/zip/6f350f5d68e3ddcd90041bf46cac5541677f04b5"
|
|
303
303
|
},
|
|
304
304
|
{
|
|
305
305
|
"name": "data/samples/dynamsoft-capture-vision-react-native",
|
|
@@ -120,7 +120,7 @@
|
|
|
120
120
|
"dbr-server": {
|
|
121
121
|
"name": "Dynamsoft Barcode Reader Server/Desktop SDK",
|
|
122
122
|
"description": "Barcode scanning for server/desktop platforms including Python, .NET, Java, C++, and Node.js.",
|
|
123
|
-
"version": "11.6.
|
|
123
|
+
"version": "11.6.5000",
|
|
124
124
|
"default_platform": "python",
|
|
125
125
|
"snippet_path": "dynamsoft-barcode-reader/server",
|
|
126
126
|
"platforms": {
|
|
@@ -255,7 +255,7 @@
|
|
|
255
255
|
"dcv-core": {
|
|
256
256
|
"name": "Dynamsoft Capture Vision Core",
|
|
257
257
|
"description": "Unified architecture documentation for Capture Vision capabilities across barcode, MRZ, document normalization, and parsing workflows.",
|
|
258
|
-
"version": "3.6.
|
|
258
|
+
"version": "3.6.5000",
|
|
259
259
|
"default_platform": "core",
|
|
260
260
|
"snippet_path": "dynamsoft-capture-vision/core",
|
|
261
261
|
"platforms": {
|
|
@@ -270,7 +270,7 @@
|
|
|
270
270
|
"dcv-server": {
|
|
271
271
|
"name": "Dynamsoft Capture Vision Server/Desktop SDK",
|
|
272
272
|
"description": "Capture Vision workflows for Python, .NET, Java, C++, and Node.js, including MRZ, document normalization, and driver license scenarios.",
|
|
273
|
-
"version": "3.6.
|
|
273
|
+
"version": "3.6.5000",
|
|
274
274
|
"default_platform": "python",
|
|
275
275
|
"snippet_path": "dynamsoft-capture-vision/server",
|
|
276
276
|
"platforms": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "simple-dynamsoft-mcp",
|
|
3
|
-
"version": "7.7.
|
|
3
|
+
"version": "7.7.2",
|
|
4
4
|
"description": "MCP server for Dynamsoft SDKs - Capture Vision, Barcode Reader (Mobile/Python/Web), Dynamic Web TWAIN, and Document Viewer. Provides documentation, code snippets, and API guidance.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"scripts": {
|
|
25
25
|
"start": "node src/index.js",
|
|
26
26
|
"test": "npm run test:lite",
|
|
27
|
-
"test:unit": "node --test test/unit/gemini-retry.test.js test/unit/profile-config.test.js test/unit/lexical-provider.test.js test/unit/hydration-mode.test.js test/unit/hydration-policy.test.js test/unit/repo-map.test.js test/unit/download-utils.test.js test/unit/logging.test.js test/unit/request-logging.test.js test/unit/request-log-file.test.js test/unit/rag-signature-manifest.test.js test/unit/startup-timing.test.js test/unit/http-startup-readiness.test.js test/unit/resource-index-startup.test.js test/unit/create-server.test.js test/unit/server-helpers.test.js test/unit/update-sdk-versions.test.js test/unit/version-policy.test.js test/unit/query-expansion.test.js test/unit/docs-loader-enrich.test.js test/unit/shared-docs.test.js test/unit/vector-cache.test.js test/unit/doc-slugs.test.js",
|
|
27
|
+
"test:unit": "node --test test/unit/gemini-retry.test.js test/unit/profile-config.test.js test/unit/lexical-provider.test.js test/unit/hydration-mode.test.js test/unit/hydration-policy.test.js test/unit/repo-map.test.js test/unit/download-utils.test.js test/unit/logging.test.js test/unit/request-logging.test.js test/unit/request-log-file.test.js test/unit/rag-signature-manifest.test.js test/unit/startup-timing.test.js test/unit/http-startup-readiness.test.js test/unit/resource-index-startup.test.js test/unit/create-server.test.js test/unit/server-helpers.test.js test/unit/update-sdk-versions.test.js test/unit/version-policy.test.js test/unit/query-expansion.test.js test/unit/docs-loader-enrich.test.js test/unit/shared-docs.test.js test/unit/vector-cache.test.js test/unit/doc-slugs.test.js test/unit/incremental-embedding.test.js",
|
|
28
28
|
"test:lite": "npm run test:stdio && npm run test:http && npm run test:package",
|
|
29
29
|
"test:lexical": "node --test test/integration/stdio.test.js test/integration/http.test.js",
|
|
30
30
|
"test:gemini": "node scripts/run-gemini-tests.mjs",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { basename, join, resolve } from "node:path";
|
|
5
5
|
|
|
6
6
|
function toPosixPath(path) {
|
|
@@ -25,7 +25,7 @@ ensureEnvDefaults();
|
|
|
25
25
|
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
26
26
|
const { prewarmRagIndex, ragConfig } = await import("../src/rag/index.js");
|
|
27
27
|
|
|
28
|
-
await prewarmRagIndex();
|
|
28
|
+
await prewarmRagIndex({ throwOnError: true });
|
|
29
29
|
|
|
30
30
|
const cacheDir = resolve(ragConfig.cacheDir);
|
|
31
31
|
if (!existsSync(cacheDir)) {
|
|
@@ -33,8 +33,10 @@ if (!existsSync(cacheDir)) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
// Each cache is a JSON header plus a sibling `.f32` vector file (see
|
|
36
|
-
// src/rag/vector-cache.js); both ship in the release archive.
|
|
37
|
-
|
|
36
|
+
// src/rag/vector-cache.js); both ship in the release archive. A header with no
|
|
37
|
+
// `.f32` is a pre-7.7 all-JSON cache (e.g. restored from an older CI cache):
|
|
38
|
+
// the runtime cannot load it, so it is deleted rather than shipped.
|
|
39
|
+
const allHeaders = readdirSync(cacheDir, { withFileTypes: true })
|
|
38
40
|
.filter((entry) => entry.isFile())
|
|
39
41
|
.map((entry) => join(cacheDir, entry.name))
|
|
40
42
|
.filter((path) => {
|
|
@@ -46,16 +48,50 @@ const cacheFiles = readdirSync(cacheDir, { withFileTypes: true })
|
|
|
46
48
|
})
|
|
47
49
|
.sort();
|
|
48
50
|
|
|
51
|
+
const toVectorFile = (path) => path.replace(/\.json$/i, ".f32");
|
|
52
|
+
const readMeta = (path) => {
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(readFileSync(path, "utf8")).meta || {};
|
|
55
|
+
} catch {
|
|
56
|
+
return {};
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
// The model the build just used; only its cache is shipped.
|
|
60
|
+
const activeModel = ragConfig.provider === "gemini" ? ragConfig.geminiModel : "";
|
|
61
|
+
|
|
62
|
+
const cacheFiles = [];
|
|
63
|
+
for (const path of allHeaders) {
|
|
64
|
+
if (!existsSync(toVectorFile(path))) {
|
|
65
|
+
rmSync(path, { force: true });
|
|
66
|
+
console.log(`[rag-prebuild] removed legacy cache without vectors: ${basename(path)}`);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const meta = readMeta(path);
|
|
70
|
+
if (meta.provider === ragConfig.provider && meta.model !== activeModel) {
|
|
71
|
+
// A seeded index for a previous model (e.g. after a model change). The
|
|
72
|
+
// build succeeded with the active model, so it is obsolete; never ship it.
|
|
73
|
+
rmSync(path, { force: true });
|
|
74
|
+
rmSync(toVectorFile(path), { force: true });
|
|
75
|
+
console.log(`[rag-prebuild] removed cache for previous model ${meta.model}: ${basename(path)}`);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
cacheFiles.push(path);
|
|
79
|
+
}
|
|
80
|
+
|
|
49
81
|
if (cacheFiles.length === 0) {
|
|
50
|
-
throw new Error(`No cache
|
|
82
|
+
throw new Error(`No cache files with vectors found in ${cacheDir}.`);
|
|
51
83
|
}
|
|
52
84
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
85
|
+
// A successful build prunes every other cache for its provider/model, so exactly
|
|
86
|
+
// one complete cache for the active model must remain; more means a stale
|
|
87
|
+
// seeded index survived and could be shipped under this version.
|
|
88
|
+
const providerCaches = cacheFiles.filter((path) => readMeta(path).provider === ragConfig.provider);
|
|
89
|
+
if (providerCaches.length !== 1) {
|
|
90
|
+
throw new Error(`Expected exactly one ${ragConfig.provider} cache for ${activeModel} after the build, found ${providerCaches.length}: ${providerCaches.map((path) => basename(path)).join(", ")}`);
|
|
57
91
|
}
|
|
58
92
|
|
|
93
|
+
const vectorFiles = cacheFiles.map(toVectorFile);
|
|
94
|
+
|
|
59
95
|
let indexSignature = "";
|
|
60
96
|
let indexCacheKey = "";
|
|
61
97
|
const geminiCacheFile = cacheFiles.find((path) => /rag-gemini-.*\.json$/i.test(path));
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Seed a RAG cache directory with the vectors from the latest published
|
|
3
|
+
# release, so the next build embeds only chunks whose text changed.
|
|
4
|
+
#
|
|
5
|
+
# scripts/seed-rag-cache.sh <cache-dir> [release-tag]
|
|
6
|
+
#
|
|
7
|
+
# release-tag: seed from that release instead of the latest one.
|
|
8
|
+
#
|
|
9
|
+
# Needs `gh` with GH_TOKEN. Skips (exit 0) when the directory already holds a
|
|
10
|
+
# vector file or no release asset exists; a seed is an optimization only.
|
|
11
|
+
set -euo pipefail
|
|
12
|
+
|
|
13
|
+
cache_dir="${1:?usage: seed-rag-cache.sh <cache-dir> [release-tag]}"
|
|
14
|
+
release_tag="${2:-}"
|
|
15
|
+
repo="${GITHUB_REPOSITORY:-yushulx/simple-dynamsoft-mcp}"
|
|
16
|
+
mkdir -p "$cache_dir"
|
|
17
|
+
|
|
18
|
+
# Only a complete cache (header + vectors) counts; a leftover partial
|
|
19
|
+
# checkpoint from an interrupted run must not block seeding.
|
|
20
|
+
for header in "$cache_dir"/rag-gemini-*.json; do
|
|
21
|
+
[[ -e "$header" && "$header" != *.checkpoint.json && -f "${header%.json}.f32" ]] || continue
|
|
22
|
+
echo "[seed] $cache_dir already has a complete cache ($(basename "$header")); not seeding"
|
|
23
|
+
exit 0
|
|
24
|
+
done
|
|
25
|
+
|
|
26
|
+
work="$(mktemp -d)"
|
|
27
|
+
trap 'rm -rf "$work"' EXIT
|
|
28
|
+
|
|
29
|
+
if ! gh release download ${release_tag:+"$release_tag"} --repo "$repo" --pattern 'prebuilt-rag-index-gemini-*.tar.gz' --dir "$work" 2>"$work/err.log"; then
|
|
30
|
+
echo "[seed] no previous release index available: $(tr '\n' ' ' < "$work/err.log")"
|
|
31
|
+
exit 0
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
archive="$(find "$work" -maxdepth 1 -name 'prebuilt-rag-index-gemini-*.tar.gz' | head -n 1)"
|
|
35
|
+
mkdir -p "$work/extract"
|
|
36
|
+
tar -xzf "$archive" -C "$work/extract"
|
|
37
|
+
copied=0
|
|
38
|
+
while IFS= read -r file; do
|
|
39
|
+
cp "$file" "$cache_dir/"
|
|
40
|
+
copied=$((copied + 1))
|
|
41
|
+
done < <(find "$work/extract" -type f -name 'rag-gemini-*' ! -name '*.checkpoint.*')
|
|
42
|
+
echo "[seed] copied $copied file(s) from $(basename "$archive") into $cache_dir"
|
package/src/rag/config.js
CHANGED
|
@@ -71,6 +71,9 @@ const ragConfig = {
|
|
|
71
71
|
minScore: readFloatEnv("RAG_MIN_SCORE", 0.2),
|
|
72
72
|
includeScore: readBoolEnv("RAG_INCLUDE_SCORE", false),
|
|
73
73
|
rebuild: readBoolEnv("RAG_REBUILD", false),
|
|
74
|
+
// Upper bound on chunks one build may embed (0 = no limit). CI sets it so a
|
|
75
|
+
// missing seed or an unexpected change cannot trigger a full paid re-embed.
|
|
76
|
+
maxEmbedChunks: readIntEnv("RAG_MAX_EMBED_CHUNKS", 0),
|
|
74
77
|
prewarm: readBoolEnv("RAG_PREWARM", defaultPrewarm),
|
|
75
78
|
prewarmBlock: readBoolEnv("RAG_PREWARM_BLOCK", false),
|
|
76
79
|
prebuiltIndexAutoDownload: readBoolEnv("RAG_PREBUILT_INDEX_AUTO_DOWNLOAD", true),
|
package/src/rag/index.js
CHANGED
|
@@ -215,7 +215,9 @@ async function searchResources({ query, product, edition, platform, type, limit,
|
|
|
215
215
|
return [];
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
-
|
|
218
|
+
// `throwOnError` rethrows build failures; the prebuild script uses it so a
|
|
219
|
+
// failed build cannot publish whatever older index is in the cache dir.
|
|
220
|
+
async function prewarmRagIndex({ throwOnError = false } = {}) {
|
|
219
221
|
if (!ragConfig.prewarm) return;
|
|
220
222
|
logRagConfigOnce(ragConfig);
|
|
221
223
|
const providers = providerOrchestrator.resolveProviderChain();
|
|
@@ -245,6 +247,7 @@ async function prewarmRagIndex() {
|
|
|
245
247
|
provider: primary,
|
|
246
248
|
error: error.message
|
|
247
249
|
}, { level: "error" });
|
|
250
|
+
if (throwOnError) throw error;
|
|
248
251
|
}
|
|
249
252
|
}
|
|
250
253
|
|
package/src/rag/providers.js
CHANGED
|
@@ -275,12 +275,6 @@ function createProviderOrchestrator({
|
|
|
275
275
|
const cacheKey = createHash("sha256").update(JSON.stringify(cacheMeta)).digest("hex");
|
|
276
276
|
const cacheFile = join(ragConfig.cacheDir, vectorCache.makeCacheFileName(name, model, cacheKey));
|
|
277
277
|
const checkpointFile = join(ragConfig.cacheDir, vectorCache.makeCheckpointFileName(name, model, cacheKey));
|
|
278
|
-
const expectedCacheState = {
|
|
279
|
-
cacheKey,
|
|
280
|
-
signature,
|
|
281
|
-
provider: name,
|
|
282
|
-
model
|
|
283
|
-
};
|
|
284
278
|
logRag(
|
|
285
279
|
`provider=${name} cache_file=${cacheFile} rebuild=${ragConfig.rebuild} cache_key=${cacheKey.slice(0, 12)}`
|
|
286
280
|
);
|
|
@@ -289,103 +283,110 @@ function createProviderOrchestrator({
|
|
|
289
283
|
const loadIndex = async () => {
|
|
290
284
|
if (indexPromise) return indexPromise;
|
|
291
285
|
indexPromise = (async () => {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
return {
|
|
300
|
-
items: cached.items,
|
|
301
|
-
vectors: cached.vectors
|
|
302
|
-
};
|
|
303
|
-
}
|
|
304
|
-
logRag(`cache miss provider=${name} file=${cacheFile} reason=${cacheState.reason}`);
|
|
286
|
+
// Every load goes through chunk-hash validation: a cache file is
|
|
287
|
+
// trusted only for chunks whose text hash still matches, never because
|
|
288
|
+
// its signature or filename matches.
|
|
289
|
+
if (ragConfig.rebuild) {
|
|
290
|
+
logRag(`cache bypass provider=${name} file=${cacheFile} reason=rebuild_true`);
|
|
291
|
+
vectorCache.clearVectorIndexCheckpoint(checkpointFile);
|
|
292
|
+
}
|
|
305
293
|
|
|
306
|
-
|
|
294
|
+
const items = utils.buildEmbeddingItems(resourceIndex, ragConfig);
|
|
295
|
+
// Each chunk is identified by a hash of model + text, so vectors from any
|
|
296
|
+
// earlier cache, checkpoint, or release can be reused for unchanged text
|
|
297
|
+
// and only new or edited chunks are embedded.
|
|
298
|
+
const indexedItems = items.map((item) => ({
|
|
299
|
+
id: item.id,
|
|
300
|
+
uri: item.uri,
|
|
301
|
+
hash: createHash("sha256").update(`${model}\n${item.text}`).digest("hex").slice(0, 32)
|
|
302
|
+
}));
|
|
303
|
+
const checkpointHashes = new Set();
|
|
304
|
+
const collect = () => (ragConfig.rebuild
|
|
305
|
+
? new Map()
|
|
306
|
+
: vectorCache.collectReusableVectors({ provider: name, model, checkpointHashes }));
|
|
307
|
+
let reusable = collect();
|
|
308
|
+
// Local caches do not cover every chunk (e.g. an older release's index
|
|
309
|
+
// after an upgrade): try this release's prebuilt index before paying to
|
|
310
|
+
// embed, then re-collect.
|
|
311
|
+
if (!ragConfig.rebuild && indexedItems.some((item) => !reusable.has(item.hash))) {
|
|
312
|
+
const download = await vectorCache.maybeDownloadPrebuiltVectorIndex({
|
|
307
313
|
provider: name,
|
|
308
314
|
model,
|
|
309
315
|
cacheKey,
|
|
310
316
|
signature,
|
|
311
317
|
cacheFile
|
|
312
318
|
});
|
|
313
|
-
if (
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
vectorCache.clearVectorIndexCheckpoint(checkpointFile);
|
|
319
|
+
if (download.downloaded) reusable = collect();
|
|
320
|
+
}
|
|
321
|
+
const vectors = indexedItems.map((item) => reusable.get(item.hash) || null);
|
|
322
|
+
const missing = [];
|
|
323
|
+
vectors.forEach((vector, index) => {
|
|
324
|
+
if (!vector) missing.push(index);
|
|
325
|
+
});
|
|
326
|
+
logRag(
|
|
327
|
+
`index plan provider=${name} items=${indexedItems.length} reused=${indexedItems.length - missing.length} to_embed=${missing.length}`
|
|
328
|
+
);
|
|
329
|
+
if (ragConfig.maxEmbedChunks > 0 && missing.length > ragConfig.maxEmbedChunks) {
|
|
330
|
+
throw new Error(
|
|
331
|
+
`Refusing to embed ${missing.length} chunks (RAG_MAX_EMBED_CHUNKS=${ragConfig.maxEmbedChunks}); ` +
|
|
332
|
+
`only ${indexedItems.length - missing.length}/${indexedItems.length} could be reused. ` +
|
|
333
|
+
"Check that the cache was seeded from a matching release index."
|
|
334
|
+
);
|
|
330
335
|
}
|
|
331
336
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
if (!ragConfig.rebuild) {
|
|
338
|
-
const checkpointState = vectorCache.loadVectorIndexCheckpoint(checkpointFile, cacheKey, indexedItems);
|
|
339
|
-
if (checkpointState.hit) {
|
|
340
|
-
normalized = checkpointState.payload.vectors;
|
|
341
|
-
resumeFrom = normalized.length;
|
|
342
|
-
logRag(
|
|
343
|
-
`checkpoint resume provider=${name} file=${checkpointFile} completed=${resumeFrom}/${texts.length}`
|
|
344
|
-
);
|
|
345
|
-
} else if (checkpointState.reason !== "missing") {
|
|
346
|
-
logRag(`checkpoint ignored provider=${name} file=${checkpointFile} reason=${checkpointState.reason}`);
|
|
347
|
-
}
|
|
337
|
+
if (missing.length === 0 && vectorCache.cacheHeaderMatches({ cacheFile, provider: name, model, items: indexedItems })) {
|
|
338
|
+
vectorCache.clearVectorIndexCheckpoint(checkpointFile);
|
|
339
|
+
vectorCache.pruneOtherCaches({ provider: name, model, keepFile: cacheFile });
|
|
340
|
+
logRag(`cache hit provider=${name} file=${cacheFile} items=${indexedItems.length}`);
|
|
341
|
+
return { items: indexedItems, vectors };
|
|
348
342
|
}
|
|
349
343
|
|
|
350
344
|
if (name === "gemini" && embedder.resetMetrics) {
|
|
351
345
|
embedder.resetMetrics();
|
|
352
346
|
}
|
|
353
347
|
|
|
348
|
+
// The checkpoint keeps every vector this build had to produce: newly
|
|
349
|
+
// embedded ones plus those recovered from an earlier checkpoint, so a
|
|
350
|
+
// second interruption does not lose the first attempt's progress.
|
|
351
|
+
const missingSet = new Set(missing);
|
|
352
|
+
const checkpointIndices = indexedItems
|
|
353
|
+
.map((item, index) => index)
|
|
354
|
+
.filter((index) => missingSet.has(index) || checkpointHashes.has(indexedItems[index].hash));
|
|
354
355
|
const checkpointIntervalMs = 5000;
|
|
355
356
|
let lastCheckpointAt = 0;
|
|
356
357
|
const persistCheckpoint = (force = false) => {
|
|
357
358
|
const now = Date.now();
|
|
358
359
|
if (!force && now - lastCheckpointAt < checkpointIntervalMs) return;
|
|
359
|
-
const
|
|
360
|
+
const done = checkpointIndices.filter((index) => vectors[index]);
|
|
361
|
+
vectorCache.saveVectorIndexCheckpoint(checkpointFile, {
|
|
360
362
|
cacheKey,
|
|
361
363
|
meta: cacheMeta,
|
|
362
|
-
items: indexedItems,
|
|
363
|
-
vectors:
|
|
364
|
-
completed: normalized.length,
|
|
365
|
-
total: texts.length,
|
|
364
|
+
items: done.map((index) => indexedItems[index]),
|
|
365
|
+
vectors: done.map((index) => vectors[index]),
|
|
366
366
|
updatedAt: new Date().toISOString()
|
|
367
|
-
};
|
|
368
|
-
vectorCache.saveVectorIndexCheckpoint(checkpointFile, payload);
|
|
367
|
+
});
|
|
369
368
|
lastCheckpointAt = now;
|
|
370
369
|
};
|
|
371
370
|
|
|
372
|
-
if (
|
|
373
|
-
logRag(
|
|
374
|
-
|
|
375
|
-
);
|
|
371
|
+
if (missing.length > 0) {
|
|
372
|
+
logRag(`building index provider=${name} embed_items=${missing.length} batch_size=${batchSize}`);
|
|
373
|
+
let filled = 0;
|
|
376
374
|
try {
|
|
377
375
|
const embeddingResult = await embedTextsWithProgress(
|
|
378
|
-
|
|
376
|
+
missing.map((index) => items[index].text),
|
|
379
377
|
embedder,
|
|
380
378
|
batchSize,
|
|
381
379
|
{
|
|
382
|
-
offset:
|
|
383
|
-
total:
|
|
380
|
+
offset: 0,
|
|
381
|
+
total: missing.length,
|
|
384
382
|
providerName: name,
|
|
385
383
|
logRag,
|
|
386
384
|
isRateLimitError: (error) => utils.isRateLimitError(error, isRateLimitGeminiStatus),
|
|
387
|
-
onChunk: ({ vectors, completed, total }) => {
|
|
388
|
-
|
|
385
|
+
onChunk: ({ vectors: chunk, completed, total }) => {
|
|
386
|
+
for (const vector of chunk) {
|
|
387
|
+
vectors[missing[filled]] = utils.normalizeVector(vector);
|
|
388
|
+
filled += 1;
|
|
389
|
+
}
|
|
389
390
|
persistCheckpoint(completed >= total);
|
|
390
391
|
}
|
|
391
392
|
}
|
|
@@ -409,24 +410,25 @@ function createProviderOrchestrator({
|
|
|
409
410
|
`gemini build failed provider=${name} requests=${metrics.requests || 0} retries=${metrics.retries || 0} ` +
|
|
410
411
|
`retry_delay_ms=${metrics.retryDelayMs || 0} throttle_events=${metrics.throttleEvents || 0} ` +
|
|
411
412
|
`throttle_delay_ms=${metrics.throttleDelayMs || 0} rate_limit_retries=${metrics.rateLimitRetries || 0} ` +
|
|
412
|
-
`checkpoint_completed=${
|
|
413
|
+
`checkpoint_completed=${filled}/${missing.length} error=${error.message}`
|
|
413
414
|
);
|
|
414
415
|
}
|
|
415
416
|
throw error;
|
|
416
417
|
}
|
|
417
|
-
} else {
|
|
418
|
-
logRag(`checkpoint already complete provider=${name} completed=${resumeFrom}/${texts.length}`);
|
|
419
418
|
}
|
|
420
419
|
|
|
421
420
|
const payload = {
|
|
422
421
|
cacheKey,
|
|
423
422
|
meta: cacheMeta,
|
|
424
423
|
items: indexedItems,
|
|
425
|
-
vectors
|
|
424
|
+
vectors
|
|
426
425
|
};
|
|
427
426
|
vectorCache.saveVectorIndexCache(cacheFile, payload);
|
|
428
427
|
vectorCache.clearVectorIndexCheckpoint(checkpointFile);
|
|
429
|
-
|
|
428
|
+
vectorCache.pruneOtherCaches({ provider: name, model, keepFile: cacheFile });
|
|
429
|
+
logRag(
|
|
430
|
+
`cache saved provider=${name} file=${cacheFile} items=${payload.items.length} embedded=${missing.length}`
|
|
431
|
+
);
|
|
430
432
|
return {
|
|
431
433
|
items: payload.items,
|
|
432
434
|
vectors: payload.vectors
|
package/src/rag/vector-cache.js
CHANGED
|
@@ -10,14 +10,100 @@ function ensureDirectory(path) {
|
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
function
|
|
13
|
+
function cacheFilePrefix(provider, model) {
|
|
14
14
|
const safeModel = String(model || "default").replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 32);
|
|
15
|
-
return `rag-${provider}-${safeModel}
|
|
15
|
+
return `rag-${provider}-${safeModel}-`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function makeCacheFileName(provider, model, cacheKey) {
|
|
19
|
+
return `${cacheFilePrefix(provider, model)}${cacheKey.slice(0, 12)}.json`;
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
function makeCheckpointFileName(provider, model, cacheKey) {
|
|
19
|
-
|
|
20
|
-
|
|
23
|
+
return `${cacheFilePrefix(provider, model)}${cacheKey.slice(0, 12)}.checkpoint.json`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Header metadata only (no vectors). Returns null when unreadable.
|
|
27
|
+
function readHeader(headerFile) {
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(readFileSync(headerFile, "utf8"));
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function headerBelongsTo(header, provider, model) {
|
|
36
|
+
return header?.meta?.provider === provider && header?.meta?.model === model;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Cache and checkpoint headers whose stored provider and model match exactly.
|
|
40
|
+
// The filename prefix only narrows the search: sanitized/truncated model names
|
|
41
|
+
// can share a prefix (`m` vs `m-other`).
|
|
42
|
+
function listProviderCacheHeaders(cacheDir, provider, model) {
|
|
43
|
+
if (!existsSync(cacheDir)) return [];
|
|
44
|
+
const prefix = cacheFilePrefix(provider, model);
|
|
45
|
+
return readdirSync(cacheDir)
|
|
46
|
+
.filter((name) => name.startsWith(prefix) && name.endsWith(".json"))
|
|
47
|
+
.map((name) => join(cacheDir, name))
|
|
48
|
+
.filter((path) => headerBelongsTo(readHeader(path), provider, model));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isCheckpointHeader(path) {
|
|
52
|
+
return path.endsWith(".checkpoint.json");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// True when `cacheFile` already holds exactly these items (same order and
|
|
56
|
+
// chunk hashes) with a correctly sized vector file, so a build that embeds
|
|
57
|
+
// nothing does not need to rewrite it.
|
|
58
|
+
function cacheHeaderMatches({ cacheFile, provider, model, items }) {
|
|
59
|
+
const header = readHeader(cacheFile);
|
|
60
|
+
if (!header || !headerBelongsTo(header, provider, model) || header.vectorFormat !== VECTOR_FORMAT) return false;
|
|
61
|
+
if (!Array.isArray(header.items) || header.items.length !== items.length || header.vectorCount !== items.length) return false;
|
|
62
|
+
if (!header.items.every((item, i) => item.hash && item.hash === items[i].hash && item.id === items[i].id && item.uri === items[i].uri)) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
const vectorFile = vectorFileFor(cacheFile);
|
|
66
|
+
return existsSync(vectorFile)
|
|
67
|
+
&& statSync(vectorFile).size === header.vectorCount * header.dimensions * Float32Array.BYTES_PER_ELEMENT;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
// Vectors already computed for identical chunk text, keyed by item `hash`
|
|
72
|
+
// (see providers.js). Sources are every cache and checkpoint for the same
|
|
73
|
+
// provider/model in cacheDir: a previous version's cache, an extracted
|
|
74
|
+
// previous release, or a partial checkpoint. Only caches whose items carry
|
|
75
|
+
// chunk-text hashes are used; older hash-less caches cannot prove their text
|
|
76
|
+
// is unchanged, so they are ignored.
|
|
77
|
+
// `checkpointHashes` (optional Set) receives the hashes found in checkpoints,
|
|
78
|
+
// so a resumed build can carry them into its own checkpoint.
|
|
79
|
+
function collectReusableVectors({ cacheDir, provider, model, checkpointHashes }) {
|
|
80
|
+
const reusable = new Map();
|
|
81
|
+
for (const headerFile of listProviderCacheHeaders(cacheDir, provider, model)) {
|
|
82
|
+
let payload;
|
|
83
|
+
try {
|
|
84
|
+
const result = readVectorFiles(headerFile);
|
|
85
|
+
if (result.error) continue;
|
|
86
|
+
payload = result.payload;
|
|
87
|
+
} catch {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (!payload.items.every((item) => item.hash)) continue;
|
|
91
|
+
const fromCheckpoint = isCheckpointHeader(headerFile);
|
|
92
|
+
payload.items.forEach((item, i) => {
|
|
93
|
+
if (!payload.vectors[i]) return;
|
|
94
|
+
if (fromCheckpoint) checkpointHashes?.add(item.hash);
|
|
95
|
+
if (!reusable.has(item.hash)) reusable.set(item.hash, payload.vectors[i]);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return reusable;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Keep only `keepFile` (and its vectors) among this provider/model's caches,
|
|
102
|
+
// so restored/seeded caches do not accumulate in CI caches or release assets.
|
|
103
|
+
function pruneOtherCaches({ cacheDir, provider, model, keepFile }) {
|
|
104
|
+
for (const headerFile of listProviderCacheHeaders(cacheDir, provider, model)) {
|
|
105
|
+
if (resolve(headerFile) !== resolve(keepFile)) removeVectorFiles(headerFile);
|
|
106
|
+
}
|
|
21
107
|
}
|
|
22
108
|
|
|
23
109
|
// Cache layout: a JSON header (`<name>.json`: cacheKey, meta, items, vector
|
|
@@ -126,35 +212,8 @@ function saveVectorIndexCache(cacheDir, cacheFile, payload) {
|
|
|
126
212
|
writeVectorFiles(cacheFile, payload);
|
|
127
213
|
}
|
|
128
214
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
return { hit: false, reason: "missing", payload: null };
|
|
132
|
-
}
|
|
133
|
-
try {
|
|
134
|
-
const { payload: parsed, error } = readVectorFiles(checkpointFile);
|
|
135
|
-
if (error) {
|
|
136
|
-
return { hit: false, reason: error, payload: null };
|
|
137
|
-
}
|
|
138
|
-
if (parsed.cacheKey !== expectedKey) {
|
|
139
|
-
return { hit: false, reason: "cache_key_mismatch", payload: null };
|
|
140
|
-
}
|
|
141
|
-
if (parsed.items.length !== expectedItems.length) {
|
|
142
|
-
return { hit: false, reason: "items_length_mismatch", payload: null };
|
|
143
|
-
}
|
|
144
|
-
for (let i = 0; i < expectedItems.length; i += 1) {
|
|
145
|
-
if (parsed.items[i]?.id !== expectedItems[i]?.id || parsed.items[i]?.uri !== expectedItems[i]?.uri) {
|
|
146
|
-
return { hit: false, reason: "items_mismatch", payload: null };
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
if (parsed.vectors.length > expectedItems.length) {
|
|
150
|
-
return { hit: false, reason: "vectors_overflow", payload: null };
|
|
151
|
-
}
|
|
152
|
-
return { hit: true, reason: "ok", payload: parsed };
|
|
153
|
-
} catch {
|
|
154
|
-
return { hit: false, reason: "parse_error", payload: null };
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
215
|
+
// Checkpoints hold only the items embedded so far (each with its `hash`);
|
|
216
|
+
// collectReusableVectors picks them up on the next attempt.
|
|
158
217
|
function saveVectorIndexCheckpoint(cacheDir, checkpointFile, payload) {
|
|
159
218
|
ensureDirectory(cacheDir);
|
|
160
219
|
writeVectorFiles(checkpointFile, payload);
|
|
@@ -332,21 +391,18 @@ function createVectorCacheHelpers({ ragConfig, pkgVersion, legacyPrebuiltIndexUr
|
|
|
332
391
|
continue;
|
|
333
392
|
}
|
|
334
393
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
}
|
|
344
|
-
};
|
|
345
|
-
saveVectorIndexCache(ragConfig.cacheDir, cacheFile, migratedPayload);
|
|
394
|
+
// Install unchanged (own cacheKey and signature) under a separate
|
|
395
|
+
// name, so a local cache with the same name (and vectors for local
|
|
396
|
+
// edits) is not overwritten. The build reuses only vectors whose
|
|
397
|
+
// chunk-text hash matches, then prunes this extra copy.
|
|
398
|
+
ensureDirectory(ragConfig.cacheDir);
|
|
399
|
+
const installedFile = join(ragConfig.cacheDir, basename(sourceCacheFile).replace(/\.json$/i, "-prebuilt.json"));
|
|
400
|
+
copyFileSync(sourceCacheFile, installedFile);
|
|
401
|
+
copyFileSync(vectorFileFor(sourceCacheFile), vectorFileFor(installedFile));
|
|
346
402
|
logRag(
|
|
347
|
-
`prebuilt index installed provider=${provider} cache_file=${
|
|
403
|
+
`prebuilt index installed provider=${provider} cache_file=${installedFile} version=${cachePackageVersion}`
|
|
348
404
|
);
|
|
349
|
-
return { downloaded: true, reason: "
|
|
405
|
+
return { downloaded: true, reason: "installed" };
|
|
350
406
|
}
|
|
351
407
|
|
|
352
408
|
throw new Error(
|
|
@@ -371,9 +427,11 @@ function createVectorCacheHelpers({ ragConfig, pkgVersion, legacyPrebuiltIndexUr
|
|
|
371
427
|
makeCheckpointFileName,
|
|
372
428
|
loadVectorIndexCache,
|
|
373
429
|
saveVectorIndexCache: (cacheFile, payload) => saveVectorIndexCache(ragConfig.cacheDir, cacheFile, payload),
|
|
374
|
-
loadVectorIndexCheckpoint,
|
|
375
430
|
saveVectorIndexCheckpoint: (checkpointFile, payload) => saveVectorIndexCheckpoint(ragConfig.cacheDir, checkpointFile, payload),
|
|
376
431
|
clearVectorIndexCheckpoint,
|
|
432
|
+
collectReusableVectors: (options) => collectReusableVectors({ cacheDir: ragConfig.cacheDir, ...options }),
|
|
433
|
+
cacheHeaderMatches,
|
|
434
|
+
pruneOtherCaches: (options) => pruneOtherCaches({ cacheDir: ragConfig.cacheDir, ...options }),
|
|
377
435
|
maybeDownloadPrebuiltVectorIndex
|
|
378
436
|
};
|
|
379
437
|
}
|
|
@@ -84,7 +84,11 @@ import {
|
|
|
84
84
|
} from "./resource-index/builders.js";
|
|
85
85
|
|
|
86
86
|
const registry = JSON.parse(readFileSync(registryPath, "utf8"));
|
|
87
|
-
|
|
87
|
+
// Hash LF-normalized text so Windows (CRLF) and Linux checkouts of the same
|
|
88
|
+
// registry produce the same RAG index signature.
|
|
89
|
+
const registrySha256 = createHash("sha256")
|
|
90
|
+
.update(readFileSync(registryPath, "utf8").replace(/\r\n/g, "\n"))
|
|
91
|
+
.digest("hex");
|
|
88
92
|
|
|
89
93
|
function inferDbrMobilePlatform(articlePath) {
|
|
90
94
|
const normalized = String(articlePath || "").replace(/\\/g, "/").toLowerCase();
|