fix(i18n): pass locale to background threads via thread-local storage

Background threads (graph building, simulation prep, report generation,
profile generation) now inherit the requesting user's locale preference.
Previously these fell back to 'zh' because Flask request context was
unavailable in spawned threads.
This commit is contained in:
ghostubborn 2026-04-01 16:55:51 +08:00
parent 592ee52f59
commit 7c07237544
9 changed files with 54 additions and 14 deletions

View File

@ -15,7 +15,7 @@ from ..services.graph_builder import GraphBuilderService
from ..services.text_processor import TextProcessor from ..services.text_processor import TextProcessor
from ..utils.file_parser import FileParser from ..utils.file_parser import FileParser
from ..utils.logger import get_logger from ..utils.logger import get_logger
from ..utils.locale import t from ..utils.locale import t, get_locale, set_locale
from ..models.task import TaskManager, TaskStatus from ..models.task import TaskManager, TaskStatus
from ..models.project import ProjectManager, ProjectStatus from ..models.project import ProjectManager, ProjectStatus
@ -371,8 +371,12 @@ def build_graph():
project.graph_build_task_id = task_id project.graph_build_task_id = task_id
ProjectManager.save_project(project) ProjectManager.save_project(project)
# Capture locale before spawning background thread
current_locale = get_locale()
# 启动后台任务 # 启动后台任务
def build_task(): def build_task():
set_locale(current_locale)
build_logger = get_logger('mirofish.build') build_logger = get_logger('mirofish.build')
try: try:
build_logger.info(f"[{task_id}] 开始构建图谱...") build_logger.info(f"[{task_id}] 开始构建图谱...")

View File

@ -15,7 +15,7 @@ from ..services.simulation_manager import SimulationManager
from ..models.project import ProjectManager from ..models.project import ProjectManager
from ..models.task import TaskManager, TaskStatus from ..models.task import TaskManager, TaskStatus
from ..utils.logger import get_logger from ..utils.logger import get_logger
from ..utils.locale import t from ..utils.locale import t, get_locale, set_locale
logger = get_logger('mirofish.api.report') logger = get_logger('mirofish.api.report')
@ -121,8 +121,12 @@ def generate_report():
} }
) )
# Capture locale before spawning background thread
current_locale = get_locale()
# 定义后台任务 # 定义后台任务
def run_generate(): def run_generate():
set_locale(current_locale)
try: try:
task_manager.update_task( task_manager.update_task(
task_id, task_id,

View File

@ -14,7 +14,7 @@ from ..services.oasis_profile_generator import OasisProfileGenerator
from ..services.simulation_manager import SimulationManager, SimulationStatus from ..services.simulation_manager import SimulationManager, SimulationStatus
from ..services.simulation_runner import SimulationRunner, RunnerStatus from ..services.simulation_runner import SimulationRunner, RunnerStatus
from ..utils.logger import get_logger from ..utils.logger import get_logger
from ..utils.locale import t from ..utils.locale import t, get_locale, set_locale
from ..models.project import ProjectManager from ..models.project import ProjectManager
logger = get_logger('mirofish.api.simulation') logger = get_logger('mirofish.api.simulation')
@ -501,8 +501,12 @@ def prepare_simulation():
state.status = SimulationStatus.PREPARING state.status = SimulationStatus.PREPARING
manager._save_simulation_state(state) manager._save_simulation_state(state)
# Capture locale before spawning background thread
current_locale = get_locale()
# 定义后台任务 # 定义后台任务
def run_prepare(): def run_prepare():
set_locale(current_locale)
try: try:
task_manager.update_task( task_manager.update_task(
task_id, task_id,

View File

@ -17,7 +17,7 @@ from ..config import Config
from ..models.task import TaskManager, TaskStatus from ..models.task import TaskManager, TaskStatus
from ..utils.zep_paging import fetch_all_nodes, fetch_all_edges from ..utils.zep_paging import fetch_all_nodes, fetch_all_edges
from .text_processor import TextProcessor from .text_processor import TextProcessor
from ..utils.locale import t from ..utils.locale import t, get_locale, set_locale
@dataclass @dataclass
@ -84,10 +84,13 @@ class GraphBuilderService:
} }
) )
# Capture locale before spawning background thread
current_locale = get_locale()
# 在后台线程中执行构建 # 在后台线程中执行构建
thread = threading.Thread( thread = threading.Thread(
target=self._build_graph_worker, target=self._build_graph_worker,
args=(task_id, text, ontology, graph_name, chunk_size, chunk_overlap, batch_size) args=(task_id, text, ontology, graph_name, chunk_size, chunk_overlap, batch_size, current_locale)
) )
thread.daemon = True thread.daemon = True
thread.start() thread.start()
@ -102,9 +105,11 @@ class GraphBuilderService:
graph_name: str, graph_name: str,
chunk_size: int, chunk_size: int,
chunk_overlap: int, chunk_overlap: int,
batch_size: int batch_size: int,
locale: str = 'zh'
): ):
"""图谱构建工作线程""" """图谱构建工作线程"""
set_locale(locale)
try: try:
self.task_manager.update_task( self.task_manager.update_task(
task_id, task_id,

View File

@ -20,7 +20,7 @@ from zep_cloud.client import Zep
from ..config import Config from ..config import Config
from ..utils.logger import get_logger from ..utils.logger import get_logger
from ..utils.locale import get_language_instruction, t from ..utils.locale import get_language_instruction, get_locale, set_locale, t
from .zep_entity_reader import EntityNode, ZepEntityReader from .zep_entity_reader import EntityNode, ZepEntityReader
logger = get_logger('mirofish.oasis_profile') logger = get_logger('mirofish.oasis_profile')
@ -916,8 +916,12 @@ class OasisProfileGenerator:
except Exception as e: except Exception as e:
logger.warning(f"实时保存 profiles 失败: {e}") logger.warning(f"实时保存 profiles 失败: {e}")
# Capture locale before spawning thread pool workers
current_locale = get_locale()
def generate_single_profile(idx: int, entity: EntityNode) -> tuple: def generate_single_profile(idx: int, entity: EntityNode) -> tuple:
"""生成单个profile的工作函数""" """生成单个profile的工作函数"""
set_locale(current_locale)
entity_type = entity.get_entity_type() or "Entity" entity_type = entity.get_entity_type() or "Entity"
try: try:

View File

@ -20,6 +20,7 @@ from queue import Queue
from ..config import Config from ..config import Config
from ..utils.logger import get_logger from ..utils.logger import get_logger
from ..utils.locale import get_locale, set_locale
from .zep_graph_memory_updater import ZepGraphMemoryManager from .zep_graph_memory_updater import ZepGraphMemoryManager
from .simulation_ipc import SimulationIPCClient, CommandType, IPCResponse from .simulation_ipc import SimulationIPCClient, CommandType, IPCResponse
@ -455,10 +456,13 @@ class SimulationRunner:
cls._processes[simulation_id] = process cls._processes[simulation_id] = process
cls._save_run_state(state) cls._save_run_state(state)
# Capture locale before spawning monitor thread
current_locale = get_locale()
# 启动监控线程 # 启动监控线程
monitor_thread = threading.Thread( monitor_thread = threading.Thread(
target=cls._monitor_simulation, target=cls._monitor_simulation,
args=(simulation_id,), args=(simulation_id, current_locale),
daemon=True daemon=True
) )
monitor_thread.start() monitor_thread.start()
@ -475,8 +479,9 @@ class SimulationRunner:
return state return state
@classmethod @classmethod
def _monitor_simulation(cls, simulation_id: str): def _monitor_simulation(cls, simulation_id: str, locale: str = 'zh'):
"""监控模拟进程,解析动作日志""" """监控模拟进程,解析动作日志"""
set_locale(locale)
sim_dir = os.path.join(cls.RUN_STATE_DIR, simulation_id) sim_dir = os.path.join(cls.RUN_STATE_DIR, simulation_id)
# 新的日志结构:分平台的动作日志 # 新的日志结构:分平台的动作日志

View File

@ -16,6 +16,7 @@ from zep_cloud.client import Zep
from ..config import Config from ..config import Config
from ..utils.logger import get_logger from ..utils.logger import get_logger
from ..utils.locale import get_locale, set_locale
logger = get_logger('mirofish.zep_graph_memory_updater') logger = get_logger('mirofish.zep_graph_memory_updater')
@ -276,9 +277,13 @@ class ZepGraphMemoryUpdater:
if self._running: if self._running:
return return
# Capture locale before spawning background thread
current_locale = get_locale()
self._running = True self._running = True
self._worker_thread = threading.Thread( self._worker_thread = threading.Thread(
target=self._worker_loop, target=self._worker_loop,
args=(current_locale,),
daemon=True, daemon=True,
name=f"ZepMemoryUpdater-{self.graph_id[:8]}" name=f"ZepMemoryUpdater-{self.graph_id[:8]}"
) )
@ -356,8 +361,9 @@ class ZepGraphMemoryUpdater:
self.add_activity(activity) self.add_activity(activity)
def _worker_loop(self): def _worker_loop(self, locale: str = 'zh'):
"""后台工作循环 - 按平台批量发送活动到Zep""" """后台工作循环 - 按平台批量发送活动到Zep"""
set_locale(locale)
while self._running or not self._activity_queue.empty(): while self._running or not self._activity_queue.empty():
try: try:
# 尝试从队列获取活动超时1秒 # 尝试从队列获取活动超时1秒

View File

@ -4,7 +4,7 @@
from .file_parser import FileParser from .file_parser import FileParser
from .llm_client import LLMClient from .llm_client import LLMClient
from .locale import t, get_locale, get_language_instruction from .locale import t, get_locale, set_locale, get_language_instruction
__all__ = ['FileParser', 'LLMClient', 't', 'get_locale', 'get_language_instruction'] __all__ = ['FileParser', 'LLMClient', 't', 'get_locale', 'set_locale', 'get_language_instruction']

View File

@ -1,7 +1,10 @@
import json import json
import os import os
import threading
from flask import request, has_request_context from flask import request, has_request_context
_thread_local = threading.local()
_locales_dir = os.path.join(os.path.dirname(__file__), '..', '..', '..', 'locales') _locales_dir = os.path.join(os.path.dirname(__file__), '..', '..', '..', 'locales')
# Load language registry # Load language registry
@ -17,10 +20,15 @@ for filename in os.listdir(_locales_dir):
_translations[locale_name] = json.load(f) _translations[locale_name] = json.load(f)
def set_locale(locale: str):
"""Set locale for current thread. Call at the start of background threads."""
_thread_local.locale = locale
def get_locale() -> str: def get_locale() -> str:
if has_request_context(): if has_request_context():
return request.headers.get('Accept-Language', 'zh') return request.headers.get('Accept-Language', 'zh')
return 'zh' return getattr(_thread_local, 'locale', 'zh')
def t(key: str, **kwargs) -> str: def t(key: str, **kwargs) -> str: