Files
Lu Fan(SonnyCalcr) 692bed7d7c fix(engine): 出厂词库换过时,回放不再用旧量级的调频权重压住新词频 (#1049)
用户词库日志存的是调频写下的绝对权重。词库升级换了权重量级后(msime-dictionary
把拼音词频对齐万象,「了 le」从 348 万变成 5200 万),安装时的回放仍把旧词库上
调出来的 650 万写回去,打 l 时「了」排到「来」「里」后面。

回放现在用数据目录里 dictionary-manifest.json 记的 msime.db SHA-256 认出厂词库,
并在日志新增的 dictionary_state 表记下回放到哪份。同一份词库照旧原样写回;换了
词库(或日志还没有记录)时,自动调频的行只在高过新出厂权重时写回,否则保留新
权重并把日志改记成新权重。用户手动加的词和删除不变。

设置页改权重改走新的 record_weight_edit,记在新增的 weight_edited 列里,换词库时
照写不让位;之后的自动调频覆盖同一行时清掉这个标记。
2026-10-18 10:40:59 +08:00

151 lines
11 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#pragma once
#include <string>
#include <vector>
#include <functional>
#include <optional>
#include "../core/word_item.h"
namespace user_dictionary
{
enum class DictionaryKind
{
Pinyin,
Wubi,
QuickPhrase,
English,
};
std::string default_user_db_path();
// Compatibility no-op; journal connections are operation-scoped.
void close_default_user_database();
bool record_upsert(const std::string &user_db_path, DictionaryKind kind, const std::string &key,
const std::string &value, std::int64_t weight, const std::string &display = {});
// 用户在设置页给词库里已有的词改了权重。和 record_upsert 一样写一条 upsert,但记下这是用户
// 亲手定的值:出厂词库换过时回放照写这个权重,不像自动调频那样让位给新的出厂权重(见 replay)。
bool record_weight_edit(const std::string &user_db_path, DictionaryKind kind, const std::string &key,
const std::string &value, std::int64_t weight, const std::string &display = {});
bool record_user_insert(const std::string &user_db_path, DictionaryKind kind, const std::string &key,
const std::string &value, std::int64_t weight, const std::string &display = {});
bool record_delete(const std::string &user_db_path, DictionaryKind kind, const std::string &key,
const std::string &value);
bool is_user_inserted(const std::string &user_db_path, DictionaryKind kind, const std::string &key,
const std::string &value);
// 该键下是否有用户留下的权重(调频或造词的 upsert,不看具体词)。删除记录不算:
// 它表达的是「不要这个词」,不是对这组读音的偏好。
bool has_user_upsert_for_key(const std::string &user_db_path, DictionaryKind kind, const std::string &key);
bool ensure_user_database(const std::string &user_db_path);
// Switch an existing dictionary database (msime.db) to WAL so the key thread's
// queries never wait for a learning write's commit. The user journal does this
// on every connection it opens. Returns whether the database is now in WAL mode.
bool enable_write_ahead_log(const std::string &db_path);
bool record_pinyin_upsert_from_database(const std::string &main_db_path, const std::string &key,
const std::string &value,
const std::string &user_db_path = default_user_db_path());
// Raise the selected wubi candidate to the top of its own code group (max weight + 1, same
// semantics as the quanpin side) and persist the new weight plus a journal upsert in one
// attached-database transaction. Existing rows only: a missing (key,value) pair returns false
// without inserting. Row-missing behaviour matches update_wubi_weight.
bool bump_wubi_weight(const std::string &main_db_path, const std::string &user_db_path, const std::string &key,
const std::string &value);
// 快捷短语混排的调频状态。快捷短语与拼音候选的权重不在同一个量纲上,不能互相比较,所以
// 按编码记一个槽位:同码快捷短语组前面排几个普通候选(见
// metasequoia::local_modes::counts_toward_quick_phrase_slot)。没有记录即 0,组在首位。
int quick_phrase_slot(const std::string &user_db_path, const std::string &code);
// 用户选中了组里的一条快捷短语。累计到 trigger_count 次后按调频模式把组往前挪(槽位按
// ranking_target 缩小),选中的不是组内第一条时再把它的权重升到组内最高 + 1 并记日志。
// mode 为 "disabled" 时什么都不做。
bool learn_quick_phrase_selection(const std::string &main_db_path, const std::string &user_db_path,
const std::string &code, const std::string &value, bool first_in_group,
const std::string &mode, int linear_step, int trigger_count);
// 用户越过快捷短语组选了排在组后面的普通候选,ordinary_rank 是它在普通候选里的名次。
// 把整组当成它前面的一个位置,它按调频模式的目标位置越过这一组时,累计到 trigger_count
// 次后槽位 + 1,最多到 max_slot(首页最后一位)。
bool learn_quick_phrase_bypass(const std::string &user_db_path, const std::string &code, int ordinary_rank,
const std::string &mode, int linear_step, int trigger_count, int max_slot);
// 中英混输里英文候选的调频状态,思路同快捷短语:英文词库和中文词库的权重不在同一个量纲上,
// 不能比大小,所以按输入(小写)记一个槽位:首个英文候选在整个候选列表里的下标,0 是首位。
// 没有记录时由调用方决定默认位置(精确匹配紧跟第一个中文候选,补全词在首页末位)。
std::optional<int> english_slot(const std::string &user_db_path, const std::string &code);
// 用户显式置顶英文候选:直接写槽位,不走计数。
bool set_english_slot(const std::string &user_db_path, const std::string &code, int slot);
// 用户选中了占着槽位的英文候选,english_index 是它此刻在列表里的下标。累计到 trigger_count
// 次后,按调频模式从这个名次算出目标位置写成槽位,和中文调频一样可以一路升到首位。
bool learn_english_slot_selection(const std::string &user_db_path, const std::string &code, int english_index,
const std::string &mode, int linear_step, int trigger_count);
// 用户越过英文候选选了排在它后面的候选。english_index / selected_index 是两者在列表里的下标,
// 选中的候选按调频模式的目标位置越过英文时,累计到 trigger_count 次后英文后退一位,最多到 max_slot。
bool learn_english_slot_bypass(const std::string &user_db_path, const std::string &code, int english_index,
int selected_index, const std::string &mode, int linear_step, int trigger_count,
int max_slot);
// 日期时间格式(rq / sj / xq 那几组)的调频状态。日期文本每天都变,不能按文字记,所以按格式 ID
// (WordItem::pinyin,见 local_modes::query_date_time)记一份整组的顺序,category 是
// local_modes::date_time_category 的分组名。Shift+T 模式和混输共用这一份:混输取的就是排第一的格式。
// default_ids 是这组格式的出厂顺序(local_modes::date_time_format_ids),后来新增的格式按它插回去。
//
// 按学到的顺序重排一组日期时间候选(learned_order 为 false 时跳过,调频关闭时用),再按固定位置摆放
// (fixed_position 置为固定的位置)。
void apply_date_time_order(const std::string &user_db_path, const std::string &category,
std::vector<WordItem> &candidates, bool learned_order = true);
// 用户选中了 format_id。累计到 trigger_count 次后按调频模式把它往前挪,mode 为 "disabled" 时什么都不做。
bool learn_date_time_selection(const std::string &user_db_path, const std::string &category,
const std::vector<std::string> &default_ids, const std::string &format_id,
const std::string &mode, int linear_step, int trigger_count);
// 用户显式置顶:直接挪到首位,不走计数。
bool pin_date_time_format(const std::string &user_db_path, const std::string &category,
const std::vector<std::string> &default_ids, const std::string &format_id);
// 固定位置写进 fixed_candidate_positions 用的 context_key,entry_key 与 value 都写格式 ID。
std::string date_time_fixed_position_context(const std::string &category);
struct ReplayResult
{
int applied = 0;
int skipped = 0;
int failed = 0;
// 出厂词库换过时,日志里低于新出厂权重、因而保留新权重并改记进日志的自动调频行数。
int rebased = 0;
std::string error;
};
ReplayResult replay(const std::string &user_db_path, const std::string &main_db_path,
const std::string &english_db_path);
bool adjust_candidate_ranking(const std::string &main_db_path, const std::string &user_db_path,
const std::string &context_key, const std::vector<WordItem> &ordered_candidates,
const std::string &entry_key, const std::string &value, const std::string &mode,
int linear_step, int trigger_count, bool force_top, bool *ranking_changed = nullptr,
DictionaryKind kind = DictionaryKind::Pinyin);
bool adjust_english_candidate_ranking(const std::string &english_db_path, const std::string &user_db_path,
const std::string &context_key, const std::vector<WordItem> &ordered_candidates,
const std::string &entry_key, const std::string &value, const std::string &mode,
int linear_step, int trigger_count, bool force_top,
bool *ranking_changed = nullptr);
// Delete an exact dictionary key and persist its tombstone in one attached-database
// transaction. Supports Pinyin, Wubi and English; false preserves candidate rows and journal operations
// on statement/commit failures (not a cross-file power-loss guarantee in WAL mode).
bool delete_dictionary_candidate(const std::string &dictionary_db_path, const std::string &user_db_path,
DictionaryKind kind, const std::string &entry_key, const std::string &value);
bool delete_english_candidate(const std::string &english_db_path, const std::string &user_db_path,
const std::string &entry_key, const std::string &value);
bool learn_entered_english_word(const std::string &english_db_path, const std::string &user_db_path,
const std::string &display, std::int64_t weight = 10);
bool set_fixed_position(const std::string &user_db_path, const std::string &context_key, const std::string &entry_key,
const std::string &value, int position);
bool clear_fixed_position(const std::string &user_db_path, const std::string &context_key, const std::string &entry_key,
const std::string &value);
bool is_fixed(const std::string &user_db_path, const std::string &context_key, const std::string &entry_key,
const std::string &value);
// Cloud/AI suggestions are normally hoisted back to their fixed slots (cloud at
// index 1, AI at index 2). Set keep_dynamic_candidate_positions when the caller
// already decided where they belong, e.g. after helpcode filtering.
void apply_fixed_positions(
const std::string &user_db_path, const std::string &context_key, std::vector<WordItem> &candidates,
bool include_missing,
const std::function<std::optional<WordItem>(const std::string &, const std::string &)> &find_candidate = {},
bool keep_dynamic_candidate_positions = false);
} // namespace user_dictionary