Files
msime-windows/server/src/settings/dictionary_manager.cpp
T
2026-07-19 13:17:23 +08:00

350 lines
18 KiB
C++

#include "settings/dictionary_manager.h"
#include "config/ime_config.h"
#include "utils/common_utils.h"
#include "MetasequoiaImeEngine/common/helpcode_utils.h"
#include "MetasequoiaImeEngine/quanpin/quanpin_query.h"
#include "MetasequoiaImeEngine/quanpin/quanpin_utils.h"
#include <sqlite3.h>
#include <algorithm>
#include <cctype>
#include <memory>
#include <vector>
namespace SettingsDictionary
{
namespace json = boost::json;
namespace
{
struct DbCloser { void operator()(sqlite3 *db) const { if (db) sqlite3_close(db); } };
using Db = std::unique_ptr<sqlite3, DbCloser>;
struct StmtCloser { void operator()(sqlite3_stmt *stmt) const { if (stmt) sqlite3_finalize(stmt); } };
using Stmt = std::unique_ptr<sqlite3_stmt, StmtCloser>;
json::object Result(bool ok, std::string message)
{
return {{"ok", ok}, {"message", std::move(message)}, {"rows", json::array{}}};
}
std::string StringValue(const json::object &obj, const char *key)
{
const auto *value = obj.if_contains(key);
return value && value->is_string() ? std::string(value->as_string()) : std::string{};
}
int IntValue(const json::object &obj, const char *key, int fallback)
{
const auto *value = obj.if_contains(key);
return value && value->is_int64() ? static_cast<int>(value->as_int64()) : fallback;
}
bool IsAsciiWord(const std::string &value)
{
return !value.empty() && std::all_of(value.begin(), value.end(), [](unsigned char ch) {
return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '-' || ch == '\'';
});
}
Db OpenDatabase(const std::string &name, std::string &error)
{
sqlite3 *raw = nullptr;
const std::string path = CommonUtils::get_ime_data_path() + "\\" + name;
if (sqlite3_open_v2(path.c_str(), &raw, SQLITE_OPEN_READWRITE | SQLITE_OPEN_FULLMUTEX, nullptr) != SQLITE_OK)
{
error = raw ? sqlite3_errmsg(raw) : "无法打开数据库";
if (raw) sqlite3_close(raw);
return {};
}
sqlite3_busy_timeout(raw, 3000);
return Db(raw);
}
Stmt Prepare(sqlite3 *db, const std::string &sql, std::string &error)
{
sqlite3_stmt *raw = nullptr;
if (sqlite3_prepare_v2(db, sql.c_str(), -1, &raw, nullptr) != SQLITE_OK)
{
error = sqlite3_errmsg(db);
return {};
}
return Stmt(raw);
}
bool BindText(sqlite3_stmt *stmt, int index, const std::string &value)
{
return sqlite3_bind_text(stmt, index, value.c_str(), -1, SQLITE_TRANSIENT) == SQLITE_OK;
}
bool NormalizePinyin(const std::string &mode, const std::string &input, quanpin::Segments &segments,
std::string &normalized, std::string &message)
{
std::string source = input;
source.erase(std::remove_if(source.begin(), source.end(), [](unsigned char ch) { return std::isspace(ch); }), source.end());
std::transform(source.begin(), source.end(), source.begin(), [](unsigned char ch) { return static_cast<char>(std::tolower(ch)); });
source.erase(std::remove(source.begin(), source.end(), '\''), source.end());
const auto cuts = quanpin::cut_pinyin_by_mode(source, "correction");
std::string joined = cuts.empty() ? std::string{} : quanpin::join_segments(cuts.front());
std::string joined_without_delimiters = joined;
joined_without_delimiters.erase(std::remove(joined_without_delimiters.begin(), joined_without_delimiters.end(), '\''),
joined_without_delimiters.end());
if (cuts.empty() || cuts.front().empty() || joined_without_delimiters != source)
{
message = "全拼拼音不合法";
return false;
}
segments = cuts.front();
normalized = std::move(joined);
return true;
}
bool ValidateChineseEntry(const std::string &mode, const std::string &code, const std::string &word,
quanpin::Segments &segments, std::string &normalized, std::string &message)
{
if (word.empty()) { message = "词条不能为空"; return false; }
if (!NormalizePinyin(mode, code, segments, normalized, message)) return false;
const size_t han_count = HelpcodeUtils::count_han_chars(word);
if (han_count == 0 || han_count != segments.size())
{
message = "拼音音节数量必须与汉字数量一致";
return false;
}
return true;
}
json::object QueryChinese(const std::string &mode, const std::string &search)
{
std::string error;
Db db = OpenDatabase("msime.db", error);
if (!db) return Result(false, "打开拼音词库失败:" + error);
quanpin::Segments segments;
std::string normalized;
if (!search.empty() && !NormalizePinyin(mode, search, segments, normalized, error)) return Result(false, error);
json::array rows;
if (!normalized.empty())
{
const std::string table = quanpin::build_table_name(segments);
Stmt stmt = Prepare(db.get(), "SELECT key,value,weight FROM \"" + table + "\" WHERE key=?1 ORDER BY weight DESC", error);
if (!stmt || !BindText(stmt.get(), 1, normalized)) return Result(false, "查询失败:" + error);
while (sqlite3_step(stmt.get()) == SQLITE_ROW)
{
rows.push_back({{"code", reinterpret_cast<const char *>(sqlite3_column_text(stmt.get(), 0))},
{"word", reinterpret_cast<const char *>(sqlite3_column_text(stmt.get(), 1))},
{"weight", sqlite3_column_int(stmt.get(), 2)}});
}
}
json::object result = Result(true, rows.empty() ? "没有找到词条" : "查询成功");
result["rows"] = std::move(rows);
return result;
}
bool ChineseExists(sqlite3 *db, const quanpin::Segments &segments, const std::string &key, const std::string &word)
{
std::string error;
Stmt stmt = Prepare(db, "SELECT 1 FROM \"" + quanpin::build_table_name(segments) + "\" WHERE key=?1 AND value=?2 LIMIT 1", error);
if (!stmt || !BindText(stmt.get(), 1, key) || !BindText(stmt.get(), 2, word)) return false;
return sqlite3_step(stmt.get()) == SQLITE_ROW;
}
json::object MutateChinese(const json::object &request)
{
const std::string mode = StringValue(request, "dictionary");
const std::string action = StringValue(request, "action");
const std::string code = StringValue(request, "code");
const std::string word = StringValue(request, "word");
const int weight = (std::max)(0, IntValue(request, "weight", 10000));
quanpin::Segments segments;
std::string key, error;
if (!ValidateChineseEntry(mode, code, word, segments, key, error)) return Result(false, error);
Db db = OpenDatabase("msime.db", error);
if (!db) return Result(false, "打开拼音词库失败:" + error);
const std::string table = quanpin::build_table_name(segments);
if (action == "create")
{
if (ChineseExists(db.get(), segments, key, word)) return Result(false, "词条已经存在");
Stmt stmt = Prepare(db.get(), "INSERT INTO \"" + table + "\"(key,jp,value,weight) VALUES(?1,?2,?3,?4)", error);
if (!stmt || !BindText(stmt.get(), 1, key) || !BindText(stmt.get(), 2, quanpin::segments_to_jianpin(segments)) ||
!BindText(stmt.get(), 3, word) || sqlite3_bind_int(stmt.get(), 4, weight) != SQLITE_OK || sqlite3_step(stmt.get()) != SQLITE_DONE)
return Result(false, "新增失败:" + std::string(sqlite3_errmsg(db.get())));
return Result(true, "词条新增成功");
}
const std::string old_code = StringValue(request, "oldCode");
const std::string old_word = StringValue(request, "oldWord");
quanpin::Segments old_segments;
std::string old_key;
if (!ValidateChineseEntry(mode, old_code, old_word, old_segments, old_key, error)) return Result(false, "原词条无效:" + error);
const std::string old_table = quanpin::build_table_name(old_segments);
if (!ChineseExists(db.get(), old_segments, old_key, old_word)) return Result(false, "原词条不存在或已被删除");
if (action == "delete")
{
Stmt stmt = Prepare(db.get(), "DELETE FROM \"" + old_table + "\" WHERE key=?1 AND value=?2", error);
if (!stmt || !BindText(stmt.get(), 1, old_key) || !BindText(stmt.get(), 2, old_word) || sqlite3_step(stmt.get()) != SQLITE_DONE)
return Result(false, "删除失败:" + std::string(sqlite3_errmsg(db.get())));
return Result(true, "词条删除成功");
}
if (action != "update") return Result(false, "未知操作");
if ((old_key != key || old_word != word) && ChineseExists(db.get(), segments, key, word)) return Result(false, "修改后的词条已经存在");
sqlite3_exec(db.get(), "BEGIN IMMEDIATE", nullptr, nullptr, nullptr);
Stmt remove = Prepare(db.get(), "DELETE FROM \"" + old_table + "\" WHERE key=?1 AND value=?2", error);
Stmt insert = Prepare(db.get(), "INSERT INTO \"" + table + "\"(key,jp,value,weight) VALUES(?1,?2,?3,?4)", error);
const bool ok = remove && insert && BindText(remove.get(), 1, old_key) && BindText(remove.get(), 2, old_word) &&
sqlite3_step(remove.get()) == SQLITE_DONE && BindText(insert.get(), 1, key) &&
BindText(insert.get(), 2, quanpin::segments_to_jianpin(segments)) && BindText(insert.get(), 3, word) &&
sqlite3_bind_int(insert.get(), 4, weight) == SQLITE_OK && sqlite3_step(insert.get()) == SQLITE_DONE;
sqlite3_exec(db.get(), ok ? "COMMIT" : "ROLLBACK", nullptr, nullptr, nullptr);
return Result(ok, ok ? "词条修改成功" : "修改失败:" + std::string(sqlite3_errmsg(db.get())));
}
json::object HandleEnglish(const json::object &request)
{
const std::string action = StringValue(request, "action");
std::string word = StringValue(request, "word");
const std::string display = StringValue(request, "display");
std::transform(word.begin(), word.end(), word.begin(), [](unsigned char ch) { return static_cast<char>(std::tolower(ch)); });
std::string error;
Db db = OpenDatabase("english.db", error);
if (!db) return Result(false, "打开英文词库失败:" + error);
if (action == "query")
{
Stmt stmt = Prepare(db.get(), "SELECT word,display FROM english_words WHERE word LIKE ?1 ORDER BY word", error);
const std::string pattern = word + "%";
if (!stmt || !BindText(stmt.get(), 1, pattern)) return Result(false, "查询失败");
json::array rows;
while (sqlite3_step(stmt.get()) == SQLITE_ROW)
rows.push_back({{"word", reinterpret_cast<const char *>(sqlite3_column_text(stmt.get(), 0))},
{"display", reinterpret_cast<const char *>(sqlite3_column_text(stmt.get(), 1))}});
json::object result = Result(true, rows.empty() ? "没有找到词条" : "查询成功"); result["rows"] = std::move(rows); return result;
}
if (!IsAsciiWord(word) || display.empty()) return Result(false, "英文单词仅支持英文字母、连字符和撇号,显示内容不能为空");
const std::string old_word = StringValue(request, "oldWord");
std::string sql;
if (action == "create") sql = "INSERT INTO english_words(word,display) VALUES(?1,?2)";
else if (action == "update") sql = "UPDATE english_words SET word=?1,display=?2 WHERE word=?3";
else if (action == "delete") sql = "DELETE FROM english_words WHERE word=?1";
else return Result(false, "未知操作");
Stmt stmt = Prepare(db.get(), sql, error);
bool ok = stmt && BindText(stmt.get(), 1, action == "delete" ? old_word : word);
if (ok && action != "delete") ok = BindText(stmt.get(), 2, display);
if (ok && action == "update") ok = BindText(stmt.get(), 3, old_word);
ok = ok && sqlite3_step(stmt.get()) == SQLITE_DONE && sqlite3_changes(db.get()) > 0;
const char *label = action == "create" ? "新增" : action == "update" ? "修改" : "删除";
return Result(ok, ok ? std::string("英文词条") + label + "成功" : std::string(label) + "失败:" + sqlite3_errmsg(db.get()));
}
json::object HandleWubi(const json::object &request)
{
const std::string action = StringValue(request, "action");
std::string code = StringValue(request, "code");
const std::string word = StringValue(request, "word");
const int weight = (std::max)(0, IntValue(request, "weight", 10000));
std::transform(code.begin(), code.end(), code.begin(), [](unsigned char ch) { return static_cast<char>(std::tolower(ch)); });
std::string error;
Db db = OpenDatabase("msime.db", error);
if (!db) return Result(false, "打开五笔词库失败:" + error);
if (action == "query")
{
if (code.empty() || !std::all_of(code.begin(), code.end(), [](unsigned char ch) { return ch >= 'a' && ch <= 'z'; }))
return Result(false, "请输入合法的五笔编码");
Stmt stmt = Prepare(db.get(), "SELECT key,value,weight FROM wubi86 WHERE key LIKE ?1 ORDER BY weight DESC", error);
if (!stmt || !BindText(stmt.get(), 1, code + "%")) return Result(false, "查询失败:" + error);
json::array rows;
while (sqlite3_step(stmt.get()) == SQLITE_ROW)
rows.push_back({{"code", reinterpret_cast<const char *>(sqlite3_column_text(stmt.get(), 0))},
{"word", reinterpret_cast<const char *>(sqlite3_column_text(stmt.get(), 1))},
{"weight", sqlite3_column_int(stmt.get(), 2)}});
json::object result = Result(true, rows.empty() ? "没有找到词条" : "查询成功"); result["rows"] = std::move(rows); return result;
}
if (code.empty() || word.empty() || !std::all_of(code.begin(), code.end(), [](unsigned char ch) { return ch >= 'a' && ch <= 'z'; }))
return Result(false, "五笔编码和词条不能为空,编码只能包含英文字母");
const std::string old_code = StringValue(request, "oldCode");
const std::string old_word = StringValue(request, "oldWord");
std::string sql;
if (action == "create") sql = "INSERT INTO wubi86(key,value,weight) VALUES(?1,?2,?3)";
else if (action == "update") sql = "UPDATE wubi86 SET key=?1,value=?2,weight=?3 WHERE key=?4 AND value=?5";
else if (action == "delete") sql = "DELETE FROM wubi86 WHERE key=?1 AND value=?2";
else return Result(false, "未知操作");
Stmt stmt = Prepare(db.get(), sql, error);
bool ok = stmt != nullptr;
if (ok && action == "delete") ok = BindText(stmt.get(), 1, old_code) && BindText(stmt.get(), 2, old_word);
else if (ok)
{
ok = BindText(stmt.get(), 1, code) && BindText(stmt.get(), 2, word) && sqlite3_bind_int(stmt.get(), 3, weight) == SQLITE_OK;
if (ok && action == "update") ok = BindText(stmt.get(), 4, old_code) && BindText(stmt.get(), 5, old_word);
}
ok = ok && sqlite3_step(stmt.get()) == SQLITE_DONE && sqlite3_changes(db.get()) > 0;
const char *label = action == "create" ? "新增" : action == "update" ? "修改" : "删除";
return Result(ok, ok ? std::string("五笔词条") + label + "成功" : std::string(label) + "失败:" + sqlite3_errmsg(db.get()));
}
json::object HandleQuickPhrase(const json::object &request)
{
const std::string action = StringValue(request, "action");
std::string code = StringValue(request, "code");
const std::string phrase = StringValue(request, "word");
const int weight = (std::max)(0, IntValue(request, "weight", 10));
std::transform(code.begin(), code.end(), code.begin(), [](unsigned char ch) { return static_cast<char>(std::tolower(ch)); });
const auto valid_code = [](const std::string &value) {
return !value.empty() && std::all_of(value.begin(), value.end(), [](unsigned char ch) { return ch >= 'a' && ch <= 'z'; });
};
std::string error;
Db db = OpenDatabase("msime.db", error);
if (!db) return Result(false, "打开快捷短语表失败:" + error);
if (action == "query")
{
if (!code.empty() && !valid_code(code)) return Result(false, "编码只能包含英文字母");
Stmt stmt = Prepare(db.get(), "SELECT key,value,weight FROM quick_parases WHERE key LIKE ?1 "
"ORDER BY weight DESC,key,value", error);
if (!stmt || !BindText(stmt.get(), 1, code + "%")) return Result(false, "查询失败:" + error);
json::array rows;
while (sqlite3_step(stmt.get()) == SQLITE_ROW)
rows.push_back({{"code", reinterpret_cast<const char *>(sqlite3_column_text(stmt.get(), 0))},
{"word", reinterpret_cast<const char *>(sqlite3_column_text(stmt.get(), 1))},
{"weight", sqlite3_column_int(stmt.get(), 2)}});
json::object result = Result(true, rows.empty() ? "没有找到快捷短语" : "查询成功");
result["rows"] = std::move(rows);
return result;
}
if (!valid_code(code) || phrase.empty()) return Result(false, "编码只能包含英文字母,短语不能为空");
const std::string old_code = StringValue(request, "oldCode");
const std::string old_phrase = StringValue(request, "oldWord");
std::string sql;
if (action == "create") sql = "INSERT INTO quick_parases(key,value,weight) VALUES(?1,?2,?3)";
else if (action == "update") sql = "UPDATE quick_parases SET key=?1,value=?2,weight=?3 WHERE key=?4 AND value=?5";
else if (action == "delete") sql = "DELETE FROM quick_parases WHERE key=?1 AND value=?2";
else return Result(false, "未知操作");
Stmt stmt = Prepare(db.get(), sql, error);
bool ok = stmt != nullptr;
if (ok && action == "delete") ok = BindText(stmt.get(), 1, old_code) && BindText(stmt.get(), 2, old_phrase);
else if (ok)
{
ok = BindText(stmt.get(), 1, code) && BindText(stmt.get(), 2, phrase) &&
sqlite3_bind_int(stmt.get(), 3, weight) == SQLITE_OK;
if (ok && action == "update") ok = BindText(stmt.get(), 4, old_code) && BindText(stmt.get(), 5, old_phrase);
}
ok = ok && sqlite3_step(stmt.get()) == SQLITE_DONE && sqlite3_changes(db.get()) > 0;
const char *label = action == "create" ? "新增" : action == "update" ? "修改" : "删除";
return Result(ok, ok ? std::string("快捷短语") + label + "成功" : std::string(label) + "失败:" + sqlite3_errmsg(db.get()));
}
}
json::object HandleRequest(const json::object &request)
{
const std::string dictionary = StringValue(request, "dictionary");
const std::string action = StringValue(request, "action");
if (dictionary == "english") return HandleEnglish(request);
if (dictionary == "wubi") return HandleWubi(request);
if (dictionary == "quick") return HandleQuickPhrase(request);
if (dictionary != "quanpin") return Result(false, "未知词库");
if (action == "query") return QueryChinese(dictionary, StringValue(request, "code"));
return MutateChinese(request);
}
}