Skip to content

Commit dd255cd

Browse files
committed
fix(schema): 按方案隔离 codetable-extra 附加层, 防止跨方案污染
切到虎码再切回五笔时, 虎码的 tigress* 附加层残留在共享 CompositeDict 上, 导致 wq 首选变成"寒"而不是五笔的"你". 根因是 applySwitchLocked 只清理主层(codetable-system / pinyin-system), 没清理附加层. - EngineBundle 增加 ExtraLayers, 工厂收集所有附加层供 Manager 缓存 - ExtraLayerName 把层名加上 <schemaID>__ 前缀, 避免不同方案同名 dict 互相覆盖 (cacheKey 已经按 schemaID_dictID 命名, 现在 layerName 也对齐) - Manager.systemExtras 按方案缓存附加层列表; applySwitchLocked 在切换前 清理所有方案的 extras, reRegisterSystemLayer 再仅重挂当前方案的 - ReloadExtraDicts 返回当前方案启用的 layer 列表, reload_handler 通过 SetSystemExtras 同步到 Manager, 保证"切走再切回"能恢复热重载后的状态 wdb 缓存文件名未变, 升级零迁移成本.
1 parent f839d81 commit dd255cd

3 files changed

Lines changed: 96 additions & 20 deletions

File tree

wind_input/internal/coordinator/reload_handler.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,10 +190,15 @@ func (h *ReloadHandler) reloadActiveSchemaConfig() {
190190
}
191191
}
192192

193-
// 附加词库热重载(根据 enabled 字段动态加载/卸载 dict layer)
193+
// 附加词库热重载(根据 enabled 字段动态加载/卸载 dict layer)。
194+
// 同步结果到 engineMgr.systemExtras,保证后续"切走再切回该方案"时仍能恢复附加层;
195+
// 否则 applySwitchLocked 会按旧缓存清理 / 重挂,热重载后的状态会被覆盖。
194196
if h.dictMgr != nil && s.Engine.Type == schema.EngineTypeCodeTable {
195197
exeDir, dataDir := h.schemaMgr.GetDirs()
196-
schema.ReloadExtraDicts(h.dictMgr, s, exeDir, dataDir, h.logger)
198+
layers := schema.ReloadExtraDicts(h.dictMgr, s, exeDir, dataDir, h.logger)
199+
if h.engineMgr != nil {
200+
h.engineMgr.SetSystemExtras(s.Schema.ID, layers)
201+
}
197202
}
198203

199204
// 学习配置热更新(调频 + 造词)

wind_input/internal/engine/manager.go

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,10 @@ type Manager struct {
4545
// engineBuildMu 串行化引擎创建过程,避免同一方案被并发构建。
4646
// 与 m.mu 解耦,重 IO 期间不持 m.mu,按键路径不被阻塞。
4747
engineBuildMu sync.Mutex
48-
engines map[string]Engine // schemaID -> Engine
49-
systemLayers map[string]dict.DictLayer // schemaID -> 该方案注册的系统词库层
50-
currentID string // 当前活跃方案 ID
48+
engines map[string]Engine // schemaID -> Engine
49+
systemLayers map[string]dict.DictLayer // schemaID -> 该方案注册的主系统词库层
50+
systemExtras map[string][]dict.DictLayer // schemaID -> 该方案注册的附加词库层(codetable-extra-*)
51+
currentID string // 当前活跃方案 ID
5152
currentEngine Engine
5253

5354
// 临时方案切换
@@ -96,6 +97,7 @@ func NewManager(logger *slog.Logger) *Manager {
9697
m := &Manager{
9798
engines: make(map[string]Engine),
9899
systemLayers: make(map[string]dict.DictLayer),
100+
systemExtras: make(map[string][]dict.DictLayer),
99101
logger: logger,
100102
learningCh: make(chan learningEvent, learningChanCapacity),
101103
}
@@ -285,10 +287,24 @@ func (m *Manager) SwitchSchema(schemaID string) error {
285287

286288
// applySwitchLocked 执行系统词库层切换并更新 currentID/currentEngine。
287289
// 调用方必须持有 m.mu 写锁。
290+
//
291+
// 隔离策略:
292+
// - 主层(codetable-system / pinyin-system)固定按名清理。
293+
// - 附加层(codetable-extra-*)按缓存的 systemExtras 列表逐个清理"所有方案"的注册,
294+
// 再仅重挂当前方案的,确保切到方案 A 时不会看到方案 B 注册的扩展词库。
295+
// 这是修复"切到虎码后再切回五笔,wq 出"寒"而不是"你""问题的关键。
288296
func (m *Manager) applySwitchLocked(schemaID string) {
289297
if m.dictManager != nil {
290298
m.dictManager.UnregisterSystemLayer("codetable-system")
291299
m.dictManager.UnregisterSystemLayer("pinyin-system")
300+
// 清理所有方案已注册的附加层;reRegisterSystemLayer 再按需重挂当前方案的。
301+
for _, layers := range m.systemExtras {
302+
for _, layer := range layers {
303+
if layer != nil {
304+
m.dictManager.UnregisterSystemLayer(layer.Name())
305+
}
306+
}
307+
}
292308
}
293309
m.currentID = schemaID
294310
m.currentEngine = m.engines[schemaID]
@@ -550,20 +566,47 @@ func (m *Manager) ensureEngineBuilt(schemaID string, opts ...schema.EngineCreate
550566
if bundle.SystemLayer != nil {
551567
m.systemLayers[schemaID] = bundle.SystemLayer
552568
}
569+
// 缓存方案的附加层列表,供 applySwitchLocked 在切换时做"按方案隔离"。
570+
if len(bundle.ExtraLayers) > 0 {
571+
m.systemExtras[schemaID] = bundle.ExtraLayers
572+
}
553573

554574
return nil
555575
}
556576

577+
// SetSystemExtras 设置某方案的附加词库层缓存。
578+
// 由 ReloadExtraDicts 等热更新路径调用,保证后续"切走再切回"能恢复正确的 extras。
579+
// 传 nil 或空切片将清空该方案的缓存。
580+
func (m *Manager) SetSystemExtras(schemaID string, layers []dict.DictLayer) {
581+
m.mu.Lock()
582+
defer m.mu.Unlock()
583+
if len(layers) == 0 {
584+
delete(m.systemExtras, schemaID)
585+
return
586+
}
587+
m.systemExtras[schemaID] = layers
588+
}
589+
557590
// reRegisterSystemLayer 为缓存引擎重新注册系统词库层到 CompositeDict
558591
func (m *Manager) reRegisterSystemLayer(schemaID string) {
559592
if m.dictManager == nil {
560593
return
561594
}
562-
// 从缓存的 systemLayers 中取出该方案的系统词库层并重新注册
595+
// 从缓存的 systemLayers 中取出该方案的主系统词库层并重新注册
563596
if layer, ok := m.systemLayers[schemaID]; ok && layer != nil {
564597
m.dictManager.RegisterSystemLayer(layer.Name(), layer)
565598
m.logger.Debug("重新注册系统词库层", "layer", layer.Name(), "schemaID", schemaID)
566599
}
600+
// 重新挂上该方案的所有附加层(与 applySwitchLocked 的清理配对,确保隔离)
601+
if extras, ok := m.systemExtras[schemaID]; ok {
602+
for _, layer := range extras {
603+
if layer == nil {
604+
continue
605+
}
606+
m.dictManager.RegisterSystemLayer(layer.Name(), layer)
607+
m.logger.Debug("重新注册附加词库层", "layer", layer.Name(), "schemaID", schemaID)
608+
}
609+
}
567610
}
568611

569612
// --- 查询方法 ---

wind_input/internal/schema/factory.go

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,10 @@ type EngineBundle struct {
103103
// 由 EngineManager 缓存到 systemLayers,方案切换时按缓存重新注册,
104104
// 避免依赖共享 CompositeDict 的当前状态(防止与并发切换发生竞态)。
105105
SystemLayer dict.DictLayer
106+
// ExtraLayers 该方案加载的所有附加码表层(codetable-extra-<schemaID>__<dictID>)。
107+
// 由 EngineManager 缓存到 systemExtras,方案切换时按缓存清理上一个方案的 extras,
108+
// 重新注册当前方案的 extras,确保不同方案的扩展词库相互隔离。
109+
ExtraLayers []dict.DictLayer
106110
}
107111

108112
// SchemaResolver 方案解析器,用于混输引擎查找被引用的方案
@@ -263,15 +267,21 @@ func createCodeTableEngine(s *Schema, exeDir, dataDir string, dm *dict.DictManag
263267
}
264268

265269
// 加载附加词库(非 default 且 enabled 的词库条目)
270+
var extraLayers []dict.DictLayer
266271
if dm != nil {
267272
for _, dictSpec := range s.Dicts {
268273
if dictSpec.Default || !dictSpec.IsEnabled() {
269274
continue
270275
}
271276
srcPath := resolvePath(exeDir, dataDir, dictSpec.Path)
272277
cacheKey := s.Schema.ID + "_" + dictSpec.ID
273-
if err := loadExtraCodetable(dm, srcPath, dictSpec, cacheKey, logger); err != nil {
278+
layer, err := loadExtraCodetable(dm, s.Schema.ID, srcPath, dictSpec, cacheKey, logger)
279+
if err != nil {
274280
logger.Warn("附加词库加载失败,跳过", "dictID", dictSpec.ID, "error", err)
281+
continue
282+
}
283+
if layer != nil {
284+
extraLayers = append(extraLayers, layer)
275285
}
276286
}
277287
}
@@ -298,6 +308,7 @@ func createCodeTableEngine(s *Schema, exeDir, dataDir string, dm *dict.DictManag
298308
SchemaID: s.Schema.ID,
299309
Engine: engine,
300310
SystemLayer: ctSystemLayer,
311+
ExtraLayers: extraLayers,
301312
}, nil
302313
}
303314

@@ -613,11 +624,19 @@ func loadCodetable(engine *codetable.Engine, srcPath string, dictType DictType,
613624
return nil
614625
}
615626

616-
// loadExtraCodetable 加载附加词库为独立 CodeTable,注册为 DictManager 额外 system layer。
627+
// ExtraLayerName 生成附加词库层名,使用 schemaID 前缀确保不同方案的扩展词库互相隔离。
628+
// 形式:codetable-extra-<schemaID>__<dictID>
629+
// 注意:分隔符使用 "__",避免与 schemaID/dictID 内可能出现的单下划线发生歧义。
630+
func ExtraLayerName(schemaID, dictID string) string {
631+
return "codetable-extra-" + schemaID + "__" + dictID
632+
}
633+
634+
// loadExtraCodetable 加载附加词库为独立 CodeTable,注册为 DictManager 额外 system layer,
635+
// 并返回注册到 CompositeDict 的 layer,供调用方缓存以便方案切换时清理/重挂。
617636
// 附加词库加载失败为非致命错误:调用方记录警告后跳过,不影响主词库工作。
618-
func loadExtraCodetable(dm *dict.DictManager, srcPath string, spec DictSpec, cacheKey string, logger *slog.Logger) error {
637+
func loadExtraCodetable(dm *dict.DictManager, schemaID, srcPath string, spec DictSpec, cacheKey string, logger *slog.Logger) (dict.DictLayer, error) {
619638
if dm == nil {
620-
return nil
639+
return nil, nil
621640
}
622641

623642
srcDir := filepath.Dir(srcPath)
@@ -630,6 +649,8 @@ func loadExtraCodetable(dm *dict.DictManager, srcPath string, spec DictSpec, cac
630649

631650
logger.Info("附加词库源文件清单", "cacheKey", cacheKey, "type", spec.Type, "count", len(srcPaths))
632651

652+
layerName := ExtraLayerName(schemaID, spec.ID)
653+
633654
// 快捷路径:尝试加载源目录中的预编译 wdb(与 loadCodetable 行为对齐)
634655
wdbInDir := filepath.Join(srcDir, cacheKey+".wdb")
635656
if len(srcPaths) > 0 && !dictcache.NeedsRegenerate(srcPaths, wdbInDir) {
@@ -638,11 +659,10 @@ func loadExtraCodetable(dm *dict.DictManager, srcPath string, spec DictSpec, cac
638659
} else {
639660
ct := dict.NewCodeTable()
640661
if err := ct.LoadBinary(wdbInDir); err == nil {
641-
layerName := "codetable-extra-" + spec.ID
642662
layer := dict.NewCodeTableLayer(layerName, dict.LayerTypeSystem, ct)
643663
dm.RegisterSystemLayer(layerName, layer)
644664
logger.Info("附加词库已注册(预编译 wdb)", "layer", layerName, "entryCount", ct.EntryCount())
645-
return nil
665+
return layer, nil
646666
}
647667
}
648668
}
@@ -667,7 +687,7 @@ func loadExtraCodetable(dm *dict.DictManager, srcPath string, spec DictSpec, cac
667687
convertErr = dictcache.ConvertCodeTableToWdb(srcPath, wdbCachePath, logger)
668688
}
669689
if convertErr != nil {
670-
return fmt.Errorf("转换附加词库 %s 到 wdb 失败: %w", cacheKey, convertErr)
690+
return nil, fmt.Errorf("转换附加词库 %s 到 wdb 失败: %w", cacheKey, convertErr)
671691
}
672692
}
673693

@@ -687,18 +707,17 @@ func loadExtraCodetable(dm *dict.DictManager, srcPath string, spec DictSpec, cac
687707
convertErr = dictcache.ConvertCodeTableToWdb(srcPath, wdbCachePath, logger)
688708
}
689709
if convertErr != nil {
690-
return fmt.Errorf("重新生成附加词库失败: %w", convertErr)
710+
return nil, fmt.Errorf("重新生成附加词库失败: %w", convertErr)
691711
}
692712
if err := ct.LoadBinary(wdbCachePath); err != nil {
693-
return fmt.Errorf("加载重新生成的附加词库 %s 失败: %w", cacheKey, err)
713+
return nil, fmt.Errorf("加载重新生成的附加词库 %s 失败: %w", cacheKey, err)
694714
}
695715
}
696716

697-
layerName := "codetable-extra-" + spec.ID
698717
layer := dict.NewCodeTableLayer(layerName, dict.LayerTypeSystem, ct)
699718
dm.RegisterSystemLayer(layerName, layer)
700719
logger.Info("附加词库已注册", "layer", layerName, "entryCount", ct.EntryCount())
701-
return nil
720+
return layer, nil
702721
}
703722

704723
func loadCodetableFromWdb(engine *codetable.Engine, wdbPath string) error {
@@ -909,22 +928,31 @@ func resolvePath(exeDir, dataDir, path string) string {
909928

910929
// ReloadExtraDicts 根据方案 Dicts 配置动态加载/卸载附加词库层。
911930
// 用于 dict enabled 状态变更后的热重载,不重建主词库。
912-
func ReloadExtraDicts(dm *dict.DictManager, s *Schema, exeDir, dataDir string, logger *slog.Logger) {
931+
// 返回该方案当前已启用并成功加载的所有附加层,调用方负责把这份列表同步回
932+
// EngineManager.systemExtras,使得"切走 → 切回"路径仍能正确恢复 extras。
933+
func ReloadExtraDicts(dm *dict.DictManager, s *Schema, exeDir, dataDir string, logger *slog.Logger) []dict.DictLayer {
934+
var layers []dict.DictLayer
913935
for _, dictSpec := range s.Dicts {
914936
if dictSpec.Default {
915937
continue
916938
}
917-
layerName := "codetable-extra-" + dictSpec.ID
939+
layerName := ExtraLayerName(s.Schema.ID, dictSpec.ID)
918940
if !dictSpec.IsEnabled() {
919941
dm.UnregisterSystemLayer(layerName)
920942
continue
921943
}
922944
srcPath := resolvePath(exeDir, dataDir, dictSpec.Path)
923945
cacheKey := s.Schema.ID + "_" + dictSpec.ID
924-
if err := loadExtraCodetable(dm, srcPath, dictSpec, cacheKey, logger); err != nil {
946+
layer, err := loadExtraCodetable(dm, s.Schema.ID, srcPath, dictSpec, cacheKey, logger)
947+
if err != nil {
925948
logger.Warn("附加词库热重载失败", "dictID", dictSpec.ID, "error", err)
949+
continue
950+
}
951+
if layer != nil {
952+
layers = append(layers, layer)
926953
}
927954
}
955+
return layers
928956
}
929957

930958
// createMixedEngine 创建混输引擎(五笔+拼音并行查询)

0 commit comments

Comments
 (0)