ErisPulse 2.5.0.dev2__py3-none-any.whl → 2.5.2.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. ErisPulse/CLI/commands/run.py +20 -0
  2. ErisPulse/CLI/utils/package_manager.py +45 -4
  3. ErisPulse/CLI/utils/package_manager.pyi +3 -0
  4. ErisPulse/Core/Bases/adapter.py +19 -9
  5. ErisPulse/Core/Bases/adapter.pyi +2 -2
  6. ErisPulse/Core/adapter.py +22 -11
  7. ErisPulse/Core/adapter.pyi +3 -3
  8. ErisPulse/Core/constants.py +23 -0
  9. ErisPulse/Core/i18n/__init__.py +83 -7
  10. ErisPulse/Core/i18n/__init__.pyi +11 -5
  11. ErisPulse/Core/i18n/locales/en.py +16 -0
  12. ErisPulse/Core/i18n/locales/ja.py +16 -0
  13. ErisPulse/Core/i18n/locales/ru.py +16 -0
  14. ErisPulse/Core/i18n/locales/zh_cn.py +16 -0
  15. ErisPulse/Core/i18n/locales/zh_tw.py +16 -0
  16. ErisPulse/Core/logger.py +34 -12
  17. ErisPulse/Core/logger.pyi +10 -6
  18. ErisPulse/Core/router.py +22 -12
  19. ErisPulse/Core/storage.py +293 -9
  20. ErisPulse/Core/storage.pyi +40 -3
  21. ErisPulse/loaders/__init__.py +4 -0
  22. ErisPulse/loaders/__init__.pyi +1 -0
  23. ErisPulse/loaders/adapter.py +28 -0
  24. ErisPulse/loaders/adapter.pyi +1 -0
  25. ErisPulse/loaders/bases/loader.py +32 -1
  26. ErisPulse/loaders/bases/loader.pyi +1 -1
  27. ErisPulse/loaders/module.py +27 -4
  28. ErisPulse/loaders/strict.py +297 -0
  29. ErisPulse/loaders/strict.pyi +119 -0
  30. ErisPulse/runtime/frame_config.py +6 -0
  31. ErisPulse/runtime/frame_config.pyi +1 -1
  32. ErisPulse/sdk.py +95 -2
  33. ErisPulse/sdk.pyi +5 -0
  34. ErisPulse/web_status/4xx.png +0 -0
  35. ErisPulse/web_status/5xx.png +0 -0
  36. ErisPulse/web_status/unknow.png +0 -0
  37. {erispulse-2.5.0.dev2.dist-info → erispulse-2.5.2.dev0.dist-info}/METADATA +269 -137
  38. {erispulse-2.5.0.dev2.dist-info → erispulse-2.5.2.dev0.dist-info}/RECORD +41 -39
  39. {erispulse-2.5.0.dev2.dist-info → erispulse-2.5.2.dev0.dist-info}/WHEEL +0 -0
  40. {erispulse-2.5.0.dev2.dist-info → erispulse-2.5.2.dev0.dist-info}/entry_points.txt +0 -0
  41. {erispulse-2.5.0.dev2.dist-info → erispulse-2.5.2.dev0.dist-info}/licenses/LICENSE +0 -0
@@ -168,11 +168,28 @@ class RunCommand(Command):
168
168
  "asyncio.run(sdk.run(keep_running=True))",
169
169
  ]
170
170
 
171
+ import signal
172
+
171
173
  try:
172
174
  while True:
173
175
  process = subprocess.Popen(cmd)
176
+
177
+ # 转发 SIGTERM/SIGINT 到子进程,使 SDK 能优雅关闭
178
+ def _forward_signal(signum, frame):
179
+ try:
180
+ process.send_signal(signum)
181
+ except Exception:
182
+ pass
183
+
184
+ old_term = signal.signal(signal.SIGTERM, _forward_signal)
185
+ old_int = signal.signal(signal.SIGINT, _forward_signal)
186
+
174
187
  process.wait()
175
188
 
189
+ # 恢复默认信号处理
190
+ signal.signal(signal.SIGTERM, old_term)
191
+ signal.signal(signal.SIGINT, old_int)
192
+
176
193
  if process.returncode == self._RESTART_EXIT_CODE:
177
194
  console.print(f"[info]{i18n.t('cli.run.restart_request')}[/]")
178
195
  time.sleep(0.5)
@@ -193,6 +210,9 @@ class RunCommand(Command):
193
210
  if reload_mode:
194
211
  self._run_script_with_reload(script_path_abs)
195
212
  else:
213
+ script_dir = os.path.dirname(script_path_abs)
214
+ if script_dir not in sys.path:
215
+ sys.path.insert(0, script_dir)
196
216
  try:
197
217
  runpy.run_path(script_path_abs, run_name="__main__")
198
218
  except SystemExit:
@@ -673,18 +673,59 @@ class PackageManager:
673
673
  并将目标 Python 解析为当前虚拟环境的解释器,
674
674
  避免安装到全局环境。
675
675
 
676
+ 操作成功后自动保存包快照到持久化目录,
677
+ 供 Docker 容器重建后恢复使用。
678
+
676
679
  :param args: [List[str]] pip 子命令与参数,如 ["install", "--upgrade", pkg]
677
680
  :param description: [str] 展示给用户的操作描述
678
681
  :return: [bool] 执行成功返回 True
679
682
  """
680
683
  uv_cmd = self._get_uv_command()
684
+ success = False
681
685
  if uv_cmd:
682
686
  if self._execute_backend(uv_cmd + ["pip"], args, description, "uv"):
683
- return True
684
- console.print(f"[warning]{i18n.t('cli.package.uv_fallback_to_pip')}[/]")
687
+ success = True
688
+ else:
689
+ console.print(f"[warning]{i18n.t('cli.package.uv_fallback_to_pip')}[/]")
690
+
691
+ if not success:
692
+ pip_cmd = [self._get_target_python(), "-m", "pip"]
693
+ success = self._execute_backend(pip_cmd, args, description, "pip")
694
+
695
+ # 操作成功后保存包快照(非关键,失败不影响主流程)
696
+ if success:
697
+ self._save_package_snapshot()
698
+
699
+ return success
700
+
701
+ def _save_package_snapshot(self) -> None:
702
+ """
703
+ {!--< internal-use >!--}
704
+ 将当前已安装的包列表保存到持久化目录。
705
+
706
+ 快照文件用于 Docker 容器重建后的包恢复:
707
+ entrypoint 创建新 venv 时会读取 .pip-snapshot.txt
708
+ 并尝试重新安装这些包。
685
709
 
686
- pip_cmd = [self._get_target_python(), "-m", "pip"]
687
- return self._execute_backend(pip_cmd, args, description, "pip")
710
+ 仅在 config/ 目录存在时保存(即项目环境或 Docker 环境)。
711
+ """
712
+ try:
713
+ config_dir = os.path.join(os.getcwd(), "config")
714
+ if not os.path.isdir(config_dir):
715
+ return
716
+
717
+ result = subprocess.run(
718
+ [sys.executable, "-m", "pip", "freeze"],
719
+ capture_output=True,
720
+ text=True,
721
+ timeout=30,
722
+ )
723
+ if result.returncode == 0:
724
+ snapshot_path = os.path.join(config_dir, ".pip-snapshot.txt")
725
+ with open(snapshot_path, "w", encoding="utf-8") as f:
726
+ f.write(result.stdout)
727
+ except Exception:
728
+ pass
688
729
 
689
730
  def _version_key(self, version: str) -> tuple:
690
731
  """
@@ -243,6 +243,9 @@ class PackageManager:
243
243
  并将目标 Python 解析为当前虚拟环境的解释器,
244
244
  避免安装到全局环境。
245
245
 
246
+ 操作成功后自动保存包快照到持久化目录,
247
+ 供 Docker 容器重建后恢复使用。
248
+
246
249
  :param args: [List[str]] pip 子命令与参数,如 ["install", "--upgrade", pkg]
247
250
  :param description: [str] 展示给用户的操作描述
248
251
  :return: [bool] 执行成功返回 True
@@ -11,17 +11,17 @@ ErisPulse 适配器基础模块
11
11
 
12
12
  import asyncio
13
13
  from abc import ABC, abstractmethod
14
- from typing import Any
15
14
  from collections.abc import Awaitable, Callable
15
+ from typing import Any
16
+
16
17
  from ..constants import (
17
- RETCODE_NOT_IMPLEMENTED,
18
- STATUS_FAILED,
19
18
  DEFAULT_SEND_METHOD,
20
19
  DEFAULT_SEND_TARGET_TYPE,
21
20
  DETAIL_TYPE_PRIVATE,
21
+ RETCODE_NOT_IMPLEMENTED,
22
+ STATUS_FAILED,
22
23
  )
23
24
 
24
-
25
25
  _CHAIN_MODIFIER_NAMES = frozenset({"At", "AtAll", "Reply", "To", "Using", "Account"})
26
26
 
27
27
 
@@ -32,6 +32,7 @@ def _wrap_send_method(method_name: str, original_method: Callable, send_dsl: "Se
32
32
  仅对返回 Task/Awaitable 的发送方法生效,链式修饰方法(返回 SendDSL)不受影响。
33
33
  不改变原方法的返回值类型或执行行为,仅在 Task 上添加回调来触发钩子。
34
34
  """
35
+
35
36
  def hooked(*args, **kwargs):
36
37
  result = original_method(*args, **kwargs)
37
38
 
@@ -51,23 +52,32 @@ def _wrap_send_method(method_name: str, original_method: Callable, send_dsl: "Se
51
52
  }
52
53
 
53
54
  from ..adapter import _msg_logger
55
+
54
56
  target_type = send_dsl._target_type or ""
55
57
  target_id = send_dsl._target_id or ""
56
- log_target = f"{target_type}/{target_id}" if target_type and target_id else target_id or "?"
58
+ log_target = (
59
+ f"{target_type}/{target_id}"
60
+ if target_type and target_id
61
+ else target_id or "?"
62
+ )
57
63
  if method_name in ("Text", "Markdown", "Html") and args:
58
64
  content = str(args[0])
59
65
  if len(content) > 50:
60
66
  content = content[:50] + "..."
61
- _msg_logger.message(f"[Send] {platform}/{method_name} -> {log_target}: {content}")
67
+ _msg_logger.event(
68
+ f"[Send] {platform}/{method_name} -> {log_target}: {content}"
69
+ )
62
70
  else:
63
- _msg_logger.message(f"[Send] {platform}/{method_name} -> {log_target}")
71
+ _msg_logger.event(f"[Send] {platform}/{method_name} -> {log_target}")
64
72
 
65
73
  async def _emit_hooks():
66
74
  from ..lifecycle import lifecycle
75
+
67
76
  await lifecycle.emit("message.sending", send_ctx)
68
77
 
69
78
  async def _emit_hooks_done(_):
70
79
  from ..lifecycle import lifecycle
80
+
71
81
  await lifecycle.emit("message.sent", send_ctx)
72
82
 
73
83
  sending_task = asyncio.ensure_future(_emit_hooks())
@@ -783,9 +793,9 @@ class BaseAdapter(ABC):
783
793
  :return: AdapterConfig 实例
784
794
  """
785
795
  from ...runtime.config_schema import (
786
- dict_to_dataclass,
787
796
  dataclass_to_defaults_dict,
788
797
  dataclass_to_toml_with_comments,
798
+ dict_to_dataclass,
789
799
  )
790
800
  from ..config import config as config_mgr
791
801
 
@@ -812,8 +822,8 @@ class BaseAdapter(ABC):
812
822
  :return: 账户配置字典 {name: config_instance}
813
823
  """
814
824
  from ...runtime.config_schema import (
815
- dict_to_dataclass,
816
825
  dataclass_to_defaults_dict,
826
+ dict_to_dataclass,
817
827
  validate_config,
818
828
  )
819
829
  from ..config import config as config_mgr
@@ -17,9 +17,9 @@ ErisPulse 适配器基础模块
17
17
 
18
18
  import asyncio
19
19
  from abc import ABC, abstractmethod
20
- from typing import Any
21
20
  from collections.abc import Awaitable, Callable
22
- from ..constants import RETCODE_NOT_IMPLEMENTED, STATUS_FAILED, DEFAULT_SEND_METHOD, DEFAULT_SEND_TARGET_TYPE, DETAIL_TYPE_PRIVATE
21
+ from typing import Any
22
+ from ..constants import DEFAULT_SEND_METHOD, DEFAULT_SEND_TARGET_TYPE, DETAIL_TYPE_PRIVATE, RETCODE_NOT_IMPLEMENTED, STATUS_FAILED
23
23
 
24
24
  def _wrap_send_method(method_name: str, original_method: Callable, send_dsl: SendDSL) -> ...:
25
25
  """
ErisPulse/Core/adapter.py CHANGED
@@ -13,15 +13,10 @@ from collections import defaultdict
13
13
  from collections.abc import Callable
14
14
  from typing import Any
15
15
 
16
- from .Bases.adapter import BaseAdapter
17
- from .config import config
18
- from .i18n import i18n
19
- from .lifecycle import lifecycle
20
- from .logger import logger
21
-
22
- _msg_logger = logger.get_child("Message", relative=False)
23
16
  from ..runtime.context import current_owner, handler_waits
17
+ from .Bases.adapter import BaseAdapter
24
18
  from .Bases.manager import ManagerBase
19
+ from .config import config
25
20
  from .constants import (
26
21
  ADAPTER_RETRY_BACKOFF_INTERVALS,
27
22
  ADAPTER_RETRY_FIXED_DELAY_SECS,
@@ -30,6 +25,23 @@ from .constants import (
30
25
  DEFAULT_ADAPTER_ENABLED,
31
26
  HANDLER_SLOW_THRESHOLD_SECS,
32
27
  )
28
+ from .i18n import i18n
29
+ from .lifecycle import lifecycle
30
+ from .logger import logger
31
+
32
+ # 按事件类型分类的日志器
33
+ _msg_logger = logger.get_child("Message", relative=False)
34
+ _notice_logger = logger.get_child("Notice", relative=False)
35
+ _request_logger = logger.get_child("Request", relative=False)
36
+ _meta_logger = logger.get_child("Meta", relative=False)
37
+
38
+ # 事件类型 -> 日志器 映射
39
+ _event_loggers = {
40
+ "message": _msg_logger,
41
+ "notice": _notice_logger,
42
+ "request": _request_logger,
43
+ "meta": _meta_logger,
44
+ }
33
45
 
34
46
 
35
47
  class AdapterManager(ManagerBase):
@@ -936,11 +948,10 @@ class AdapterManager(ManagerBase):
936
948
  alt_msg = data.get("alt_message", "")
937
949
  if len(alt_msg) > 50:
938
950
  alt_msg = alt_msg[:50] + "..."
939
- _msg_logger.message(
940
- f"[Recv] {platform}/{detail_type}({user_id}): {alt_msg}"
941
- )
951
+ _msg_logger.event(f"[Recv] {platform}/{detail_type}({user_id}): {alt_msg}")
942
952
  else:
943
- _msg_logger.message(f"[Recv] {platform}/{event_type}/{detail_type}")
953
+ _logger = _event_loggers.get(event_type, _meta_logger)
954
+ _logger.event(f"[Recv] {platform}/{detail_type}")
944
955
 
945
956
  # 钩子: 事件接收(最早期,所有事件都经过此处)
946
957
  await lifecycle.emit(
@@ -18,14 +18,14 @@ import warnings
18
18
  from collections import defaultdict
19
19
  from collections.abc import Callable
20
20
  from typing import Any
21
+ from ..runtime.context import current_owner, handler_waits
21
22
  from .Bases.adapter import BaseAdapter
23
+ from .Bases.manager import ManagerBase
22
24
  from .config import config
25
+ from .constants import ADAPTER_RETRY_BACKOFF_INTERVALS, ADAPTER_RETRY_FIXED_DELAY_SECS, CONFIG_KEY_ADAPTER_STATUS, CONFIG_KEY_ADAPTER_STATUS_OF, DEFAULT_ADAPTER_ENABLED, HANDLER_SLOW_THRESHOLD_SECS
23
26
  from .i18n import i18n
24
27
  from .lifecycle import lifecycle
25
28
  from .logger import logger
26
- from ..runtime.context import current_owner, handler_waits
27
- from .Bases.manager import ManagerBase
28
- from .constants import ADAPTER_RETRY_BACKOFF_INTERVALS, ADAPTER_RETRY_FIXED_DELAY_SECS, CONFIG_KEY_ADAPTER_STATUS, CONFIG_KEY_ADAPTER_STATUS_OF, DEFAULT_ADAPTER_ENABLED, HANDLER_SLOW_THRESHOLD_SECS
29
29
 
30
30
  class AdapterManager(ManagerBase):
31
31
  """
@@ -122,6 +122,21 @@ DEFAULT_LOG_LEVEL = "INFO"
122
122
  # 修改影响: 终端日志输出的时间显示样式。
123
123
  LOG_TIME_FORMAT = "[%H:%M:%S]"
124
124
 
125
+ # Rich 日志配色主题。
126
+ # 键为 Rich 样式名(Theme key),值为 rich 样式字符串。
127
+ # 修改影响: 终端中各级别日志的显示颜色。
128
+ # 设计原则: 低饱和度、语义化——INFO 不着色只加粗,仅 WARNING/ERROR 用颜色高亮。
129
+ LOG_RICH_THEME = {
130
+ "log.time": "dim", # 时间戳
131
+ "logging.level.trace": "dim", # TRACE
132
+ "logging.level.debug": "dim cyan", # DEBUG
133
+ "logging.level.event": "cyan", # EVENT
134
+ "logging.level.info": "bold", # INFO(加粗无颜色)
135
+ "logging.level.warning": "yellow", # WARNING
136
+ "logging.level.error": "red", # ERROR
137
+ "logging.level.critical": "bold white on red", # CRITICAL(红底白字)
138
+ }
139
+
125
140
  # ==============================================================================
126
141
  # SQLite 存储引擎
127
142
  #
@@ -491,6 +506,14 @@ DEFAULT_MODULE_PRIORITY = 0
491
506
  # True 时模块在首次被访问时才加载,False 时框架启动时立即加载。
492
507
  DEFAULT_LAZY_LOADING_ENABLED = True
493
508
 
509
+ # 严格模式默认级别。
510
+ # 配置默认值,可被 ErisPulse.framework.strict_mode 覆盖。
511
+ # 0 = 宽松(违规仅警告,未继承基类的组件仍尝试加载)
512
+ # 1 = 严格-跳过(拒绝未继承基类的组件,继续启动其余;默认值)
513
+ # 2 = 严格-致命(收集所有违规后中止整个启动)
514
+ # 修改影响: 模块/适配器加载失败或不合规时的处理策略。
515
+ DEFAULT_STRICT_MODE = 1
516
+
494
517
  # ==============================================================================
495
518
  # 国际化 (i18n) 默认值
496
519
  #
@@ -11,10 +11,12 @@ ErisPulse 国际化模块
11
11
  {!--< /tips >!--}
12
12
  """
13
13
 
14
+ import json
14
15
  import locale as _locale_module
15
16
  import os
16
17
  import sys
17
18
  import threading
19
+ from pathlib import Path
18
20
  from typing import Any
19
21
 
20
22
  from .constants import (
@@ -32,10 +34,11 @@ class I18nManager:
32
34
 
33
35
  语言检测优先级:
34
36
  1. 手动通过 set_language() 设置的语言
35
- 2. 配置项 ErisPulse.i18n.language
36
- 3. 环境变量 LANGUAGE / LC_ALL / LC_MESSAGES / LANG
37
- 4. 系统默认 locale (locale.getdefaultlocale)
38
- 5. 默认语言 (zh-CN)
37
+ 2. 环境变量 ERISPULSE_LANG(临时覆盖)
38
+ 3. 全局持久化设置(epsdk i18n 写入 ~/.erispulse/cli_state.json)
39
+ 4. 配置项 ErisPulse.i18n.language("auto" 表示自动检测)
40
+ 5. 系统默认 locale (locale.getdefaultlocale)
41
+ 6. 默认语言 (zh-CN)
39
42
 
40
43
  就近映射规则:
41
44
  - zh-TW, zh-HK, zh-MO, zh-Hant -> zh-TW (繁体中文)
@@ -338,7 +341,7 @@ class I18nManager:
338
341
  """
339
342
  获取当前生效的语言
340
343
 
341
- 优先级: 手动设置 > 配置项 > 检测到的语言
344
+ 优先级: 手动设置 > ERISPULSE_LANG 环境变量 > 全局持久化设置 > 配置项 > 检测到的语言
342
345
 
343
346
  配置值为 "auto" 时使用自动检测的语言。
344
347
 
@@ -348,7 +351,17 @@ class I18nManager:
348
351
  if self._current_lang is not None:
349
352
  return self._current_lang
350
353
 
351
- # 尝试从配置读取
354
+ # 环境变量 ERISPULSE_LANG(临时覆盖,用于测试/运维)
355
+ erispulse_lang = os.environ.get("ERISPULSE_LANG", "")
356
+ if erispulse_lang:
357
+ return self._resolve_nearest(erispulse_lang)
358
+
359
+ # 全局持久化设置(epsdk i18n 写入,所有项目共享)
360
+ global_lang = self._load_global_language()
361
+ if global_lang:
362
+ return global_lang
363
+
364
+ # 项目配置项
352
365
  try:
353
366
  from ..config import config
354
367
 
@@ -364,14 +377,50 @@ class I18nManager:
364
377
 
365
378
  return self._detected_lang
366
379
 
380
+ @staticmethod
381
+ def _global_state_path() -> Path:
382
+ """
383
+ 全局状态文件路径
384
+
385
+ :return: Path 全局状态文件路径 (~/.erispulse/cli_state.json)
386
+
387
+ {!--< internal-use >!--}
388
+ 与 CLI 的 i18n 共享同一文件,作为跨项目的语言持久化位置
389
+ {!--< /internal-use >!--}
390
+ """
391
+ return Path.home() / ".erispulse" / "cli_state.json"
392
+
393
+ def _load_global_language(self) -> str | None:
394
+ """
395
+ 从全局状态文件读取持久化的语言选择
396
+
397
+ :return: 语言代码或 None
398
+
399
+ {!--< internal-use >!--}
400
+ 读取失败或未设置时返回 None,不影响后续优先级
401
+ {!--< /internal-use >!--}
402
+ """
403
+ try:
404
+ with open(self._global_state_path(), "r", encoding="utf-8") as f:
405
+ state = json.load(f)
406
+ lang = state.get("language")
407
+ if lang and isinstance(lang, str):
408
+ return self._resolve_nearest(lang)
409
+ except (FileNotFoundError, json.JSONDecodeError, OSError):
410
+ pass
411
+ return None
412
+
367
413
  # ==================== 公开 API ====================
368
414
 
369
415
  def set_language(self, lang: str) -> None:
370
416
  """
371
- 手动设置当前语言
417
+ 手动设置当前语言,同时写入全局持久化
372
418
 
373
419
  :param lang: 语言代码,如 "zh-CN", "en", "ja", "ru"
374
420
  会自动按就近原则映射到支持的语言。
421
+ 设置后立即生效,并写入 `~/.erispulse/cli_state.json`
422
+ 跨所有项目生效(等效于 `epsdk i18n`)。
423
+ 如需临时覆盖,使用环境变量 `ERISPULSE_LANG`
375
424
 
376
425
  :example:
377
426
  >>> i18n.set_language("en")
@@ -380,6 +429,33 @@ class I18nManager:
380
429
  resolved = self._resolve_nearest(lang)
381
430
  with self._lock:
382
431
  self._current_lang = resolved
432
+ self._persist_global_language(resolved)
433
+
434
+ def _persist_global_language(self, lang: str) -> None:
435
+ """
436
+ 将语言选择写入全局状态文件
437
+
438
+ :param lang: 已解析的语言代码
439
+
440
+ {!--< internal-use >!--}
441
+ 与 CLI i18n 的 _persist_language 写入同一文件,覆盖 language 键
442
+ {!--< /internal-use >!--}
443
+ """
444
+ try:
445
+ state_path = self._global_state_path()
446
+ state_path.parent.mkdir(parents=True, exist_ok=True)
447
+ # 读取现有状态,保留其它键(如 lang_hint_count)
448
+ state = {}
449
+ try:
450
+ with open(state_path, "r", encoding="utf-8") as f:
451
+ state = json.load(f)
452
+ except (FileNotFoundError, json.JSONDecodeError):
453
+ pass
454
+ state["language"] = lang
455
+ with open(state_path, "w", encoding="utf-8") as f:
456
+ json.dump(state, f, ensure_ascii=False, indent=2)
457
+ except OSError:
458
+ pass
383
459
 
384
460
  def get_language(self) -> str:
385
461
  """
@@ -17,10 +17,12 @@ ErisPulse 国际化模块
17
17
  {!--< /tips >!--}
18
18
  """
19
19
 
20
+ import json
20
21
  import locale as _locale_module
21
22
  import os
22
23
  import sys
23
24
  import threading
25
+ from pathlib import Path
24
26
  from typing import Any
25
27
  from .constants import DEFAULT_LANGUAGE, FALLBACK_LANGUAGE, SUPPORTED_LANGUAGES
26
28
 
@@ -32,10 +34,11 @@ class I18nManager:
32
34
 
33
35
  语言检测优先级:
34
36
  1. 手动通过 set_language() 设置的语言
35
- 2. 配置项 ErisPulse.i18n.language
36
- 3. 环境变量 LANGUAGE / LC_ALL / LC_MESSAGES / LANG
37
- 4. 系统默认 locale (locale.getdefaultlocale)
38
- 5. 默认语言 (zh-CN)
37
+ 2. 环境变量 ERISPULSE_LANG(临时覆盖)
38
+ 3. 全局持久化设置(epsdk i18n 写入 ~/.erispulse/cli_state.json)
39
+ 4. 配置项 ErisPulse.i18n.language("auto" 表示自动检测)
40
+ 5. 系统默认 locale (locale.getdefaultlocale)
41
+ 6. 默认语言 (zh-CN)
39
42
 
40
43
  就近映射规则:
41
44
  - zh-TW, zh-HK, zh-MO, zh-Hant -> zh-TW (繁体中文)
@@ -49,10 +52,13 @@ class I18nManager:
49
52
  ...
50
53
  def set_language(self: object, lang: str) -> None:
51
54
  """
52
- 手动设置当前语言
55
+ 手动设置当前语言,同时写入全局持久化
53
56
 
54
57
  :param lang: 语言代码,如 "zh-CN", "en", "ja", "ru"
55
58
  会自动按就近原则映射到支持的语言。
59
+ 设置后立即生效,并写入 `~/.erispulse/cli_state.json`
60
+ 跨所有项目生效(等效于 `epsdk i18n`)。
61
+ 如需临时覆盖,使用环境变量 `ERISPULSE_LANG`
56
62
 
57
63
  :example:
58
64
  >>> i18n.set_language("en")
@@ -25,6 +25,12 @@ TRANSLATIONS = {
25
25
  "core.sdk.init.adapter_none": "Adapters: none",
26
26
  "core.sdk.init.module_count": "Modules: {count}",
27
27
  "core.sdk.init.module_none": "Modules: none",
28
+ "core.sdk.init.tag_lazy": "[lazy]",
29
+ "core.sdk.init.tag_eager": "[eager]",
30
+ "core.sdk.init.disabled_adapters": "Disabled adapters: {names}",
31
+ "core.sdk.init.disabled_modules": "Disabled modules: {names}",
32
+ "core.sdk.init.strict_rejected": "Rejected by strict mode ({count}):",
33
+ "core.sdk.init.strict_rejected_reason": "({reason})",
28
34
  "core.sdk.init.success": "SDK initialized successfully (duration: {duration})",
29
35
  "core.sdk.init.module_init_complete": "Module initialization complete",
30
36
  "core.sdk.init.module_init_partial_failed": "Module initialization partially failed",
@@ -197,6 +203,7 @@ TRANSLATIONS = {
197
203
  "core.router.already_running": "Server is already running",
198
204
  "core.router.starting": "Starting router server {url}",
199
205
  "core.router.routes_registered": "Registered {count} routes: {routes}",
206
+ "core.router.routes_summary": "Routes ready: HTTP={http}, WebSocket={ws}, SSE={sse}",
200
207
  "core.router.started": "Router server started",
201
208
  "core.router.start_failed_msg": "Router server startup failed",
202
209
  "core.router.start_failed": "Failed to start server: {error}",
@@ -289,6 +296,15 @@ TRANSLATIONS = {
289
296
  "loader.module.mount_lazy": "Mounting lazy-load module to sdk: {name}",
290
297
  "loader.module.mount_eager": "Mounting eager-load module to sdk: {name}",
291
298
  "loader.module.init_lazy_start": "Initializing lazy-load module {name}...",
299
+ # ==================== Strict Mode ====================
300
+ "loader.strict.config_failed": "Failed to read strict_mode config, using default: {error}",
301
+ "loader.strict.exempted": "{type} '{name}' is exempted from strict mode ({reason}), continuing to load",
302
+ "loader.strict.tolerated": "Lenient mode: {type} '{name}' {reason}, still loading",
303
+ "loader.strict.rejected": "Strict mode: rejected {type} '{name}' ({reason}), skipped. To load it anyway, add '{name}' to ErisPulse.framework.strict_mode_exceptions.{list_key}, or set ErisPulse.framework.strict_mode = 0 (lenient)",
304
+ "loader.strict.rejected_fatal": "Strict mode (fatal): rejected {type} '{name}' ({reason}), recorded. To load it anyway, add '{name}' to ErisPulse.framework.strict_mode_exceptions.{list_key}, or set ErisPulse.framework.strict_mode = 0 (lenient)",
305
+ "loader.strict.fatal_report_header": "Strict mode (fatal) detected {count} violation(s), aborting startup:",
306
+ "loader.strict.fatal_report_item": " [{index}] {type} '{name}': {reason} {detail}",
307
+ "loader.strict.fatal_abort": "Strict mode (fatal): {count} violation(s) collected, startup aborted",
292
308
  # ==================== Storage Router Display ====================
293
309
  "core.router.lan_ipv4": "LAN IPv4",
294
310
  "core.router.lan_ipv6": "LAN IPv6",
@@ -25,6 +25,12 @@ TRANSLATIONS = {
25
25
  "core.sdk.init.adapter_none": "アダプター: なし",
26
26
  "core.sdk.init.module_count": "モジュール: {count} 個",
27
27
  "core.sdk.init.module_none": "モジュール: なし",
28
+ "core.sdk.init.tag_lazy": "[遅延]",
29
+ "core.sdk.init.tag_eager": "[即時]",
30
+ "core.sdk.init.disabled_adapters": "無効なアダプター: {names}",
31
+ "core.sdk.init.disabled_modules": "無効なモジュール: {names}",
32
+ "core.sdk.init.strict_rejected": "厳格モードで拒否 ({count}):",
33
+ "core.sdk.init.strict_rejected_reason": "({reason})",
28
34
  "core.sdk.init.success": "SDK の初期化に成功しました (所要時間: {duration})",
29
35
  "core.sdk.init.module_init_complete": "モジュールの初期化が完了しました",
30
36
  "core.sdk.init.module_init_partial_failed": "モジュールの初期化が一部失敗しました",
@@ -197,6 +203,7 @@ TRANSLATIONS = {
197
203
  "core.router.already_running": "サーバーは既に実行中です",
198
204
  "core.router.starting": "ルーターサーバーを起動しています {url}",
199
205
  "core.router.routes_registered": "{count} 件のルートを登録しました: {routes}",
206
+ "core.router.routes_summary": "ルート準備完了: HTTP={http}, WebSocket={ws}, SSE={sse}",
200
207
  "core.router.started": "ルーターサーバーが起動しました",
201
208
  "core.router.start_failed_msg": "ルーターサーバーの起動に失敗しました",
202
209
  "core.router.start_failed": "サーバーの起動に失敗しました: {error}",
@@ -289,6 +296,15 @@ TRANSLATIONS = {
289
296
  "loader.module.mount_lazy": "遅延読み込みモジュールを sdk にマウント中: {name}",
290
297
  "loader.module.mount_eager": "即時読み込みモジュールを sdk にマウント中: {name}",
291
298
  "loader.module.init_lazy_start": "遅延読み込みモジュール {name} を初期化しています...",
299
+ # ==================== 厳格モード ====================
300
+ "loader.strict.config_failed": "厳格モード設定の読み取りに失敗、デフォルトを使用します: {error}",
301
+ "loader.strict.exempted": "{type} '{name}' は厳格モードから除外されています({reason})、読み込みを続行します",
302
+ "loader.strict.tolerated": "寛容モード:{type} '{name}' {reason}、読み込みを続行します",
303
+ "loader.strict.rejected": "厳格モード:{type} '{name}' を拒否しました({reason})、スキップ。必ず読み込む必要がある場合は、'{name}' を ErisPulse.framework.strict_mode_exceptions.{list_key} 除外リストに追加するか、ErisPulse.framework.strict_mode を 0(寛容モード)に設定してください",
304
+ "loader.strict.rejected_fatal": "厳格モード(致命):{type} '{name}' を拒否しました({reason})、記録済み。必ず読み込む必要がある場合は、'{name}' を ErisPulse.framework.strict_mode_exceptions.{list_key} 除外リストに追加するか、ErisPulse.framework.strict_mode を 0(寛容モード)に設定してください",
305
+ "loader.strict.fatal_report_header": "厳格モード(致命)が {count} 件の違反を検出、起動を中止します:",
306
+ "loader.strict.fatal_report_item": " [{index}] {type} '{name}': {reason} {detail}",
307
+ "loader.strict.fatal_abort": "厳格モード(致命):{count} 件の違反を収集、起動を中止しました",
292
308
  # ==================== ストレージルーター表示 ====================
293
309
  "core.router.lan_ipv4": "LAN IPv4",
294
310
  "core.router.lan_ipv6": "LAN IPv6",
@@ -25,6 +25,12 @@ TRANSLATIONS = {
25
25
  "core.sdk.init.adapter_none": "Адаптеры: нет",
26
26
  "core.sdk.init.module_count": "Модули: {count} шт.",
27
27
  "core.sdk.init.module_none": "Модули: нет",
28
+ "core.sdk.init.tag_lazy": "[отложено]",
29
+ "core.sdk.init.tag_eager": "[немедленно]",
30
+ "core.sdk.init.disabled_adapters": "Отключённые адаптеры: {names}",
31
+ "core.sdk.init.disabled_modules": "Отключённые модули: {names}",
32
+ "core.sdk.init.strict_rejected": "Отклонено строгим режимом ({count}):",
33
+ "core.sdk.init.strict_rejected_reason": "({reason})",
28
34
  "core.sdk.init.success": "Инициализация SDK успешно завершена (время: {duration})",
29
35
  "core.sdk.init.module_init_complete": "Инициализация модулей завершена",
30
36
  "core.sdk.init.module_init_partial_failed": "Инициализация модулей частично не удалась",
@@ -197,6 +203,7 @@ TRANSLATIONS = {
197
203
  "core.router.already_running": "Сервер уже запущен",
198
204
  "core.router.starting": "Запуск сервера маршрутизации {url}",
199
205
  "core.router.routes_registered": "Зарегистрировано {count} маршрутов: {routes}",
206
+ "core.router.routes_summary": "Маршруты готовы: HTTP={http}, WebSocket={ws}, SSE={sse}",
200
207
  "core.router.started": "Сервер маршрутизации запущен",
201
208
  "core.router.start_failed_msg": "Ошибка запуска сервера маршрутизации",
202
209
  "core.router.start_failed": "Ошибка запуска сервера: {error}",
@@ -289,6 +296,15 @@ TRANSLATIONS = {
289
296
  "loader.module.mount_lazy": "Монтирование модуля отложенной загрузки в sdk: {name}",
290
297
  "loader.module.mount_eager": "Монтирование модуля немедленной загрузки в sdk: {name}",
291
298
  "loader.module.init_lazy_start": "Инициализация модуля отложенной загрузки {name}...",
299
+ # ==================== Строгий режим ====================
300
+ "loader.strict.config_failed": "Не удалось прочитать конфигурацию строгого режима, используется значение по умолчанию: {error}",
301
+ "loader.strict.exempted": "{type} ‘{name}’ исключён из строгого режима ({reason}), загрузка продолжается",
302
+ "loader.strict.tolerated": "Мягкий режим: {type} ‘{name}’ {reason}, загрузка продолжается",
303
+ "loader.strict.rejected": "Строгий режим: {type} '{name}' отклонён ({reason}), пропущен. Чтобы загрузить принудительно, добавьте '{name}' в список исключений ErisPulse.framework.strict_mode_exceptions.{list_key} или установите ErisPulse.framework.strict_mode = 0 (мягкий режим)",
304
+ "loader.strict.rejected_fatal": "Строгий режим (критический): {type} '{name}' отклонён ({reason}), записано. Чтобы загрузить принудительно, добавьте '{name}' в список исключений ErisPulse.framework.strict_mode_exceptions.{list_key} или установите ErisPulse.framework.strict_mode = 0 (мягкий режим)",
305
+ "loader.strict.fatal_report_header": "Строгий режим (критический) обнаружил {count} нарушений, запуск прерван:",
306
+ "loader.strict.fatal_report_item": " [{index}] {type} ‘{name}’: {reason} {detail}",
307
+ "loader.strict.fatal_abort": "Строгий режим (критический): собрано {count} нарушений, запуск прерван",
292
308
  # ==================== Отображение маршрутизатора хранилища ====================
293
309
  "core.router.lan_ipv4": "Локальный IPv4",
294
310
  "core.router.lan_ipv6": "Локальный IPv6",