@ohos-cpf/3rdloop 0.0.7 → 0.0.9
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/README.md +44 -25
- package/lib/cli.js +54 -29
- package/lib/config-cmd.js +1 -1
- package/lib/config.js +32 -0
- package/lib/doctor.js +266 -48
- package/lib/mcp.js +498 -0
- package/lib/opencode.js +75 -29
- package/lib/orch.js +32 -10
- package/lib/runner.js +8 -5
- package/lib/serve.js +162 -149
- package/lib/step.js +15 -4
- package/lib/web.js +107 -265
- package/lib/workflow.js +18 -8
- package/package.json +6 -2
- package/vendor/Archive/data/.index/meta.json +7 -0
- package/vendor/Archive/data/.index/search.db +0 -0
- package/vendor/Archive/data/docs/experiences/arkts-check-tool-pitfalls-baseline-comparison.md +13 -0
- package/vendor/Archive/data/docs/experiences/arkts-deep-path-import-recognition.md +31 -0
- package/vendor/Archive/data/docs/experiences/arkts-demo-coverage-entry-file-case-mismatch.md +25 -0
- package/vendor/Archive/data/docs/experiences/arkts-demo-coverage-import-trailing-slash.md +20 -0
- package/vendor/Archive/data/docs/experiences/arkts-library-source-files-may-be-ts.md +18 -0
- package/vendor/Archive/data/docs/experiences/arkts-typedarray-buffer-interface-cast-compile-fixes.md +36 -0
- package/vendor/Archive/data/docs/experiences/arkts-xts-compile-error-clustering-fix.md +24 -0
- package/vendor/Archive/data/docs/experiences/arkts-xts-doc-expected-value-runtime-probe.md +35 -0
- package/vendor/Archive/data/docs/experiences/arkts-xts-fixture-rawfile-testability-injection.md +33 -0
- package/vendor/Archive/data/docs/experiences/code-linter-chinese-comment-false-positive.md +16 -0
- package/vendor/Archive/data/docs/experiences/esobject-new-expression-variable-tracking.md +33 -0
- package/vendor/Archive/data/docs/experiences/filesystem-case-sensitivity-module-resolution.md +31 -0
- package/vendor/Archive/data/docs/experiences/gitcode-clone-redirect-warning-normal.md +13 -0
- package/vendor/Archive/data/docs/experiences/kn_msgvq1w585ec8319.md +77 -0
- package/vendor/Archive/data/docs/experiences/kn_msovnm6rd46de72c.md +98 -0
- package/vendor/Archive/data/docs/experiences/kn_mspt8vntfe708576.md +87 -0
- package/vendor/Archive/data/docs/experiences/kn_mspu1ei77adbbb48.md +133 -0
- package/vendor/Archive/data/docs/experiences/kn_mspubgak8d30bfce.md +128 -0
- package/vendor/Archive/data/docs/experiences/kn_mspug2de3cb1f7c3.md +115 -0
- package/vendor/Archive/data/docs/experiences/kn_skill_opt_001.md +38 -0
- package/vendor/Archive/data/docs/experiences/loop-artifacts-verification-evidence-triangle.md +19 -0
- package/vendor/Archive/data/docs/experiences/macos-bash-replace-build-ps1.md +19 -0
- package/vendor/Archive/data/docs/experiences/macos-bsd-diff-q-illegal-option.md +15 -0
- package/vendor/Archive/data/docs/experiences/migrate-cjs-compatible-api-version-map-limit.md +17 -0
- package/vendor/Archive/data/docs/experiences/mp3agic-arkts-library-behavior-quirks.md +21 -0
- package/vendor/Archive/data/docs/experiences/ohos-face-detection-scan-api-mapping.md +15 -0
- package/vendor/Archive/data/docs/experiences/pr-metadata-and-diff-integrity-verification.md +12 -0
- package/vendor/Archive/data/docs/experiences/rn-coverage-index-signature-pseudo-member-false-negative.md +32 -0
- package/vendor/Archive/data/docs/experiences/rn-memory-check-severity-calibration.md +21 -0
- package/vendor/Archive/data/docs/experiences/rn-platform-specific-api-exclusion-evidence.md +24 -0
- package/vendor/Archive/data/docs/experiences/rn-upstream-version-diff-pitfalls.md +17 -0
- package/vendor/Archive/data/docs/experiences/rn-version-gap-risk-rating-rules.md +13 -0
- package/vendor/Archive/data/docs/experiences/rn-view-lib-native-contract-diff-fabric-spec.md +16 -0
- package/vendor/Archive/data/docs/experiences/rnoh-adapter-package-version-field-semantics.md +18 -0
- package/vendor/Archive/data/docs/experiences/rnoh-arkts-sqlite-rows-array-format.md +18 -0
- package/vendor/Archive/data/docs/experiences/rnoh-dependency-residue-scan-strategy.md +18 -0
- package/vendor/Archive/data/docs/experiences/rnoh-example-local-lib-dep-use-tgz.md +53 -0
- package/vendor/Archive/data/docs/experiences/rnoh-example-prereserve-rntesterlist.md +17 -0
- package/vendor/Archive/data/docs/experiences/rnoh-example-template-compatiblesdkversion-upgrade.md +29 -0
- package/vendor/Archive/data/docs/experiences/rnoh-example-turbomodule-shell-adaptation-checklist.md +41 -0
- package/vendor/Archive/data/docs/experiences/rnoh-mcp-build-project-tool-risk.md +18 -0
- package/vendor/Archive/data/docs/experiences/rnoh-npm-devdependencies-version-precheck.md +29 -0
- package/vendor/Archive/data/docs/experiences/rnoh-repo-baseline-version-verification.md +13 -0
- package/vendor/Archive/data/docs/experiences/rnoh-repo-upstream-directory-mapping.md +18 -0
- package/vendor/Archive/data/docs/experiences/rnoh-stateless-library-zero-leak-verification.md +31 -0
- package/vendor/Archive/data/docs/experiences/rnoh-template-npmrc-credential-removal.md +15 -0
- package/vendor/Archive/data/docs/experiences/rnoh-turbomodule-codegen-wiring-verification.md +19 -0
- package/vendor/Archive/data/docs/experiences/rnoh-turbomodule-cpp-methodmap-compatibility-check.md +20 -0
- package/vendor/Archive/data/docs/experiences/scenario-doc-vs-source-signature-diff-four-patterns.md +24 -0
- package/vendor/Archive/data/docs/experiences/static-analysis-grep-cross-validation.md +29 -0
- package/vendor/Archive/demo.js +489 -0
- package/vendor/Archive/index.js +43 -0
- package/vendor/Archive/package.json +16 -0
- package/vendor/Archive/src/constants.js +83 -0
- package/vendor/Archive/src/data/stopwords.txt +110 -0
- package/vendor/Archive/src/data/synonyms.json +25 -0
- package/vendor/Archive/src/data/terms.txt +59 -0
- package/vendor/Archive/src/db/sqlite-backend.js +312 -0
- package/vendor/Archive/src/db/sqlite-schema.js +64 -0
- package/vendor/Archive/src/engine.js +501 -0
- package/vendor/Archive/src/index/index-manager.js +203 -0
- package/vendor/Archive/src/index/index-state.js +94 -0
- package/vendor/Archive/src/index/search-text-builder.js +106 -0
- package/vendor/Archive/src/index.js +22 -0
- package/vendor/Archive/src/parser/api-identifiers.js +121 -0
- package/vendor/Archive/src/parser/api-suffixes.js +48 -0
- package/vendor/Archive/src/parser/heading-normalize.js +40 -0
- package/vendor/Archive/src/parser/markdown-splitter.js +215 -0
- package/vendor/Archive/src/search/catalog-routing.js +87 -0
- package/vendor/Archive/src/search/fts-search.js +289 -0
- package/vendor/Archive/src/search/query-normalizer.js +159 -0
- package/vendor/Archive/src/search/query-rewriter.js +72 -0
- package/vendor/Archive/src/search/search-engine.js +72 -0
- package/vendor/Archive/src/search/snippet.js +64 -0
- package/vendor/Archive/src/store/dir-store.js +103 -0
- package/vendor/Archive/src/store/doc-store.js +36 -0
- package/vendor/Archive/src/tokenizer/lexicon.js +151 -0
- package/vendor/Archive/src/tokenizer/tokenizer.js +167 -0
- package/vendor/Archive/src/types.js +86 -0
- package/vendor/Archive/src/utils/hash.js +16 -0
- package/vendor/Archive/src/utils/lock.js +73 -0
- package/vendor/Archive/src/utils/path-safety.js +96 -0
- package/vendor/Archive/src/utils/paths.js +39 -0
- package/vendor/BadCase/rn/INDEX.md +127 -0
- package/vendor/BadCase/rn/cases/KI-001-webview/350/247/206/351/242/221/346/227/240/346/263/225/346/250/252/345/261/217.md +52 -0
- package/vendor/BadCase/rn/cases/KI-002-fastImage/347/203/255/346/233/264/346/226/260/345/233/276/347/211/207/345/244/261/350/264/245.md +37 -0
- package/vendor/BadCase/rn/cases/KI-003-wechat/347/274/251/347/225/245/345/233/276/346/234/254/345/234/260/345/233/276/347/211/207.md +62 -0
- package/vendor/BadCase/rn/cases/KI-004-wechat/345/233/236/350/260/203/346/227/266/345/272/217.md +47 -0
- package/vendor/BadCase/rn/cases/KI-005-geolocation/345/257/274/345/205/245/350/267/257/345/276/204/345/220/216/347/274/200.md +31 -0
- package/vendor/BadCase/rn/cases/KI-006-SmartRefreshLayout/346/231/272/350/203/275/346/214/207/351/222/210/345/264/251/346/272/203.md +55 -0
- package/vendor/BadCase/rn/cases/KI-007-orientation-locker/344/274/240/346/204/237/345/231/250/345/210/235/345/247/213/345/214/226.md +58 -0
- package/vendor/BadCase/rn/cases/KI-008-orientation-locker/344/274/240/346/204/237/345/231/250/345/274/225/347/224/250/350/256/241/346/225/260.md +47 -0
- package/vendor/BadCase/rn/cases/KI-009-pager-view/345/265/214/345/245/227/346/273/221/345/212/250.md +38 -0
- package/vendor/BadCase/rn/cases/KI-010-screens/346/211/213/345/212/277/345/206/262/347/252/201.md +48 -0
- package/vendor/BadCase/rn/cases/KI-011-spring-scrollview/346/211/213/345/212/277/351/207/215/345/244/215/347/273/221/345/256/232.md +49 -0
- package/vendor/BadCase/rn/cases/KI-012-qr-camera/346/250/252/345/261/217/351/200/202/351/205/215.md +59 -0
- package/vendor/BadCase/rn/cases/KI-013-webview/346/267/261/350/211/262/346/250/241/345/274/217/350/203/214/346/231/257/350/211/262.md +37 -0
- package/vendor/BadCase/rn/cases/KI-014-amap-geolocation/345/233/236/350/260/203/347/274/272/345/244/261.md +42 -0
- package/vendor/BadCase/rn/cases/KI-015-reanimated/350/277/220/350/241/214/346/227/266/346/236/220/346/236/204/345/264/251/346/272/203.md +54 -0
- package/vendor/BadCase/rn/cases/KI-016-fs/346/226/207/344/273/266/350/257/273/345/217/226/347/274/226/347/240/201.md +58 -0
- package/vendor/BadCase/rn/cases/KI-017-blob-util/345/217/202/346/225/260/347/274/226/347/240/201/351/224/231/350/257/257.md +39 -0
- package/vendor/BadCase/rn/cases/KI-018-modal/345/274/271/347/252/227/351/227/252/347/203/201.md +53 -0
- package/vendor/BadCase/rn/cases/KI-019-code-push/351/207/215/345/220/257/351/200/273/350/276/221/345/244/261/346/225/210.md +79 -0
- package/vendor/BadCase/rn/cases/KI-020-netinfo/346/225/217/346/204/237/344/277/241/346/201/257/346/263/204/351/234/262.md +60 -0
- package/vendor/BadCase/rn/cases/KI-021-video/346/234/254/345/234/260/350/247/206/351/242/221/346/222/255/346/224/276/345/274/202/345/270/270.md +79 -0
- package/vendor/BadCase/rn/cases/KI-022-audio-recorder-player/346/236/232/344/270/276/347/274/272/345/244/261.md +38 -0
- package/vendor/BadCase/rn/cases/KI-023-svg/346/270/220/345/217/230/347/273/247/346/211/277/345/244/261/346/225/210.md +62 -0
- package/vendor/BadCase/rn/generalization-log.md +55 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-SmartRefreshLayout-32.json +328 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-SmartRefreshLayout-32.md +382 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-amap-geolocation-27.json +1351 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-amap-geolocation-27.md +1176 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-audio-recorder-player-22.json +538 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-audio-recorder-player-22.md +1488 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-blob-util-23.json +211 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-blob-util-23.md +35 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-code-push-17.json +2048 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-code-push-17.md +3866 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-fast-image-102.json +198 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-fast-image-102.md +40 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-fs-38.json +751 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-fs-38.md +883 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-geolocation-24.json +1544 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-geolocation-24.md +2598 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-modal-20.json +246 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-modal-20.md +554 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-netinfo-31.json +211 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-netinfo-31.md +54 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-orientation-locker-45.json +1626 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-orientation-locker-45.md +2213 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-orientation-locker-51.json +208 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-orientation-locker-51.md +77 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-pager-view-32.json +377 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-pager-view-32.md +174 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-qr-decode-image-camera-37.json +232 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-qr-decode-image-camera-37.md +194 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-reanimated-82.json +198 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-reanimated-82.md +67 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-screens-87.json +198 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-screens-87.md +38 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-screens-93.json +208 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-screens-93.md +34 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-spring-scrollview-38.json +222 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-spring-scrollview-38.md +56 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-svg-117.json +463 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-svg-117.md +383 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-video-66.json +464 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-video-66.md +565 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-webview-112.json +198 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-webview-112.md +47 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-webview-121.json +232 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-webview-121.md +422 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-wechat-lib-15.json +295 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-wechat-lib-15.md +1038 -0
- package/vendor/BadCase/rn/rules/R-01-/345/205/250/345/261/217/347/252/227/345/217/243/347/212/266/346/200/201/347/256/241/347/220/206/345/256/214/346/225/264/346/200/247.md +20 -0
- package/vendor/BadCase/rn/rules/R-02-/350/265/204/346/272/220URI/345/275/242/346/200/201/345/210/206/346/224/257/350/246/206/347/233/226.md +21 -0
- package/vendor/BadCase/rn/rules/R-03-/351/231/204/345/261/236/346/223/215/344/275/234/345/244/261/350/264/245/351/230/273/346/226/255/344/270/273/346/265/201/347/250/213.md +20 -0
- package/vendor/BadCase/rn/rules/R-04-/345/274/202/346/255/245/345/233/236/350/260/203/346/263/250/345/206/214/346/227/266/345/272/217.md +20 -0
- package/vendor/BadCase/rn/rules/R-05-/346/250/241/345/235/227/345/257/274/345/205/245/350/267/257/345/276/204/350/247/204/350/214/203.md +19 -0
- package/vendor/BadCase/rn/rules/R-06-/344/274/240/346/204/237/345/231/250/347/241/254/344/273/266/346/235/203/351/231/220/346/214/211/351/234/200/350/216/267/345/217/226.md +21 -0
- package/vendor/BadCase/rn/rules/R-07-/346/231/272/350/203/275/346/214/207/351/222/210/347/224/237/345/221/275/345/221/250/346/234/237/347/256/241/347/220/206.md +20 -0
- package/vendor/BadCase/rn/rules/R-08-/346/211/213/345/212/277/344/272/213/344/273/266/346/263/250/345/206/214/344/270/216/345/206/262/347/252/201/345/244/204/347/220/206.md +22 -0
- package/vendor/BadCase/rn/rules/R-09-/346/250/252/347/253/226/345/261/217/345/244/232/345/275/242/346/200/201/345/270/203/345/261/200/351/200/202/351/205/215.md +20 -0
- package/vendor/BadCase/rn/rules/R-10-/346/267/261/350/211/262/346/250/241/345/274/217/344/270/273/351/242/230/351/242/234/350/211/262/351/200/202/351/205/215.md +19 -0
- package/vendor/BadCase/rn/rules/R-11-/346/216/245/345/217/243/345/233/236/350/260/203/345/256/236/347/216/260/345/256/214/346/225/264/346/200/247.md +20 -0
- package/vendor/BadCase/rn/rules/R-12-/345/216/237/347/224/237/350/277/220/350/241/214/346/227/266/350/265/204/346/272/220/346/236/220/346/236/204/346/227/266/345/272/217.md +20 -0
- package/vendor/BadCase/rn/rules/R-13-/346/226/207/344/273/266/346/225/260/346/215/256/347/274/226/347/240/201/344/270/216/346/240/274/345/274/217/345/205/274/345/256/271.md +21 -0
- package/vendor/BadCase/rn/rules/R-14-/345/212/250/347/224/273/345/210/235/345/247/213/345/214/226/346/227/266/345/272/217/344/270/216/347/212/266/346/200/201/345/220/214/346/255/245.md +20 -0
- package/vendor/BadCase/rn/rules/R-15-/347/212/266/346/200/201/346/214/201/344/271/205/345/214/226/344/270/216/351/207/215/345/220/257/351/200/273/350/276/221/344/270/200/350/207/264/346/200/247.md +20 -0
- package/vendor/BadCase/rn/rules/R-16-/346/225/217/346/204/237/344/277/241/346/201/257/346/227/245/345/277/227/346/263/204/351/234/262.md +19 -0
- package/vendor/BadCase/rn/rules/R-17-/345/252/222/344/275/223/350/265/204/346/272/220/347/224/237/345/221/275/345/221/250/346/234/237/344/270/216/351/224/231/350/257/257/345/244/204/347/220/206.md +21 -0
- package/vendor/BadCase/rn/rules/R-18-/350/203/275/345/212/233/346/236/232/344/270/276/351/205/215/347/275/256/345/256/214/346/225/264/346/200/247.md +19 -0
- package/vendor/BadCase/rn/rules/R-19-/345/233/276/345/275/242/346/270/262/346/237/223/345/261/236/346/200/247/347/273/247/346/211/277/344/270/216/351/207/215/347/275/256.md +19 -0
- package/vendor/MCP/README.md +490 -0
- package/vendor/MCP/config.json +29 -0
- package/vendor/MCP/package.json +23 -0
- package/vendor/MCP/scripts/analyze-demo-coverage.cjs +1203 -0
- package/vendor/MCP/scripts/analyze-xts-coverage.cjs +1245 -0
- package/vendor/MCP/scripts/deveco_docs.js +247 -0
- package/vendor/MCP/scripts/extract-interfaces.cjs +1487 -0
- package/vendor/MCP/scripts/flutter-analyze-demo-coverage.cjs +666 -0
- package/vendor/MCP/scripts/flutter-analyze-xts-coverage.cjs +684 -0
- package/vendor/MCP/scripts/flutter-extract-interfaces.cjs +1251 -0
- package/vendor/MCP/scripts/flutter-render-spec-doc.cjs +351 -0
- package/vendor/MCP/scripts/render-spec-doc.cjs +347 -0
- package/vendor/MCP/scripts/rn-analyze-demo-coverage.cjs +645 -0
- package/vendor/MCP/scripts/rn-analyze-test-coverage.cjs +614 -0
- package/vendor/MCP/scripts/rn-extract-interfaces.cjs +1462 -0
- package/vendor/MCP/scripts/rn-render-spec-doc.cjs +327 -0
- package/vendor/MCP/scripts/rn-scan-exports.cjs +1336 -0
- package/vendor/MCP/src/client/MCPClientPool.js +220 -0
- package/vendor/MCP/src/config/ConfigLoader.js +189 -0
- package/vendor/MCP/src/executor/ScriptExecutor.js +110 -0
- package/vendor/MCP/src/gateway/Gateway.js +451 -0
- package/vendor/MCP/src/index.js +87 -0
- package/vendor/MCP/src/library/KBToolProvider.js +229 -0
- package/vendor/MCP/src/library/logger.js +60 -0
- package/vendor/MCP/src/registry/ScriptRegistry.js +238 -0
- package/vendor/MCP/src/registry/ToolRegistry.js +111 -0
- package/vendor/MCP/src/types.js +115 -0
- package/vendor/Server/CLI/cli.js +3 -3
- package/vendor/Server/Routes/server.js +3 -3
- package/vendor/Server/Skills/arkts-library-test-coverage-check/references/MCP_TOOL_USAGE.md +4 -2
- package/vendor/Server/Skills/arkts-library-xts-coverage/SKILL.md +3 -3
- package/vendor/Server/Skills/knowledge-import/SKILL.md +1 -1
- package/vendor/Server/Skills/knowledge-import/references/MCP_TOOL_USAGE.md +12 -4
- package/vendor/Server/Skills/ohos-lib-pr-push/SKILL.md +5 -4
- package/vendor/Server/Skills/rn-library-issue-generalizer/SKILL.md +2 -2
- package/vendor/Server/Skills/rn-library-known-issue-check/SKILL.md +2 -2
- package/vendor/Server/Skills/rnoh-lib-demo-coverage/SKILL.md +1 -2
- package/vendor/Server/Skills/rnoh-lib-test-align/references/port-test-demo-guide.md +1 -1
- package/vendor/Server/Skills/rnoh-lib-xts-coverage/SKILL.md +1 -2
- package/vendor/Server/package.json +5 -1
- package/vendor/VERSION +3 -3
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* catalog-routing.js — 目录权重路由
|
|
3
|
+
*
|
|
4
|
+
* 根据查询特征(API 符号、中文/英文混合、"如何/步骤"等)
|
|
5
|
+
* 动态计算各目录的搜索 boost 倍数。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {Object} CatalogBoostResult
|
|
10
|
+
* @property {number} catalogId
|
|
11
|
+
* @property {number} boost
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 计算目录 boost。
|
|
16
|
+
* 规则:
|
|
17
|
+
* - @ohos. 查询 → references ×1.5
|
|
18
|
+
* - "如何/步骤" 查询 → guides ×1.45
|
|
19
|
+
* - 中文+英文API混合 → guides ×1.45 + references ×1.35
|
|
20
|
+
*
|
|
21
|
+
* @param {string} query 规范化后的查询
|
|
22
|
+
* @param {string[]} tokens 分词后的 token
|
|
23
|
+
* @param {Map<string, {id:string,title:string,searchBoost?:number,tailInGlobalSearch?:boolean}>} catalogMap
|
|
24
|
+
* @param {function(string):number} catalogToId 将 catalog id 转为数字 ID
|
|
25
|
+
* @returns {Map<number, number>} catalogId → boost 倍数
|
|
26
|
+
*/
|
|
27
|
+
export function computeCatalogBoosts(query, tokens, catalogMap, catalogToId) {
|
|
28
|
+
/** @type {Map<number, number>} */
|
|
29
|
+
const boosts = new Map();
|
|
30
|
+
|
|
31
|
+
// 初始化:所有目录使用配置的 searchBoost(默认 1.0)
|
|
32
|
+
for (const config of catalogMap.values()) {
|
|
33
|
+
boosts.set(catalogToId(config.id), config.searchBoost ?? 1.0);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const hasOhosModule = /@[a-z]+\.[a-z]/i.test(query);
|
|
37
|
+
const hasHowTo = /如何|怎么|步骤|教程|入门|guide/i.test(query);
|
|
38
|
+
const hasChinese = /[\u4e00-\u9fa5]/.test(query);
|
|
39
|
+
const hasEnglishApi = tokens.some((t) => /^[A-Z][a-zA-Z0-9]+|@[a-z]+\.[a-z]/i.test(t));
|
|
40
|
+
|
|
41
|
+
if (hasOhosModule) {
|
|
42
|
+
applyBoost(boosts, catalogMap, catalogToId, 'references', 1.5);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (hasHowTo) {
|
|
46
|
+
applyBoost(boosts, catalogMap, catalogToId, 'guides', 1.45);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (hasChinese && hasEnglishApi) {
|
|
50
|
+
applyBoost(boosts, catalogMap, catalogToId, 'guides', 1.45);
|
|
51
|
+
applyBoost(boosts, catalogMap, catalogToId, 'references', 1.35);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return boosts;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 应用 boost(取较大值,不覆盖更高的已有 boost)。
|
|
59
|
+
* @param {Map<number, number>} boosts
|
|
60
|
+
* @param {Map<string, any>} catalogMap
|
|
61
|
+
* @param {function(string):number} catalogToId
|
|
62
|
+
* @param {string} catalogId
|
|
63
|
+
* @param {number} value
|
|
64
|
+
*/
|
|
65
|
+
function applyBoost(boosts, catalogMap, catalogToId, catalogId, value) {
|
|
66
|
+
if (!catalogMap.has(catalogId)) return;
|
|
67
|
+
const numId = catalogToId(catalogId);
|
|
68
|
+
const current = boosts.get(numId) ?? 1.0;
|
|
69
|
+
boosts.set(numId, Math.max(current, value));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 获取全局搜索时排到尾部的目录 ID 集合。
|
|
74
|
+
* @param {Map<string, any>} catalogMap
|
|
75
|
+
* @param {function(string):number} catalogToId
|
|
76
|
+
* @returns {Set<number>}
|
|
77
|
+
*/
|
|
78
|
+
export function getTailCatalogIds(catalogMap, catalogToId) {
|
|
79
|
+
/** @type {Set<number>} */
|
|
80
|
+
const tail = new Set();
|
|
81
|
+
for (const config of catalogMap.values()) {
|
|
82
|
+
if (config.tailInGlobalSearch) {
|
|
83
|
+
tail.add(catalogToId(config.id));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return tail;
|
|
87
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fts-search.js — FTS5 查询执行 + Rerank 重排序
|
|
3
|
+
*
|
|
4
|
+
* 职责:
|
|
5
|
+
* 1. 执行 FTS5 MATCH 查询,获取 BM25 排序结果。
|
|
6
|
+
* 2. 多层 boost 调整:
|
|
7
|
+
* - 目录权重(catalogBoost)
|
|
8
|
+
* - 标题匹配 boost(精确/前缀/全分词包含)
|
|
9
|
+
* - API 符号匹配 boost(Manager API 强提升)
|
|
10
|
+
* 3. 同文档多段去重(只保留最佳段)。
|
|
11
|
+
* 4. AND/OR 回退策略。
|
|
12
|
+
*
|
|
13
|
+
* 查询使用只读内存 DB(反序列化),高性能无磁盘 I/O。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { RERANK_POOL_FACTOR } from '../constants.js';
|
|
17
|
+
import {
|
|
18
|
+
buildFtsOrMatch,
|
|
19
|
+
buildFtsAndMatch,
|
|
20
|
+
} from './query-normalizer.js';
|
|
21
|
+
import { extractSnippet } from './snippet.js';
|
|
22
|
+
import { matchApiSymbol } from '../parser/api-identifiers.js';
|
|
23
|
+
import { isManagerApi } from '../parser/api-suffixes.js';
|
|
24
|
+
import {
|
|
25
|
+
computeCatalogBoosts,
|
|
26
|
+
getTailCatalogIds,
|
|
27
|
+
} from './catalog-routing.js';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {import('../types.js').SearchResult} SearchResult
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* FTS5 查询基础 SQL(全局)。
|
|
35
|
+
*/
|
|
36
|
+
const FTS_SEARCH_SQL = `
|
|
37
|
+
SELECT
|
|
38
|
+
d.document_id,
|
|
39
|
+
d.doc_title,
|
|
40
|
+
s.section_title,
|
|
41
|
+
s.lead_text,
|
|
42
|
+
s.excerpt_truncated,
|
|
43
|
+
d.catalog_id,
|
|
44
|
+
s.id AS segment_id,
|
|
45
|
+
bm25(segments_fts) AS bm25
|
|
46
|
+
FROM segments_fts
|
|
47
|
+
JOIN segments s ON s.id = segments_fts.rowid
|
|
48
|
+
JOIN documents d ON d.id = s.doc_id
|
|
49
|
+
WHERE segments_fts MATCH ?
|
|
50
|
+
ORDER BY bm25(segments_fts)
|
|
51
|
+
LIMIT ?;
|
|
52
|
+
`;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* FTS5 查询基础 SQL(目录限定)。
|
|
56
|
+
*/
|
|
57
|
+
const FTS_SEARCH_CATALOG_SQL = `
|
|
58
|
+
SELECT
|
|
59
|
+
d.document_id,
|
|
60
|
+
d.doc_title,
|
|
61
|
+
s.section_title,
|
|
62
|
+
s.lead_text,
|
|
63
|
+
s.excerpt_truncated,
|
|
64
|
+
d.catalog_id,
|
|
65
|
+
s.id AS segment_id,
|
|
66
|
+
bm25(segments_fts) AS bm25
|
|
67
|
+
FROM segments_fts
|
|
68
|
+
JOIN segments s ON s.id = segments_fts.rowid
|
|
69
|
+
JOIN documents d ON d.id = s.doc_id
|
|
70
|
+
WHERE segments_fts MATCH ? AND d.catalog_id = ?
|
|
71
|
+
ORDER BY bm25(segments_fts)
|
|
72
|
+
LIMIT ?;
|
|
73
|
+
`;
|
|
74
|
+
|
|
75
|
+
export default class FtsSearch {
|
|
76
|
+
/**
|
|
77
|
+
* @param {import('../db/sqlite-backend.js').default} backend
|
|
78
|
+
* @param {string} dbPath
|
|
79
|
+
* @param {Map<string, any>} catalogMap
|
|
80
|
+
* @param {function(string):number} catalogToId
|
|
81
|
+
* @param {function(number):string} idToCatalog
|
|
82
|
+
*/
|
|
83
|
+
constructor(backend, dbPath, catalogMap, catalogToId, idToCatalog) {
|
|
84
|
+
this.backend = backend;
|
|
85
|
+
this.dbPath = dbPath;
|
|
86
|
+
this.catalogMap = catalogMap;
|
|
87
|
+
this.catalogToId = catalogToId;
|
|
88
|
+
this.idToCatalog = idToCatalog;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 执行搜索(含 AND/OR 回退 + Rerank + 去重)。
|
|
93
|
+
* @param {{rawQuery:string, tokens:string[], preferAnd:boolean, ftsMatch:string|null}} prepared
|
|
94
|
+
* @param {{catalog?:string, limit?:number, includeSnippet?:boolean, snippetMaxChars?:number}} query
|
|
95
|
+
* @param {string[]} [apiSymbols] 当前段 API 符号(可选,用于 boost)
|
|
96
|
+
* @returns {Promise<SearchResult[]>}
|
|
97
|
+
*/
|
|
98
|
+
async search(prepared, query) {
|
|
99
|
+
if (!prepared.tokens || prepared.tokens.length === 0) return [];
|
|
100
|
+
|
|
101
|
+
const limit = query.limit || 10;
|
|
102
|
+
const poolSize = limit * RERANK_POOL_FACTOR;
|
|
103
|
+
const catalogId = query.catalog ? this.catalogToId(query.catalog) : undefined;
|
|
104
|
+
|
|
105
|
+
// AND/OR 回退策略
|
|
106
|
+
let rows = [];
|
|
107
|
+
if (prepared.preferAnd) {
|
|
108
|
+
const andMatch = buildFtsAndMatch(prepared.tokens);
|
|
109
|
+
const andRows = await this.runMatch(andMatch, catalogId, poolSize);
|
|
110
|
+
if (andRows.length >= limit) {
|
|
111
|
+
rows = andRows;
|
|
112
|
+
} else {
|
|
113
|
+
// 不足 limit,用 OR 补全
|
|
114
|
+
const orMatch = buildFtsOrMatch(prepared.tokens);
|
|
115
|
+
const orRows = await this.runMatch(orMatch, catalogId, poolSize);
|
|
116
|
+
rows = mergeAndDedupe(andRows, orRows, poolSize);
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
const match = prepared.ftsMatch || buildFtsOrMatch(prepared.tokens);
|
|
120
|
+
rows = await this.runMatch(match, catalogId, poolSize);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Rerank
|
|
124
|
+
const reranked = this.rerank(rows, prepared);
|
|
125
|
+
|
|
126
|
+
// 同文档去重
|
|
127
|
+
const deduped = pickBestSegmentForDocument(reranked);
|
|
128
|
+
|
|
129
|
+
// 截取 limit
|
|
130
|
+
const topResults = deduped.slice(0, limit);
|
|
131
|
+
|
|
132
|
+
// 转换为 SearchResult
|
|
133
|
+
const includeSnippet = query.includeSnippet !== false;
|
|
134
|
+
const snippetMaxChars = query.snippetMaxChars || 200;
|
|
135
|
+
|
|
136
|
+
return topResults.map((row) => ({
|
|
137
|
+
documentId: row.document_id,
|
|
138
|
+
title: row.doc_title,
|
|
139
|
+
sectionTitle: row.section_title || row.doc_title,
|
|
140
|
+
snippet: includeSnippet
|
|
141
|
+
? extractSnippet(row.lead_text || '', prepared.rawQuery, snippetMaxChars)
|
|
142
|
+
: '',
|
|
143
|
+
score: row.effectiveScore,
|
|
144
|
+
rawScore: row.bm25,
|
|
145
|
+
catalog: this.idToCatalog(row.catalog_id),
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* 执行一次 FTS MATCH 查询。
|
|
151
|
+
* @param {string} matchExpr FTS MATCH 表达式
|
|
152
|
+
* @param {number|undefined} catalogId 目录限定
|
|
153
|
+
* @param {number} limit
|
|
154
|
+
* @returns {Promise<any[]>}
|
|
155
|
+
*/
|
|
156
|
+
async runMatch(matchExpr, catalogId, limit) {
|
|
157
|
+
if (!matchExpr) return [];
|
|
158
|
+
const db = await this.backend.openReadDb(this.dbPath);
|
|
159
|
+
|
|
160
|
+
let stmt;
|
|
161
|
+
if (catalogId !== undefined) {
|
|
162
|
+
stmt = db.prepare(FTS_SEARCH_CATALOG_SQL);
|
|
163
|
+
stmt.bind([matchExpr, catalogId, limit]);
|
|
164
|
+
} else {
|
|
165
|
+
stmt = db.prepare(FTS_SEARCH_SQL);
|
|
166
|
+
stmt.bind([matchExpr, limit]);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** @type {any[]} */
|
|
170
|
+
const rows = [];
|
|
171
|
+
while (stmt.step()) {
|
|
172
|
+
rows.push(stmt.get());
|
|
173
|
+
}
|
|
174
|
+
stmt.finalize();
|
|
175
|
+
return rows;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Rerank:多层 boost 调整 effectiveScore。
|
|
180
|
+
* @param {any[]} rows
|
|
181
|
+
* @param {{rawQuery:string, tokens:string[]}} prepared
|
|
182
|
+
* @returns {any[]}
|
|
183
|
+
*/
|
|
184
|
+
rerank(rows, prepared) {
|
|
185
|
+
const boosts = computeCatalogBoosts(
|
|
186
|
+
prepared.rawQuery,
|
|
187
|
+
prepared.tokens,
|
|
188
|
+
this.catalogMap,
|
|
189
|
+
this.catalogToId
|
|
190
|
+
);
|
|
191
|
+
const tailIds = getTailCatalogIds(this.catalogMap, this.catalogToId);
|
|
192
|
+
|
|
193
|
+
for (const row of rows) {
|
|
194
|
+
const catalogBoost = boosts.get(row.catalog_id) ?? 1.0;
|
|
195
|
+
const tailPenalty = tailIds.has(row.catalog_id) ? 1.2 : 1.0;
|
|
196
|
+
|
|
197
|
+
// baseScore: BM25 除以目录 boost(boost 越大分数越小 = 更靠前)
|
|
198
|
+
const baseScore = (row.bm25 || 0) / catalogBoost * tailPenalty;
|
|
199
|
+
|
|
200
|
+
// titleBoost
|
|
201
|
+
const titleLower = (row.section_title || row.doc_title || '').toLowerCase();
|
|
202
|
+
let titleMultiplier = 1.0;
|
|
203
|
+
const queryLower = prepared.rawQuery.toLowerCase();
|
|
204
|
+
if (titleLower === queryLower) {
|
|
205
|
+
titleMultiplier = 4.0;
|
|
206
|
+
} else if (titleLower.startsWith(queryLower.slice(0, 6)) && queryLower.length >= 6) {
|
|
207
|
+
titleMultiplier = 1.8;
|
|
208
|
+
} else {
|
|
209
|
+
// 标题包含所有分词
|
|
210
|
+
const allInTitle = prepared.tokens.every((t) => titleLower.includes(t.toLowerCase()));
|
|
211
|
+
if (allInTitle) titleMultiplier = 1.35;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// apiSymbolBoost: 判断标题是否含 Manager API
|
|
215
|
+
let apiMultiplier = 1.0;
|
|
216
|
+
if (isManagerApi(row.section_title || '')) {
|
|
217
|
+
apiMultiplier = 3.5;
|
|
218
|
+
if (this.idToCatalog(row.catalog_id) === 'references') {
|
|
219
|
+
apiMultiplier *= 1.4;
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
// 检查 token 是否匹配 API 符号
|
|
223
|
+
const symbols = [row.section_title, row.doc_title].filter(Boolean);
|
|
224
|
+
const anyMatch = prepared.tokens.some((t) => matchApiSymbol(t, symbols));
|
|
225
|
+
if (anyMatch) apiMultiplier = 1.5;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
row.effectiveScore = baseScore / (titleMultiplier * apiMultiplier);
|
|
229
|
+
row.rawScore = row.bm25;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// 按 effectiveScore 升序(越小越相关)
|
|
233
|
+
rows.sort((a, b) => a.effectiveScore - b.effectiveScore);
|
|
234
|
+
return rows;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* AND + OR 结果合并去重。
|
|
240
|
+
* @param {any[]} andRows
|
|
241
|
+
* @param {any[]} orRows
|
|
242
|
+
* @param {number} limit
|
|
243
|
+
* @returns {any[]}
|
|
244
|
+
*/
|
|
245
|
+
function mergeAndDedupe(andRows, orRows, limit) {
|
|
246
|
+
/** @type {Set<string>} */
|
|
247
|
+
const seen = new Set();
|
|
248
|
+
/** @type {any[]} */
|
|
249
|
+
const result = [];
|
|
250
|
+
for (const row of andRows) {
|
|
251
|
+
const key = `${row.document_id}:${row.segment_id}`;
|
|
252
|
+
if (!seen.has(key)) {
|
|
253
|
+
seen.add(key);
|
|
254
|
+
result.push(row);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
for (const row of orRows) {
|
|
258
|
+
const key = `${row.document_id}:${row.segment_id}`;
|
|
259
|
+
if (!seen.has(key)) {
|
|
260
|
+
seen.add(key);
|
|
261
|
+
result.push(row);
|
|
262
|
+
}
|
|
263
|
+
if (result.length >= limit) break;
|
|
264
|
+
}
|
|
265
|
+
return result;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* 同文档多段去重:只保留最佳段。
|
|
270
|
+
* @param {any[]} rows 已按 effectiveScore 排序
|
|
271
|
+
* @returns {any[]}
|
|
272
|
+
*/
|
|
273
|
+
function pickBestSegmentForDocument(rows) {
|
|
274
|
+
/** @type {Map<string, any>} */
|
|
275
|
+
const bestByDoc = new Map();
|
|
276
|
+
/** @type {any[]} */
|
|
277
|
+
const ordered = [];
|
|
278
|
+
|
|
279
|
+
for (const row of rows) {
|
|
280
|
+
const docId = row.document_id;
|
|
281
|
+
if (!bestByDoc.has(docId)) {
|
|
282
|
+
bestByDoc.set(docId, row);
|
|
283
|
+
ordered.push(row);
|
|
284
|
+
}
|
|
285
|
+
// 已有该文档的段,跳过(保留第一个 = 最佳)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return ordered;
|
|
289
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* query-normalizer.js — 查询预处理
|
|
3
|
+
*
|
|
4
|
+
* 对用户输入的原始查询进行规范化:
|
|
5
|
+
* - 合并多余空白、截断超长查询。
|
|
6
|
+
* - 识别特殊查询模式(API 短语拆分、单个 API 符号)。
|
|
7
|
+
* - 与 jieba 分词 + 同义词扩展协同产出 FTS token。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { QUERY_MAX_RAW_CHARS, QUERY_MAX_FTS_TOKENS, QUERY_AND_MAX_TOKENS } from '../constants.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {Object} PreparedQuery
|
|
14
|
+
* @property {string} rawQuery 原始查询
|
|
15
|
+
* @property {string} expandedQuery 同义词扩展后的查询
|
|
16
|
+
* @property {string[]} tokens FTS token 列表
|
|
17
|
+
* @property {boolean} preferAnd 是否优先 AND 匹配
|
|
18
|
+
* @property {string|null} ftsMatch 预构建的 FTS MATCH 表达式
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 规范化搜索输入:合并空格 + 截断。
|
|
23
|
+
* @param {string} raw
|
|
24
|
+
* @returns {string}
|
|
25
|
+
*/
|
|
26
|
+
export function normalizeSearchInput(raw) {
|
|
27
|
+
if (!raw) return '';
|
|
28
|
+
return raw.replace(/\s+/g, ' ').trim().slice(0, QUERY_MAX_RAW_CHARS);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 判断是否为单个 API 模块符号(如 @ohos.camera)。
|
|
33
|
+
* @param {string} query
|
|
34
|
+
* @returns {boolean}
|
|
35
|
+
*/
|
|
36
|
+
function isSingleApiSymbol(query) {
|
|
37
|
+
return /^@[a-z]+\.[a-z][a-zA-Z0-9.]*$/i.test(query.trim());
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 检测 API 短语拆分场景(如 "camera manager")。
|
|
42
|
+
* @param {string} query
|
|
43
|
+
* @returns {{left:string, right:string}|null}
|
|
44
|
+
*/
|
|
45
|
+
function detectSplitApiPhrase(query) {
|
|
46
|
+
const parts = query.trim().split(/\s+/);
|
|
47
|
+
if (parts.length === 2) {
|
|
48
|
+
// 形如 "camera manager" → left=camera, right=manager
|
|
49
|
+
if (/^[a-z]/i.test(parts[0]) && /^[a-z]/i.test(parts[1])) {
|
|
50
|
+
return { left: parts[0], right: parts[1] };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 预处理查询,产出 PreparedQuery。
|
|
58
|
+
*
|
|
59
|
+
* @param {string} rawQuery 用户原始查询
|
|
60
|
+
* @param {import('../tokenizer/tokenizer.js').default} tokenizer
|
|
61
|
+
* @param {import('../tokenizer/lexicon.js').default} lexicon
|
|
62
|
+
* @param {{expandSynonyms?:boolean, synonymExpandLimit?:number, preferAnd?:boolean}} [options]
|
|
63
|
+
* @returns {Promise<PreparedQuery>}
|
|
64
|
+
*/
|
|
65
|
+
export async function prepareQuery(rawQuery, tokenizer, lexicon, options = {}) {
|
|
66
|
+
const normalized = normalizeSearchInput(rawQuery);
|
|
67
|
+
if (!normalized) {
|
|
68
|
+
return { rawQuery: '', expandedQuery: '', tokens: [], preferAnd: false, ftsMatch: null };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const expandSynonyms = options.expandSynonyms !== false;
|
|
72
|
+
const synonymLimit = options.synonymExpandLimit || 8;
|
|
73
|
+
|
|
74
|
+
// 同义词扩展
|
|
75
|
+
const expandedQuery = expandSynonyms
|
|
76
|
+
? lexicon.expandSynonyms(normalized, synonymLimit)
|
|
77
|
+
: normalized;
|
|
78
|
+
|
|
79
|
+
// jieba 分词(查询模式)
|
|
80
|
+
let tokens = await tokenizer.tokenizeForQuery(expandedQuery);
|
|
81
|
+
|
|
82
|
+
// 特殊查询模式处理
|
|
83
|
+
let ftsMatch = null;
|
|
84
|
+
|
|
85
|
+
if (isSingleApiSymbol(normalized)) {
|
|
86
|
+
// 单个 API 符号:OR 匹配变体
|
|
87
|
+
const variants = await tokenizer.tokenizeForQuery(normalized);
|
|
88
|
+
tokens = dedupe([...tokens, ...variants]).slice(0, QUERY_MAX_FTS_TOKENS);
|
|
89
|
+
ftsMatch = buildFtsOrMatch(tokens);
|
|
90
|
+
} else {
|
|
91
|
+
const split = detectSplitApiPhrase(normalized);
|
|
92
|
+
if (split) {
|
|
93
|
+
const leftTokens = await tokenizer.tokenizeForQuery(split.left);
|
|
94
|
+
const rightTokens = await tokenizer.tokenizeForQuery(split.right);
|
|
95
|
+
ftsMatch = buildSplitApiAndMatch(leftTokens, rightTokens);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 限制 token 数量
|
|
100
|
+
tokens = tokens.slice(0, QUERY_MAX_FTS_TOKENS);
|
|
101
|
+
|
|
102
|
+
// 策略选择:2-4 个 token → preferAnd
|
|
103
|
+
let preferAnd = options.preferAnd;
|
|
104
|
+
if (preferAnd === undefined) {
|
|
105
|
+
preferAnd = tokens.length >= 2 && tokens.length <= QUERY_AND_MAX_TOKENS;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { rawQuery: normalized, expandedQuery, tokens, preferAnd, ftsMatch };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 去重。
|
|
113
|
+
* @param {string[]} arr
|
|
114
|
+
* @returns {string[]}
|
|
115
|
+
*/
|
|
116
|
+
function dedupe(arr) {
|
|
117
|
+
return [...new Set(arr)];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* FTS5 token 转义(防注入)。
|
|
122
|
+
* @param {string} token
|
|
123
|
+
* @returns {string}
|
|
124
|
+
*/
|
|
125
|
+
export function escapeFtsToken(token) {
|
|
126
|
+
return `"${token.replace(/"/g, '""')}"`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 构建 OR 匹配表达式。
|
|
131
|
+
* @param {string[]} tokens
|
|
132
|
+
* @returns {string}
|
|
133
|
+
*/
|
|
134
|
+
export function buildFtsOrMatch(tokens) {
|
|
135
|
+
const filtered = tokens.filter(Boolean);
|
|
136
|
+
if (filtered.length === 0) return '';
|
|
137
|
+
return filtered.map(escapeFtsToken).join(' OR ');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 构建 AND 匹配表达式。
|
|
142
|
+
* @param {string[]} tokens
|
|
143
|
+
* @returns {string}
|
|
144
|
+
*/
|
|
145
|
+
export function buildFtsAndMatch(tokens) {
|
|
146
|
+
const filtered = tokens.filter(Boolean);
|
|
147
|
+
if (filtered.length === 0) return '';
|
|
148
|
+
return filtered.map(escapeFtsToken).join(' AND ');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* 构建拆分 API 的 AND 匹配。
|
|
153
|
+
* @param {string[]} left
|
|
154
|
+
* @param {string[]} right
|
|
155
|
+
* @returns {string}
|
|
156
|
+
*/
|
|
157
|
+
function buildSplitApiAndMatch(left, right) {
|
|
158
|
+
return `(${buildFtsOrMatch(left)}) AND (${buildFtsOrMatch(right)})`;
|
|
159
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* query-rewriter.js — 查询重写器
|
|
3
|
+
*
|
|
4
|
+
* 封装同义词扩展的动态注册能力:
|
|
5
|
+
* - 运行时注册自定义同义词组。
|
|
6
|
+
* - 与 Lexicon 的文件词典合并,不影响磁盘词典。
|
|
7
|
+
*
|
|
8
|
+
* 通过内存同义词组叠加到 Lexicon 的 getSynonymMap() 之上,
|
|
9
|
+
* 避免频繁修改磁盘文件。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export default class QueryRewriter {
|
|
13
|
+
constructor() {
|
|
14
|
+
/** @type {string[][]} */
|
|
15
|
+
this.customGroups = [];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 注册自定义同义词组。
|
|
20
|
+
* @param {string[]} words
|
|
21
|
+
*/
|
|
22
|
+
addSynonyms(words) {
|
|
23
|
+
if (Array.isArray(words) && words.length >= 2) {
|
|
24
|
+
this.customGroups.push([...words]);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 将自定义同义词组合并进 Lexicon 的同义词映射。
|
|
30
|
+
* @param {import('../tokenizer/lexicon.js').default} lexicon
|
|
31
|
+
* @returns {Map<string, Set<string>>}
|
|
32
|
+
*/
|
|
33
|
+
mergeWithLexicon(lexicon) {
|
|
34
|
+
const map = new Map(lexicon.getSynonymMap());
|
|
35
|
+
for (const group of this.customGroups) {
|
|
36
|
+
for (const word of group) {
|
|
37
|
+
const key = word.toLowerCase();
|
|
38
|
+
if (!map.has(key)) map.set(key, new Set());
|
|
39
|
+
for (const other of group) {
|
|
40
|
+
if (other !== word) map.get(key).add(other);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return map;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 扩展查询(使用合并后的同义词映射)。
|
|
49
|
+
* @param {string} query
|
|
50
|
+
* @param {import('../tokenizer/lexicon.js').default} lexicon
|
|
51
|
+
* @param {number} [maxParts=8]
|
|
52
|
+
* @returns {string}
|
|
53
|
+
*/
|
|
54
|
+
expand(query, lexicon, maxParts = 8) {
|
|
55
|
+
const map = this.mergeWithLexicon(lexicon);
|
|
56
|
+
const parts = query.split(/\s+/).filter(Boolean);
|
|
57
|
+
/** @type {Set<string>} */
|
|
58
|
+
const expanded = new Set();
|
|
59
|
+
|
|
60
|
+
for (let i = 0; i < parts.length && expanded.size < maxParts; i++) {
|
|
61
|
+
expanded.add(parts[i]);
|
|
62
|
+
const syns = map.get(parts[i].toLowerCase());
|
|
63
|
+
if (syns) {
|
|
64
|
+
for (const s of syns) {
|
|
65
|
+
if (expanded.size >= maxParts) break;
|
|
66
|
+
expanded.add(s);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return [...expanded].join(' ');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* search-engine.js — 搜索调度层
|
|
3
|
+
*
|
|
4
|
+
* 编排查询预处理 → FTS5 检索 → Rerank → 摘要提取的完整管道。
|
|
5
|
+
* 作为 IndexManager/Engine 与底层 FtsSearch 之间的中间层,
|
|
6
|
+
* 隔离查询逻辑与索引写入逻辑。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { prepareQuery } from './query-normalizer.js';
|
|
10
|
+
import FtsSearch from './fts-search.js';
|
|
11
|
+
|
|
12
|
+
export default class SearchEngine {
|
|
13
|
+
/**
|
|
14
|
+
* @param {import('../db/sqlite-backend.js').default} backend
|
|
15
|
+
* @param {import('../tokenizer/tokenizer.js').default} tokenizer
|
|
16
|
+
* @param {import('../tokenizer/lexicon.js').default} lexicon
|
|
17
|
+
* @param {string} dbPath
|
|
18
|
+
* @param {Map<string, any>} catalogMap
|
|
19
|
+
* @param {function(string):number} catalogToId
|
|
20
|
+
* @param {function(number):string} idToCatalog
|
|
21
|
+
* @param {import('./query-rewriter.js').default} [rewriter]
|
|
22
|
+
*/
|
|
23
|
+
constructor(backend, tokenizer, lexicon, dbPath, catalogMap, catalogToId, idToCatalog, rewriter) {
|
|
24
|
+
this.backend = backend;
|
|
25
|
+
this.tokenizer = tokenizer;
|
|
26
|
+
this.lexicon = lexicon;
|
|
27
|
+
this.dbPath = dbPath;
|
|
28
|
+
this.catalogMap = catalogMap;
|
|
29
|
+
this.catalogToId = catalogToId;
|
|
30
|
+
this.idToCatalog = idToCatalog;
|
|
31
|
+
this.rewriter = rewriter || null;
|
|
32
|
+
|
|
33
|
+
this.fts = new FtsSearch(backend, dbPath, catalogMap, catalogToId, idToCatalog);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 执行搜索。
|
|
38
|
+
* @param {import('../types.js').SearchQuery} query
|
|
39
|
+
* @param {import('../types.js').SearchOptions} [options]
|
|
40
|
+
* @returns {Promise<import('../types.js').SearchResult[]>}
|
|
41
|
+
*/
|
|
42
|
+
async search(query, options = {}) {
|
|
43
|
+
if (!query.query || !query.query.trim()) return [];
|
|
44
|
+
|
|
45
|
+
// 如果有自定义重写器,先扩展同义词
|
|
46
|
+
let effectiveQuery = query.query;
|
|
47
|
+
if (this.rewriter && options.expandSynonyms !== false) {
|
|
48
|
+
effectiveQuery = this.rewriter.expand(
|
|
49
|
+
query.query,
|
|
50
|
+
this.lexicon,
|
|
51
|
+
options.synonymExpandLimit
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 查询预处理
|
|
56
|
+
const prepared = await prepareQuery(
|
|
57
|
+
effectiveQuery,
|
|
58
|
+
this.tokenizer,
|
|
59
|
+
this.lexicon,
|
|
60
|
+
{
|
|
61
|
+
expandSynonyms: options.expandSynonyms,
|
|
62
|
+
synonymExpandLimit: options.synonymExpandLimit,
|
|
63
|
+
preferAnd: options.preferAnd,
|
|
64
|
+
}
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
if (!prepared.tokens || prepared.tokens.length === 0) return [];
|
|
68
|
+
|
|
69
|
+
// FTS5 检索 + Rerank
|
|
70
|
+
return this.fts.search(prepared, query);
|
|
71
|
+
}
|
|
72
|
+
}
|