diff --git a/src/dfm-search/dfm-search-lib/semantic/extractors/actionextractor.cpp b/src/dfm-search/dfm-search-lib/semantic/extractors/actionextractor.cpp index 1875ccc..943101d 100644 --- a/src/dfm-search/dfm-search-lib/semantic/extractors/actionextractor.cpp +++ b/src/dfm-search/dfm-search-lib/semantic/extractors/actionextractor.cpp @@ -10,6 +10,7 @@ #include #include #include +#include DFM_SEARCH_BEGIN_NS @@ -137,16 +138,29 @@ QStringList ActionExtractor::resolveImReceivedPaths() const } const QString xdgType = meta.value("xdg_type").toString(); - const QString customSubdir = meta.value("custom_path").toString(); + const QVariant customPathVar = meta.value("custom_path"); QStringList resolved; - if (!customSubdir.isEmpty()) { - resolved = LocationExtractor::expandCustomPath( - QDir::homePath(), customSubdir); - } else if (!xdgType.isEmpty()) { - const QString path = LocationExtractor::resolveXdgPath(xdgType); - if (!path.isEmpty()) { - resolved = { path }; + if (customPathVar.userType() == QMetaType::QVariantList) { + for (const QVariant &pathVar : customPathVar.toList()) { + const QString relPath = pathVar.toString(); + if (relPath.isEmpty()) continue; + const QStringList expanded = LocationExtractor::expandCustomPath( + QDir::homePath(), relPath); + for (const QString &p : expanded) { + if (!resolved.contains(p)) resolved.append(p); + } + } + } else { + const QString customSubdir = customPathVar.toString(); + if (!customSubdir.isEmpty()) { + resolved = LocationExtractor::expandCustomPath( + QDir::homePath(), customSubdir); + } else if (!xdgType.isEmpty()) { + const QString path = LocationExtractor::resolveXdgPath(xdgType); + if (!path.isEmpty()) { + resolved = { path }; + } } } diff --git a/src/dfm-search/dfm-search-lib/semantic/extractors/locationextractor.cpp b/src/dfm-search/dfm-search-lib/semantic/extractors/locationextractor.cpp index fb20be4..e677a99 100644 --- a/src/dfm-search/dfm-search-lib/semantic/extractors/locationextractor.cpp +++ b/src/dfm-search/dfm-search-lib/semantic/extractors/locationextractor.cpp @@ -9,6 +9,7 @@ #include #include #include +#include DFM_SEARCH_BEGIN_NS @@ -34,7 +35,7 @@ void LocationExtractor::extract(const QString &input, ParsedIntent &intent) const QVariantMap metadata = m_engine->ruleMetadata("location", ruleIds[i]); const QString xdgType = metadata.value("xdg_type").toString(); - const QString customSubdir = metadata.value("custom_path").toString(); + const QVariant customPathVar = metadata.value("custom_path"); const bool includeHidden = metadata.value("include_hidden", false).toBool(); // Always record the consumed span when a rule matches, @@ -47,12 +48,24 @@ void LocationExtractor::extract(const QString &input, ParsedIntent &intent) intent.consumedSpans().append(span); QStringList resolvedPaths; - if (!customSubdir.isEmpty()) { - resolvedPaths = expandCustomPath(QDir::homePath(), customSubdir); + if (customPathVar.userType() == QMetaType::QVariantList) { + for (const QVariant &pathVar : customPathVar.toList()) { + const QString relPath = pathVar.toString(); + if (relPath.isEmpty()) continue; + const QStringList expanded = expandCustomPath(QDir::homePath(), relPath); + for (const QString &p : expanded) { + if (!resolvedPaths.contains(p)) resolvedPaths.append(p); + } + } } else { - const QString path = resolveXdgPath(xdgType); - if (!path.isEmpty()) { - resolvedPaths = { path }; + const QString customSubdir = customPathVar.toString(); + if (!customSubdir.isEmpty()) { + resolvedPaths = expandCustomPath(QDir::homePath(), customSubdir); + } else { + const QString path = resolveXdgPath(xdgType); + if (!path.isEmpty()) { + resolvedPaths = { path }; + } } } if (resolvedPaths.isEmpty()) { diff --git a/src/dfm-search/dfm-search-lib/semantic/rules/zh_CN/location_rules.json b/src/dfm-search/dfm-search-lib/semantic/rules/zh_CN/location_rules.json index 123dc24..cbfb3ed 100644 --- a/src/dfm-search/dfm-search-lib/semantic/rules/zh_CN/location_rules.json +++ b/src/dfm-search/dfm-search-lib/semantic/rules/zh_CN/location_rules.json @@ -79,7 +79,11 @@ "enabled": true, "priority": 180, "metadata": { - "custom_path": "Documents/xwechat_files/*/msg/file", + "custom_path": [ + "Documents/xwechat_files/*/msg/file", + "Documents/xwechat_files/*/msg/video" + ], + "exclude_patterns": ["*_thumb.jpg"], "include_hidden": false, "im_received": true } diff --git a/src/dfm-search/dfm-search-lib/semantic/semanticruleengine.cpp b/src/dfm-search/dfm-search-lib/semantic/semanticruleengine.cpp index 8980e5a..fecca14 100644 --- a/src/dfm-search/dfm-search-lib/semantic/semanticruleengine.cpp +++ b/src/dfm-search/dfm-search-lib/semantic/semanticruleengine.cpp @@ -178,6 +178,18 @@ QVariantMap SemanticRuleEngine::ruleMetadata(const QString &group, const QString return {}; } +QVariantMap SemanticRuleEngine::ruleMetadataById(const QString &ruleId) const +{ + for (const RuleGroup &group : m_groups) { + for (const Rule &rule : group.rules) { + if (rule.id == ruleId) { + return rule.metadata; + } + } + } + return {}; +} + bool SemanticRuleEngine::hasGroup(const QString &group) const { return m_groups.contains(group); diff --git a/src/dfm-search/dfm-search-lib/semantic/semanticruleengine.h b/src/dfm-search/dfm-search-lib/semantic/semanticruleengine.h index 6f172e6..34ccb8d 100644 --- a/src/dfm-search/dfm-search-lib/semantic/semanticruleengine.h +++ b/src/dfm-search/dfm-search-lib/semantic/semanticruleengine.h @@ -86,6 +86,17 @@ class SemanticRuleEngine : public QObject */ QVariantMap ruleMetadata(const QString &group, const QString &ruleId) const; + /** + * @brief Get a rule's metadata by rule ID, searching across all groups. + * @param ruleId The rule ID to look up + * @return The metadata map, or empty if not found + * + * Unlike ruleMetadata(group, ruleId), this searches all groups. + * Useful when the caller has a ruleId from consumedSpans but doesn't + * know which group it belongs to. + */ + QVariantMap ruleMetadataById(const QString &ruleId) const; + /** * @brief Get all rule IDs in a group. */ diff --git a/src/dfm-search/dfm-search-lib/semantic/semanticsearcher.cpp b/src/dfm-search/dfm-search-lib/semantic/semanticsearcher.cpp index df511a5..85e43dc 100644 --- a/src/dfm-search/dfm-search-lib/semantic/semanticsearcher.cpp +++ b/src/dfm-search/dfm-search-lib/semantic/semanticsearcher.cpp @@ -14,7 +14,11 @@ #include #include #include +#include #include +#include + +#include DFM_SEARCH_BEGIN_NS @@ -47,13 +51,13 @@ int countSemanticDimensions(const ParsedIntent &intent) { int count = 0; if (!intent.searchDirectories().isEmpty()) ++count; // location - if (intent.hiddenOnly()) ++count; // hiddenOnly 属性 - if (intent.recentOnly()) ++count; // recentOnly 属性 - if (intent.timeConstraint().isValid()) ++count; // time 约束 - if (intent.sizeConstraint().isValid()) ++count; // size 约束 - if (!intent.fileExtensions().isEmpty()) ++count; // fileType - if (!intent.keywords().isEmpty()) ++count; // keyword (unconsumed text) - if (hasTargetRuleSpan(intent)) ++count; // target 名词 (文件/文件名/文件夹) + if (intent.hiddenOnly()) ++count; // hiddenOnly 属性 + if (intent.recentOnly()) ++count; // recentOnly 属性 + if (intent.timeConstraint().isValid()) ++count; // time 约束 + if (intent.sizeConstraint().isValid()) ++count; // size 约束 + if (!intent.fileExtensions().isEmpty()) ++count; // fileType + if (!intent.keywords().isEmpty()) ++count; // keyword (unconsumed text) + if (hasTargetRuleSpan(intent)) ++count; // target 名词 (文件/文件名/文件夹) // 当 specific filetype 与 general filetype 同时命中(如 "PPT文档"、"pdf文档"), // general 类型词(文档/表格/幻灯片)充当目标名词,贡献额外维度。 @@ -161,48 +165,22 @@ void SemanticSearcherData::doSearch(const QString &naturalLanguage, const QStrin dirs = QStringList { QDir::homePath() }; } - // Step 5: Set up signal/slot handlers - auto onFinished = [this](const SearchResultList &results) { - // Collect and deduplicate results from each engine's final result list - for (const SearchResult &r : results) { - if (!seenPaths.contains(r.path())) { - seenPaths.insert(r.path()); - allResults.append(r); - } - } - - if (pendingFinishCount.fetch_sub(1) == 1) { - // All engines finished - timeoutTimer->stop(); - - // Truncate final deduplicated results to maxResults - if (maxResults > 0 && allResults.size() > maxResults) { - allResults = allResults.mid(0, maxResults); - } - - if (cancelled.load()) { - status.store(SearchStatus::Cancelled); - Q_EMIT q->statusChanged(SearchStatus::Cancelled); - Q_EMIT q->searchCancelled(); - } else { - status.store(SearchStatus::Finished); - Q_EMIT q->statusChanged(SearchStatus::Finished); - Q_EMIT q->searchFinished(allResults); + excludeNamePatterns.clear(); + for (const MatchSpan &span : intent.consumedSpans()) { + const QVariantMap meta = ruleEngine->ruleMetadataById(span.ruleId()); + const QVariant ep = meta.value("exclude_patterns"); + if (ep.userType() == QMetaType::QVariantList) { + for (const auto &v : ep.toList()) { + const QString p = v.toString(); + if (!p.isEmpty() && !excludeNamePatterns.contains(p)) + excludeNamePatterns.append(p); } } - }; + } - // 引擎错误也必须触发计数归零检查,否则上层会一直阻塞到 60s 超时。 - // 校验失败(如 KeywordIsEmpty)会让 GenericSearchEngine::search 在 - // emit errorOccurred 后直接 return,不会 emit searchFinished —— - // 此时必须由 onError 兜底减计数,把错误视为引擎完成。 - auto onError = [this, onFinished](const SearchError &error) { - qWarning() << "Search error:" << error.message(); - // Don't propagate individual engine errors to caller - // The other engines may still produce valid results - // 但仍需减计数,否则 pendingFinishCount 永不到 0 - onFinished({}); - }; + // Step 5: Set up signal/slot handlers + auto onFinished = [this](const SearchResultList &r) { onEngineFinished(r); }; + auto onError = [this](const SearchError &e) { onEngineError(e); }; // Step 6: Clean up any previous search engines pendingFinishCount.store(0); @@ -307,6 +285,48 @@ void SemanticSearcherData::createAndLaunchEngine( engine->search(query); } +void SemanticSearcherData::onEngineFinished(const SearchResultList &results) +{ + for (const SearchResult &r : results) { + if (!excludeNamePatterns.isEmpty()) { + const QString fileName = QFileInfo(r.path()).fileName(); + bool excluded = std::any_of(excludeNamePatterns.cbegin(), excludeNamePatterns.cend(), + [&fileName](const QString &pattern) { + return QDir::match(pattern, fileName); + }); + if (excluded) continue; + } + if (!seenPaths.contains(r.path())) { + seenPaths.insert(r.path()); + allResults.append(r); + } + } + + if (pendingFinishCount.fetch_sub(1) == 1) { + timeoutTimer->stop(); + + if (maxResults > 0 && allResults.size() > maxResults) { + allResults = allResults.mid(0, maxResults); + } + + if (cancelled.load()) { + status.store(SearchStatus::Cancelled); + Q_EMIT q->statusChanged(SearchStatus::Cancelled); + Q_EMIT q->searchCancelled(); + } else { + status.store(SearchStatus::Finished); + Q_EMIT q->statusChanged(SearchStatus::Finished); + Q_EMIT q->searchFinished(allResults); + } + } +} + +void SemanticSearcherData::onEngineError(const SearchError &error) +{ + qWarning() << "Search error:" << error.message(); + onEngineFinished({}); +} + void SemanticSearcherData::doCancel() { cancelled.store(true); diff --git a/src/dfm-search/dfm-search-lib/semantic/semanticsearcher_p.h b/src/dfm-search/dfm-search-lib/semantic/semanticsearcher_p.h index facdd74..c70a6c5 100644 --- a/src/dfm-search/dfm-search-lib/semantic/semanticsearcher_p.h +++ b/src/dfm-search/dfm-search-lib/semantic/semanticsearcher_p.h @@ -62,6 +62,9 @@ class SemanticSearcherData std::function onFinished, std::function onError); + void onEngineFinished(const SearchResultList &results); + void onEngineError(const SearchError &error); + SemanticSearcher *q = nullptr; // State @@ -89,6 +92,7 @@ class SemanticSearcherData bool detailedResultsEnabled = false; int maxResults = 0; // 0 = unlimited QStringList excludedPaths; // directories to exclude from search + QStringList excludeNamePatterns; // glob patterns to filter from results (e.g. *_thumb.jpg) }; DFM_SEARCH_END_NS