CreoOtkPluging/ModelSearchHandler.cpp
sladro b874b17534 实现模型搜索功能 - 支持名称模糊匹配和完整层级路径显示
新增ModelSearchEngine和ModelSearchHandler模块,提供零件和装配体名称搜索功能:
• 支持prefix、contains、fuzzy三种匹配模式
• 从根装配体构建完整模型树层级路径
• 智能去重算法自动去除重复结果,保留最长路径
• 递归搜索支持多层级装配体遍历
• 向后兼容,不影响现有功能

解决搜索重复结果和缺少层级路径问题,提升用户体验。

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-28 18:36:12 +08:00

379 lines
14 KiB
C++

#include "pch.h"
#include "ModelSearchHandler.h"
#include <algorithm>
#include <cctype>
// Main HTTP request handler
HttpResponse ModelSearchHandler::HandleModelSearchRequest(const HttpRequest& request) {
HttpResponse response;
// Validate request method
if (request.method != "POST") {
return FormatErrorResponse(405, "Method not allowed. Use POST.");
}
// Validate request
std::string error_message;
if (!ValidateHttpRequest(request, error_message)) {
return FormatErrorResponse(400, error_message);
}
try {
// Parse search request
ModelSearchRequest search_request = ParseSearchRequest(request.body);
// Execute search
ModelSearchResult search_result = ModelSearchEngine::Instance().SearchModels(search_request);
// Format response
if (search_result.success) {
response = FormatSuccessResponse(search_result);
} else {
response = FormatErrorResponse(500, search_result.error_message);
}
}
catch (const std::exception& e) {
response = FormatErrorResponse(500, "Request processing error: " + std::string(e.what()));
}
catch (...) {
response = FormatErrorResponse(500, "Unknown error during model search");
}
return response;
}
// Request validation method
bool ModelSearchHandler::ValidateHttpRequest(const HttpRequest& request, std::string& error_message) {
if (request.body.empty()) {
error_message = "Request body cannot be empty";
return false;
}
// Check basic JSON format
if (request.body.find("{") == std::string::npos || request.body.find("}") == std::string::npos) {
error_message = "Invalid JSON format in request body";
return false;
}
return true;
}
// Parse search request
ModelSearchRequest ModelSearchHandler::ParseSearchRequest(const std::string& json_body) {
ModelSearchRequest request;
// Parse required parameters
request.query = Trim(ExtractJsonValue(json_body, "query"));
if (request.query.empty()) {
request.query = Trim(ExtractJsonValue(json_body, "search_query"));
}
// Parse optional parameters
std::string match_mode = ExtractJsonValue(json_body, "match_mode");
if (!match_mode.empty()) {
request.match_mode = match_mode;
}
std::string search_scope = ExtractJsonValue(json_body, "search_scope");
if (!search_scope.empty()) {
request.search_scope = search_scope;
}
// Parse model type filter
std::vector<std::string> model_types = ExtractJsonArrayValue(json_body, "model_types");
if (!model_types.empty()) {
request.model_types = model_types;
}
// Parse numeric parameters
int max_results = ExtractJsonIntValue(json_body, "max_results", 0);
if (max_results > 0) {
request.max_results = max_results;
}
double similarity_threshold = ExtractJsonDoubleValue(json_body, "similarity_threshold", -1.0);
if (similarity_threshold >= 0.0 && similarity_threshold <= 1.0) {
request.similarity_threshold = similarity_threshold;
}
// Parse boolean parameters
request.include_components = ExtractJsonBoolValue(json_body, "include_components", request.include_components);
request.include_features = ExtractJsonBoolValue(json_body, "include_features", request.include_features);
return request;
}
// Format success response
HttpResponse ModelSearchHandler::FormatSuccessResponse(const ModelSearchResult& result) {
HttpResponse response;
response.status_code = 200;
std::ostringstream json;
json << "{"
<< "\"success\": true,"
<< "\"data\": {"
<< "\"results\": [";
// Build search results list
bool first_item = true;
for (const auto& item : result.results) {
if (!first_item) json << ",";
first_item = false;
json << "{"
<< "\"model_name\": \"" << EscapeJsonString(item.model_name) << "\","
<< "\"display_name\": \"" << EscapeJsonString(item.display_name) << "\","
<< "\"full_path\": \"" << EscapeJsonString(item.full_path) << "\","
<< "\"model_type\": \"" << EscapeJsonString(item.model_type) << "\","
<< "\"component_path\": \"" << EscapeJsonString(item.component_path) << "\","
<< "\"match_score\": " << item.match_score << ","
<< "\"match_reason\": \"" << EscapeJsonString(item.match_reason) << "\","
<< "\"match_type\": \"" << EscapeJsonString(item.match_type) << "\","
<< "\"is_in_session\": " << (item.is_in_session ? "true" : "false") << ","
<< "\"is_assembly\": " << (item.is_assembly ? "true" : "false") << ","
<< "\"component_count\": " << item.component_count << ","
<< "\"file_size\": \"" << EscapeJsonString(item.file_size) << "\","
<< "\"last_modified\": \"" << EscapeJsonString(item.last_modified) << "\","
<< "\"matched_keywords\": [";
// Build matched keywords list
bool first_keyword = true;
for (const auto& keyword : item.matched_keywords) {
if (!first_keyword) json << ",";
first_keyword = false;
json << "\"" << EscapeJsonString(keyword) << "\"";
}
json << "]"
<< "}";
}
json << "],"
<< "\"total_found\": " << result.total_found << ","
<< "\"returned_count\": " << result.returned_count << ","
<< "\"search_time_ms\": \"" << EscapeJsonString(result.search_time_ms) << "\","
<< "\"stats\": {"
<< "\"session_models_count\": " << result.stats.session_models_count << ","
<< "\"components_searched\": " << result.stats.components_searched << ","
<< "\"features_searched\": " << result.stats.features_searched << ","
<< "\"search_scope_used\": \"" << EscapeJsonString(result.stats.search_scope_used) << "\""
<< "}"
<< "},"
<< "\"error\": null"
<< "}";
response.body = json.str();
return response;
}
// Format error response
HttpResponse ModelSearchHandler::FormatErrorResponse(int status_code, const std::string& error_message) {
HttpResponse response;
response.status_code = status_code;
std::ostringstream json;
json << "{"
<< "\"success\": false,"
<< "\"data\": null,"
<< "\"error\": \"" << EscapeJsonString(error_message) << "\""
<< "}";
response.body = json.str();
return response;
}
// JSON parsing helper methods
std::string ModelSearchHandler::ExtractJsonValue(const std::string& json, const std::string& key) {
// Find key-value pair "key": "value" or "key":"value"
std::string key_pattern = "\"" + key + "\"";
size_t key_pos = json.find(key_pattern);
if (key_pos != std::string::npos) {
// Find colon
size_t colon_pos = json.find(":", key_pos);
if (colon_pos != std::string::npos) {
// Skip spaces to find value start
size_t value_start = colon_pos + 1;
while (value_start < json.length() &&
(json[value_start] == ' ' || json[value_start] == '\t' ||
json[value_start] == '\n' || json[value_start] == '\r')) {
value_start++;
}
// Check if it's a string value (starts with double quote)
if (value_start < json.length() && json[value_start] == '"') {
value_start++; // Skip opening quote
size_t value_end = json.find('"', value_start);
if (value_end != std::string::npos) {
return json.substr(value_start, value_end - value_start);
}
}
else {
// Non-string value, find until comma, brace or string end
size_t value_end = value_start;
while (value_end < json.length() &&
json[value_end] != ',' && json[value_end] != '}' &&
json[value_end] != ']' && json[value_end] != '\n') {
value_end++;
}
std::string value = Trim(json.substr(value_start, value_end - value_start));
return value;
}
}
}
return "";
}
bool ModelSearchHandler::ExtractJsonBoolValue(const std::string& json, const std::string& key, bool default_value) {
std::string value = ExtractJsonValue(json, key);
if (value.empty()) {
return default_value;
}
// Convert to lowercase for comparison
std::transform(value.begin(), value.end(), value.begin(), ::tolower);
return (value == "true" || value == "1");
}
int ModelSearchHandler::ExtractJsonIntValue(const std::string& json, const std::string& key, int default_value) {
std::string value = ExtractJsonValue(json, key);
if (value.empty()) {
return default_value;
}
try {
return std::stoi(value);
}
catch (...) {
return default_value;
}
}
double ModelSearchHandler::ExtractJsonDoubleValue(const std::string& json, const std::string& key, double default_value) {
std::string value = ExtractJsonValue(json, key);
if (value.empty()) {
return default_value;
}
try {
return std::stod(value);
}
catch (...) {
return default_value;
}
}
std::vector<std::string> ModelSearchHandler::ExtractJsonArrayValue(const std::string& json, const std::string& key) {
std::vector<std::string> result;
// Find key-value pair "key": [...]
std::string key_pattern = "\"" + key + "\"";
size_t key_pos = json.find(key_pattern);
if (key_pos != std::string::npos) {
// Find colon
size_t colon_pos = json.find(":", key_pos);
if (colon_pos != std::string::npos) {
// Skip spaces to find array start
size_t array_start = colon_pos + 1;
while (array_start < json.length() &&
(json[array_start] == ' ' || json[array_start] == '\t' ||
json[array_start] == '\n' || json[array_start] == '\r')) {
array_start++;
}
// Check if it's an array (starts with [)
if (array_start < json.length() && json[array_start] == '[') {
size_t array_end = json.find(']', array_start);
if (array_end != std::string::npos) {
std::string array_content = json.substr(array_start + 1, array_end - array_start - 1);
// Parse array elements
size_t pos = 0;
while (pos < array_content.length()) {
// Skip spaces
while (pos < array_content.length() &&
(array_content[pos] == ' ' || array_content[pos] == '\t' ||
array_content[pos] == '\n' || array_content[pos] == '\r')) {
pos++;
}
if (pos >= array_content.length()) break;
// Find string value
if (array_content[pos] == '"') {
pos++; // Skip opening quote
size_t end_quote = array_content.find('"', pos);
if (end_quote != std::string::npos) {
result.push_back(array_content.substr(pos, end_quote - pos));
pos = end_quote + 1;
}
}
// Find next comma
size_t next_comma = array_content.find(',', pos);
if (next_comma != std::string::npos) {
pos = next_comma + 1;
} else {
break;
}
}
}
}
}
}
return result;
}
// JSON escape method
std::string ModelSearchHandler::EscapeJsonString(const std::string& input) {
std::string escaped;
escaped.reserve(input.length() * 2);
for (char c : input) {
switch (c) {
case '"': escaped += "\\\""; break;
case '\\': escaped += "\\\\"; break;
case '\b': escaped += "\\b"; break;
case '\f': escaped += "\\f"; break;
case '\n': escaped += "\\n"; break;
case '\r': escaped += "\\r"; break;
case '\t': escaped += "\\t"; break;
default:
if (c < 0x20) {
// Control character escape to Unicode
char buffer[7];
snprintf(buffer, sizeof(buffer), "\\u%04x", static_cast<unsigned char>(c));
escaped += buffer;
} else {
escaped += c;
}
break;
}
}
return escaped;
}
// Helper utility methods
std::string ModelSearchHandler::Trim(const std::string& str) {
size_t start = str.find_first_not_of(" \t\n\r");
if (start == std::string::npos) return "";
size_t end = str.find_last_not_of(" \t\n\r");
return str.substr(start, end - start + 1);
}
bool ModelSearchHandler::StartsWith(const std::string& str, const std::string& prefix) {
return str.size() >= prefix.size() && str.substr(0, prefix.size()) == prefix;
}
bool ModelSearchHandler::EndsWith(const std::string& str, const std::string& suffix) {
return str.size() >= suffix.size() &&
str.substr(str.size() - suffix.size()) == suffix;
}