@yinsen/deveco-cli-docs-zh 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/engine/constants-Bi5VwlSi.d.ts +4 -0
- package/dist/engine/index.d.ts +50 -0
- package/dist/engine/index.js +2575 -0
- package/docs.zip +0 -0
- package/index.js +7 -0
- package/index.zip +0 -0
- package/package.json +35 -0
|
@@ -0,0 +1,2575 @@
|
|
|
1
|
+
// src/data-dir.ts
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
var CacheDirError = class extends Error {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "CacheDirError";
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
var _cacheDir = null;
|
|
11
|
+
function setCacheDir(dir) {
|
|
12
|
+
_cacheDir = path.resolve(dir);
|
|
13
|
+
}
|
|
14
|
+
function getCacheDir() {
|
|
15
|
+
if (_cacheDir === null) {
|
|
16
|
+
throw new CacheDirError(
|
|
17
|
+
"Cache directory not initialized. Call createDocsEngine first."
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
return _cacheDir;
|
|
21
|
+
}
|
|
22
|
+
async function assertCacheDirSafe() {
|
|
23
|
+
const dataDir = getCacheDir();
|
|
24
|
+
await fs.promises.mkdir(dataDir, { recursive: true });
|
|
25
|
+
const realDir = await fs.promises.realpath(dataDir);
|
|
26
|
+
const stat2 = await fs.promises.stat(realDir);
|
|
27
|
+
if (!stat2.isDirectory()) {
|
|
28
|
+
throw new CacheDirError("Cache directory must be a writable directory.");
|
|
29
|
+
}
|
|
30
|
+
return realDir;
|
|
31
|
+
}
|
|
32
|
+
function isPathInside(child, parent) {
|
|
33
|
+
const relative2 = path.relative(parent, child);
|
|
34
|
+
return relative2 === "" || !path.isAbsolute(relative2) && !relative2.startsWith(`..${path.sep}`) && relative2 !== "..";
|
|
35
|
+
}
|
|
36
|
+
function resolveCanonicalPath(value) {
|
|
37
|
+
const resolved = path.resolve(value);
|
|
38
|
+
const missing = [];
|
|
39
|
+
let current = resolved;
|
|
40
|
+
for (; ; ) {
|
|
41
|
+
try {
|
|
42
|
+
const real = fs.realpathSync(current);
|
|
43
|
+
return missing.length === 0 ? real : path.join(real, ...missing.reverse());
|
|
44
|
+
} catch (error) {
|
|
45
|
+
if (error.code !== "ENOENT") {
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
const parent = path.dirname(current);
|
|
49
|
+
if (parent === current) {
|
|
50
|
+
return resolved;
|
|
51
|
+
}
|
|
52
|
+
missing.push(path.basename(current));
|
|
53
|
+
current = parent;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function resolvePathInsideRoot(child, root) {
|
|
58
|
+
const realRoot = resolveCanonicalPath(root);
|
|
59
|
+
const realChild = resolveCanonicalPath(child);
|
|
60
|
+
return isPathInside(realChild, realRoot) ? realChild : null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/init/doc-initializer.ts
|
|
64
|
+
import * as fs9 from "fs";
|
|
65
|
+
import lockfile from "proper-lockfile";
|
|
66
|
+
import ora from "ora";
|
|
67
|
+
|
|
68
|
+
// src/doc-index/index-state.ts
|
|
69
|
+
import * as fs8 from "fs";
|
|
70
|
+
import * as path7 from "path";
|
|
71
|
+
|
|
72
|
+
// src/portal/catalog.ts
|
|
73
|
+
var CATALOG_NAMES = [
|
|
74
|
+
"harmonyos-guides",
|
|
75
|
+
"harmonyos-references",
|
|
76
|
+
"best-practices",
|
|
77
|
+
"harmonyos-faqs",
|
|
78
|
+
"harmonyos-releases",
|
|
79
|
+
"harmonyos-roadmap"
|
|
80
|
+
];
|
|
81
|
+
var CATALOG_TITLES = {
|
|
82
|
+
"harmonyos-guides": "\u5F00\u53D1\u6307\u5357",
|
|
83
|
+
"harmonyos-references": "API\u53C2\u8003",
|
|
84
|
+
"best-practices": "\u6700\u4F73\u5B9E\u8DF5",
|
|
85
|
+
"harmonyos-faqs": "FAQ",
|
|
86
|
+
"harmonyos-releases": "\u7248\u672C\u8BF4\u660E",
|
|
87
|
+
"harmonyos-roadmap": "\u53D8\u66F4\u9884\u544A"
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// src/doc-index/constants.ts
|
|
91
|
+
var INDEX_VERSION = "1.9.1";
|
|
92
|
+
var INDEX_DB_MAX_BYTES = 48 * 1024 * 1024;
|
|
93
|
+
var QUERY_MAX_RAW_CHARS = 200;
|
|
94
|
+
var QUERY_MAX_FTS_TOKENS = 12;
|
|
95
|
+
var QUERY_AND_MAX_TOKENS = 4;
|
|
96
|
+
var SYNONYM_EXPAND_MAX_PARTS = 8;
|
|
97
|
+
var CATALOG_RERANK_POOL_FACTOR = 6;
|
|
98
|
+
var DOC_SEARCH_TEXT_MAX_CHARS = 1320;
|
|
99
|
+
var DOC_SEARCH_BUDGET_TITLE = 120;
|
|
100
|
+
var DOC_SEARCH_BUDGET_API_SYMBOLS = 450;
|
|
101
|
+
var DOC_SEARCH_BUDGET_HEADINGS = 250;
|
|
102
|
+
var DOC_SEARCH_BUDGET_BODY = 500;
|
|
103
|
+
var DOC_SECTION_SEARCH_TEXT_MAX_CHARS = 480;
|
|
104
|
+
var DOC_SECTION_BUDGET_TITLE = 80;
|
|
105
|
+
var DOC_SECTION_BUDGET_API_SYMBOLS = 200;
|
|
106
|
+
var DOC_SECTION_BUDGET_HEADINGS = 60;
|
|
107
|
+
var DOC_SECTION_BUDGET_BODY = 200;
|
|
108
|
+
var DOC_SNIPPET_MAX_CHARS = 200;
|
|
109
|
+
var DOC_SNIPPET_CONTEXT_CHARS = 40;
|
|
110
|
+
var INSERT_BATCH_SIZE = 500;
|
|
111
|
+
var CATALOG_TITLE_TO_ID = Object.fromEntries(
|
|
112
|
+
CATALOG_NAMES.map((name, index) => [CATALOG_TITLES[name], index])
|
|
113
|
+
);
|
|
114
|
+
var CATALOG_NAME_TO_ID = Object.fromEntries(
|
|
115
|
+
CATALOG_NAMES.map((name, index) => [name, index])
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
// src/doc-index/doc-paths.ts
|
|
119
|
+
import * as fs2 from "fs";
|
|
120
|
+
import * as path2 from "path";
|
|
121
|
+
import { fileURLToPath } from "url";
|
|
122
|
+
var DOCS_DIR_NAME = "docs";
|
|
123
|
+
function getDocsDir() {
|
|
124
|
+
return path2.join(getCacheDir(), DOCS_DIR_NAME);
|
|
125
|
+
}
|
|
126
|
+
function getIndexDir() {
|
|
127
|
+
return path2.join(getDocsDir(), ".index");
|
|
128
|
+
}
|
|
129
|
+
function getBuildLockFile() {
|
|
130
|
+
return path2.join(getIndexDir(), "build.lock");
|
|
131
|
+
}
|
|
132
|
+
function getBuildStatusFile() {
|
|
133
|
+
return path2.join(getIndexDir(), "build-status.json");
|
|
134
|
+
}
|
|
135
|
+
function getBuildMetaFile() {
|
|
136
|
+
return path2.join(getIndexDir(), "build-meta.json");
|
|
137
|
+
}
|
|
138
|
+
function getSearchDbFile() {
|
|
139
|
+
return path2.join(getIndexDir(), "search.db");
|
|
140
|
+
}
|
|
141
|
+
function getIndexTmpDir() {
|
|
142
|
+
return path2.join(getIndexDir(), ".tmp");
|
|
143
|
+
}
|
|
144
|
+
function getDistDir(currentFilePath, currentDir) {
|
|
145
|
+
let dir = currentDir;
|
|
146
|
+
while (!dir.endsWith(`${path2.sep}dist`) && dir !== path2.dirname(dir)) {
|
|
147
|
+
dir = path2.dirname(dir);
|
|
148
|
+
}
|
|
149
|
+
return dir;
|
|
150
|
+
}
|
|
151
|
+
function getPackageRootFromDist(currentFilePath, currentDir) {
|
|
152
|
+
return path2.dirname(getDistDir(currentFilePath, currentDir));
|
|
153
|
+
}
|
|
154
|
+
function getPackageRoot() {
|
|
155
|
+
const currentFilePath = fileURLToPath(import.meta.url);
|
|
156
|
+
const currentDir = path2.dirname(currentFilePath);
|
|
157
|
+
if (currentFilePath.includes(`${path2.sep}dist${path2.sep}`)) {
|
|
158
|
+
return getPackageRootFromDist(currentFilePath, currentDir);
|
|
159
|
+
}
|
|
160
|
+
return path2.join(currentDir, "..", "..", "..");
|
|
161
|
+
}
|
|
162
|
+
function resolvePackageRelativePath(...segments) {
|
|
163
|
+
const currentFilePath = fileURLToPath(import.meta.url);
|
|
164
|
+
const currentDir = path2.dirname(currentFilePath);
|
|
165
|
+
if (currentFilePath.includes(`${path2.sep}dist${path2.sep}`)) {
|
|
166
|
+
return [
|
|
167
|
+
path2.join(
|
|
168
|
+
getPackageRootFromDist(currentFilePath, currentDir),
|
|
169
|
+
...segments
|
|
170
|
+
)
|
|
171
|
+
];
|
|
172
|
+
}
|
|
173
|
+
return [path2.join(currentDir, "..", "..", "..", ...segments)];
|
|
174
|
+
}
|
|
175
|
+
function findBundledAsset(...segments) {
|
|
176
|
+
const packageRoot = getPackageRoot();
|
|
177
|
+
const realPackageRoot = fs2.realpathSync(packageRoot);
|
|
178
|
+
for (const candidate of resolvePackageRelativePath(...segments)) {
|
|
179
|
+
try {
|
|
180
|
+
const entry = fs2.lstatSync(candidate);
|
|
181
|
+
if (entry.isSymbolicLink() || !entry.isFile()) {
|
|
182
|
+
throw new Error(
|
|
183
|
+
`Unsafe documentation package asset: ${segments.join("/")} must be a regular file.`
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
const realCandidate = fs2.realpathSync(candidate);
|
|
187
|
+
if (!isPathInside(realCandidate, realPackageRoot)) {
|
|
188
|
+
throw new Error(
|
|
189
|
+
`Unsafe documentation package asset: ${segments.join("/")} is outside the package.`
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
return realCandidate;
|
|
193
|
+
} catch (error) {
|
|
194
|
+
if (error.code !== "ENOENT") {
|
|
195
|
+
throw error;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
function findDocsZip() {
|
|
202
|
+
return findBundledAsset("docs.zip");
|
|
203
|
+
}
|
|
204
|
+
function findBundledIndexZip() {
|
|
205
|
+
return findBundledAsset("index.zip");
|
|
206
|
+
}
|
|
207
|
+
function getBuildLexiconDir() {
|
|
208
|
+
return path2.join(getPackageRoot(), "index", "data");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// src/doc-index/lexicon.ts
|
|
212
|
+
import * as fs3 from "fs";
|
|
213
|
+
import * as path3 from "path";
|
|
214
|
+
var INDEX_LEXICON_FILES = [
|
|
215
|
+
"harmonyos-terms.txt",
|
|
216
|
+
"harmonyos-synonyms.json",
|
|
217
|
+
"harmonyos-stopwords.txt"
|
|
218
|
+
];
|
|
219
|
+
var LexiconNotFoundError = class extends Error {
|
|
220
|
+
constructor(message) {
|
|
221
|
+
super(message);
|
|
222
|
+
this.name = "LexiconNotFoundError";
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
var lexiconDirOverride = null;
|
|
226
|
+
function resolveLexiconSourceDir() {
|
|
227
|
+
if (lexiconDirOverride) {
|
|
228
|
+
return lexiconDirOverride;
|
|
229
|
+
}
|
|
230
|
+
const indexDir = getIndexDir();
|
|
231
|
+
const fromIndex = INDEX_LEXICON_FILES.every(
|
|
232
|
+
(name) => fs3.existsSync(path3.join(indexDir, name))
|
|
233
|
+
);
|
|
234
|
+
if (fromIndex) {
|
|
235
|
+
return indexDir;
|
|
236
|
+
}
|
|
237
|
+
const buildDir = getBuildLexiconDir();
|
|
238
|
+
const fromBuild = INDEX_LEXICON_FILES.every(
|
|
239
|
+
(name) => fs3.existsSync(path3.join(buildDir, name))
|
|
240
|
+
);
|
|
241
|
+
if (fromBuild) {
|
|
242
|
+
return buildDir;
|
|
243
|
+
}
|
|
244
|
+
throw new LexiconNotFoundError(
|
|
245
|
+
"Lexicon files not found. Install the documentation index first (index.zip)."
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
function readIndexLexiconFile(name) {
|
|
249
|
+
const filePath = path3.join(resolveLexiconSourceDir(), name);
|
|
250
|
+
return fs3.readFileSync(filePath, "utf-8");
|
|
251
|
+
}
|
|
252
|
+
function readBuildLexiconFile(name, lexiconDir) {
|
|
253
|
+
return fs3.readFileSync(path3.join(lexiconDir, name), "utf-8");
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// src/doc-index/docs-zip-reader.ts
|
|
257
|
+
import * as fs4 from "fs";
|
|
258
|
+
import * as path4 from "path";
|
|
259
|
+
import * as yauzl from "yauzl";
|
|
260
|
+
var docsZipCache = null;
|
|
261
|
+
function openZip(zipPath) {
|
|
262
|
+
return new Promise((resolve3, reject) => {
|
|
263
|
+
yauzl.open(
|
|
264
|
+
zipPath,
|
|
265
|
+
{ lazyEntries: true, decodeStrings: false, autoClose: false },
|
|
266
|
+
(error, zipfile) => {
|
|
267
|
+
if (error || !zipfile) {
|
|
268
|
+
reject(error ?? new Error(`Failed to open zip: ${zipPath}`));
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
resolve3(zipfile);
|
|
272
|
+
}
|
|
273
|
+
);
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
function normalizeEntryFileName(entry) {
|
|
277
|
+
const rawFileName = entry.fileName;
|
|
278
|
+
const fileName = Buffer.isBuffer(rawFileName) ? rawFileName.toString("utf-8") : String(rawFileName);
|
|
279
|
+
return fileName.replace(/\\/g, "/");
|
|
280
|
+
}
|
|
281
|
+
function loadEntryIndex(zipfile) {
|
|
282
|
+
const entries = /* @__PURE__ */ new Map();
|
|
283
|
+
return new Promise((resolve3, reject) => {
|
|
284
|
+
zipfile.readEntry();
|
|
285
|
+
zipfile.on("entry", (entry) => {
|
|
286
|
+
const fileName = normalizeEntryFileName(entry);
|
|
287
|
+
if (!fileName.endsWith("/")) {
|
|
288
|
+
entries.set(fileName, entry);
|
|
289
|
+
}
|
|
290
|
+
zipfile.readEntry();
|
|
291
|
+
});
|
|
292
|
+
zipfile.on("end", () => {
|
|
293
|
+
resolve3(entries);
|
|
294
|
+
});
|
|
295
|
+
zipfile.on("error", reject);
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
function closeDocsZipCache() {
|
|
299
|
+
docsZipCache?.zipfile.close();
|
|
300
|
+
docsZipCache = null;
|
|
301
|
+
}
|
|
302
|
+
async function getDocsZipCache(zipPath) {
|
|
303
|
+
const resolvedPath = path4.resolve(zipPath);
|
|
304
|
+
const stat2 = await fs4.promises.stat(resolvedPath);
|
|
305
|
+
const cached = docsZipCache;
|
|
306
|
+
if (cached && cached.zipPath === resolvedPath && cached.mtimeMs === stat2.mtimeMs && cached.size === stat2.size) {
|
|
307
|
+
return cached;
|
|
308
|
+
}
|
|
309
|
+
closeDocsZipCache();
|
|
310
|
+
const zipfile = await openZip(resolvedPath);
|
|
311
|
+
let entries;
|
|
312
|
+
try {
|
|
313
|
+
entries = await loadEntryIndex(zipfile);
|
|
314
|
+
} catch (error) {
|
|
315
|
+
zipfile.close();
|
|
316
|
+
throw new Error(`Failed to read entries from zip: ${resolvedPath}`, { cause: error });
|
|
317
|
+
}
|
|
318
|
+
docsZipCache = {
|
|
319
|
+
zipPath: resolvedPath,
|
|
320
|
+
mtimeMs: stat2.mtimeMs,
|
|
321
|
+
size: stat2.size,
|
|
322
|
+
zipfile,
|
|
323
|
+
entries,
|
|
324
|
+
readChain: Promise.resolve()
|
|
325
|
+
};
|
|
326
|
+
return docsZipCache;
|
|
327
|
+
}
|
|
328
|
+
function readEntryStream(zipfile, entry) {
|
|
329
|
+
return new Promise((resolve3, reject) => {
|
|
330
|
+
zipfile.openReadStream(entry, (error, stream) => {
|
|
331
|
+
if (error || !stream) {
|
|
332
|
+
reject(
|
|
333
|
+
error ?? new Error(`Failed to read zip entry: ${entry.fileName}`)
|
|
334
|
+
);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const chunks = [];
|
|
338
|
+
stream.on("data", (chunk) => {
|
|
339
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
340
|
+
});
|
|
341
|
+
stream.on("end", () => {
|
|
342
|
+
resolve3(Buffer.concat(chunks));
|
|
343
|
+
});
|
|
344
|
+
stream.on("error", reject);
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
async function readEntryWithLock(cache, entry) {
|
|
349
|
+
const previous = cache.readChain;
|
|
350
|
+
let release;
|
|
351
|
+
cache.readChain = new Promise((resolve3) => {
|
|
352
|
+
release = resolve3;
|
|
353
|
+
});
|
|
354
|
+
await previous;
|
|
355
|
+
try {
|
|
356
|
+
return await readEntryStream(cache.zipfile, entry);
|
|
357
|
+
} finally {
|
|
358
|
+
release();
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
function documentIdToZipEntries(documentId) {
|
|
362
|
+
const normalized = documentId.replace(/\\/g, "/").replace(/\.md$/, "");
|
|
363
|
+
if (normalized.split("/").includes("..")) {
|
|
364
|
+
throw new Error("Invalid document ID: path traversal is not allowed.");
|
|
365
|
+
}
|
|
366
|
+
return [
|
|
367
|
+
`docs/${normalized}.md`,
|
|
368
|
+
`${normalized}.md`,
|
|
369
|
+
`docs/${normalized}`,
|
|
370
|
+
normalized
|
|
371
|
+
];
|
|
372
|
+
}
|
|
373
|
+
function lookupEntry(entries, entryNames) {
|
|
374
|
+
for (const name of entryNames) {
|
|
375
|
+
const entry = entries.get(name.replace(/\\/g, "/"));
|
|
376
|
+
if (entry) {
|
|
377
|
+
return entry;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
async function readMarkdownFromDocsZip(documentId) {
|
|
383
|
+
const zipPath = findDocsZip();
|
|
384
|
+
if (!zipPath) {
|
|
385
|
+
throw new Error("docs.zip not found");
|
|
386
|
+
}
|
|
387
|
+
const cache = await getDocsZipCache(zipPath);
|
|
388
|
+
const entry = lookupEntry(cache.entries, documentIdToZipEntries(documentId));
|
|
389
|
+
if (!entry) {
|
|
390
|
+
throw new Error(`Document not found: ${documentId}`);
|
|
391
|
+
}
|
|
392
|
+
const buffer = await readEntryWithLock(cache, entry);
|
|
393
|
+
return buffer.toString("utf-8");
|
|
394
|
+
}
|
|
395
|
+
function isDocsZipAvailable() {
|
|
396
|
+
const zipPath = findDocsZip();
|
|
397
|
+
return zipPath !== null && fs4.existsSync(zipPath);
|
|
398
|
+
}
|
|
399
|
+
function resetDocsZipReaderCache() {
|
|
400
|
+
closeDocsZipCache();
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// src/doc-index/index-bundle.ts
|
|
404
|
+
import * as fs6 from "fs";
|
|
405
|
+
import * as path6 from "path";
|
|
406
|
+
import AdmZip from "adm-zip";
|
|
407
|
+
|
|
408
|
+
// src/doc-index/path-safety.ts
|
|
409
|
+
import * as fs5 from "fs";
|
|
410
|
+
import * as path5 from "path";
|
|
411
|
+
var DocPathSafetyError = class extends Error {
|
|
412
|
+
constructor(message) {
|
|
413
|
+
super(`Unsafe documentation path: ${message}`);
|
|
414
|
+
this.name = "DocPathSafetyError";
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
function isDocStorageError(error) {
|
|
418
|
+
return error instanceof CacheDirError || error instanceof DocPathSafetyError || error instanceof Error && (error.name === "CacheDirError" || error.name === "DocPathSafetyError");
|
|
419
|
+
}
|
|
420
|
+
function unsafePath(message) {
|
|
421
|
+
return new DocPathSafetyError(message);
|
|
422
|
+
}
|
|
423
|
+
function dataRootForContainment() {
|
|
424
|
+
return resolveCanonicalPath(getCacheDir());
|
|
425
|
+
}
|
|
426
|
+
function assertInsideDataRoot(target, dataRoot, label) {
|
|
427
|
+
const realTarget = resolvePathInsideRoot(target, dataRoot);
|
|
428
|
+
if (realTarget === null) {
|
|
429
|
+
throw unsafePath(`${label} resolves outside the data directory.`);
|
|
430
|
+
}
|
|
431
|
+
return realTarget;
|
|
432
|
+
}
|
|
433
|
+
async function assertSafeDirectory(directory, dataRoot) {
|
|
434
|
+
await fs5.promises.mkdir(directory, { recursive: true });
|
|
435
|
+
const realDir = assertInsideDataRoot(directory, dataRoot, "directory");
|
|
436
|
+
const entry = await fs5.promises.stat(realDir);
|
|
437
|
+
if (!entry.isDirectory()) {
|
|
438
|
+
throw unsafePath("path must be a directory.");
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
function assertSafeRegularFile(filePath) {
|
|
442
|
+
const dataRoot = dataRootForContainment();
|
|
443
|
+
try {
|
|
444
|
+
const realFile = assertInsideDataRoot(filePath, dataRoot, "file");
|
|
445
|
+
if (!fs5.statSync(realFile).isFile()) {
|
|
446
|
+
throw unsafePath("path must be a regular file.");
|
|
447
|
+
}
|
|
448
|
+
} catch (error) {
|
|
449
|
+
if (error.code === "ENOENT") {
|
|
450
|
+
assertInsideDataRoot(path5.dirname(filePath), dataRoot, "file parent");
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
throw error;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
async function assertDocStorageSafe(options = {}) {
|
|
457
|
+
const mode = options.mode ?? "write";
|
|
458
|
+
const dataRoot = await assertCacheDirSafe();
|
|
459
|
+
await assertSafeDirectory(getDocsDir(), dataRoot);
|
|
460
|
+
await assertSafeDirectory(getIndexDir(), dataRoot);
|
|
461
|
+
if (mode === "write") {
|
|
462
|
+
await assertSafeDirectory(getIndexTmpDir(), dataRoot);
|
|
463
|
+
}
|
|
464
|
+
for (const filePath of [
|
|
465
|
+
getSearchDbFile(),
|
|
466
|
+
getBuildMetaFile(),
|
|
467
|
+
getBuildStatusFile(),
|
|
468
|
+
getBuildLockFile(),
|
|
469
|
+
...INDEX_LEXICON_FILES.map((name) => path5.join(getIndexDir(), name))
|
|
470
|
+
]) {
|
|
471
|
+
assertSafeRegularFile(filePath);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// src/doc-index/index-bundle.ts
|
|
476
|
+
var BUNDLE_FILES = [
|
|
477
|
+
"search.db",
|
|
478
|
+
"build-meta.json",
|
|
479
|
+
...INDEX_LEXICON_FILES
|
|
480
|
+
];
|
|
481
|
+
var LEGACY_INDEX_FILES = [
|
|
482
|
+
"corpus.json",
|
|
483
|
+
"corpus-offsets.json",
|
|
484
|
+
"orama.dpack"
|
|
485
|
+
];
|
|
486
|
+
async function cleanLegacyIndexArtifacts(indexDir) {
|
|
487
|
+
for (const name of LEGACY_INDEX_FILES) {
|
|
488
|
+
await fs6.promises.rm(path6.join(indexDir, name), { force: true });
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
function readBundleMetaFromZip(indexZipPath) {
|
|
492
|
+
const zip = new AdmZip(indexZipPath);
|
|
493
|
+
const entry = zip.getEntry("build-meta.json");
|
|
494
|
+
if (!entry) {
|
|
495
|
+
throw new Error("index.zip is missing build-meta.json");
|
|
496
|
+
}
|
|
497
|
+
return JSON.parse(entry.getData().toString("utf-8"));
|
|
498
|
+
}
|
|
499
|
+
async function commitBundleFiles(tmpDir) {
|
|
500
|
+
const indexDir = getIndexDir();
|
|
501
|
+
await fs6.promises.mkdir(indexDir, { recursive: true });
|
|
502
|
+
for (const file of BUNDLE_FILES) {
|
|
503
|
+
const target = path6.join(indexDir, file);
|
|
504
|
+
await fs6.promises.rm(target, { force: true });
|
|
505
|
+
await fs6.promises.rename(path6.join(tmpDir, file), target);
|
|
506
|
+
}
|
|
507
|
+
await cleanLegacyIndexArtifacts(indexDir);
|
|
508
|
+
await fs6.promises.rm(getIndexTmpDir(), { recursive: true, force: true });
|
|
509
|
+
}
|
|
510
|
+
function isBundledIndexUsable(docsZipSha256) {
|
|
511
|
+
const bundlePath = findBundledIndexZip();
|
|
512
|
+
if (!bundlePath) {
|
|
513
|
+
return false;
|
|
514
|
+
}
|
|
515
|
+
try {
|
|
516
|
+
const meta = readBundleMetaFromZip(bundlePath);
|
|
517
|
+
return meta.indexVersion === INDEX_VERSION && meta.docsZipSha256 === docsZipSha256 && meta.segmentCount > 0;
|
|
518
|
+
} catch {
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
async function installBundledIndex(docsZipSha256) {
|
|
523
|
+
await assertDocStorageSafe({ mode: "write" });
|
|
524
|
+
const bundlePath = findBundledIndexZip();
|
|
525
|
+
if (!bundlePath) {
|
|
526
|
+
throw new Error("index.zip not found");
|
|
527
|
+
}
|
|
528
|
+
const bundleMeta = readBundleMetaFromZip(bundlePath);
|
|
529
|
+
if (bundleMeta.docsZipSha256 !== docsZipSha256) {
|
|
530
|
+
throw new Error(
|
|
531
|
+
"Bundled index.zip does not match docs.zip. Rebuild index.zip with npm run build:index."
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
if (bundleMeta.segmentCount <= 0) {
|
|
535
|
+
throw new Error("Bundled index.zip is empty");
|
|
536
|
+
}
|
|
537
|
+
const tmpDir = getIndexTmpDir();
|
|
538
|
+
await fs6.promises.rm(tmpDir, { recursive: true, force: true });
|
|
539
|
+
await fs6.promises.mkdir(tmpDir, { recursive: true });
|
|
540
|
+
const zip = new AdmZip(bundlePath);
|
|
541
|
+
for (const file of BUNDLE_FILES) {
|
|
542
|
+
const entry = zip.getEntry(file);
|
|
543
|
+
if (!entry) {
|
|
544
|
+
throw new Error(`index.zip is missing ${file}`);
|
|
545
|
+
}
|
|
546
|
+
await fs6.promises.writeFile(path6.join(tmpDir, file), entry.getData());
|
|
547
|
+
}
|
|
548
|
+
const installedMeta = JSON.parse(
|
|
549
|
+
await fs6.promises.readFile(path6.join(tmpDir, "build-meta.json"), "utf-8")
|
|
550
|
+
);
|
|
551
|
+
if (!fs6.existsSync(path6.join(tmpDir, "search.db"))) {
|
|
552
|
+
throw new Error("index.zip is missing search.db");
|
|
553
|
+
}
|
|
554
|
+
await commitBundleFiles(tmpDir);
|
|
555
|
+
return installedMeta;
|
|
556
|
+
}
|
|
557
|
+
async function resetBundledInstallScratch() {
|
|
558
|
+
await fs6.promises.rm(getIndexTmpDir(), { recursive: true, force: true });
|
|
559
|
+
const indexDir = getIndexDir();
|
|
560
|
+
for (const file of BUNDLE_FILES) {
|
|
561
|
+
await fs6.promises.rm(path6.join(indexDir, file), { force: true });
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// src/doc-index/hash-utils.ts
|
|
566
|
+
import { createHash } from "crypto";
|
|
567
|
+
import * as fs7 from "fs";
|
|
568
|
+
async function sha256File(filePath) {
|
|
569
|
+
return new Promise((resolve3, reject) => {
|
|
570
|
+
const hash = createHash("sha256");
|
|
571
|
+
const stream = fs7.createReadStream(filePath);
|
|
572
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
573
|
+
stream.on("error", reject);
|
|
574
|
+
stream.on("end", () => resolve3(hash.digest("hex")));
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
function sha256Text(text) {
|
|
578
|
+
return createHash("sha256").update(text, "utf8").digest("hex");
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// src/doc-index/query-rewriter.ts
|
|
582
|
+
var synonymMap = null;
|
|
583
|
+
function loadSynonymGroups() {
|
|
584
|
+
const raw = readIndexLexiconFile("harmonyos-synonyms.json");
|
|
585
|
+
return JSON.parse(raw);
|
|
586
|
+
}
|
|
587
|
+
function buildSynonymIndex(groups) {
|
|
588
|
+
const idx = /* @__PURE__ */ new Map();
|
|
589
|
+
for (const group of groups) {
|
|
590
|
+
const normalized = group.map((item) => item.trim()).filter(Boolean);
|
|
591
|
+
for (const term of normalized) {
|
|
592
|
+
const others = normalized.filter((item) => item !== term);
|
|
593
|
+
idx.set(term.toLowerCase(), others);
|
|
594
|
+
if (term !== term.toLowerCase()) {
|
|
595
|
+
idx.set(term, others);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
return idx;
|
|
600
|
+
}
|
|
601
|
+
function buildSynonymMap() {
|
|
602
|
+
const idx = buildSynonymIndex(loadSynonymGroups());
|
|
603
|
+
const map = /* @__PURE__ */ new Map();
|
|
604
|
+
for (const [key, others] of idx) {
|
|
605
|
+
map.set(key, new Set(others));
|
|
606
|
+
}
|
|
607
|
+
return map;
|
|
608
|
+
}
|
|
609
|
+
function getSynonymMap() {
|
|
610
|
+
if (!synonymMap) {
|
|
611
|
+
synonymMap = buildSynonymMap();
|
|
612
|
+
}
|
|
613
|
+
return synonymMap;
|
|
614
|
+
}
|
|
615
|
+
function expandSynonymsLimited(query, maxParts) {
|
|
616
|
+
const map = getSynonymMap();
|
|
617
|
+
const parts = query.split(/\s+/).filter(Boolean);
|
|
618
|
+
const expanded = /* @__PURE__ */ new Set();
|
|
619
|
+
const limit = Number.isFinite(maxParts) ? maxParts : parts.length;
|
|
620
|
+
for (let index = 0; index < parts.length && expanded.size < limit; index += 1) {
|
|
621
|
+
const part = parts[index];
|
|
622
|
+
expanded.add(part);
|
|
623
|
+
const synonyms = map.get(part) ?? map.get(part.toLowerCase());
|
|
624
|
+
if (!synonyms) {
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
for (const synonym of synonyms) {
|
|
628
|
+
if (expanded.size >= limit) {
|
|
629
|
+
break;
|
|
630
|
+
}
|
|
631
|
+
expanded.add(synonym);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
return [...expanded].join(" ");
|
|
635
|
+
}
|
|
636
|
+
function hashLexiconFile(name, lexiconDir) {
|
|
637
|
+
const content = lexiconDir ? readBuildLexiconFile(name, lexiconDir) : readIndexLexiconFile(name);
|
|
638
|
+
return sha256Text(content);
|
|
639
|
+
}
|
|
640
|
+
function getSynonymsHash(lexiconDir) {
|
|
641
|
+
return hashLexiconFile("harmonyos-synonyms.json", lexiconDir);
|
|
642
|
+
}
|
|
643
|
+
function getTermsHash(lexiconDir) {
|
|
644
|
+
return hashLexiconFile("harmonyos-terms.txt", lexiconDir);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// src/doc-index/index-state.ts
|
|
648
|
+
var DEFAULT_STATUS = {
|
|
649
|
+
state: "idle",
|
|
650
|
+
phase: 0,
|
|
651
|
+
phaseLabel: "Idle",
|
|
652
|
+
current: 0,
|
|
653
|
+
total: 0,
|
|
654
|
+
message: "",
|
|
655
|
+
startedAt: 0,
|
|
656
|
+
updatedAt: 0,
|
|
657
|
+
error: null
|
|
658
|
+
};
|
|
659
|
+
async function readBuildStatus() {
|
|
660
|
+
try {
|
|
661
|
+
const raw = await fs8.promises.readFile(getBuildStatusFile(), "utf-8");
|
|
662
|
+
return JSON.parse(raw);
|
|
663
|
+
} catch {
|
|
664
|
+
return { ...DEFAULT_STATUS };
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
async function writeBuildStatus(status) {
|
|
668
|
+
const filePath = getBuildStatusFile();
|
|
669
|
+
await fs8.promises.mkdir(path7.dirname(filePath), { recursive: true });
|
|
670
|
+
await fs8.promises.writeFile(filePath, JSON.stringify(status, null, 2));
|
|
671
|
+
}
|
|
672
|
+
function createInitialStatus(message) {
|
|
673
|
+
const now = Date.now();
|
|
674
|
+
return {
|
|
675
|
+
state: "installing",
|
|
676
|
+
phase: 1,
|
|
677
|
+
phaseLabel: "Preparing documentation index",
|
|
678
|
+
current: 0,
|
|
679
|
+
total: 0,
|
|
680
|
+
message,
|
|
681
|
+
startedAt: now,
|
|
682
|
+
updatedAt: now,
|
|
683
|
+
error: null
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
async function updateBuildStatus(partial) {
|
|
687
|
+
const current = await readBuildStatus();
|
|
688
|
+
const next = {
|
|
689
|
+
...current,
|
|
690
|
+
...partial,
|
|
691
|
+
updatedAt: Date.now()
|
|
692
|
+
};
|
|
693
|
+
await writeBuildStatus(next);
|
|
694
|
+
return next;
|
|
695
|
+
}
|
|
696
|
+
async function getBundledDocsZipSha256() {
|
|
697
|
+
const zipPath = findDocsZip();
|
|
698
|
+
if (!zipPath) {
|
|
699
|
+
return null;
|
|
700
|
+
}
|
|
701
|
+
return sha256File(zipPath);
|
|
702
|
+
}
|
|
703
|
+
async function readBuildMeta() {
|
|
704
|
+
try {
|
|
705
|
+
const raw = await fs8.promises.readFile(getBuildMetaFile(), "utf-8");
|
|
706
|
+
return JSON.parse(raw);
|
|
707
|
+
} catch {
|
|
708
|
+
return null;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
async function needsRebuildIndex(force = false) {
|
|
712
|
+
if (force) {
|
|
713
|
+
return "no-index";
|
|
714
|
+
}
|
|
715
|
+
const meta = await readBuildMeta();
|
|
716
|
+
if (!meta || meta.segmentCount === 0) {
|
|
717
|
+
return "no-index";
|
|
718
|
+
}
|
|
719
|
+
const bundled = await getBundledDocsZipSha256();
|
|
720
|
+
if (bundled && meta.docsZipSha256 !== bundled) {
|
|
721
|
+
return "docs-changed";
|
|
722
|
+
}
|
|
723
|
+
if (meta.indexVersion !== INDEX_VERSION) {
|
|
724
|
+
return "engine-upgraded";
|
|
725
|
+
}
|
|
726
|
+
if (meta.termsHash !== getTermsHash()) {
|
|
727
|
+
return "terms-changed";
|
|
728
|
+
}
|
|
729
|
+
if (meta.synonymsHash !== getSynonymsHash()) {
|
|
730
|
+
return "synonyms-changed";
|
|
731
|
+
}
|
|
732
|
+
return null;
|
|
733
|
+
}
|
|
734
|
+
function isIndexReady() {
|
|
735
|
+
if (!isDocsZipAvailable()) {
|
|
736
|
+
return false;
|
|
737
|
+
}
|
|
738
|
+
if (!fs8.existsSync(getSearchDbFile()) || !fs8.existsSync(getBuildMetaFile())) {
|
|
739
|
+
return false;
|
|
740
|
+
}
|
|
741
|
+
const indexDir = path7.dirname(getSearchDbFile());
|
|
742
|
+
if (!INDEX_LEXICON_FILES.every(
|
|
743
|
+
(name) => fs8.existsSync(path7.join(indexDir, name))
|
|
744
|
+
)) {
|
|
745
|
+
return false;
|
|
746
|
+
}
|
|
747
|
+
try {
|
|
748
|
+
const raw = fs8.readFileSync(getBuildMetaFile(), "utf-8");
|
|
749
|
+
const meta = JSON.parse(raw);
|
|
750
|
+
return meta.segmentCount > 0;
|
|
751
|
+
} catch {
|
|
752
|
+
return false;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
async function needsIndexInstall(force = false) {
|
|
756
|
+
if (force) {
|
|
757
|
+
return true;
|
|
758
|
+
}
|
|
759
|
+
if (!isDocsZipAvailable()) {
|
|
760
|
+
return false;
|
|
761
|
+
}
|
|
762
|
+
if (!isIndexReady()) {
|
|
763
|
+
return true;
|
|
764
|
+
}
|
|
765
|
+
return await needsRebuildIndex() !== null;
|
|
766
|
+
}
|
|
767
|
+
async function isBuildInProgress() {
|
|
768
|
+
const status = await readBuildStatus();
|
|
769
|
+
return ["installing", "indexing", "persisting"].includes(status.state);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// src/doc-index/api-suffixes.ts
|
|
773
|
+
var API_COMPOUND_SUFFIXES = /* @__PURE__ */ new Set([
|
|
774
|
+
// Selectors — cameraPicker, @ohos.file.picker, DatePicker, DocumentViewPicker, avCastPicker
|
|
775
|
+
"picker",
|
|
776
|
+
// Overlays — AlertDialog, ActionSheet, bindSheet, …
|
|
777
|
+
"dialog",
|
|
778
|
+
"sheet",
|
|
779
|
+
"panel",
|
|
780
|
+
"popup",
|
|
781
|
+
"menu",
|
|
782
|
+
// ArkUI structure / motion — Navigation, GestureRecognizer, PageTransition, …
|
|
783
|
+
"layout",
|
|
784
|
+
"view",
|
|
785
|
+
"gesture",
|
|
786
|
+
"animation",
|
|
787
|
+
"transition",
|
|
788
|
+
"recognizer",
|
|
789
|
+
// @ohos.*Manager modules users describe as "wifi manager", "notification manager", …
|
|
790
|
+
"manager",
|
|
791
|
+
// Kit-oriented queries — "camera kit", "location kit" (maps to *Kit docs / symbols)
|
|
792
|
+
"kit"
|
|
793
|
+
]);
|
|
794
|
+
var MANAGER_REFERENCE_MERGE_FIRST_WORDS = /* @__PURE__ */ new Set([
|
|
795
|
+
"wifi",
|
|
796
|
+
"wlan",
|
|
797
|
+
"notification",
|
|
798
|
+
"bluetooth",
|
|
799
|
+
"location",
|
|
800
|
+
"sensor",
|
|
801
|
+
"power",
|
|
802
|
+
"battery",
|
|
803
|
+
"account",
|
|
804
|
+
"pasteboard",
|
|
805
|
+
"calendar",
|
|
806
|
+
"telephony",
|
|
807
|
+
"contact"
|
|
808
|
+
]);
|
|
809
|
+
var COMPOUND_FIRST_BLOCKLIST = /* @__PURE__ */ new Set([
|
|
810
|
+
"a",
|
|
811
|
+
"an",
|
|
812
|
+
"and",
|
|
813
|
+
"api",
|
|
814
|
+
"app",
|
|
815
|
+
"application",
|
|
816
|
+
"arkts",
|
|
817
|
+
"arkui",
|
|
818
|
+
"for",
|
|
819
|
+
"get",
|
|
820
|
+
"harmonyos",
|
|
821
|
+
"how",
|
|
822
|
+
"in",
|
|
823
|
+
"oh",
|
|
824
|
+
"ohos",
|
|
825
|
+
"on",
|
|
826
|
+
"or",
|
|
827
|
+
"set",
|
|
828
|
+
"the",
|
|
829
|
+
"to",
|
|
830
|
+
"use",
|
|
831
|
+
"using",
|
|
832
|
+
"what",
|
|
833
|
+
"when",
|
|
834
|
+
"where",
|
|
835
|
+
"which",
|
|
836
|
+
"with",
|
|
837
|
+
"without",
|
|
838
|
+
// Generic nouns that precede real APIs but should not auto-merge
|
|
839
|
+
"data",
|
|
840
|
+
"file",
|
|
841
|
+
"main",
|
|
842
|
+
"model",
|
|
843
|
+
"name",
|
|
844
|
+
"network",
|
|
845
|
+
"stage",
|
|
846
|
+
"system",
|
|
847
|
+
"type",
|
|
848
|
+
"user"
|
|
849
|
+
]);
|
|
850
|
+
var COMPOUND_SECOND_BLOCKLIST = /* @__PURE__ */ new Set([
|
|
851
|
+
"ability",
|
|
852
|
+
"extension",
|
|
853
|
+
"module",
|
|
854
|
+
"service",
|
|
855
|
+
"context",
|
|
856
|
+
"options",
|
|
857
|
+
"config",
|
|
858
|
+
"info",
|
|
859
|
+
"event",
|
|
860
|
+
"code",
|
|
861
|
+
"state",
|
|
862
|
+
"type",
|
|
863
|
+
"data",
|
|
864
|
+
"request",
|
|
865
|
+
"response",
|
|
866
|
+
"client",
|
|
867
|
+
"server",
|
|
868
|
+
"handler",
|
|
869
|
+
"helper",
|
|
870
|
+
"utils",
|
|
871
|
+
"factory",
|
|
872
|
+
"builder",
|
|
873
|
+
"listener",
|
|
874
|
+
"callback",
|
|
875
|
+
"observer",
|
|
876
|
+
"provider",
|
|
877
|
+
"consumer",
|
|
878
|
+
"delegate",
|
|
879
|
+
"adapter",
|
|
880
|
+
"driver",
|
|
881
|
+
"buffer",
|
|
882
|
+
"stream",
|
|
883
|
+
"channel",
|
|
884
|
+
"session",
|
|
885
|
+
"task",
|
|
886
|
+
"worker",
|
|
887
|
+
"controller",
|
|
888
|
+
"component",
|
|
889
|
+
"container",
|
|
890
|
+
"attribute",
|
|
891
|
+
"descriptor",
|
|
892
|
+
"modifier",
|
|
893
|
+
"validator",
|
|
894
|
+
"parser",
|
|
895
|
+
"formatter",
|
|
896
|
+
"encoder",
|
|
897
|
+
"decoder",
|
|
898
|
+
"filter",
|
|
899
|
+
"converter",
|
|
900
|
+
"generator",
|
|
901
|
+
"iterator",
|
|
902
|
+
"dispatcher",
|
|
903
|
+
"resolver",
|
|
904
|
+
"scanner",
|
|
905
|
+
"tracker",
|
|
906
|
+
"monitor",
|
|
907
|
+
"scheduler",
|
|
908
|
+
"renderer",
|
|
909
|
+
"loader",
|
|
910
|
+
"subscriber",
|
|
911
|
+
"proxy",
|
|
912
|
+
"button",
|
|
913
|
+
"surface",
|
|
914
|
+
"stack",
|
|
915
|
+
"heap",
|
|
916
|
+
"map",
|
|
917
|
+
"set",
|
|
918
|
+
"array",
|
|
919
|
+
"list"
|
|
920
|
+
]);
|
|
921
|
+
|
|
922
|
+
// src/doc-index/api-identifiers.ts
|
|
923
|
+
var OHOS_MODULE_RE = /@ohos\.[a-z][a-zA-Z0-9.]+/gi;
|
|
924
|
+
var CAMEL_CASE_API_RE = /\b[a-z][a-z0-9]*[A-Z][a-zA-Z0-9]+\b/g;
|
|
925
|
+
var PASCAL_CASE_API_RE = /[A-Z][a-zA-Z0-9]{2,}/g;
|
|
926
|
+
var DECORATOR_TOKEN_RE = /@[A-Z][a-zA-Z0-9]*/g;
|
|
927
|
+
var QUALIFIED_SYMBOL_RE = /[a-z][a-zA-Z0-9]*\.[a-zA-Z][a-zA-Z0-9]+/g;
|
|
928
|
+
var CAMEL_CASE_MIN_LEN = 6;
|
|
929
|
+
var IDENTIFIER_PART_RE = /^[a-z][a-z0-9]{2,}$/;
|
|
930
|
+
var SPLIT_API_QUERY_RE = /^([a-z][a-z0-9]{2,})\s+([a-z][a-z0-9]{2,})$/;
|
|
931
|
+
var OHOS_MODULE_QUERY_RE = /^@ohos\.[a-z][a-zA-Z0-9.]+$/i;
|
|
932
|
+
var SINGLE_CAMEL_CASE_QUERY_RE = /^[a-z][a-z0-9]*[A-Z][a-zA-Z0-9]+$/;
|
|
933
|
+
var SINGLE_PASCAL_CASE_QUERY_RE = /^[A-Z][a-zA-Z0-9]+$/;
|
|
934
|
+
function escapeFtsToken(token) {
|
|
935
|
+
return `"${token.replace(/"/g, '""')}"`;
|
|
936
|
+
}
|
|
937
|
+
function buildFtsMatch(tokens, operator) {
|
|
938
|
+
const unique = [...new Set(tokens.filter(Boolean))];
|
|
939
|
+
if (unique.length === 0) {
|
|
940
|
+
return '""';
|
|
941
|
+
}
|
|
942
|
+
return unique.map(escapeFtsToken).join(` ${operator} `);
|
|
943
|
+
}
|
|
944
|
+
function buildFtsOrMatch(tokens) {
|
|
945
|
+
return buildFtsMatch(tokens, "OR");
|
|
946
|
+
}
|
|
947
|
+
function buildSplitApiAndMatch(left, right) {
|
|
948
|
+
const l = [...new Set(left.filter(Boolean))];
|
|
949
|
+
const r = [...new Set(right.filter(Boolean))];
|
|
950
|
+
if (l.length === 0 || r.length === 0) {
|
|
951
|
+
return buildFtsOrMatch([...l, ...r]);
|
|
952
|
+
}
|
|
953
|
+
return `(${buildFtsOrMatch(l)}) AND (${buildFtsOrMatch(r)})`;
|
|
954
|
+
}
|
|
955
|
+
function isOhosModuleQuery(trimmed) {
|
|
956
|
+
return OHOS_MODULE_QUERY_RE.test(trimmed);
|
|
957
|
+
}
|
|
958
|
+
function isSingleCamelCaseApiQuery(trimmed) {
|
|
959
|
+
return SINGLE_CAMEL_CASE_QUERY_RE.test(trimmed) && trimmed.length >= CAMEL_CASE_MIN_LEN;
|
|
960
|
+
}
|
|
961
|
+
function isSinglePascalCaseApiQuery(trimmed) {
|
|
962
|
+
return SINGLE_PASCAL_CASE_QUERY_RE.test(trimmed);
|
|
963
|
+
}
|
|
964
|
+
function isSingleApiLikeQuery(trimmed) {
|
|
965
|
+
return isOhosModuleQuery(trimmed) || isSingleCamelCaseApiQuery(trimmed) || isSinglePascalCaseApiQuery(trimmed);
|
|
966
|
+
}
|
|
967
|
+
function isApiCompoundSuffix(word) {
|
|
968
|
+
const lower = word.trim().toLowerCase();
|
|
969
|
+
if (COMPOUND_SECOND_BLOCKLIST.has(lower)) {
|
|
970
|
+
return false;
|
|
971
|
+
}
|
|
972
|
+
return API_COMPOUND_SUFFIXES.has(lower);
|
|
973
|
+
}
|
|
974
|
+
function buildCamelCaseCompound(first, second) {
|
|
975
|
+
const head = second.trim().charAt(0).toUpperCase();
|
|
976
|
+
const tail = second.trim().slice(1).toLowerCase();
|
|
977
|
+
return `${first.trim().toLowerCase()}${head}${tail}`;
|
|
978
|
+
}
|
|
979
|
+
function ohosModuleLastSegment(modulePath) {
|
|
980
|
+
const body = modulePath.replace(/^@ohos\./i, "");
|
|
981
|
+
const parts = body.split(".").filter(Boolean);
|
|
982
|
+
const last = parts[parts.length - 1]?.trim();
|
|
983
|
+
return last || void 0;
|
|
984
|
+
}
|
|
985
|
+
function extractOhosModules(text) {
|
|
986
|
+
return [...text.matchAll(OHOS_MODULE_RE)].map((match) => match[0]);
|
|
987
|
+
}
|
|
988
|
+
function extractCamelCaseIdentifiers(text, minLen = CAMEL_CASE_MIN_LEN) {
|
|
989
|
+
const out = [];
|
|
990
|
+
for (const match of text.matchAll(CAMEL_CASE_API_RE)) {
|
|
991
|
+
if (match[0].length >= minLen) {
|
|
992
|
+
out.push(match[0]);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
return out;
|
|
996
|
+
}
|
|
997
|
+
function filterSubsumedTokens(tokens) {
|
|
998
|
+
return tokens.filter((token) => {
|
|
999
|
+
const lower = token.toLowerCase();
|
|
1000
|
+
return !tokens.some((other) => {
|
|
1001
|
+
if (other === token) {
|
|
1002
|
+
return false;
|
|
1003
|
+
}
|
|
1004
|
+
const otherLower = other.toLowerCase();
|
|
1005
|
+
return otherLower.length > lower.length && otherLower.endsWith(lower) && lower.length >= 4;
|
|
1006
|
+
});
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
function appendSymbolVariants(target, symbol) {
|
|
1010
|
+
const trimmed = symbol.trim();
|
|
1011
|
+
if (!trimmed) {
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
target.add(trimmed);
|
|
1015
|
+
const lower = trimmed.toLowerCase();
|
|
1016
|
+
if (lower !== trimmed) {
|
|
1017
|
+
target.add(lower);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
function buildOhosModuleQueryTokens(modulePath) {
|
|
1021
|
+
const tokens = /* @__PURE__ */ new Set();
|
|
1022
|
+
appendSymbolVariants(tokens, modulePath);
|
|
1023
|
+
const last = ohosModuleLastSegment(modulePath);
|
|
1024
|
+
if (last) {
|
|
1025
|
+
appendSymbolVariants(tokens, last);
|
|
1026
|
+
}
|
|
1027
|
+
return filterSubsumedTokens([...tokens]);
|
|
1028
|
+
}
|
|
1029
|
+
function buildSingleApiQueryTokens(trimmed) {
|
|
1030
|
+
if (isOhosModuleQuery(trimmed)) {
|
|
1031
|
+
return buildOhosModuleQueryTokens(trimmed);
|
|
1032
|
+
}
|
|
1033
|
+
const tokens = /* @__PURE__ */ new Set();
|
|
1034
|
+
appendSymbolVariants(tokens, trimmed);
|
|
1035
|
+
return filterSubsumedTokens([...tokens]);
|
|
1036
|
+
}
|
|
1037
|
+
function parseSplitApiPhraseQuery(raw) {
|
|
1038
|
+
const trimmed = raw.trim();
|
|
1039
|
+
if (/[A-Z]/.test(trimmed)) {
|
|
1040
|
+
return null;
|
|
1041
|
+
}
|
|
1042
|
+
const match = trimmed.match(SPLIT_API_QUERY_RE);
|
|
1043
|
+
if (!match) {
|
|
1044
|
+
return null;
|
|
1045
|
+
}
|
|
1046
|
+
const first = match[1].toLowerCase();
|
|
1047
|
+
const second = match[2].toLowerCase();
|
|
1048
|
+
if (!IDENTIFIER_PART_RE.test(first) || COMPOUND_FIRST_BLOCKLIST.has(first)) {
|
|
1049
|
+
return null;
|
|
1050
|
+
}
|
|
1051
|
+
if (!isApiCompoundSuffix(second)) {
|
|
1052
|
+
return null;
|
|
1053
|
+
}
|
|
1054
|
+
const camelCase = buildCamelCaseCompound(first, second);
|
|
1055
|
+
return { first, second, camelCase, lower: camelCase.toLowerCase() };
|
|
1056
|
+
}
|
|
1057
|
+
function getManagerSplitApiCompound(rawQuery) {
|
|
1058
|
+
const split = parseSplitApiPhraseQuery(rawQuery.trim());
|
|
1059
|
+
if (!split || split.second !== "manager") {
|
|
1060
|
+
return null;
|
|
1061
|
+
}
|
|
1062
|
+
return split;
|
|
1063
|
+
}
|
|
1064
|
+
function managerMergeFirstWord(camelCase) {
|
|
1065
|
+
const match = camelCase.match(/^([a-z][a-z0-9]*)Manager$/i);
|
|
1066
|
+
if (!match) {
|
|
1067
|
+
return null;
|
|
1068
|
+
}
|
|
1069
|
+
return match[1].toLowerCase();
|
|
1070
|
+
}
|
|
1071
|
+
function shouldReferencesFirstForManagerQuery(rawQuery) {
|
|
1072
|
+
const trimmed = rawQuery.trim();
|
|
1073
|
+
const split = getManagerSplitApiCompound(trimmed);
|
|
1074
|
+
if (split && MANAGER_REFERENCE_MERGE_FIRST_WORDS.has(split.first)) {
|
|
1075
|
+
return true;
|
|
1076
|
+
}
|
|
1077
|
+
if (isSingleCamelCaseApiQuery(trimmed)) {
|
|
1078
|
+
const first = managerMergeFirstWord(trimmed);
|
|
1079
|
+
return first !== null && MANAGER_REFERENCE_MERGE_FIRST_WORDS.has(first);
|
|
1080
|
+
}
|
|
1081
|
+
return false;
|
|
1082
|
+
}
|
|
1083
|
+
function rowMatchesManagerApiModule(docTitle, sectionTitle, compoundCamel) {
|
|
1084
|
+
const compoundLower = compoundCamel.toLowerCase();
|
|
1085
|
+
const ohosModuleRe = new RegExp(`@ohos\\.[^\\s(]*${compoundCamel}`, "i");
|
|
1086
|
+
for (const field of [docTitle, sectionTitle]) {
|
|
1087
|
+
if (!field) {
|
|
1088
|
+
continue;
|
|
1089
|
+
}
|
|
1090
|
+
if (field.toLowerCase().includes(compoundLower)) {
|
|
1091
|
+
return true;
|
|
1092
|
+
}
|
|
1093
|
+
if (ohosModuleRe.test(field)) {
|
|
1094
|
+
return true;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
return false;
|
|
1098
|
+
}
|
|
1099
|
+
function extractPriorityApiTokens(text) {
|
|
1100
|
+
const trimmed = text.trim();
|
|
1101
|
+
if (isSingleApiLikeQuery(trimmed)) {
|
|
1102
|
+
return buildSingleApiQueryTokens(trimmed);
|
|
1103
|
+
}
|
|
1104
|
+
const tokens = /* @__PURE__ */ new Set();
|
|
1105
|
+
for (const modulePath of extractOhosModules(text)) {
|
|
1106
|
+
appendSymbolVariants(tokens, modulePath);
|
|
1107
|
+
const last = ohosModuleLastSegment(modulePath);
|
|
1108
|
+
if (last) {
|
|
1109
|
+
appendSymbolVariants(tokens, last);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
for (const camel of extractCamelCaseIdentifiers(text)) {
|
|
1113
|
+
appendSymbolVariants(tokens, camel);
|
|
1114
|
+
}
|
|
1115
|
+
for (const match of text.matchAll(DECORATOR_TOKEN_RE)) {
|
|
1116
|
+
tokens.add(match[0]);
|
|
1117
|
+
}
|
|
1118
|
+
for (const match of text.matchAll(QUALIFIED_SYMBOL_RE)) {
|
|
1119
|
+
tokens.add(match[0]);
|
|
1120
|
+
}
|
|
1121
|
+
for (const match of text.matchAll(PASCAL_CASE_API_RE)) {
|
|
1122
|
+
if (match[0].length >= 4) {
|
|
1123
|
+
tokens.add(match[0]);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
return filterSubsumedTokens([...tokens]);
|
|
1127
|
+
}
|
|
1128
|
+
function extractQueryApiSymbolTokens(rawQuery) {
|
|
1129
|
+
const trimmed = rawQuery.trim();
|
|
1130
|
+
if (isSingleApiLikeQuery(trimmed)) {
|
|
1131
|
+
return buildSingleApiQueryTokens(trimmed);
|
|
1132
|
+
}
|
|
1133
|
+
const tokens = /* @__PURE__ */ new Set();
|
|
1134
|
+
const split = parseSplitApiPhraseQuery(trimmed);
|
|
1135
|
+
if (split) {
|
|
1136
|
+
appendSymbolVariants(tokens, split.camelCase);
|
|
1137
|
+
}
|
|
1138
|
+
for (const token of extractPriorityApiTokens(trimmed)) {
|
|
1139
|
+
tokens.add(token);
|
|
1140
|
+
}
|
|
1141
|
+
for (const match of trimmed.matchAll(/\b[a-z][a-zA-Z0-9]{3,}\b/g)) {
|
|
1142
|
+
const value = match[0];
|
|
1143
|
+
if (value !== value.toLowerCase()) {
|
|
1144
|
+
tokens.add(value);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
return filterSubsumedTokens([...tokens]);
|
|
1148
|
+
}
|
|
1149
|
+
function shouldSkipSynonymExpansion(trimmed) {
|
|
1150
|
+
return isSingleApiLikeQuery(trimmed);
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// src/doc-index/query-named-rules.ts
|
|
1154
|
+
var NAMED_QUERY_RE = {
|
|
1155
|
+
pureApiSymbol: /^[A-Z][a-zA-Z0-9]+$/,
|
|
1156
|
+
stageModelExact: /^Stage\s*模型$/i,
|
|
1157
|
+
/** English alias for Stage 模型 (exact two-word query only). */
|
|
1158
|
+
stageModelEnglishExact: /^stage\s+model$/i,
|
|
1159
|
+
stageModel: /Stage\s*模型/i,
|
|
1160
|
+
stageModelEntryPage: /Stage\s*模型.*EntryAbility.*(页面|新页面)/,
|
|
1161
|
+
declarePermissionBoost: /声明.*权限|应用权限.*声明|如何声明应用权限/,
|
|
1162
|
+
declarePermissionCatalog: /如何声明应用权限/,
|
|
1163
|
+
declarePermissionTokens: /如何声明应用权限|声明应用权限/,
|
|
1164
|
+
uiAbilityLifecycleBoost: /UIAbility.*生命周期|生命周期.*UIAbility|UIAblity/i,
|
|
1165
|
+
uiAbilityLifecycleCatalog: /UIAblity.*生命周期|UIAbility.*生命周期/i,
|
|
1166
|
+
entryAbilityPage: /EntryAbility.*(页面|启动|跳转|新页面)/,
|
|
1167
|
+
stateDecoratorBoost: /@State|@Prop|@Link|@Provide|@Consume/,
|
|
1168
|
+
stateDecoratorCatalog: /@State\s*装饰器|@Prop\s*装饰器|@Link\s*装饰器/,
|
|
1169
|
+
stateManagement: /状态管理原理/,
|
|
1170
|
+
routerRoute: /Router\s+路由/,
|
|
1171
|
+
dialogPopupExact: /^Dialog\s+弹窗$/,
|
|
1172
|
+
dialogPopupBoost: /Dialog\s+弹窗/
|
|
1173
|
+
};
|
|
1174
|
+
function isPureApiSymbolQuery(rawQuery) {
|
|
1175
|
+
return NAMED_QUERY_RE.pureApiSymbol.test(rawQuery.trim());
|
|
1176
|
+
}
|
|
1177
|
+
function isStageModelExactQuery(rawQuery) {
|
|
1178
|
+
const trimmed = rawQuery.trim();
|
|
1179
|
+
return NAMED_QUERY_RE.stageModelExact.test(trimmed) || NAMED_QUERY_RE.stageModelEnglishExact.test(trimmed);
|
|
1180
|
+
}
|
|
1181
|
+
function extractNamedConceptTokens(text) {
|
|
1182
|
+
const tokens = [];
|
|
1183
|
+
if (isStageModelExactQuery(text.trim())) {
|
|
1184
|
+
tokens.push("Stage\u6A21\u578B", "Stage\u6A21\u578B\u5F00\u53D1\u6982\u8FF0");
|
|
1185
|
+
}
|
|
1186
|
+
if (NAMED_QUERY_RE.declarePermissionTokens.test(text)) {
|
|
1187
|
+
tokens.push("\u58F0\u660E\u6743\u9650");
|
|
1188
|
+
}
|
|
1189
|
+
if (NAMED_QUERY_RE.stageModelEntryPage.test(text)) {
|
|
1190
|
+
tokens.push("\u9875\u9762\u8DEF\u7531", "pushUrl");
|
|
1191
|
+
}
|
|
1192
|
+
if (/UIAblity|UIAbility/.test(text) && /生命周期/.test(text)) {
|
|
1193
|
+
tokens.push("UIAbility\u7EC4\u4EF6\u751F\u547D\u5468\u671F");
|
|
1194
|
+
}
|
|
1195
|
+
return tokens;
|
|
1196
|
+
}
|
|
1197
|
+
function getStageModelPreparedOverride(trimmedRawQuery) {
|
|
1198
|
+
if (!isStageModelExactQuery(trimmedRawQuery)) {
|
|
1199
|
+
return null;
|
|
1200
|
+
}
|
|
1201
|
+
return {
|
|
1202
|
+
expandedQuery: "Stage\u6A21\u578B\u5F00\u53D1\u6982\u8FF0",
|
|
1203
|
+
tokens: ["Stage\u6A21\u578B\u5F00\u53D1\u6982\u8FF0", "Stage\u6A21\u578B"]
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
var NAMED_CONCEPT_BOOST_RULES = [
|
|
1207
|
+
{
|
|
1208
|
+
matches: (q) => NAMED_QUERY_RE.uiAbilityLifecycleBoost.test(q),
|
|
1209
|
+
weights: [
|
|
1210
|
+
{ catalog: "harmonyos-guides", multiplier: 1.85 },
|
|
1211
|
+
{ catalog: "harmonyos-references", multiplier: 0.65 },
|
|
1212
|
+
{ catalog: "harmonyos-faqs", multiplier: 0.82 }
|
|
1213
|
+
]
|
|
1214
|
+
},
|
|
1215
|
+
{
|
|
1216
|
+
matches: (q) => isStageModelExactQuery(q),
|
|
1217
|
+
weights: [
|
|
1218
|
+
{ catalog: "harmonyos-guides", multiplier: 1.85 },
|
|
1219
|
+
{ catalog: "harmonyos-references", multiplier: 0.55 },
|
|
1220
|
+
{ catalog: "harmonyos-faqs", multiplier: 0.85 }
|
|
1221
|
+
]
|
|
1222
|
+
},
|
|
1223
|
+
{
|
|
1224
|
+
matches: (q) => !isStageModelExactQuery(q) && NAMED_QUERY_RE.stageModel.test(q),
|
|
1225
|
+
weights: [
|
|
1226
|
+
{ catalog: "harmonyos-guides", multiplier: 1.65 },
|
|
1227
|
+
{ catalog: "harmonyos-references", multiplier: 0.65 }
|
|
1228
|
+
]
|
|
1229
|
+
},
|
|
1230
|
+
{
|
|
1231
|
+
matches: (q) => NAMED_QUERY_RE.entryAbilityPage.test(q),
|
|
1232
|
+
weights: [
|
|
1233
|
+
{ catalog: "harmonyos-guides", multiplier: 1.75 },
|
|
1234
|
+
{ catalog: "harmonyos-references", multiplier: 0.48 }
|
|
1235
|
+
]
|
|
1236
|
+
},
|
|
1237
|
+
{
|
|
1238
|
+
matches: (q) => NAMED_QUERY_RE.declarePermissionBoost.test(q),
|
|
1239
|
+
weights: [
|
|
1240
|
+
{ catalog: "harmonyos-guides", multiplier: 1.65 },
|
|
1241
|
+
{ catalog: "best-practices", multiplier: 0.68 }
|
|
1242
|
+
]
|
|
1243
|
+
},
|
|
1244
|
+
{
|
|
1245
|
+
matches: (q) => NAMED_QUERY_RE.stateDecoratorBoost.test(q),
|
|
1246
|
+
weights: [
|
|
1247
|
+
{ catalog: "harmonyos-guides", multiplier: 1.75 },
|
|
1248
|
+
{ catalog: "harmonyos-references", multiplier: 0.42 }
|
|
1249
|
+
]
|
|
1250
|
+
},
|
|
1251
|
+
{
|
|
1252
|
+
matches: (q) => NAMED_QUERY_RE.stateManagement.test(q),
|
|
1253
|
+
weights: [
|
|
1254
|
+
{ catalog: "harmonyos-guides", multiplier: 1.6 },
|
|
1255
|
+
{ catalog: "best-practices", multiplier: 0.78 }
|
|
1256
|
+
]
|
|
1257
|
+
},
|
|
1258
|
+
{
|
|
1259
|
+
matches: (q) => NAMED_QUERY_RE.routerRoute.test(q),
|
|
1260
|
+
weights: [
|
|
1261
|
+
{ catalog: "harmonyos-guides", multiplier: 1.65 },
|
|
1262
|
+
{ catalog: "harmonyos-references", multiplier: 1.4 },
|
|
1263
|
+
{ catalog: "harmonyos-faqs", multiplier: 0.72 },
|
|
1264
|
+
{ catalog: "best-practices", multiplier: 0.72 }
|
|
1265
|
+
]
|
|
1266
|
+
},
|
|
1267
|
+
{
|
|
1268
|
+
matches: (q) => NAMED_QUERY_RE.dialogPopupBoost.test(q),
|
|
1269
|
+
weights: [
|
|
1270
|
+
{ catalog: "harmonyos-guides", multiplier: 1.65 },
|
|
1271
|
+
{ catalog: "harmonyos-faqs", multiplier: 0.72 },
|
|
1272
|
+
{ catalog: "best-practices", multiplier: 0.72 }
|
|
1273
|
+
]
|
|
1274
|
+
}
|
|
1275
|
+
];
|
|
1276
|
+
function applyWeights(boosts, weights) {
|
|
1277
|
+
for (const { catalog, multiplier } of weights) {
|
|
1278
|
+
const catalogId = CATALOG_NAME_TO_ID[catalog];
|
|
1279
|
+
boosts.set(catalogId, (boosts.get(catalogId) ?? 1) * multiplier);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
function applyNamedConceptBoosts(rawQuery, boosts) {
|
|
1283
|
+
for (const rule of NAMED_CONCEPT_BOOST_RULES) {
|
|
1284
|
+
if (rule.matches(rawQuery)) {
|
|
1285
|
+
applyWeights(boosts, rule.weights);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
function shouldMergeReferencesWithGlobalFallback(rawQuery, explicitCatalog) {
|
|
1290
|
+
if (explicitCatalog !== void 0) {
|
|
1291
|
+
return false;
|
|
1292
|
+
}
|
|
1293
|
+
const trimmed = rawQuery.trim();
|
|
1294
|
+
return isOhosModuleQuery(trimmed) || NAMED_QUERY_RE.pureApiSymbol.test(trimmed) || shouldReferencesFirstForManagerQuery(trimmed);
|
|
1295
|
+
}
|
|
1296
|
+
function inferNamedSearchCatalog(rawQuery) {
|
|
1297
|
+
const trimmed = rawQuery.trim();
|
|
1298
|
+
if (isOhosModuleQuery(trimmed) || NAMED_QUERY_RE.pureApiSymbol.test(trimmed)) {
|
|
1299
|
+
return "harmonyos-references";
|
|
1300
|
+
}
|
|
1301
|
+
if (isStageModelExactQuery(trimmed)) {
|
|
1302
|
+
return "harmonyos-guides";
|
|
1303
|
+
}
|
|
1304
|
+
if (NAMED_QUERY_RE.uiAbilityLifecycleCatalog.test(trimmed)) {
|
|
1305
|
+
return "harmonyos-guides";
|
|
1306
|
+
}
|
|
1307
|
+
if (NAMED_QUERY_RE.stateDecoratorCatalog.test(trimmed)) {
|
|
1308
|
+
return "harmonyos-guides";
|
|
1309
|
+
}
|
|
1310
|
+
if (NAMED_QUERY_RE.stateManagement.test(trimmed)) {
|
|
1311
|
+
return "harmonyos-guides";
|
|
1312
|
+
}
|
|
1313
|
+
if (NAMED_QUERY_RE.declarePermissionCatalog.test(trimmed)) {
|
|
1314
|
+
return "harmonyos-guides";
|
|
1315
|
+
}
|
|
1316
|
+
if (NAMED_QUERY_RE.stageModelEntryPage.test(trimmed)) {
|
|
1317
|
+
return "harmonyos-guides";
|
|
1318
|
+
}
|
|
1319
|
+
if (NAMED_QUERY_RE.routerRoute.test(trimmed)) {
|
|
1320
|
+
return "harmonyos-guides";
|
|
1321
|
+
}
|
|
1322
|
+
if (NAMED_QUERY_RE.dialogPopupExact.test(trimmed)) {
|
|
1323
|
+
return "harmonyos-guides";
|
|
1324
|
+
}
|
|
1325
|
+
return void 0;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
// src/doc-index/tokenizer.ts
|
|
1329
|
+
import { createRequire } from "module";
|
|
1330
|
+
|
|
1331
|
+
// src/logger.ts
|
|
1332
|
+
function debugLog(message) {
|
|
1333
|
+
if (process.env.DEVECO_CLI_DEBUG) {
|
|
1334
|
+
const msg = typeof message === "function" ? message() : message;
|
|
1335
|
+
console.log(`[DEBUG] ${msg}`);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
// src/doc-index/tokenizer.ts
|
|
1340
|
+
var require2 = createRequire(import.meta.url);
|
|
1341
|
+
var stopWords = null;
|
|
1342
|
+
var jiebaInstance = null;
|
|
1343
|
+
var initPromise = null;
|
|
1344
|
+
function loadStopWords() {
|
|
1345
|
+
if (stopWords) {
|
|
1346
|
+
return stopWords;
|
|
1347
|
+
}
|
|
1348
|
+
const raw = readIndexLexiconFile("harmonyos-stopwords.txt");
|
|
1349
|
+
stopWords = new Set(
|
|
1350
|
+
raw.split("\n").map((line) => line.trim()).filter(Boolean)
|
|
1351
|
+
);
|
|
1352
|
+
return stopWords;
|
|
1353
|
+
}
|
|
1354
|
+
function mergeDecoratorTokens(tokens) {
|
|
1355
|
+
const merged = [];
|
|
1356
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
1357
|
+
const current = tokens[i]?.trim();
|
|
1358
|
+
if (!current) {
|
|
1359
|
+
continue;
|
|
1360
|
+
}
|
|
1361
|
+
const next = tokens[i + 1]?.trim();
|
|
1362
|
+
if (current === "@" && next && /^[A-Z][a-zA-Z0-9]*$/.test(next)) {
|
|
1363
|
+
merged.push(`@${next}`);
|
|
1364
|
+
i += 1;
|
|
1365
|
+
continue;
|
|
1366
|
+
}
|
|
1367
|
+
merged.push(current);
|
|
1368
|
+
}
|
|
1369
|
+
return merged;
|
|
1370
|
+
}
|
|
1371
|
+
function normalizeTokens(tokens) {
|
|
1372
|
+
const stops = loadStopWords();
|
|
1373
|
+
const out = [];
|
|
1374
|
+
for (const raw of mergeDecoratorTokens(tokens)) {
|
|
1375
|
+
const token = raw.trim();
|
|
1376
|
+
if (!token || stops.has(token)) {
|
|
1377
|
+
continue;
|
|
1378
|
+
}
|
|
1379
|
+
out.push(token.toLowerCase());
|
|
1380
|
+
if (/[A-Z]/.test(token) && /[a-zA-Z]/.test(token)) {
|
|
1381
|
+
out.push(token);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
return out;
|
|
1385
|
+
}
|
|
1386
|
+
async function createJieba() {
|
|
1387
|
+
const { cut, cut_for_search, with_dict } = require2("jieba-wasm");
|
|
1388
|
+
const userDict = readIndexLexiconFile("harmonyos-terms.txt");
|
|
1389
|
+
with_dict(userDict);
|
|
1390
|
+
debugLog("doc-index: using jieba-wasm backend");
|
|
1391
|
+
return {
|
|
1392
|
+
cut: (text, hmm) => cut(text, hmm),
|
|
1393
|
+
cutForSearch: (text, hmm) => cut_for_search(text, hmm)
|
|
1394
|
+
};
|
|
1395
|
+
}
|
|
1396
|
+
async function getJieba() {
|
|
1397
|
+
if (jiebaInstance) {
|
|
1398
|
+
return jiebaInstance;
|
|
1399
|
+
}
|
|
1400
|
+
if (!initPromise) {
|
|
1401
|
+
initPromise = createJieba().then((instance) => {
|
|
1402
|
+
jiebaInstance = instance;
|
|
1403
|
+
return instance;
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1406
|
+
return initPromise;
|
|
1407
|
+
}
|
|
1408
|
+
async function tokenizeForIndex(text) {
|
|
1409
|
+
const jieba = await getJieba();
|
|
1410
|
+
return normalizeTokens(jieba.cutForSearch(text, true));
|
|
1411
|
+
}
|
|
1412
|
+
async function tokenizeForQuery(text) {
|
|
1413
|
+
const jieba = await getJieba();
|
|
1414
|
+
return normalizeTokens(jieba.cut(text, true));
|
|
1415
|
+
}
|
|
1416
|
+
async function tokenizeAndCap(text, maxChars) {
|
|
1417
|
+
const trimmed = text.trim();
|
|
1418
|
+
if (!trimmed || maxChars <= 0) {
|
|
1419
|
+
return "";
|
|
1420
|
+
}
|
|
1421
|
+
const joined = (await tokenizeForIndex(trimmed)).join(" ");
|
|
1422
|
+
if (joined.length <= maxChars) {
|
|
1423
|
+
return joined;
|
|
1424
|
+
}
|
|
1425
|
+
return joined.slice(0, maxChars);
|
|
1426
|
+
}
|
|
1427
|
+
async function capApiSymbolsForIndex(symbols, maxChars) {
|
|
1428
|
+
const tokens = [];
|
|
1429
|
+
for (const symbol of symbols) {
|
|
1430
|
+
const trimmed = symbol.trim();
|
|
1431
|
+
if (!trimmed) {
|
|
1432
|
+
continue;
|
|
1433
|
+
}
|
|
1434
|
+
tokens.push(trimmed.toLowerCase());
|
|
1435
|
+
if (/[A-Z]/.test(trimmed)) {
|
|
1436
|
+
tokens.push(trimmed);
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
const joined = [...new Set(tokens)].join(" ");
|
|
1440
|
+
if (joined.length <= maxChars) {
|
|
1441
|
+
return joined;
|
|
1442
|
+
}
|
|
1443
|
+
return joined.slice(0, maxChars);
|
|
1444
|
+
}
|
|
1445
|
+
async function buildDocumentSearchText(source) {
|
|
1446
|
+
const isSectionRow = Boolean(source.sectionTitle.trim());
|
|
1447
|
+
const titleRaw = source.titleTokens.trim();
|
|
1448
|
+
const maxChars = isSectionRow ? DOC_SECTION_SEARCH_TEXT_MAX_CHARS : DOC_SEARCH_TEXT_MAX_CHARS;
|
|
1449
|
+
const apiBudget = isSectionRow ? DOC_SECTION_BUDGET_API_SYMBOLS : DOC_SEARCH_BUDGET_API_SYMBOLS;
|
|
1450
|
+
const apiText = isSectionRow ? await capApiSymbolsForIndex(source.apiSymbols, apiBudget) : await tokenizeAndCap(source.apiSymbols.join(" "), apiBudget);
|
|
1451
|
+
const parts = await Promise.all([
|
|
1452
|
+
tokenizeAndCap(
|
|
1453
|
+
titleRaw,
|
|
1454
|
+
isSectionRow ? DOC_SECTION_BUDGET_TITLE : DOC_SEARCH_BUDGET_TITLE
|
|
1455
|
+
),
|
|
1456
|
+
Promise.resolve(apiText),
|
|
1457
|
+
tokenizeAndCap(
|
|
1458
|
+
source.headingsText,
|
|
1459
|
+
isSectionRow ? DOC_SECTION_BUDGET_HEADINGS : DOC_SEARCH_BUDGET_HEADINGS
|
|
1460
|
+
),
|
|
1461
|
+
tokenizeAndCap(
|
|
1462
|
+
source.bodySample,
|
|
1463
|
+
isSectionRow ? DOC_SECTION_BUDGET_BODY : DOC_SEARCH_BUDGET_BODY
|
|
1464
|
+
)
|
|
1465
|
+
]);
|
|
1466
|
+
const joined = parts.filter(Boolean).join(" ");
|
|
1467
|
+
if (joined.length <= maxChars) {
|
|
1468
|
+
return joined;
|
|
1469
|
+
}
|
|
1470
|
+
return joined.slice(0, maxChars);
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
// src/doc-index/query-normalizer.ts
|
|
1474
|
+
function normalizeSearchInput(keywords) {
|
|
1475
|
+
return keywords.join(" ").replace(/\s+/g, " ").trim().slice(0, QUERY_MAX_RAW_CHARS);
|
|
1476
|
+
}
|
|
1477
|
+
function capTokens(priority, general) {
|
|
1478
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1479
|
+
const out = [];
|
|
1480
|
+
for (const token of [...priority, ...general]) {
|
|
1481
|
+
const key = token.toLowerCase();
|
|
1482
|
+
if (!token || seen.has(key)) {
|
|
1483
|
+
continue;
|
|
1484
|
+
}
|
|
1485
|
+
seen.add(key);
|
|
1486
|
+
out.push(token);
|
|
1487
|
+
if (out.length >= QUERY_MAX_FTS_TOKENS) {
|
|
1488
|
+
break;
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
return out;
|
|
1492
|
+
}
|
|
1493
|
+
function shouldPreferAndMatch(tokens) {
|
|
1494
|
+
return tokens.length >= 2 && tokens.length <= QUERY_AND_MAX_TOKENS;
|
|
1495
|
+
}
|
|
1496
|
+
function buildSplitApiPrepared(rawQuery, split) {
|
|
1497
|
+
const expandedQuery = expandSynonymsLimited(
|
|
1498
|
+
rawQuery,
|
|
1499
|
+
SYNONYM_EXPAND_MAX_PARTS
|
|
1500
|
+
);
|
|
1501
|
+
const synonymParts = expandedQuery.split(/\s+/).filter(Boolean);
|
|
1502
|
+
const left = [
|
|
1503
|
+
split.first,
|
|
1504
|
+
...synonymParts.filter((part) => part !== split.second),
|
|
1505
|
+
split.lower,
|
|
1506
|
+
split.camelCase
|
|
1507
|
+
];
|
|
1508
|
+
const right = [split.second, split.lower, split.camelCase];
|
|
1509
|
+
const tokens = capTokens(left, right);
|
|
1510
|
+
return {
|
|
1511
|
+
rawQuery,
|
|
1512
|
+
expandedQuery,
|
|
1513
|
+
tokens,
|
|
1514
|
+
preferAnd: false,
|
|
1515
|
+
ftsMatch: buildSplitApiAndMatch(left, right)
|
|
1516
|
+
};
|
|
1517
|
+
}
|
|
1518
|
+
function buildSingleApiPrepared(rawQuery, trimmed) {
|
|
1519
|
+
const tokens = capTokens(buildSingleApiQueryTokens(trimmed), []);
|
|
1520
|
+
return {
|
|
1521
|
+
rawQuery,
|
|
1522
|
+
expandedQuery: rawQuery,
|
|
1523
|
+
tokens,
|
|
1524
|
+
preferAnd: false,
|
|
1525
|
+
ftsMatch: buildFtsOrMatch(tokens)
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1528
|
+
async function prepareSearchQuery(keywords) {
|
|
1529
|
+
const rawQuery = normalizeSearchInput(keywords);
|
|
1530
|
+
const trimmed = rawQuery.trim();
|
|
1531
|
+
const stageOverride = getStageModelPreparedOverride(trimmed);
|
|
1532
|
+
if (stageOverride) {
|
|
1533
|
+
return {
|
|
1534
|
+
rawQuery,
|
|
1535
|
+
expandedQuery: stageOverride.expandedQuery,
|
|
1536
|
+
tokens: stageOverride.tokens,
|
|
1537
|
+
preferAnd: false
|
|
1538
|
+
};
|
|
1539
|
+
}
|
|
1540
|
+
const splitApi = parseSplitApiPhraseQuery(trimmed);
|
|
1541
|
+
if (splitApi) {
|
|
1542
|
+
return buildSplitApiPrepared(rawQuery, splitApi);
|
|
1543
|
+
}
|
|
1544
|
+
if (isSingleApiLikeQuery(trimmed)) {
|
|
1545
|
+
return buildSingleApiPrepared(rawQuery, trimmed);
|
|
1546
|
+
}
|
|
1547
|
+
const conceptPriority = extractNamedConceptTokens(rawQuery);
|
|
1548
|
+
const priorityTokens = [
|
|
1549
|
+
...extractPriorityApiTokens(rawQuery),
|
|
1550
|
+
...conceptPriority
|
|
1551
|
+
];
|
|
1552
|
+
const skipSynonyms = shouldSkipSynonymExpansion(trimmed);
|
|
1553
|
+
const expandedQuery = skipSynonyms ? rawQuery : expandSynonymsLimited(rawQuery, SYNONYM_EXPAND_MAX_PARTS);
|
|
1554
|
+
const generalTokens = await tokenizeForQuery(expandedQuery);
|
|
1555
|
+
const tokens = capTokens(priorityTokens, generalTokens);
|
|
1556
|
+
return {
|
|
1557
|
+
rawQuery,
|
|
1558
|
+
expandedQuery,
|
|
1559
|
+
tokens,
|
|
1560
|
+
preferAnd: conceptPriority.length === 0 && shouldPreferAndMatch(tokens)
|
|
1561
|
+
};
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
// src/doc-index/catalog-routing.ts
|
|
1565
|
+
var TAIL_SEARCH_CATALOGS = [
|
|
1566
|
+
"harmonyos-releases",
|
|
1567
|
+
"harmonyos-roadmap"
|
|
1568
|
+
];
|
|
1569
|
+
var TAIL_CATALOG_IDS = new Set(
|
|
1570
|
+
TAIL_SEARCH_CATALOGS.map((name) => CATALOG_NAME_TO_ID[name])
|
|
1571
|
+
);
|
|
1572
|
+
var TAIL_CATALOG_DOCUMENT_PREFIXES = TAIL_SEARCH_CATALOGS.map(
|
|
1573
|
+
(name) => `${CATALOG_TITLES[name]}/`
|
|
1574
|
+
);
|
|
1575
|
+
function isTailSearchCatalogId(catalogId) {
|
|
1576
|
+
return TAIL_CATALOG_IDS.has(catalogId);
|
|
1577
|
+
}
|
|
1578
|
+
function isTailCatalogDocumentId(documentId) {
|
|
1579
|
+
return TAIL_CATALOG_DOCUMENT_PREFIXES.some(
|
|
1580
|
+
(prefix) => documentId.startsWith(prefix)
|
|
1581
|
+
);
|
|
1582
|
+
}
|
|
1583
|
+
function orderRowsWithTailCatalogsLast(rows) {
|
|
1584
|
+
const head = [];
|
|
1585
|
+
const tail = [];
|
|
1586
|
+
for (const row of rows) {
|
|
1587
|
+
if (isTailSearchCatalogId(row.catalog_id)) {
|
|
1588
|
+
tail.push(row);
|
|
1589
|
+
} else {
|
|
1590
|
+
head.push(row);
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
return [...head, ...tail];
|
|
1594
|
+
}
|
|
1595
|
+
function orderLocalResultsWithTailCatalogsLast(results) {
|
|
1596
|
+
const head = [];
|
|
1597
|
+
const tail = [];
|
|
1598
|
+
for (const row of results) {
|
|
1599
|
+
if (isTailCatalogDocumentId(row.documentId)) {
|
|
1600
|
+
tail.push(row);
|
|
1601
|
+
} else {
|
|
1602
|
+
head.push(row);
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
return [...head, ...tail];
|
|
1606
|
+
}
|
|
1607
|
+
var CATALOG_ROUTING_RULES = [
|
|
1608
|
+
{ pattern: /@ohos\./i, catalog: "harmonyos-references", multiplier: 1.5 },
|
|
1609
|
+
{ pattern: /@[A-Z][a-zA-Z]+/, catalog: "harmonyos-guides", multiplier: 1.35 },
|
|
1610
|
+
{
|
|
1611
|
+
pattern: /(如何|怎么|怎样|步骤)/,
|
|
1612
|
+
catalog: "harmonyos-guides",
|
|
1613
|
+
multiplier: 1.45
|
|
1614
|
+
},
|
|
1615
|
+
{
|
|
1616
|
+
pattern: /(生命周期|原理|概述|什么是|模型|介绍)/,
|
|
1617
|
+
catalog: "harmonyos-guides",
|
|
1618
|
+
multiplier: 1.35
|
|
1619
|
+
},
|
|
1620
|
+
{
|
|
1621
|
+
pattern: /(报错|失败|错误|异常|排查)/,
|
|
1622
|
+
catalog: "harmonyos-guides",
|
|
1623
|
+
multiplier: 1.25
|
|
1624
|
+
},
|
|
1625
|
+
{
|
|
1626
|
+
pattern: /(报错|失败|FAQ)/i,
|
|
1627
|
+
catalog: "harmonyos-faqs",
|
|
1628
|
+
multiplier: 1.2
|
|
1629
|
+
},
|
|
1630
|
+
{
|
|
1631
|
+
pattern: /(构建|签名|打包|发布|上架|应用市场|hvigor|ohpm)/i,
|
|
1632
|
+
catalog: "harmonyos-guides",
|
|
1633
|
+
multiplier: 1.3
|
|
1634
|
+
},
|
|
1635
|
+
{
|
|
1636
|
+
pattern: /(行为变更|changelog|升级|适配|版本说明)/i,
|
|
1637
|
+
catalog: "harmonyos-releases",
|
|
1638
|
+
multiplier: 1.3
|
|
1639
|
+
},
|
|
1640
|
+
{
|
|
1641
|
+
pattern: /(路由|弹窗|手势|页面|权限|装饰器)/,
|
|
1642
|
+
catalog: "harmonyos-guides",
|
|
1643
|
+
multiplier: 1.25,
|
|
1644
|
+
skipForPureApiSymbol: true
|
|
1645
|
+
}
|
|
1646
|
+
];
|
|
1647
|
+
function applyCatalogWeight(boosts, catalog, multiplier) {
|
|
1648
|
+
const catalogId = CATALOG_NAME_TO_ID[catalog];
|
|
1649
|
+
boosts.set(catalogId, (boosts.get(catalogId) ?? 1) * multiplier);
|
|
1650
|
+
}
|
|
1651
|
+
function applyQueryShapeHeuristics(rawQuery, boosts) {
|
|
1652
|
+
const trimmed = rawQuery.trim();
|
|
1653
|
+
const hasChinese = /[\u4e00-\u9fff]/.test(trimmed);
|
|
1654
|
+
const hasPascalApi = /\b[A-Z][a-zA-Z0-9]{2,}\b/.test(trimmed);
|
|
1655
|
+
const hasCamelApi = /\b[a-z][a-zA-Z0-9]{3,}\b/.test(trimmed);
|
|
1656
|
+
if (hasChinese && (hasPascalApi || hasCamelApi)) {
|
|
1657
|
+
applyCatalogWeight(boosts, "harmonyos-guides", 1.45);
|
|
1658
|
+
applyCatalogWeight(boosts, "harmonyos-references", 1.35);
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
if (hasPascalApi || hasCamelApi) {
|
|
1662
|
+
applyCatalogWeight(boosts, "harmonyos-references", 1.55);
|
|
1663
|
+
}
|
|
1664
|
+
if (/\b[A-Z][a-zA-Z]*(Gesture|Dialog|Sheet|Transition|Recognizer)\b/.test(
|
|
1665
|
+
trimmed
|
|
1666
|
+
)) {
|
|
1667
|
+
applyCatalogWeight(boosts, "harmonyos-references", 1.75);
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
function inferCatalogBoosts(rawQuery) {
|
|
1671
|
+
const boosts = /* @__PURE__ */ new Map();
|
|
1672
|
+
const trimmed = rawQuery.trim();
|
|
1673
|
+
if (!trimmed) {
|
|
1674
|
+
return boosts;
|
|
1675
|
+
}
|
|
1676
|
+
const pureApiSymbol = isPureApiSymbolQuery(trimmed);
|
|
1677
|
+
applyQueryShapeHeuristics(trimmed, boosts);
|
|
1678
|
+
applyNamedConceptBoosts(trimmed, boosts);
|
|
1679
|
+
for (const rule of CATALOG_ROUTING_RULES) {
|
|
1680
|
+
if (!rule.pattern.test(trimmed)) {
|
|
1681
|
+
continue;
|
|
1682
|
+
}
|
|
1683
|
+
if (rule.skipForPureApiSymbol && pureApiSymbol) {
|
|
1684
|
+
continue;
|
|
1685
|
+
}
|
|
1686
|
+
applyCatalogWeight(boosts, rule.catalog, rule.multiplier);
|
|
1687
|
+
}
|
|
1688
|
+
return boosts;
|
|
1689
|
+
}
|
|
1690
|
+
function inferSearchCatalog(rawQuery) {
|
|
1691
|
+
return inferNamedSearchCatalog(rawQuery);
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
// src/doc-index/sqlite-backend.ts
|
|
1695
|
+
import { readFile, stat, writeFile } from "fs/promises";
|
|
1696
|
+
|
|
1697
|
+
// src/doc-index/sqlite-snippet.ts
|
|
1698
|
+
var WORD_BOUNDARY_CHARS = [
|
|
1699
|
+
" ",
|
|
1700
|
+
"\u3002",
|
|
1701
|
+
"\uFF0C",
|
|
1702
|
+
"\uFF1B",
|
|
1703
|
+
"\u3001",
|
|
1704
|
+
".",
|
|
1705
|
+
"!",
|
|
1706
|
+
"?"
|
|
1707
|
+
];
|
|
1708
|
+
function findLastWordBoundary(text, minIndex) {
|
|
1709
|
+
let best = -1;
|
|
1710
|
+
for (const ch of WORD_BOUNDARY_CHARS) {
|
|
1711
|
+
const idx = text.lastIndexOf(ch);
|
|
1712
|
+
if (idx > best) {
|
|
1713
|
+
best = idx;
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
return best >= minIndex ? best : -1;
|
|
1717
|
+
}
|
|
1718
|
+
function extractSearchSnippet(content, query, opts = {}) {
|
|
1719
|
+
const {
|
|
1720
|
+
maxLen = DOC_SNIPPET_MAX_CHARS,
|
|
1721
|
+
contextChars = DOC_SNIPPET_CONTEXT_CHARS,
|
|
1722
|
+
excerptTruncated = false
|
|
1723
|
+
} = opts;
|
|
1724
|
+
const normalized = content.replace(/\s+/g, " ").trim();
|
|
1725
|
+
if (!normalized) {
|
|
1726
|
+
return "";
|
|
1727
|
+
}
|
|
1728
|
+
if (normalized.length <= maxLen) {
|
|
1729
|
+
return excerptTruncated ? `${normalized}...` : normalized;
|
|
1730
|
+
}
|
|
1731
|
+
const tokens = query.split(/\s+/).map((token) => token.trim()).filter(Boolean);
|
|
1732
|
+
let bestIndex = 0;
|
|
1733
|
+
for (const token of tokens) {
|
|
1734
|
+
const idx = normalized.toLowerCase().indexOf(token.toLowerCase());
|
|
1735
|
+
if (idx >= 0) {
|
|
1736
|
+
bestIndex = idx;
|
|
1737
|
+
break;
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
const start = Math.max(0, bestIndex - contextChars);
|
|
1741
|
+
let end = Math.min(normalized.length, start + maxLen);
|
|
1742
|
+
const window = normalized.slice(start, end);
|
|
1743
|
+
const lastBreak = findLastWordBoundary(window, maxLen - 25);
|
|
1744
|
+
if (lastBreak >= 0) {
|
|
1745
|
+
end = start + lastBreak;
|
|
1746
|
+
}
|
|
1747
|
+
const snippet = normalized.slice(start, end).trim();
|
|
1748
|
+
const prefix = start > 0 ? "..." : "";
|
|
1749
|
+
const suffix = end < normalized.length || excerptTruncated ? "..." : "";
|
|
1750
|
+
return `${prefix}${snippet}${suffix}`;
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
// src/doc-index/sqlite-fts-search.ts
|
|
1754
|
+
var FTS_SEARCH_SQL = `
|
|
1755
|
+
SELECT d.document_id, d.doc_title, s.section_title, s.lead_text, s.excerpt_truncated,
|
|
1756
|
+
d.catalog_id, bm25(segments_fts) AS bm25
|
|
1757
|
+
FROM segments_fts
|
|
1758
|
+
JOIN segments s ON s.id = segments_fts.rowid
|
|
1759
|
+
JOIN documents d ON d.id = s.doc_id
|
|
1760
|
+
WHERE segments_fts MATCH ?
|
|
1761
|
+
ORDER BY bm25(segments_fts)
|
|
1762
|
+
LIMIT ?
|
|
1763
|
+
`;
|
|
1764
|
+
var FTS_SEARCH_CATALOG_SQL = `
|
|
1765
|
+
SELECT d.document_id, d.doc_title, s.section_title, s.lead_text, s.excerpt_truncated,
|
|
1766
|
+
d.catalog_id, bm25(segments_fts) AS bm25
|
|
1767
|
+
FROM segments_fts
|
|
1768
|
+
JOIN segments s ON s.id = segments_fts.rowid
|
|
1769
|
+
JOIN documents d ON d.id = s.doc_id
|
|
1770
|
+
WHERE segments_fts MATCH ? AND d.catalog_id = ?
|
|
1771
|
+
ORDER BY bm25(segments_fts)
|
|
1772
|
+
LIMIT ?
|
|
1773
|
+
`;
|
|
1774
|
+
var MANAGER_API_RERANK_BOOST = 3.5;
|
|
1775
|
+
var MANAGER_API_REFERENCE_EXTRA = 1.4;
|
|
1776
|
+
var TITLE_EXACT_MATCH_BOOST = 4;
|
|
1777
|
+
var TITLE_PREFIX_MATCH_BOOST = 1.8;
|
|
1778
|
+
var TITLE_TERMS_MATCH_BOOST = 1.35;
|
|
1779
|
+
var BROAD_SINGLE_TERM_EXACT_TITLE_BOOST = 3.5;
|
|
1780
|
+
var BROAD_SINGLE_TERM_PREFIX_TITLE_BOOST = 1.8;
|
|
1781
|
+
var MIN_RERANK_FETCH_LIMIT = 120;
|
|
1782
|
+
var TITLE_PREFIX_MIN_CHARS = 6;
|
|
1783
|
+
function normalizeForRerank(text) {
|
|
1784
|
+
return text.toLowerCase().replace(/[^\p{L}\p{N}@.]+/gu, "");
|
|
1785
|
+
}
|
|
1786
|
+
function splitRerankTerms(rawQuery) {
|
|
1787
|
+
return rawQuery.split(/[^\p{L}\p{N}@.]+/u).map(normalizeForRerank).filter((term) => term.length >= 2);
|
|
1788
|
+
}
|
|
1789
|
+
function containsAllTerms(field, terms) {
|
|
1790
|
+
return terms.length > 1 && terms.every((term) => field.includes(term));
|
|
1791
|
+
}
|
|
1792
|
+
function isBroadSingleAsciiTerm(rawQuery) {
|
|
1793
|
+
return /^[a-z0-9]{1,4}$/.test(rawQuery.trim().toLowerCase());
|
|
1794
|
+
}
|
|
1795
|
+
function broadSingleTermTitleBoost(title, term) {
|
|
1796
|
+
if (title === term) {
|
|
1797
|
+
return BROAD_SINGLE_TERM_EXACT_TITLE_BOOST;
|
|
1798
|
+
}
|
|
1799
|
+
if (title.startsWith(term) || title.includes(`@ohos.${term}`)) {
|
|
1800
|
+
return BROAD_SINGLE_TERM_PREFIX_TITLE_BOOST;
|
|
1801
|
+
}
|
|
1802
|
+
return 1;
|
|
1803
|
+
}
|
|
1804
|
+
function titleMatchBoost(row, rawQuery) {
|
|
1805
|
+
const normalizedQuery = normalizeForRerank(rawQuery);
|
|
1806
|
+
if (normalizedQuery.length < 2) {
|
|
1807
|
+
return 1;
|
|
1808
|
+
}
|
|
1809
|
+
const title = normalizeForRerank(row.doc_title);
|
|
1810
|
+
if (isBroadSingleAsciiTerm(rawQuery)) {
|
|
1811
|
+
return broadSingleTermTitleBoost(title, normalizedQuery);
|
|
1812
|
+
}
|
|
1813
|
+
if (title.includes(normalizedQuery)) {
|
|
1814
|
+
return TITLE_EXACT_MATCH_BOOST;
|
|
1815
|
+
}
|
|
1816
|
+
if (normalizedQuery.length >= TITLE_PREFIX_MIN_CHARS && title.includes(normalizedQuery.slice(0, TITLE_PREFIX_MIN_CHARS))) {
|
|
1817
|
+
return TITLE_PREFIX_MATCH_BOOST;
|
|
1818
|
+
}
|
|
1819
|
+
const terms = splitRerankTerms(rawQuery);
|
|
1820
|
+
if (containsAllTerms(title, terms)) {
|
|
1821
|
+
return TITLE_TERMS_MATCH_BOOST;
|
|
1822
|
+
}
|
|
1823
|
+
return 1;
|
|
1824
|
+
}
|
|
1825
|
+
function applyTitleBoost(score, boost) {
|
|
1826
|
+
if (boost <= 1) {
|
|
1827
|
+
return score;
|
|
1828
|
+
}
|
|
1829
|
+
return score < 0 ? score * boost : score / boost;
|
|
1830
|
+
}
|
|
1831
|
+
function effectiveBm25Score(row, boosts, titleQuery, managerCompound) {
|
|
1832
|
+
let score = row.bm25 / (boosts.get(row.catalog_id) ?? 1);
|
|
1833
|
+
score = applyTitleBoost(score, titleMatchBoost(row, titleQuery));
|
|
1834
|
+
if (managerCompound && rowMatchesManagerApiModule(
|
|
1835
|
+
row.doc_title,
|
|
1836
|
+
row.section_title,
|
|
1837
|
+
managerCompound
|
|
1838
|
+
)) {
|
|
1839
|
+
score /= MANAGER_API_RERANK_BOOST;
|
|
1840
|
+
if (row.catalog_id === CATALOG_NAME_TO_ID["harmonyos-references"]) {
|
|
1841
|
+
score /= MANAGER_API_REFERENCE_EXTRA;
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
return score;
|
|
1845
|
+
}
|
|
1846
|
+
function minScoreRow(rows, boosts, titleQuery, managerCompound) {
|
|
1847
|
+
return rows.reduce(
|
|
1848
|
+
(best, row) => effectiveBm25Score(row, boosts, titleQuery, managerCompound) < effectiveBm25Score(best, boosts, titleQuery, managerCompound) ? row : best
|
|
1849
|
+
);
|
|
1850
|
+
}
|
|
1851
|
+
function segmentMatchesApiSymbols(row, apiSymbols) {
|
|
1852
|
+
return apiSymbols.some(
|
|
1853
|
+
(symbol) => row.section_title.includes(symbol) || row.doc_title.includes(symbol)
|
|
1854
|
+
);
|
|
1855
|
+
}
|
|
1856
|
+
function pickBestSegmentForDocument(segments, apiSymbols, boosts, titleQuery, managerCompound) {
|
|
1857
|
+
if (managerCompound) {
|
|
1858
|
+
const managerMatches = segments.filter(
|
|
1859
|
+
(row) => rowMatchesManagerApiModule(
|
|
1860
|
+
row.doc_title,
|
|
1861
|
+
row.section_title,
|
|
1862
|
+
managerCompound
|
|
1863
|
+
)
|
|
1864
|
+
);
|
|
1865
|
+
if (managerMatches.length > 0) {
|
|
1866
|
+
return minScoreRow(managerMatches, boosts, titleQuery, managerCompound);
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
if (apiSymbols.length > 0) {
|
|
1870
|
+
const titleMatches = segments.filter(
|
|
1871
|
+
(row) => segmentMatchesApiSymbols(row, apiSymbols)
|
|
1872
|
+
);
|
|
1873
|
+
if (titleMatches.length > 0) {
|
|
1874
|
+
return minScoreRow(titleMatches, boosts, titleQuery, managerCompound);
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
return minScoreRow(segments, boosts, titleQuery, managerCompound);
|
|
1878
|
+
}
|
|
1879
|
+
function finalizeSearchRows(rows, rawQuery, titleQuery, limit, deprioritizeTailCatalogs) {
|
|
1880
|
+
const boosts = inferCatalogBoosts(rawQuery);
|
|
1881
|
+
const apiSymbols = extractQueryApiSymbolTokens(rawQuery);
|
|
1882
|
+
const managerSplit = getManagerSplitApiCompound(rawQuery);
|
|
1883
|
+
const managerCompound = managerSplit?.camelCase;
|
|
1884
|
+
const byDocument = /* @__PURE__ */ new Map();
|
|
1885
|
+
for (const row of rows) {
|
|
1886
|
+
const segments = byDocument.get(row.document_id) ?? [];
|
|
1887
|
+
segments.push(row);
|
|
1888
|
+
byDocument.set(row.document_id, segments);
|
|
1889
|
+
}
|
|
1890
|
+
const representatives = [];
|
|
1891
|
+
for (const segments of byDocument.values()) {
|
|
1892
|
+
representatives.push(
|
|
1893
|
+
pickBestSegmentForDocument(
|
|
1894
|
+
segments,
|
|
1895
|
+
apiSymbols,
|
|
1896
|
+
boosts,
|
|
1897
|
+
titleQuery,
|
|
1898
|
+
managerCompound
|
|
1899
|
+
)
|
|
1900
|
+
);
|
|
1901
|
+
}
|
|
1902
|
+
const sorted = representatives.sort(
|
|
1903
|
+
(left, right) => effectiveBm25Score(left, boosts, titleQuery, managerCompound) - effectiveBm25Score(right, boosts, titleQuery, managerCompound)
|
|
1904
|
+
);
|
|
1905
|
+
const ordered = deprioritizeTailCatalogs ? orderRowsWithTailCatalogsLast(sorted) : sorted;
|
|
1906
|
+
return ordered.slice(0, limit);
|
|
1907
|
+
}
|
|
1908
|
+
function runFtsSearch(reader, catalog, limit, match, query, rawQuery) {
|
|
1909
|
+
const catalogId = catalog ? CATALOG_NAME_TO_ID[catalog] : void 0;
|
|
1910
|
+
const fetchLimit = Math.max(
|
|
1911
|
+
limit,
|
|
1912
|
+
limit * CATALOG_RERANK_POOL_FACTOR,
|
|
1913
|
+
MIN_RERANK_FETCH_LIMIT
|
|
1914
|
+
);
|
|
1915
|
+
const rows = catalogId === void 0 ? reader.all(FTS_SEARCH_SQL, match, fetchLimit) : reader.all(
|
|
1916
|
+
FTS_SEARCH_CATALOG_SQL,
|
|
1917
|
+
match,
|
|
1918
|
+
catalogId,
|
|
1919
|
+
fetchLimit
|
|
1920
|
+
);
|
|
1921
|
+
const ranked = catalogId === void 0 ? finalizeSearchRows(rows, rawQuery, query, limit, true) : finalizeSearchRows(rows, rawQuery, query, limit, false);
|
|
1922
|
+
return ranked.map((row) => ({
|
|
1923
|
+
title: row.doc_title,
|
|
1924
|
+
documentId: row.document_id,
|
|
1925
|
+
sectionTitle: row.section_title || void 0,
|
|
1926
|
+
snippet: extractSearchSnippet(row.lead_text, query, {
|
|
1927
|
+
excerptTruncated: Boolean(row.excerpt_truncated)
|
|
1928
|
+
})
|
|
1929
|
+
}));
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
// src/doc-index/sqlite-schema.ts
|
|
1933
|
+
var SQLITE_INDEX_SCHEMA_SQL = `
|
|
1934
|
+
CREATE TABLE documents (
|
|
1935
|
+
id INTEGER PRIMARY KEY,
|
|
1936
|
+
document_id TEXT NOT NULL UNIQUE,
|
|
1937
|
+
catalog_id INTEGER NOT NULL,
|
|
1938
|
+
doc_title TEXT NOT NULL
|
|
1939
|
+
);
|
|
1940
|
+
|
|
1941
|
+
CREATE TABLE segments (
|
|
1942
|
+
id INTEGER PRIMARY KEY,
|
|
1943
|
+
doc_id INTEGER NOT NULL REFERENCES documents(id),
|
|
1944
|
+
section_title TEXT NOT NULL DEFAULT '',
|
|
1945
|
+
lead_text TEXT NOT NULL DEFAULT '',
|
|
1946
|
+
search_text TEXT NOT NULL,
|
|
1947
|
+
excerpt_truncated INTEGER NOT NULL DEFAULT 0
|
|
1948
|
+
);
|
|
1949
|
+
|
|
1950
|
+
CREATE VIRTUAL TABLE segments_fts USING fts5(
|
|
1951
|
+
search_text,
|
|
1952
|
+
content='segments',
|
|
1953
|
+
content_rowid='id',
|
|
1954
|
+
tokenize='unicode61'
|
|
1955
|
+
);
|
|
1956
|
+
|
|
1957
|
+
CREATE TRIGGER segments_ai AFTER INSERT ON segments BEGIN
|
|
1958
|
+
INSERT INTO segments_fts(rowid, search_text) VALUES (new.id, new.search_text);
|
|
1959
|
+
END;
|
|
1960
|
+
|
|
1961
|
+
CREATE TRIGGER segments_ad AFTER DELETE ON segments BEGIN
|
|
1962
|
+
INSERT INTO segments_fts(segments_fts, rowid, search_text) VALUES('delete', old.id, old.search_text);
|
|
1963
|
+
END;
|
|
1964
|
+
|
|
1965
|
+
CREATE TRIGGER segments_au AFTER UPDATE ON segments BEGIN
|
|
1966
|
+
INSERT INTO segments_fts(segments_fts, rowid, search_text) VALUES('delete', old.id, old.search_text);
|
|
1967
|
+
INSERT INTO segments_fts(rowid, search_text) VALUES (new.id, new.search_text);
|
|
1968
|
+
END;
|
|
1969
|
+
|
|
1970
|
+
CREATE INDEX segments_doc_id_idx ON segments(doc_id);
|
|
1971
|
+
`;
|
|
1972
|
+
|
|
1973
|
+
// src/doc-index/sqlite-backend.ts
|
|
1974
|
+
var sqliteModulePromise = null;
|
|
1975
|
+
var cachedDb = null;
|
|
1976
|
+
var openChain = Promise.resolve();
|
|
1977
|
+
async function getSqliteModule() {
|
|
1978
|
+
if (!sqliteModulePromise) {
|
|
1979
|
+
sqliteModulePromise = (async () => {
|
|
1980
|
+
const init = (await import("@sqlite.org/sqlite-wasm")).default;
|
|
1981
|
+
return init();
|
|
1982
|
+
})();
|
|
1983
|
+
}
|
|
1984
|
+
return sqliteModulePromise;
|
|
1985
|
+
}
|
|
1986
|
+
function createWasmReader(db) {
|
|
1987
|
+
return {
|
|
1988
|
+
all(sql, ...params) {
|
|
1989
|
+
const stmt = db.prepare(sql);
|
|
1990
|
+
if (params.length > 0) {
|
|
1991
|
+
stmt.bind(params);
|
|
1992
|
+
}
|
|
1993
|
+
const rows = [];
|
|
1994
|
+
while (stmt.step()) {
|
|
1995
|
+
rows.push(stmt.get({}));
|
|
1996
|
+
}
|
|
1997
|
+
stmt.finalize();
|
|
1998
|
+
return rows;
|
|
1999
|
+
}
|
|
2000
|
+
};
|
|
2001
|
+
}
|
|
2002
|
+
async function openReadonlyDbInner(dbPath) {
|
|
2003
|
+
const fileStat = await stat(dbPath);
|
|
2004
|
+
if (cachedDb && cachedDb.dbPath === dbPath && cachedDb.mtimeMs === fileStat.mtimeMs) {
|
|
2005
|
+
return cachedDb.db;
|
|
2006
|
+
}
|
|
2007
|
+
cachedDb?.db.close();
|
|
2008
|
+
const sqlite3 = await getSqliteModule();
|
|
2009
|
+
const capi = sqlite3.capi;
|
|
2010
|
+
const wasm = sqlite3.wasm;
|
|
2011
|
+
const bytes = new Uint8Array(await readFile(dbPath));
|
|
2012
|
+
const ptr = wasm.allocFromTypedArray(bytes);
|
|
2013
|
+
let db;
|
|
2014
|
+
let stored = false;
|
|
2015
|
+
try {
|
|
2016
|
+
db = new sqlite3.oo1.DB(":memory:");
|
|
2017
|
+
const flags = capi.SQLITE_DESERIALIZE_READONLY | capi.SQLITE_DESERIALIZE_RESIZEABLE | capi.SQLITE_DESERIALIZE_FREEONCLOSE;
|
|
2018
|
+
const rc = capi.sqlite3_deserialize(
|
|
2019
|
+
db.pointer,
|
|
2020
|
+
"main",
|
|
2021
|
+
ptr,
|
|
2022
|
+
bytes.byteLength,
|
|
2023
|
+
bytes.byteLength,
|
|
2024
|
+
flags
|
|
2025
|
+
);
|
|
2026
|
+
if (rc !== capi.SQLITE_OK) {
|
|
2027
|
+
throw new Error(
|
|
2028
|
+
`Failed to open readonly database: sqlite3_deserialize returned code ${rc} for: ${dbPath}`
|
|
2029
|
+
);
|
|
2030
|
+
}
|
|
2031
|
+
cachedDb = { dbPath, mtimeMs: fileStat.mtimeMs, db };
|
|
2032
|
+
stored = true;
|
|
2033
|
+
return db;
|
|
2034
|
+
} finally {
|
|
2035
|
+
if (!stored) {
|
|
2036
|
+
db?.close();
|
|
2037
|
+
wasm.dealloc(ptr);
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
async function openReadonlyDb(dbPath) {
|
|
2042
|
+
let release;
|
|
2043
|
+
const previous = openChain;
|
|
2044
|
+
openChain = new Promise((resolve3) => {
|
|
2045
|
+
release = resolve3;
|
|
2046
|
+
});
|
|
2047
|
+
await previous;
|
|
2048
|
+
try {
|
|
2049
|
+
return await openReadonlyDbInner(dbPath);
|
|
2050
|
+
} finally {
|
|
2051
|
+
release();
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
function resetWasmCache() {
|
|
2055
|
+
cachedDb?.db.close();
|
|
2056
|
+
cachedDb = null;
|
|
2057
|
+
}
|
|
2058
|
+
async function insertIndexedBatch(db, insertSegment, getOrCreateDocId, batch) {
|
|
2059
|
+
db.exec("BEGIN");
|
|
2060
|
+
for (const row of batch) {
|
|
2061
|
+
const docId = getOrCreateDocId(row.source);
|
|
2062
|
+
insertSegment.bind([
|
|
2063
|
+
docId,
|
|
2064
|
+
row.source.sectionTitle,
|
|
2065
|
+
row.source.leadText,
|
|
2066
|
+
row.searchText,
|
|
2067
|
+
row.source.excerptTruncated ? 1 : 0
|
|
2068
|
+
]);
|
|
2069
|
+
insertSegment.step();
|
|
2070
|
+
insertSegment.reset();
|
|
2071
|
+
}
|
|
2072
|
+
db.exec("COMMIT");
|
|
2073
|
+
}
|
|
2074
|
+
function createDocIdResolver(db, cache, insertDocument) {
|
|
2075
|
+
return (source) => {
|
|
2076
|
+
const cached = cache.get(source.documentId);
|
|
2077
|
+
if (cached !== void 0) {
|
|
2078
|
+
return cached;
|
|
2079
|
+
}
|
|
2080
|
+
const existing = db.selectValue(
|
|
2081
|
+
"SELECT id FROM documents WHERE document_id = ?",
|
|
2082
|
+
[source.documentId]
|
|
2083
|
+
);
|
|
2084
|
+
if (existing !== void 0 && existing !== null) {
|
|
2085
|
+
const docId2 = Number(existing);
|
|
2086
|
+
cache.set(source.documentId, docId2);
|
|
2087
|
+
return docId2;
|
|
2088
|
+
}
|
|
2089
|
+
insertDocument.bind([source.documentId, source.catalogId, source.docTitle]);
|
|
2090
|
+
insertDocument.step();
|
|
2091
|
+
insertDocument.reset();
|
|
2092
|
+
const docId = Number(db.selectValue("SELECT last_insert_rowid()"));
|
|
2093
|
+
cache.set(source.documentId, docId);
|
|
2094
|
+
return docId;
|
|
2095
|
+
};
|
|
2096
|
+
}
|
|
2097
|
+
async function buildWasmIndex(dbPath, sources, onProgress) {
|
|
2098
|
+
await getJieba();
|
|
2099
|
+
const sqlite3 = await getSqliteModule();
|
|
2100
|
+
const db = new sqlite3.oo1.DB(":memory:", "c");
|
|
2101
|
+
db.exec(SQLITE_INDEX_SCHEMA_SQL);
|
|
2102
|
+
const docIdCache = /* @__PURE__ */ new Map();
|
|
2103
|
+
const insertDocument = db.prepare(
|
|
2104
|
+
"INSERT INTO documents(document_id, catalog_id, doc_title) VALUES (?, ?, ?)"
|
|
2105
|
+
);
|
|
2106
|
+
const insertSegment = db.prepare(
|
|
2107
|
+
"INSERT INTO segments(doc_id, section_title, lead_text, search_text, excerpt_truncated) VALUES (?, ?, ?, ?, ?)"
|
|
2108
|
+
);
|
|
2109
|
+
const getOrCreateDocId = createDocIdResolver(db, docIdCache, insertDocument);
|
|
2110
|
+
const total = sources.length;
|
|
2111
|
+
for (let i = 0; i < total; i += INSERT_BATCH_SIZE) {
|
|
2112
|
+
const batch = sources.slice(i, i + INSERT_BATCH_SIZE);
|
|
2113
|
+
const indexedBatch = await Promise.all(
|
|
2114
|
+
batch.map(async (source) => ({
|
|
2115
|
+
source,
|
|
2116
|
+
searchText: await buildDocumentSearchText(source)
|
|
2117
|
+
}))
|
|
2118
|
+
);
|
|
2119
|
+
await insertIndexedBatch(db, insertSegment, getOrCreateDocId, indexedBatch);
|
|
2120
|
+
await onProgress?.(Math.min(i + batch.length, total), total);
|
|
2121
|
+
}
|
|
2122
|
+
db.exec("ANALYZE");
|
|
2123
|
+
const exported = sqlite3.capi.sqlite3_js_db_export(db);
|
|
2124
|
+
await writeFile(dbPath, exported);
|
|
2125
|
+
db.close();
|
|
2126
|
+
resetWasmCache();
|
|
2127
|
+
}
|
|
2128
|
+
async function searchWasmIndex(dbPath, catalog, limit, match, query, rawQuery) {
|
|
2129
|
+
const db = await openReadonlyDb(dbPath);
|
|
2130
|
+
return runFtsSearch(
|
|
2131
|
+
createWasmReader(db),
|
|
2132
|
+
catalog,
|
|
2133
|
+
limit,
|
|
2134
|
+
match,
|
|
2135
|
+
query,
|
|
2136
|
+
rawQuery
|
|
2137
|
+
);
|
|
2138
|
+
}
|
|
2139
|
+
async function createSqliteBackend() {
|
|
2140
|
+
await getSqliteModule();
|
|
2141
|
+
return {
|
|
2142
|
+
resetCache: resetWasmCache,
|
|
2143
|
+
buildSearchIndex: buildWasmIndex,
|
|
2144
|
+
searchIndex: (_keywords, catalog, limit, dbPath, match, query, rawQuery) => searchWasmIndex(dbPath, catalog, limit, match, query, rawQuery)
|
|
2145
|
+
};
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
// src/doc-index/sqlite-backend-cache.ts
|
|
2149
|
+
var backendPromise = null;
|
|
2150
|
+
var activeBackend = null;
|
|
2151
|
+
async function getSqliteBackend() {
|
|
2152
|
+
if (!backendPromise) {
|
|
2153
|
+
backendPromise = createSqliteBackend().then((backend) => {
|
|
2154
|
+
activeBackend = backend;
|
|
2155
|
+
return backend;
|
|
2156
|
+
});
|
|
2157
|
+
}
|
|
2158
|
+
return backendPromise;
|
|
2159
|
+
}
|
|
2160
|
+
function resetSqliteBackendCache() {
|
|
2161
|
+
activeBackend?.resetCache();
|
|
2162
|
+
backendPromise = null;
|
|
2163
|
+
activeBackend = null;
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
// src/doc-index/sqlite-index.ts
|
|
2167
|
+
async function runSearch(keywords, catalog, limit, match, query, rawQuery, dbPath) {
|
|
2168
|
+
const backend = await getSqliteBackend();
|
|
2169
|
+
const resolvedDbPath = dbPath ?? getSearchDbFile();
|
|
2170
|
+
return backend.searchIndex(
|
|
2171
|
+
keywords,
|
|
2172
|
+
catalog,
|
|
2173
|
+
limit,
|
|
2174
|
+
resolvedDbPath,
|
|
2175
|
+
match,
|
|
2176
|
+
query,
|
|
2177
|
+
rawQuery
|
|
2178
|
+
);
|
|
2179
|
+
}
|
|
2180
|
+
function resetSearchDbCache() {
|
|
2181
|
+
resetSqliteBackendCache();
|
|
2182
|
+
}
|
|
2183
|
+
function mergeSearchResults(primary, secondary, limit) {
|
|
2184
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2185
|
+
const merged = [];
|
|
2186
|
+
for (const result of [...primary, ...secondary]) {
|
|
2187
|
+
if (seen.has(result.documentId)) {
|
|
2188
|
+
continue;
|
|
2189
|
+
}
|
|
2190
|
+
seen.add(result.documentId);
|
|
2191
|
+
merged.push(result);
|
|
2192
|
+
if (merged.length >= limit) {
|
|
2193
|
+
break;
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
return merged;
|
|
2197
|
+
}
|
|
2198
|
+
function runPreparedMatch(keywords, catalog, limit, prepared, match, dbPath) {
|
|
2199
|
+
return runSearch(
|
|
2200
|
+
keywords,
|
|
2201
|
+
catalog,
|
|
2202
|
+
limit,
|
|
2203
|
+
match,
|
|
2204
|
+
prepared.expandedQuery,
|
|
2205
|
+
prepared.rawQuery,
|
|
2206
|
+
dbPath
|
|
2207
|
+
);
|
|
2208
|
+
}
|
|
2209
|
+
async function searchWithAndFallback(keywords, catalog, limit, prepared, dbPath) {
|
|
2210
|
+
const andResults = await runPreparedMatch(
|
|
2211
|
+
keywords,
|
|
2212
|
+
catalog,
|
|
2213
|
+
limit,
|
|
2214
|
+
prepared,
|
|
2215
|
+
buildFtsMatch(prepared.tokens, "AND"),
|
|
2216
|
+
dbPath
|
|
2217
|
+
);
|
|
2218
|
+
if (andResults.length >= limit) {
|
|
2219
|
+
return andResults;
|
|
2220
|
+
}
|
|
2221
|
+
const orResults = await runPreparedMatch(
|
|
2222
|
+
keywords,
|
|
2223
|
+
catalog,
|
|
2224
|
+
limit,
|
|
2225
|
+
prepared,
|
|
2226
|
+
buildFtsMatch(prepared.tokens, "OR"),
|
|
2227
|
+
dbPath
|
|
2228
|
+
);
|
|
2229
|
+
return mergeSearchResults(andResults, orResults, limit);
|
|
2230
|
+
}
|
|
2231
|
+
async function searchWithPrepared(keywords, catalog, limit, prepared, dbPath) {
|
|
2232
|
+
if (prepared.ftsMatch) {
|
|
2233
|
+
return runPreparedMatch(
|
|
2234
|
+
keywords,
|
|
2235
|
+
catalog,
|
|
2236
|
+
limit,
|
|
2237
|
+
prepared,
|
|
2238
|
+
prepared.ftsMatch,
|
|
2239
|
+
dbPath
|
|
2240
|
+
);
|
|
2241
|
+
}
|
|
2242
|
+
if (!prepared.preferAnd) {
|
|
2243
|
+
return runPreparedMatch(
|
|
2244
|
+
keywords,
|
|
2245
|
+
catalog,
|
|
2246
|
+
limit,
|
|
2247
|
+
prepared,
|
|
2248
|
+
buildFtsMatch(prepared.tokens, "OR"),
|
|
2249
|
+
dbPath
|
|
2250
|
+
);
|
|
2251
|
+
}
|
|
2252
|
+
return searchWithAndFallback(keywords, catalog, limit, prepared, dbPath);
|
|
2253
|
+
}
|
|
2254
|
+
async function searchWithReferencesGlobalFallback(keywords, limit, prepared, dbPath) {
|
|
2255
|
+
const primary = await searchWithPrepared(
|
|
2256
|
+
keywords,
|
|
2257
|
+
"harmonyos-references",
|
|
2258
|
+
limit,
|
|
2259
|
+
prepared,
|
|
2260
|
+
dbPath
|
|
2261
|
+
);
|
|
2262
|
+
if (primary.length >= limit) {
|
|
2263
|
+
return primary;
|
|
2264
|
+
}
|
|
2265
|
+
const secondary = await searchWithPrepared(
|
|
2266
|
+
keywords,
|
|
2267
|
+
void 0,
|
|
2268
|
+
limit,
|
|
2269
|
+
prepared,
|
|
2270
|
+
dbPath
|
|
2271
|
+
);
|
|
2272
|
+
return mergeSearchResults(primary, secondary, limit);
|
|
2273
|
+
}
|
|
2274
|
+
function applyTailCatalogOrderWhenGlobal(results, userCatalog) {
|
|
2275
|
+
if (userCatalog !== void 0) {
|
|
2276
|
+
return results;
|
|
2277
|
+
}
|
|
2278
|
+
return orderLocalResultsWithTailCatalogsLast(results);
|
|
2279
|
+
}
|
|
2280
|
+
async function searchSqliteIndex(keywords, catalog, limit = 10, dbPath) {
|
|
2281
|
+
const prepared = await prepareSearchQuery(keywords);
|
|
2282
|
+
if (shouldMergeReferencesWithGlobalFallback(prepared.rawQuery, catalog)) {
|
|
2283
|
+
const merged = await searchWithReferencesGlobalFallback(
|
|
2284
|
+
keywords,
|
|
2285
|
+
limit,
|
|
2286
|
+
prepared,
|
|
2287
|
+
dbPath
|
|
2288
|
+
);
|
|
2289
|
+
return applyTailCatalogOrderWhenGlobal(merged, catalog);
|
|
2290
|
+
}
|
|
2291
|
+
const effectiveCatalog = catalog ?? inferSearchCatalog(prepared.rawQuery);
|
|
2292
|
+
const results = await searchWithPrepared(
|
|
2293
|
+
keywords,
|
|
2294
|
+
effectiveCatalog,
|
|
2295
|
+
limit,
|
|
2296
|
+
prepared,
|
|
2297
|
+
dbPath
|
|
2298
|
+
);
|
|
2299
|
+
return applyTailCatalogOrderWhenGlobal(results, catalog);
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
// src/init/doc-initializer.ts
|
|
2303
|
+
var DocNotReadyError = class extends Error {
|
|
2304
|
+
constructor(code, message) {
|
|
2305
|
+
super(message);
|
|
2306
|
+
this.code = code;
|
|
2307
|
+
this.name = "DocNotReadyError";
|
|
2308
|
+
}
|
|
2309
|
+
code;
|
|
2310
|
+
};
|
|
2311
|
+
function sleep(ms) {
|
|
2312
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
2313
|
+
}
|
|
2314
|
+
async function attemptBundledInstall(docsZipSha256, spinner, message) {
|
|
2315
|
+
spinner?.start(message);
|
|
2316
|
+
await updateBuildStatus({
|
|
2317
|
+
state: "installing",
|
|
2318
|
+
phase: 1,
|
|
2319
|
+
phaseLabel: "Installing index",
|
|
2320
|
+
message
|
|
2321
|
+
});
|
|
2322
|
+
await installBundledIndex(docsZipSha256);
|
|
2323
|
+
resetSearchDbCache();
|
|
2324
|
+
if (spinner) {
|
|
2325
|
+
spinner.text = "Documentation index installed.";
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
async function runBundledInstallPath(docsZipSha256, spinner) {
|
|
2329
|
+
try {
|
|
2330
|
+
await attemptBundledInstall(
|
|
2331
|
+
docsZipSha256,
|
|
2332
|
+
spinner,
|
|
2333
|
+
"Installing documentation index\u2026"
|
|
2334
|
+
);
|
|
2335
|
+
await finalizeSuccess(spinner);
|
|
2336
|
+
return true;
|
|
2337
|
+
} catch (error) {
|
|
2338
|
+
if (isDocStorageError(error)) {
|
|
2339
|
+
throw error;
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
try {
|
|
2343
|
+
await resetBundledInstallScratch();
|
|
2344
|
+
resetSearchDbCache();
|
|
2345
|
+
await attemptBundledInstall(
|
|
2346
|
+
docsZipSha256,
|
|
2347
|
+
spinner,
|
|
2348
|
+
"Retrying documentation index install\u2026"
|
|
2349
|
+
);
|
|
2350
|
+
await finalizeSuccess(spinner);
|
|
2351
|
+
return true;
|
|
2352
|
+
} catch (error) {
|
|
2353
|
+
if (isDocStorageError(error)) {
|
|
2354
|
+
throw error;
|
|
2355
|
+
}
|
|
2356
|
+
return false;
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
async function finalizeSuccess(spinner) {
|
|
2360
|
+
await updateBuildStatus({
|
|
2361
|
+
state: "done",
|
|
2362
|
+
phase: 3,
|
|
2363
|
+
phaseLabel: "Done",
|
|
2364
|
+
message: "Documentation ready.",
|
|
2365
|
+
error: null
|
|
2366
|
+
});
|
|
2367
|
+
spinner?.succeed("Documentation ready.");
|
|
2368
|
+
}
|
|
2369
|
+
async function finalizeUpToDate(spinner) {
|
|
2370
|
+
const meta = await readBuildMeta();
|
|
2371
|
+
const message = meta ? `Documentation already up to date. Documents: ${meta.segmentCount.toLocaleString()}` : "Documentation already up to date.";
|
|
2372
|
+
spinner?.succeed(message);
|
|
2373
|
+
await updateBuildStatus({
|
|
2374
|
+
state: "done",
|
|
2375
|
+
message,
|
|
2376
|
+
error: null
|
|
2377
|
+
});
|
|
2378
|
+
}
|
|
2379
|
+
async function handleInitError(error, spinner) {
|
|
2380
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2381
|
+
await updateBuildStatus({ state: "error", error: message });
|
|
2382
|
+
spinner?.fail(message);
|
|
2383
|
+
throw error;
|
|
2384
|
+
}
|
|
2385
|
+
async function ensureBuildLockFile() {
|
|
2386
|
+
const indexDir = getIndexDir();
|
|
2387
|
+
await fs9.promises.mkdir(indexDir, { recursive: true });
|
|
2388
|
+
const lockPath = getBuildLockFile();
|
|
2389
|
+
if (!fs9.existsSync(lockPath)) {
|
|
2390
|
+
await fs9.promises.writeFile(lockPath, "", "utf-8");
|
|
2391
|
+
}
|
|
2392
|
+
return lockPath;
|
|
2393
|
+
}
|
|
2394
|
+
async function acquireBuildLock() {
|
|
2395
|
+
const lockPath = await ensureBuildLockFile();
|
|
2396
|
+
return lockfile.lock(lockPath, { stale: 30 * 60 * 1e3 });
|
|
2397
|
+
}
|
|
2398
|
+
async function resolveDocsZipSha256() {
|
|
2399
|
+
const zipPath = findDocsZip();
|
|
2400
|
+
const docsZipSha256 = (zipPath ? await sha256File(zipPath) : null) ?? await getBundledDocsZipSha256();
|
|
2401
|
+
if (!docsZipSha256) {
|
|
2402
|
+
throw new Error("docs.zip not found");
|
|
2403
|
+
}
|
|
2404
|
+
return docsZipSha256;
|
|
2405
|
+
}
|
|
2406
|
+
async function runInitPipeline(options, spinner) {
|
|
2407
|
+
const docsZipSha256 = await resolveDocsZipSha256();
|
|
2408
|
+
const shouldInstall = options.force || await needsIndexInstall(options.force);
|
|
2409
|
+
const rebuildReason = await needsRebuildIndex(options.force);
|
|
2410
|
+
if (!shouldInstall && !rebuildReason && isIndexReady()) {
|
|
2411
|
+
await finalizeUpToDate(spinner);
|
|
2412
|
+
return;
|
|
2413
|
+
}
|
|
2414
|
+
if (isBundledIndexUsable(docsZipSha256) && await runBundledInstallPath(docsZipSha256, spinner)) {
|
|
2415
|
+
return;
|
|
2416
|
+
}
|
|
2417
|
+
throw new Error(
|
|
2418
|
+
"Documentation index bundle is unavailable or corrupted. Reinstall the package with: npm uninstall -g @deveco/deveco-cli && npm install -g <package.tgz>"
|
|
2419
|
+
);
|
|
2420
|
+
}
|
|
2421
|
+
var DocInitializer = class {
|
|
2422
|
+
static async run(options = {}) {
|
|
2423
|
+
const background = options.background ?? false;
|
|
2424
|
+
const quiet = options.quiet ?? background;
|
|
2425
|
+
const spinner = quiet ? void 0 : ora({ text: "Checking documentation\u2026", color: "cyan" });
|
|
2426
|
+
let release;
|
|
2427
|
+
try {
|
|
2428
|
+
if (!options.assumeStorageSafe) {
|
|
2429
|
+
await assertDocStorageSafe({ mode: "write" });
|
|
2430
|
+
}
|
|
2431
|
+
release = await acquireBuildLock();
|
|
2432
|
+
await writeBuildStatus(
|
|
2433
|
+
createInitialStatus("Starting documentation setup\u2026")
|
|
2434
|
+
);
|
|
2435
|
+
await runInitPipeline(options, spinner);
|
|
2436
|
+
} catch (error) {
|
|
2437
|
+
if (release) {
|
|
2438
|
+
await handleInitError(error, spinner);
|
|
2439
|
+
}
|
|
2440
|
+
throw error;
|
|
2441
|
+
} finally {
|
|
2442
|
+
if (release) {
|
|
2443
|
+
await release();
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
};
|
|
2448
|
+
async function pollUntilReady(spinner) {
|
|
2449
|
+
while (true) {
|
|
2450
|
+
const status = await readBuildStatus();
|
|
2451
|
+
if (status.state === "done" && isIndexReady()) {
|
|
2452
|
+
return;
|
|
2453
|
+
}
|
|
2454
|
+
if (status.state === "error") {
|
|
2455
|
+
throw new DocNotReadyError(
|
|
2456
|
+
"build-failed",
|
|
2457
|
+
status.error ?? "Documentation setup failed. Try your docs command again in a moment."
|
|
2458
|
+
);
|
|
2459
|
+
}
|
|
2460
|
+
spinner.text = status.message || "Documentation is being prepared\u2026";
|
|
2461
|
+
await sleep(500);
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
async function runSetup(spinner, force = false) {
|
|
2465
|
+
spinner.text = force ? "Repairing documentation index\u2026" : "Starting documentation setup\u2026";
|
|
2466
|
+
await DocInitializer.run({ builtBy: "doc-init", force, quiet: true });
|
|
2467
|
+
}
|
|
2468
|
+
async function ensureIndexReady() {
|
|
2469
|
+
const rebuildNeeded = await needsRebuildIndex() !== null;
|
|
2470
|
+
if (isIndexReady() && !rebuildNeeded) {
|
|
2471
|
+
return;
|
|
2472
|
+
}
|
|
2473
|
+
const spinner = ora({
|
|
2474
|
+
text: "Documentation is being prepared\u2026",
|
|
2475
|
+
color: "cyan"
|
|
2476
|
+
}).start();
|
|
2477
|
+
try {
|
|
2478
|
+
if (await isBuildInProgress()) {
|
|
2479
|
+
await pollUntilReady(spinner);
|
|
2480
|
+
spinner.succeed("Documentation ready.");
|
|
2481
|
+
return;
|
|
2482
|
+
}
|
|
2483
|
+
if (await needsIndexInstall()) {
|
|
2484
|
+
await runSetup(spinner);
|
|
2485
|
+
spinner.succeed("Documentation ready.");
|
|
2486
|
+
return;
|
|
2487
|
+
}
|
|
2488
|
+
await runSetup(spinner, true);
|
|
2489
|
+
spinner.succeed("Documentation ready.");
|
|
2490
|
+
} catch (error) {
|
|
2491
|
+
spinner.fail(error.message);
|
|
2492
|
+
throw error;
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
async function awaitDocReady() {
|
|
2496
|
+
await assertDocStorageSafe({ mode: "read" });
|
|
2497
|
+
await ensureIndexReady();
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2500
|
+
// src/service/local-doc-service.ts
|
|
2501
|
+
function isCorruptSearchIndexError(error) {
|
|
2502
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2503
|
+
return /file is not a database|database disk image is malformed|SQLITE_CORRUPT/i.test(
|
|
2504
|
+
message
|
|
2505
|
+
);
|
|
2506
|
+
}
|
|
2507
|
+
var LocalDocService = class {
|
|
2508
|
+
async search(keywords, catalog, limit = 10) {
|
|
2509
|
+
await awaitDocReady();
|
|
2510
|
+
try {
|
|
2511
|
+
return await searchSqliteIndex(keywords, catalog, limit);
|
|
2512
|
+
} catch (error) {
|
|
2513
|
+
if (!isCorruptSearchIndexError(error)) {
|
|
2514
|
+
throw error;
|
|
2515
|
+
}
|
|
2516
|
+
resetSearchDbCache();
|
|
2517
|
+
await DocInitializer.run({
|
|
2518
|
+
builtBy: "doc-init",
|
|
2519
|
+
force: true,
|
|
2520
|
+
quiet: true,
|
|
2521
|
+
assumeStorageSafe: true
|
|
2522
|
+
});
|
|
2523
|
+
return searchSqliteIndex(keywords, catalog, limit);
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
async readDocument(relativePath) {
|
|
2527
|
+
return readMarkdownFromDocsZip(relativePath);
|
|
2528
|
+
}
|
|
2529
|
+
};
|
|
2530
|
+
var localDocService = new LocalDocService();
|
|
2531
|
+
|
|
2532
|
+
// src/engine.ts
|
|
2533
|
+
var apiVersion = 1;
|
|
2534
|
+
async function createDocsEngine(options) {
|
|
2535
|
+
setCacheDir(options.cacheDir);
|
|
2536
|
+
return {
|
|
2537
|
+
async search(request) {
|
|
2538
|
+
await awaitDocReady();
|
|
2539
|
+
const catalog = request.catalog ? request.catalog : void 0;
|
|
2540
|
+
return localDocService.search(
|
|
2541
|
+
request.keywords,
|
|
2542
|
+
catalog,
|
|
2543
|
+
request.limit ?? 10
|
|
2544
|
+
);
|
|
2545
|
+
},
|
|
2546
|
+
async read(request) {
|
|
2547
|
+
return localDocService.readDocument(request.relativePath);
|
|
2548
|
+
},
|
|
2549
|
+
async catalog() {
|
|
2550
|
+
return CATALOG_NAMES.map((name, index) => ({
|
|
2551
|
+
nodeId: name,
|
|
2552
|
+
nodeName: CATALOG_TITLES[name],
|
|
2553
|
+
relateDocument: "",
|
|
2554
|
+
isLeaf: true,
|
|
2555
|
+
parent: "",
|
|
2556
|
+
children: [],
|
|
2557
|
+
catalogIndex: index
|
|
2558
|
+
}));
|
|
2559
|
+
},
|
|
2560
|
+
async close() {
|
|
2561
|
+
resetSqliteBackendCache();
|
|
2562
|
+
resetSearchDbCache();
|
|
2563
|
+
resetDocsZipReaderCache();
|
|
2564
|
+
}
|
|
2565
|
+
};
|
|
2566
|
+
}
|
|
2567
|
+
export {
|
|
2568
|
+
CATALOG_NAMES,
|
|
2569
|
+
CATALOG_TITLES,
|
|
2570
|
+
QUERY_MAX_RAW_CHARS,
|
|
2571
|
+
apiVersion,
|
|
2572
|
+
assertDocStorageSafe,
|
|
2573
|
+
createDocsEngine,
|
|
2574
|
+
isDocStorageError
|
|
2575
|
+
};
|