返回 MoneyPrinterTurbo
config.py
根目录 / app / config / config.py
1 import os
2 import shutil
3 import socket
4
5 import toml
6 from loguru import logger
7
8 root_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
9 config_file = f"{root_dir}/config.toml"
10 _CONTAINER_CGROUP_MARKERS = ("docker", "containerd", "kubepods", "libpod", "podman")
11 _DOCKER_HOST_GATEWAY_NAME = "host.docker.internal"
12
13
14 def is_running_in_container(
15 dockerenv_path: str = "/.dockerenv",
16 containerenv_path: str = "/run/.containerenv",
17 cgroup_path: str = "/proc/1/cgroup",
18 ) -> bool:
19 """
20 判断当前进程是否运行在容器内。
21
22 这个判断主要用于 Ollama 默认地址选择:
23 - 普通本机运行时,`localhost` 指向用户机器本身;
24 - Docker 容器内,`localhost` 指向容器自己,访问宿主机 Ollama
25 通常需要使用 `host.docker.internal`。
26
27 不能只判断 `/proc/1/cgroup` 是否存在,因为普通 Linux 也会有这个文件。
28 这里只在检测到明确的容器标记时返回 True,避免误伤非 Docker Linux 用户。
29 参数保留为可注入路径,便于单元测试覆盖不同运行环境。
30 """
31 if os.path.isfile(dockerenv_path) or os.path.isfile(containerenv_path):
32 return True
33
34 try:
35 with open(cgroup_path, mode="r", encoding="utf-8") as fp:
36 cgroup_content = fp.read().lower()
37 except OSError:
38 return False
39
40 return any(marker in cgroup_content for marker in _CONTAINER_CGROUP_MARKERS)
41
42
43 def _can_resolve_hostname(hostname: str) -> bool:
44 try:
45 socket.gethostbyname(hostname)
46 except OSError:
47 return False
48 return True
49
50
51 def _decode_linux_route_gateway(hex_gateway: str) -> str:
52 # /proc/net/route 里的 Gateway 是 16 进制小端序,例如 010011AC 表示
53 # 172.17.0.1。这里单独解析,是为了在原生 Linux Docker 没有
54 # host.docker.internal DNS 记录时,还能尝试访问容器默认网关上的宿主机。
55 if len(hex_gateway) != 8:
56 raise ValueError("invalid gateway length")
57
58 octets = [
59 str(int(hex_gateway[index : index + 2], 16))
60 for index in range(6, -1, -2)
61 ]
62 return ".".join(octets)
63
64
65 def get_container_default_gateway_ip(route_path: str = "/proc/net/route") -> str:
66 """
67 读取 Linux 容器里的默认网关 IP。
68
69 Docker Desktop 通常提供 `host.docker.internal`,但原生 Linux Docker
70 默认不一定提供这个 DNS 名称。默认网关通常可以作为访问宿主机服务的
71 兜底地址;如果用户的 Ollama 只监听 127.0.0.1,则仍需要用户让
72 Ollama 监听宿主机网卡或手动配置 `ollama_base_url`。
73 """
74 try:
75 with open(route_path, mode="r", encoding="utf-8") as fp:
76 route_lines = fp.readlines()
77 except OSError:
78 return ""
79
80 for line in route_lines[1:]:
81 fields = line.strip().split()
82 if len(fields) < 3:
83 continue
84
85 destination = fields[1]
86 gateway = fields[2]
87 if destination != "00000000" or gateway == "00000000":
88 continue
89
90 try:
91 return _decode_linux_route_gateway(gateway)
92 except ValueError:
93 logger.warning(f"invalid container gateway route entry: {line.strip()}")
94 return ""
95
96 return ""
97
98
99 def get_default_ollama_base_url() -> str:
100 """
101 返回 Ollama 的默认 OpenAI-compatible base_url。
102
103 用户显式配置 `ollama_base_url` 时不会走这里;这里只处理“未配置时的
104 最佳默认值”。容器内默认指向宿主机,普通本机运行默认指向 localhost。
105 """
106 if not is_running_in_container():
107 return "http://localhost:11434/v1"
108
109 if _can_resolve_hostname(_DOCKER_HOST_GATEWAY_NAME):
110 return f"http://{_DOCKER_HOST_GATEWAY_NAME}:11434/v1"
111
112 gateway_ip = get_container_default_gateway_ip()
113 if gateway_ip:
114 logger.info(
115 "host.docker.internal is not resolvable, fallback to container "
116 f"default gateway for Ollama: {gateway_ip}"
117 )
118 return f"http://{gateway_ip}:11434/v1"
119
120 logger.warning(
121 "failed to resolve host.docker.internal and container default gateway; "
122 "fallback to host.docker.internal for Ollama"
123 )
124 return f"http://{_DOCKER_HOST_GATEWAY_NAME}:11434/v1"
125
126
127 def load_config():
128 # fix: IsADirectoryError: [Errno 21] Is a directory: '/MoneyPrinterTurbo/config.toml'
129 if os.path.isdir(config_file):
130 shutil.rmtree(config_file)
131
132 if not os.path.isfile(config_file):
133 example_file = f"{root_dir}/config.example.toml"
134 if os.path.isfile(example_file):
135 shutil.copyfile(example_file, config_file)
136 logger.info("copy config.example.toml to config.toml")
137
138 logger.info(f"load config from file: {config_file}")
139
140 try:
141 _config_ = toml.load(config_file)
142 except Exception as e:
143 logger.warning(f"load config failed: {str(e)}, try to load as utf-8-sig")
144 with open(config_file, mode="r", encoding="utf-8-sig") as fp:
145 _cfg_content = fp.read()
146 _config_ = toml.loads(_cfg_content)
147 return _config_
148
149
150 def save_config():
151 with open(config_file, "w", encoding="utf-8") as f:
152 _cfg["app"] = app
153 _cfg["azure"] = azure
154 _cfg["siliconflow"] = siliconflow
155 _cfg["elevenlabs"] = elevenlabs
156 _cfg["chatterbox"] = chatterbox
157 _cfg["ui"] = ui
158 f.write(toml.dumps(_cfg))
159
160
161 _cfg = load_config()
162 app = _cfg.get("app", {})
163 whisper = _cfg.get("whisper", {})
164 proxy = _cfg.get("proxy", {})
165 azure = _cfg.get("azure", {})
166 siliconflow = _cfg.get("siliconflow", {})
167 elevenlabs = _cfg.get("elevenlabs", {})
168 chatterbox = _cfg.get("chatterbox", {})
169 ui = _cfg.get(
170 "ui",
171 {
172 "hide_log": False,
173 },
174 )
175
176 hostname = socket.gethostname()
177
178 log_level = _cfg.get("log_level", "DEBUG")
179 listen_host = _cfg.get("listen_host", "0.0.0.0")
180 listen_port = _cfg.get("listen_port", 8080)
181 project_name = _cfg.get("project_name", "MoneyPrinterTurbo")
182 project_description = _cfg.get(
183 "project_description",
184 "<a href='https://github.com/harry0703/MoneyPrinterTurbo'>https://github.com/harry0703/MoneyPrinterTurbo</a>"
185 "<br><small>Supported by <a href='https://aihubmix.com/?aff=CEve'>AIHubMix</a></small>",
186 )
187 project_version = _cfg.get("project_version", "1.3.0")
188 reload_debug = False
189
190 app["redis_host"] = os.getenv(
191 "MPT_APP_REDIS_HOST",
192 os.getenv("REDIS_HOST", app.get("redis_host", "localhost")),
193 )
194
195 imagemagick_path = app.get("imagemagick_path", "")
196 if imagemagick_path and os.path.isfile(imagemagick_path):
197 os.environ["IMAGEMAGICK_BINARY"] = imagemagick_path
198
199 ffmpeg_path = app.get("ffmpeg_path", "")
200 if ffmpeg_path and os.path.isfile(ffmpeg_path):
201 os.environ["IMAGEIO_FFMPEG_EXE"] = ffmpeg_path
202
203 logger.info(f"{project_name} v{project_version}")
204
204 lines PYTHON