simple-dynamsoft-mcp 7.3.54 → 7.4.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/package.json +4 -2
- package/scripts/check-version-drift.mjs +80 -0
- package/src/rag/config.js +5 -0
- package/src/rag/index.js +5 -1
- package/src/rag/lexical-provider.js +48 -15
- package/src/rag/providers.js +27 -6
- package/src/rag/search-utils.js +144 -4
- package/src/rag/vector-cache.js +17 -1
- package/src/server/create-server.js +121 -99
- package/src/server/normalizers.js +78 -1
- package/src/server/public-offerings.js +12 -0
- package/src/server/resource-index/builders.js +76 -16
- package/src/server/resource-index/config.js +4 -2
- package/src/server/resource-index/docs-loader.js +22 -2
- package/src/server/resource-index/samples.js +24 -3
- package/src/server/resource-index/version-policy.js +34 -8
- package/src/server/resources/register-resources.js +4 -1
- package/src/server/tools/public-routing.js +8 -0
- package/src/server/tools/register-index-tools.js +64 -10
- package/src/server/tools/register-project-tools.js +160 -18
- package/src/server/tools/register-quickstart-tools.js +357 -40
- package/src/server/tools/register-sample-tools.js +10 -5
- package/src/server/tools/register-version-tools.js +10 -5
|
@@ -61,110 +61,132 @@ export function createMcpServerInstance({ pkgVersion, resourceIndexApi, ragApi }
|
|
|
61
61
|
refreshRagIndexes
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
-
const server = new McpServer({
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
64
|
+
const server = new McpServer({
|
|
65
|
+
name: "simple-dynamsoft-mcp",
|
|
66
|
+
version: pkgVersion,
|
|
67
|
+
description: "MCP server for the public Dynamsoft offerings: Barcode Reader, Dynamic Web TWAIN, Document Viewer, MRZ, and MDS. Includes guidance for choosing the right public product by workflow."
|
|
68
|
+
}, {
|
|
69
|
+
instructions: [
|
|
70
|
+
"This server answers questions about five public Dynamsoft offerings: DBR, DWT, DDV, MRZ, MDS.",
|
|
71
|
+
"",
|
|
72
|
+
"How to use it:",
|
|
73
|
+
"- Call get_index first to see valid product/edition/platform combinations before asking the user anything.",
|
|
74
|
+
"- Only ask the user to disambiguate dimensions that actually vary for the chosen product. Do NOT ask about dimensions that don't apply.",
|
|
75
|
+
"",
|
|
76
|
+
"Platform/edition rules (important):",
|
|
77
|
+
"- DBR (Dynamsoft Barcode Reader) is the ONLY product with multiple editions: mobile, web, and server/desktop. Ask which one when it is unclear.",
|
|
78
|
+
"- DWT (Dynamic Web TWAIN), DDV (Document Viewer), MRZ (MRZ Scanner), and MDS (Mobile Document Scanner) are web/JavaScript-only in this MCP. NEVER ask the user which platform or language for these — there is no .NET/Java/C++/mobile option here. Go straight to the web quickstart or samples.",
|
|
79
|
+
"",
|
|
80
|
+
"api_level rule:",
|
|
81
|
+
"- api_level (high-level / low-level) applies ONLY to DBR mobile. Never ask about it, or pass it, for web, server, or any product other than DBR mobile.",
|
|
82
|
+
"",
|
|
83
|
+
"For scopes this MCP does not index (e.g. MRZ/MDS on mobile or server), tools return official documentation and sample links — pass those on rather than inventing code."
|
|
84
|
+
].join("\n")
|
|
85
|
+
});
|
|
69
86
|
|
|
70
|
-
registerIndexTools({
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
});
|
|
87
|
+
registerIndexTools({
|
|
88
|
+
server,
|
|
89
|
+
ensureScopeHydrated,
|
|
90
|
+
ensureLatestMajor,
|
|
91
|
+
normalizeProduct,
|
|
92
|
+
normalizePlatform,
|
|
93
|
+
normalizeEdition,
|
|
94
|
+
buildIndexData,
|
|
95
|
+
getSampleIdFromUri,
|
|
96
|
+
formatScopeLabel,
|
|
97
|
+
searchResources,
|
|
98
|
+
normalizeSampleName,
|
|
99
|
+
getSampleEntries,
|
|
100
|
+
getSampleSuggestions
|
|
101
|
+
});
|
|
85
102
|
|
|
86
|
-
registerSampleTools({
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
});
|
|
103
|
+
registerSampleTools({
|
|
104
|
+
server,
|
|
105
|
+
registry,
|
|
106
|
+
ensureScopeHydrated,
|
|
107
|
+
ensureLatestMajor,
|
|
108
|
+
normalizeProduct,
|
|
109
|
+
normalizePlatform,
|
|
110
|
+
normalizeEdition,
|
|
111
|
+
getSampleEntries,
|
|
112
|
+
getSampleIdFromUri,
|
|
113
|
+
getDisplayEdition,
|
|
114
|
+
getDisplayPlatform
|
|
115
|
+
});
|
|
99
116
|
|
|
100
|
-
registerVersionTools({
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
});
|
|
117
|
+
registerVersionTools({
|
|
118
|
+
server,
|
|
119
|
+
ensureLatestMajor,
|
|
120
|
+
normalizeProduct,
|
|
121
|
+
normalizePlatform,
|
|
122
|
+
normalizeEdition,
|
|
123
|
+
LATEST_MAJOR,
|
|
124
|
+
LATEST_VERSIONS
|
|
125
|
+
});
|
|
109
126
|
|
|
110
|
-
registerQuickstartTools({
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
127
|
+
registerQuickstartTools({
|
|
128
|
+
server,
|
|
129
|
+
registry,
|
|
130
|
+
ensureScopeHydrated,
|
|
131
|
+
ensureLatestMajor,
|
|
132
|
+
normalizeProduct,
|
|
133
|
+
normalizePlatform,
|
|
134
|
+
normalizeEdition,
|
|
135
|
+
normalizeApiLevel,
|
|
136
|
+
discoverDcvMobileSamples,
|
|
137
|
+
discoverDcvWebSamples,
|
|
138
|
+
findCodeFilesInSample,
|
|
139
|
+
getMobileSamplePath,
|
|
140
|
+
getDbrServerSamplePath,
|
|
141
|
+
getDcvMobileSamplePath,
|
|
142
|
+
getDcvServerSamplePath,
|
|
143
|
+
getDcvWebSamplePath,
|
|
144
|
+
getDwtSamplePath,
|
|
145
|
+
getDdvSamplePath,
|
|
146
|
+
readCodeFile,
|
|
147
|
+
getMainCodeFile,
|
|
148
|
+
getWebSamplePath,
|
|
149
|
+
getMrzWebSamplePath,
|
|
150
|
+
getMdsWebSamplePath,
|
|
151
|
+
getSampleSuggestions,
|
|
152
|
+
getSampleIdFromUri,
|
|
153
|
+
formatScopeLabel
|
|
154
|
+
});
|
|
133
155
|
|
|
134
|
-
registerProjectTools({
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
});
|
|
156
|
+
registerProjectTools({
|
|
157
|
+
server,
|
|
158
|
+
ensureScopeHydrated,
|
|
159
|
+
ensureLatestMajor,
|
|
160
|
+
normalizeProduct,
|
|
161
|
+
normalizePlatform,
|
|
162
|
+
normalizeEdition,
|
|
163
|
+
normalizeApiLevel,
|
|
164
|
+
normalizeSampleName,
|
|
165
|
+
parseResourceUri,
|
|
166
|
+
parseSampleUri,
|
|
167
|
+
formatScopeLabel,
|
|
168
|
+
getSampleIdFromUri,
|
|
169
|
+
discoverDwtSamples,
|
|
170
|
+
getMobileSamplePath,
|
|
171
|
+
getWebSamplePath,
|
|
172
|
+
getDbrServerSamplePath,
|
|
173
|
+
getDcvMobileSamplePath,
|
|
174
|
+
getDcvServerSamplePath,
|
|
175
|
+
getDcvWebSamplePath,
|
|
176
|
+
getMrzWebSamplePath,
|
|
177
|
+
getMdsWebSamplePath,
|
|
178
|
+
getDwtSamplePath,
|
|
179
|
+
getDdvSamplePath,
|
|
180
|
+
getSampleSuggestions
|
|
181
|
+
});
|
|
160
182
|
|
|
161
|
-
registerResourceHandlers({
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
});
|
|
183
|
+
registerResourceHandlers({
|
|
184
|
+
server,
|
|
185
|
+
getPinnedResources,
|
|
186
|
+
parseResourceUri,
|
|
187
|
+
ensureLatestMajor,
|
|
188
|
+
readResourceContent
|
|
189
|
+
});
|
|
168
190
|
|
|
169
|
-
return server;
|
|
191
|
+
return server;
|
|
170
192
|
}
|
|
@@ -306,6 +306,79 @@ function isWebPlatform(platform) {
|
|
|
306
306
|
return platform === "web" || isWebFrameworkPlatform(platform);
|
|
307
307
|
}
|
|
308
308
|
|
|
309
|
+
// Canonical platform values a caller may pass. Derived from the alias table so
|
|
310
|
+
// this stays in sync automatically as platforms are added (issue #152).
|
|
311
|
+
// "core" is a valid (DCV-internal) platform advertised in tool descriptions but
|
|
312
|
+
// isn't an alias value, so add it explicitly to keep the validator consistent
|
|
313
|
+
// with what the schema tells agents to pass.
|
|
314
|
+
const KNOWN_PLATFORMS = new Set([...Object.values(platformAliases), "core"]);
|
|
315
|
+
const KNOWN_EDITIONS = new Set(["core", "mobile", "web", "server"]);
|
|
316
|
+
|
|
317
|
+
function nearestKnownKey(value, candidates) {
|
|
318
|
+
const v = String(value || "").toLowerCase();
|
|
319
|
+
if (!v) return "";
|
|
320
|
+
// Prefix / substring match first (cheap, catches "reactnative" -> "react-native").
|
|
321
|
+
let best = "";
|
|
322
|
+
let bestScore = Infinity;
|
|
323
|
+
for (const candidate of candidates) {
|
|
324
|
+
const c = candidate.toLowerCase();
|
|
325
|
+
if (c === v) return candidate;
|
|
326
|
+
const stripped = v.replace(/[-_.\s]/g, "");
|
|
327
|
+
const cStripped = c.replace(/[-_.\s]/g, "");
|
|
328
|
+
if (cStripped === stripped || cStripped.startsWith(stripped) || stripped.startsWith(cStripped)) {
|
|
329
|
+
return candidate;
|
|
330
|
+
}
|
|
331
|
+
// Fallback: length-difference heuristic as a light distance proxy.
|
|
332
|
+
if (cStripped.includes(stripped) || stripped.includes(cStripped)) {
|
|
333
|
+
const score = Math.abs(cStripped.length - stripped.length);
|
|
334
|
+
if (score < bestScore) {
|
|
335
|
+
bestScore = score;
|
|
336
|
+
best = candidate;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return best;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function validatePlatform(value) {
|
|
344
|
+
if (value === undefined || value === null || value === "") {
|
|
345
|
+
return { ok: true, normalized: "" };
|
|
346
|
+
}
|
|
347
|
+
const normalized = normalizePlatform(value);
|
|
348
|
+
if (KNOWN_PLATFORMS.has(normalized)) {
|
|
349
|
+
return { ok: true, normalized };
|
|
350
|
+
}
|
|
351
|
+
const valid = [...KNOWN_PLATFORMS].sort();
|
|
352
|
+
const suggestion = nearestKnownKey(value, [...Object.keys(platformAliases), ...KNOWN_PLATFORMS]);
|
|
353
|
+
const suggestionText = suggestion ? ` Did you mean "${normalizePlatform(suggestion) || suggestion}"?` : "";
|
|
354
|
+
return {
|
|
355
|
+
ok: false,
|
|
356
|
+
normalized,
|
|
357
|
+
message: `Unknown platform "${value}". Valid: ${valid.join(", ")}.${suggestionText}`
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function validateEdition(value) {
|
|
362
|
+
if (value === undefined || value === null || value === "") {
|
|
363
|
+
return { ok: true, normalized: "" };
|
|
364
|
+
}
|
|
365
|
+
// Normalize first so documented aliases the tool schema advertises
|
|
366
|
+
// ("server/desktop", "desktop") and platform-implied editions ("python")
|
|
367
|
+
// are accepted rather than rejected as unknown.
|
|
368
|
+
const normalized = normalizeEdition(value);
|
|
369
|
+
if (KNOWN_EDITIONS.has(normalized)) {
|
|
370
|
+
return { ok: true, normalized };
|
|
371
|
+
}
|
|
372
|
+
const valid = [...KNOWN_EDITIONS];
|
|
373
|
+
const suggestion = nearestKnownKey(value, valid);
|
|
374
|
+
const suggestionText = suggestion ? ` Did you mean "${suggestion}"?` : "";
|
|
375
|
+
return {
|
|
376
|
+
ok: false,
|
|
377
|
+
normalized,
|
|
378
|
+
message: `Unknown edition "${value}". Valid: ${valid.join(", ")} (aliases like server/desktop, desktop are accepted).${suggestionText}`
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
309
382
|
function inferProductFromQuery(query) {
|
|
310
383
|
if (!query) return "";
|
|
311
384
|
const normalized = query.toLowerCase();
|
|
@@ -347,5 +420,9 @@ export {
|
|
|
347
420
|
isServerPlatform,
|
|
348
421
|
isWebFrameworkPlatform,
|
|
349
422
|
isWebPlatform,
|
|
350
|
-
inferProductFromQuery
|
|
423
|
+
inferProductFromQuery,
|
|
424
|
+
KNOWN_PLATFORMS,
|
|
425
|
+
KNOWN_EDITIONS,
|
|
426
|
+
validatePlatform,
|
|
427
|
+
validateEdition
|
|
351
428
|
};
|
|
@@ -35,6 +35,14 @@ const HYDRATION_PRODUCT_MAP = {
|
|
|
35
35
|
|
|
36
36
|
const PUBLIC_OFFERINGS_TEXT = "dbr, dwt, ddv, mrz, or mds";
|
|
37
37
|
|
|
38
|
+
// Shared lineup-policy wording for tool descriptions and input-schema notes.
|
|
39
|
+
// Single source: when the lineup changes (e.g. a product gains an edition),
|
|
40
|
+
// edit here and the long-form server `instructions` in create-server.js.
|
|
41
|
+
const WEB_ONLY_OMIT_NOTE = "DWT/DDV/MRZ/MDS are web/JS-only (omit for them).";
|
|
42
|
+
const DBR_ONLY_EDITIONS_NOTE = `Only DBR has multiple editions; ${WEB_ONLY_OMIT_NOTE}`;
|
|
43
|
+
const API_LEVEL_NOTE = "high-level or low-level. DBR mobile ONLY; ignored for web/server/other products.";
|
|
44
|
+
const PRODUCT_SELECTION_GUIDANCE = "DBR is the only product with multiple editions (mobile/web/server). DWT, DDV, MRZ, and MDS are web/JavaScript-only here, so do not ask the user which platform or language for them. api_level (high-level/low-level) applies only to DBR mobile.";
|
|
45
|
+
|
|
38
46
|
function normalizePublicOffering(product) {
|
|
39
47
|
if (!product) return "";
|
|
40
48
|
const normalized = product.trim().toLowerCase();
|
|
@@ -64,6 +72,10 @@ function buildUnknownPublicProductResponse(requestedProduct) {
|
|
|
64
72
|
export {
|
|
65
73
|
PUBLIC_OFFERING_PRODUCTS,
|
|
66
74
|
PUBLIC_OFFERINGS_TEXT,
|
|
75
|
+
WEB_ONLY_OMIT_NOTE,
|
|
76
|
+
DBR_ONLY_EDITIONS_NOTE,
|
|
77
|
+
API_LEVEL_NOTE,
|
|
78
|
+
PRODUCT_SELECTION_GUIDANCE,
|
|
67
79
|
normalizePublicOffering,
|
|
68
80
|
getHydrationProduct,
|
|
69
81
|
isKnownPublicOffering,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
2
2
|
import { extname, join } from "node:path";
|
|
3
3
|
import { DDV_PREFERRED_ENTRY_FILES } from "./config.js";
|
|
4
|
-
import { PUBLIC_OFFERING_PRODUCTS } from "../public-offerings.js";
|
|
4
|
+
import { PRODUCT_SELECTION_GUIDANCE, PUBLIC_OFFERING_PRODUCTS } from "../public-offerings.js";
|
|
5
5
|
|
|
6
6
|
const MRZ_MATCHER = /(?:\bmrz\b|machine[-\s]?readable[-\s]?zone|passport)/i;
|
|
7
7
|
const MDS_MATCHER = /(?:document[-\s]scan|document scanner|document scanning|document normalizer|document normalization|normaliz|auto[-\s]?capture|crop|cropping|deskew)/i;
|
|
@@ -36,18 +36,45 @@ function countDiscoveredSamplesByPlatforms(platforms, discoverSamplesForPlatform
|
|
|
36
36
|
function getDcvScenarioTags(sampleName) {
|
|
37
37
|
const normalized = String(sampleName || "").toLowerCase();
|
|
38
38
|
const tags = [];
|
|
39
|
-
if (normalized.includes("mrz")) {
|
|
39
|
+
if (normalized.includes("mrz") || normalized.includes("passport")) {
|
|
40
40
|
tags.push("mrz", "passport", "id-card", "machine-readable-zone");
|
|
41
41
|
}
|
|
42
|
-
if (normalized.includes("driver") || normalized.includes("license")) {
|
|
42
|
+
if (normalized.includes("driver") || normalized.includes("license") || normalized.includes("licence") || normalized.includes("dl")) {
|
|
43
43
|
tags.push("driver-license", "id-card", "dl", "aamva");
|
|
44
44
|
}
|
|
45
|
-
if (normalized.includes("document")) {
|
|
45
|
+
if (normalized.includes("document") || normalized.includes("normaliz") || normalized.includes("deskew") || normalized.includes("scan-a-document")) {
|
|
46
46
|
tags.push("document-scan", "document-normalization", "auto-capture", "cropping", "deskew");
|
|
47
47
|
}
|
|
48
48
|
if (normalized.includes("gs1")) {
|
|
49
49
|
tags.push("gs1", "application-identifiers", "ai");
|
|
50
50
|
}
|
|
51
|
+
if (normalized.includes("qr")) {
|
|
52
|
+
tags.push("qr", "qrcode", "qr-code");
|
|
53
|
+
}
|
|
54
|
+
if (normalized.includes("pdf417") || normalized.includes("pdf-417")) {
|
|
55
|
+
tags.push("pdf417");
|
|
56
|
+
}
|
|
57
|
+
if (normalized.includes("datamatrix") || normalized.includes("data-matrix")) {
|
|
58
|
+
tags.push("datamatrix", "data-matrix");
|
|
59
|
+
}
|
|
60
|
+
if (normalized.includes("vin")) {
|
|
61
|
+
tags.push("vin", "vehicle-identification");
|
|
62
|
+
}
|
|
63
|
+
if (normalized.includes("pdf")) {
|
|
64
|
+
tags.push("pdf", "multi-page");
|
|
65
|
+
}
|
|
66
|
+
if (normalized.includes("image") || normalized.includes("file")) {
|
|
67
|
+
tags.push("image", "still-image", "file", "upload");
|
|
68
|
+
}
|
|
69
|
+
if (normalized.includes("camera") || normalized.includes("video") || normalized.includes("webcam") || normalized.includes("scan-a-single") || normalized.includes("scan-single")) {
|
|
70
|
+
tags.push("camera", "webcam", "video", "live");
|
|
71
|
+
}
|
|
72
|
+
if (normalized.includes("hello") || normalized.includes("getting-started") || normalized.includes("quickstart")) {
|
|
73
|
+
tags.push("hello-world", "getting-started", "quickstart");
|
|
74
|
+
}
|
|
75
|
+
if (normalized.includes("multiple") || normalized.includes("batch")) {
|
|
76
|
+
tags.push("multiple", "batch");
|
|
77
|
+
}
|
|
51
78
|
return Array.from(new Set(tags));
|
|
52
79
|
}
|
|
53
80
|
|
|
@@ -194,6 +221,16 @@ function addMarkdownDocResources({
|
|
|
194
221
|
const platform = article.platform || defaultPlatform;
|
|
195
222
|
const tags = [...baseTags, platform];
|
|
196
223
|
if (article.breadcrumb) tags.push(...article.breadcrumb.toLowerCase().split(/\s*>\s*/));
|
|
224
|
+
// Fold frontmatter keywords into tags so domain-vocabulary search hits (#142).
|
|
225
|
+
if (Array.isArray(article.keywords)) {
|
|
226
|
+
for (const kw of article.keywords) {
|
|
227
|
+
const norm = String(kw).toLowerCase().trim();
|
|
228
|
+
if (norm && !tags.includes(norm)) tags.push(norm);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// Prefer the frontmatter description as the summary (distinct per doc) over
|
|
232
|
+
// the breadcrumb (identical across sibling docs) (#142).
|
|
233
|
+
const baseSummary = article.description || article.breadcrumb || defaultSummary;
|
|
197
234
|
|
|
198
235
|
addPublicResourceToIndex(addResourceToIndex, {
|
|
199
236
|
id: `${idPrefix}-${i}`,
|
|
@@ -204,12 +241,13 @@ function addMarkdownDocResources({
|
|
|
204
241
|
platform,
|
|
205
242
|
version,
|
|
206
243
|
majorVersion,
|
|
244
|
+
url: article.url,
|
|
207
245
|
title: product === "mrz" || product === "mds"
|
|
208
246
|
? rewritePublicTitle(article.title, product)
|
|
209
247
|
: article.title,
|
|
210
248
|
summary: product === "mrz" || product === "mds"
|
|
211
|
-
? rewritePublicSummary(
|
|
212
|
-
:
|
|
249
|
+
? rewritePublicSummary(baseSummary, product)
|
|
250
|
+
: baseSummary,
|
|
213
251
|
embedText: article.content,
|
|
214
252
|
mimeType: "text/markdown",
|
|
215
253
|
tags,
|
|
@@ -356,35 +394,47 @@ function buildIndexData({
|
|
|
356
394
|
|
|
357
395
|
return {
|
|
358
396
|
productSelection: {
|
|
397
|
+
guidance: PRODUCT_SELECTION_GUIDANCE,
|
|
359
398
|
publicOfferings: [...PUBLIC_OFFERING_PRODUCTS],
|
|
360
399
|
offerings: {
|
|
361
400
|
dwt: {
|
|
362
401
|
name: "Dynamic Web TWAIN",
|
|
363
402
|
abbreviation: "DWT",
|
|
364
|
-
|
|
403
|
+
webOnly: true,
|
|
404
|
+
platforms: ["js"],
|
|
405
|
+
whenToUse: ["Browser-based document acquisition and scanner control. Web/JavaScript only."]
|
|
365
406
|
},
|
|
366
407
|
ddv: {
|
|
367
408
|
name: "Dynamsoft Document Viewer",
|
|
368
409
|
abbreviation: "DDV",
|
|
369
|
-
|
|
410
|
+
webOnly: true,
|
|
411
|
+
platforms: ["js"],
|
|
412
|
+
whenToUse: ["Standalone viewing plus annotation, multi-page handling, and PDF output. Web/JavaScript only."]
|
|
370
413
|
},
|
|
371
414
|
dbr: {
|
|
372
415
|
name: "Dynamsoft Barcode Reader",
|
|
373
416
|
abbreviation: "DBR",
|
|
417
|
+
webOnly: false,
|
|
418
|
+
editions: ["mobile", "web", "server"],
|
|
374
419
|
whenToUse: [
|
|
375
|
-
"Barcode workflows across server/desktop, web, and mobile.",
|
|
376
|
-
"
|
|
420
|
+
"Barcode workflows across server/desktop, web, and mobile. The only product with multiple editions.",
|
|
421
|
+
"Web uses a foundational-first flow (BarcodeScanner RTU also available); this is not selected via api_level.",
|
|
422
|
+
"api_level (high-level = BarcodeScanner RTU, low-level = foundational) applies only to DBR mobile."
|
|
377
423
|
]
|
|
378
424
|
},
|
|
379
425
|
mrz: {
|
|
380
426
|
name: "MRZ Scanner",
|
|
381
427
|
abbreviation: "MRZ",
|
|
382
|
-
|
|
428
|
+
webOnly: true,
|
|
429
|
+
platforms: ["js"],
|
|
430
|
+
whenToUse: ["Passport and machine-readable-zone workflows. Served here as web/JavaScript only; mobile/server scopes return official reference links."]
|
|
383
431
|
},
|
|
384
432
|
mds: {
|
|
385
433
|
name: "Mobile Document Scanner",
|
|
386
434
|
abbreviation: "MDS",
|
|
387
|
-
|
|
435
|
+
webOnly: true,
|
|
436
|
+
platforms: ["js"],
|
|
437
|
+
whenToUse: ["Document scan and normalization workflows. Served here as web/JavaScript only; mobile/server scopes return official reference links."]
|
|
388
438
|
}
|
|
389
439
|
}
|
|
390
440
|
},
|
|
@@ -422,7 +472,9 @@ function buildIndexData({
|
|
|
422
472
|
|
|
423
473
|
return {
|
|
424
474
|
productSelection: {
|
|
425
|
-
|
|
475
|
+
guidance: PRODUCT_SELECTION_GUIDANCE,
|
|
476
|
+
publicOfferings: [...PUBLIC_OFFERING_PRODUCTS],
|
|
477
|
+
dcvSupersetSummary: "Dynamsoft Capture Vision (DCV) aggregates Dynamsoft Barcode Reader (DBR), Dynamsoft Label Recognizer (DLR), Dynamsoft Document Normalizer (DDN), Dynamsoft Code Parser (DCP), and Dynamsoft Camera Enhancer (DCE) into one pipeline. DCV is internal only and is not a selectable public product.",
|
|
426
478
|
useDbrWhen: [
|
|
427
479
|
"Barcode-only workflows where DCV-specific workflows are not required."
|
|
428
480
|
],
|
|
@@ -985,12 +1037,20 @@ function buildResourceIndex({
|
|
|
985
1037
|
version: dbrWebVersion,
|
|
986
1038
|
majorVersion: LATEST_MAJOR.dbr,
|
|
987
1039
|
title: `Web sample: ${sampleName} (${category})`,
|
|
988
|
-
summary: `DBR web
|
|
1040
|
+
summary: `DBR web ${category} sample: ${sampleName.replace(/[-_]/g, " ")}.`,
|
|
989
1041
|
mimeType: "text/html",
|
|
990
|
-
tags: ["sample", "dbr", "web", category, sampleName],
|
|
1042
|
+
tags: ["sample", "dbr", "web", category, sampleName, ...getDcvScenarioTags(sampleName)],
|
|
1043
|
+
embedText: `DBR web ${category} sample ${sampleName.replace(/[-_]/g, " ")} ${getDcvScenarioTags(sampleName).join(" ")}`,
|
|
991
1044
|
loadContent: async () => {
|
|
992
1045
|
const samplePath = getWebSamplePath(category, sampleName);
|
|
993
|
-
|
|
1046
|
+
let content = "Sample not found";
|
|
1047
|
+
if (samplePath && existsSync(samplePath)) {
|
|
1048
|
+
// Full-project samples (frameworks/*) resolve to a directory; point
|
|
1049
|
+
// to get_sample_files rather than reading a directory as a file.
|
|
1050
|
+
content = statSync(samplePath).isDirectory()
|
|
1051
|
+
? `This is a multi-file ${category} project. Use get_sample_files with sample_id ${category}/${sampleName} to retrieve all files.`
|
|
1052
|
+
: readCodeFile(samplePath);
|
|
1053
|
+
}
|
|
994
1054
|
return { text: content, mimeType: "text/html" };
|
|
995
1055
|
}
|
|
996
1056
|
});
|
|
@@ -63,12 +63,14 @@ const DOCS_CONFIG = {
|
|
|
63
63
|
mrzWeb: {
|
|
64
64
|
urlBase: "https://www.dynamsoft.com/mrz-scanner/docs/web/",
|
|
65
65
|
excludeDirs: [".git", ".github", ".vscode", ".vs", "_data", "_includes", "_layouts", "assets"],
|
|
66
|
-
|
|
66
|
+
// Current MRZ docs are v3.x; exclude the legacy v1.x/v2.x guides and any private drafts (#144).
|
|
67
|
+
excludeFiles: ["README.md", "search.md", "error.md", "*-v1.1.md", "*-v2.0.md", "*-v2.1.md", "*private*"]
|
|
67
68
|
},
|
|
68
69
|
mdsWeb: {
|
|
69
70
|
urlBase: "https://www.dynamsoft.com/mobile-document-scanner/docs/web/",
|
|
70
71
|
excludeDirs: [".git", ".github", ".vscode", ".vs", "_data", "_includes", "_layouts", "assets"],
|
|
71
|
-
|
|
72
|
+
// Exclude internal "private" draft docs (bodies are HTML comments) (#144).
|
|
73
|
+
excludeFiles: ["README.md", "search.md", "error.md", "*private*"]
|
|
72
74
|
},
|
|
73
75
|
dcvMobile: {
|
|
74
76
|
urlBase: "https://www.dynamsoft.com/capture-vision/docs/mobile/",
|
|
@@ -59,9 +59,20 @@ function buildBreadcrumbFromPath(relativePath) {
|
|
|
59
59
|
function collectMarkdownFiles(rootDir, options = {}) {
|
|
60
60
|
const files = [];
|
|
61
61
|
const excludeDirs = new Set(options.excludeDirs || []);
|
|
62
|
-
const
|
|
62
|
+
const rawExcludeFiles = options.excludeFiles || [];
|
|
63
|
+
// Exact names go in a Set; entries containing '*' become glob regexes so
|
|
64
|
+
// configs can exclude stale/internal docs like '*-v1.1.md' or '*private*' (#144).
|
|
65
|
+
const excludeFiles = new Set(rawExcludeFiles.filter((f) => !f.includes("*")));
|
|
66
|
+
const excludePatterns = rawExcludeFiles
|
|
67
|
+
.filter((f) => f.includes("*"))
|
|
68
|
+
.map((f) => new RegExp("^" + f.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$", "i"));
|
|
63
69
|
const includeDirNames = new Set(options.includeDirNames || []);
|
|
64
70
|
|
|
71
|
+
function isExcludedFile(name) {
|
|
72
|
+
if (excludeFiles.has(name)) return true;
|
|
73
|
+
return excludePatterns.some((re) => re.test(name));
|
|
74
|
+
}
|
|
75
|
+
|
|
65
76
|
function walk(dir) {
|
|
66
77
|
if (!existsSync(dir)) return;
|
|
67
78
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
@@ -72,7 +83,7 @@ function collectMarkdownFiles(rootDir, options = {}) {
|
|
|
72
83
|
continue;
|
|
73
84
|
}
|
|
74
85
|
if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
|
|
75
|
-
if (
|
|
86
|
+
if (isExcludedFile(entry.name)) continue;
|
|
76
87
|
files.push(join(dir, entry.name));
|
|
77
88
|
}
|
|
78
89
|
}
|
|
@@ -100,11 +111,20 @@ function loadMarkdownDocs({ rootDir, urlBase, includeDirNames = [], excludeDirs
|
|
|
100
111
|
const title = parsed.meta.title || getHeadingTitle(parsed.body) || formatSegmentLabel(relativePath.replace(/\.md$/i, "").split("/").pop());
|
|
101
112
|
if (!title) continue;
|
|
102
113
|
const breadcrumb = parsed.meta.breadcrumbText || buildBreadcrumbFromPath(relativePath) || title;
|
|
114
|
+
// Frontmatter description/keywords are the docs' own human summaries and
|
|
115
|
+
// search vocabulary — previously parsed and discarded (issue #142).
|
|
116
|
+
const description = typeof parsed.meta.description === "string" ? parsed.meta.description.trim() : "";
|
|
117
|
+
const keywordsRaw = parsed.meta.keywords;
|
|
118
|
+
const keywords = Array.isArray(keywordsRaw)
|
|
119
|
+
? keywordsRaw.map((k) => String(k).trim()).filter(Boolean)
|
|
120
|
+
: (typeof keywordsRaw === "string" ? keywordsRaw.split(",").map((k) => k.trim()).filter(Boolean) : []);
|
|
103
121
|
articles.push({
|
|
104
122
|
title,
|
|
105
123
|
url: markdownPathToUrl(urlBase, relativePath),
|
|
106
124
|
content: parsed.body.trim(),
|
|
107
125
|
breadcrumb,
|
|
126
|
+
description,
|
|
127
|
+
keywords,
|
|
108
128
|
path: relativePath
|
|
109
129
|
});
|
|
110
130
|
}
|
|
@@ -265,17 +265,19 @@ function getDcvWebFrameworkPlatforms() {
|
|
|
265
265
|
}
|
|
266
266
|
|
|
267
267
|
function discoverWebSamples() {
|
|
268
|
-
const categories = { root: [], frameworks: [], scenarios: [] };
|
|
268
|
+
const categories = { root: [], basics: [], frameworks: [], scenarios: [] };
|
|
269
269
|
const webPath = getDbrWebSamplesRoot();
|
|
270
270
|
if (!webPath || !existsSync(webPath)) return categories;
|
|
271
271
|
|
|
272
272
|
for (const entry of readdirSync(webPath, { withFileTypes: true })) {
|
|
273
|
-
|
|
273
|
+
// README.html / index.html at the repo root are navigation shells, not
|
|
274
|
+
// samples — indexing them wastes result slots and a fetch (issue #140).
|
|
275
|
+
if (entry.isFile() && entry.name.endsWith(".html") && entry.name !== "index.html" && entry.name.toLowerCase() !== "readme.html") {
|
|
274
276
|
categories.root.push(entry.name.replace(".html", ""));
|
|
275
277
|
}
|
|
276
278
|
}
|
|
277
279
|
|
|
278
|
-
for (const subdir of ["frameworks", "scenarios"]) {
|
|
280
|
+
for (const subdir of ["basics", "frameworks", "scenarios"]) {
|
|
279
281
|
const subdirPath = join(webPath, subdir);
|
|
280
282
|
if (!existsSync(subdirPath)) continue;
|
|
281
283
|
for (const entry of readdirSync(subdirPath, { withFileTypes: true })) {
|
|
@@ -363,6 +365,10 @@ function getWebSamplePath(category, sampleName) {
|
|
|
363
365
|
for (const entry of readdirSync(dirPath, { withFileTypes: true })) {
|
|
364
366
|
if (entry.isFile() && entry.name.endsWith(".html")) return join(dirPath, entry.name);
|
|
365
367
|
}
|
|
368
|
+
// Full-project sample (e.g. frameworks/react, a Vite app) with no
|
|
369
|
+
// top-level html: return the directory so get_sample_files can walk it,
|
|
370
|
+
// making the URI search emits actually fetchable (issue #130).
|
|
371
|
+
return dirPath;
|
|
366
372
|
}
|
|
367
373
|
const htmlPath = join(webPath, category, `${sampleName}.html`);
|
|
368
374
|
if (existsSync(htmlPath)) return htmlPath;
|
|
@@ -415,6 +421,16 @@ function discoverDwtSamples() {
|
|
|
415
421
|
const categories = {};
|
|
416
422
|
if (!existsSync(SAMPLE_ROOTS.dwt)) return categories;
|
|
417
423
|
|
|
424
|
+
// Root-level *.html samples (e.g. edit-image.html, localstorage.html) were
|
|
425
|
+
// previously skipped because only directories were scanned (issue #140).
|
|
426
|
+
const rootSamples = [];
|
|
427
|
+
for (const entry of readdirSync(SAMPLE_ROOTS.dwt, { withFileTypes: true })) {
|
|
428
|
+
if (entry.isFile() && entry.name.endsWith(".html") && entry.name !== "index.html" && entry.name.toLowerCase() !== "readme.html") {
|
|
429
|
+
rootSamples.push(entry.name.replace(".html", ""));
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
if (rootSamples.length > 0) categories.root = rootSamples;
|
|
433
|
+
|
|
418
434
|
for (const entry of readdirSync(SAMPLE_ROOTS.dwt, { withFileTypes: true })) {
|
|
419
435
|
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
420
436
|
const categoryPath = join(SAMPLE_ROOTS.dwt, entry.name);
|
|
@@ -594,6 +610,11 @@ function getDbrServerSamplePath(platform, sampleName) {
|
|
|
594
610
|
|
|
595
611
|
function getDwtSamplePath(category, sampleName) {
|
|
596
612
|
const fileName = sampleName.endsWith(".html") ? sampleName : `${sampleName}.html`;
|
|
613
|
+
// "root" maps to a top-level *.html directly under the DWT samples root.
|
|
614
|
+
if (category === "root") {
|
|
615
|
+
const rootFile = getExistingPath(join(SAMPLE_ROOTS.dwt, fileName));
|
|
616
|
+
return rootFile || null;
|
|
617
|
+
}
|
|
597
618
|
const categoryPath = getExistingPath(join(SAMPLE_ROOTS.dwt, category));
|
|
598
619
|
if (!categoryPath) return null;
|
|
599
620
|
|