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.
- ErisPulse/CLI/commands/run.py +20 -0
- ErisPulse/CLI/utils/package_manager.py +45 -4
- ErisPulse/CLI/utils/package_manager.pyi +3 -0
- ErisPulse/Core/Bases/adapter.py +19 -9
- ErisPulse/Core/Bases/adapter.pyi +2 -2
- ErisPulse/Core/adapter.py +22 -11
- ErisPulse/Core/adapter.pyi +3 -3
- ErisPulse/Core/constants.py +23 -0
- ErisPulse/Core/i18n/__init__.py +83 -7
- ErisPulse/Core/i18n/__init__.pyi +11 -5
- ErisPulse/Core/i18n/locales/en.py +16 -0
- ErisPulse/Core/i18n/locales/ja.py +16 -0
- ErisPulse/Core/i18n/locales/ru.py +16 -0
- ErisPulse/Core/i18n/locales/zh_cn.py +16 -0
- ErisPulse/Core/i18n/locales/zh_tw.py +16 -0
- ErisPulse/Core/logger.py +34 -12
- ErisPulse/Core/logger.pyi +10 -6
- ErisPulse/Core/router.py +22 -12
- ErisPulse/Core/storage.py +293 -9
- ErisPulse/Core/storage.pyi +40 -3
- ErisPulse/loaders/__init__.py +4 -0
- ErisPulse/loaders/__init__.pyi +1 -0
- ErisPulse/loaders/adapter.py +28 -0
- ErisPulse/loaders/adapter.pyi +1 -0
- ErisPulse/loaders/bases/loader.py +32 -1
- ErisPulse/loaders/bases/loader.pyi +1 -1
- ErisPulse/loaders/module.py +27 -4
- ErisPulse/loaders/strict.py +297 -0
- ErisPulse/loaders/strict.pyi +119 -0
- ErisPulse/runtime/frame_config.py +6 -0
- ErisPulse/runtime/frame_config.pyi +1 -1
- ErisPulse/sdk.py +95 -2
- ErisPulse/sdk.pyi +5 -0
- ErisPulse/web_status/4xx.png +0 -0
- ErisPulse/web_status/5xx.png +0 -0
- ErisPulse/web_status/unknow.png +0 -0
- {erispulse-2.5.0.dev2.dist-info → erispulse-2.5.2.dev0.dist-info}/METADATA +269 -137
- {erispulse-2.5.0.dev2.dist-info → erispulse-2.5.2.dev0.dist-info}/RECORD +41 -39
- {erispulse-2.5.0.dev2.dist-info → erispulse-2.5.2.dev0.dist-info}/WHEEL +0 -0
- {erispulse-2.5.0.dev2.dist-info → erispulse-2.5.2.dev0.dist-info}/entry_points.txt +0 -0
- {erispulse-2.5.0.dev2.dist-info → erispulse-2.5.2.dev0.dist-info}/licenses/LICENSE +0 -0
ErisPulse/CLI/commands/run.py
CHANGED
|
@@ -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
|
-
|
|
684
|
-
|
|
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
|
-
|
|
687
|
-
|
|
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
|
ErisPulse/Core/Bases/adapter.py
CHANGED
|
@@ -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 =
|
|
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.
|
|
67
|
+
_msg_logger.event(
|
|
68
|
+
f"[Send] {platform}/{method_name} -> {log_target}: {content}"
|
|
69
|
+
)
|
|
62
70
|
else:
|
|
63
|
-
_msg_logger.
|
|
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
|
ErisPulse/Core/Bases/adapter.pyi
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
-
|
|
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(
|
ErisPulse/Core/adapter.pyi
CHANGED
|
@@ -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
|
"""
|
ErisPulse/Core/constants.py
CHANGED
|
@@ -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
|
#
|
ErisPulse/Core/i18n/__init__.py
CHANGED
|
@@ -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.
|
|
36
|
-
3.
|
|
37
|
-
4.
|
|
38
|
-
5.
|
|
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
|
"""
|
ErisPulse/Core/i18n/__init__.pyi
CHANGED
|
@@ -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.
|
|
36
|
-
3.
|
|
37
|
-
4.
|
|
38
|
-
5.
|
|
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",
|