import os import shutil import socket import toml from loguru import logger root_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) config_file = f"{root_dir}/config.toml" _CONTAINER_CGROUP_MARKERS = ("docker", "containerd", "kubepods", "libpod", "podman") _DOCKER_HOST_GATEWAY_NAME = "host.docker.internal" def is_running_in_container( dockerenv_path: str = "/.dockerenv", containerenv_path: str = "/run/.containerenv", cgroup_path: str = "/proc/1/cgroup", ) -> bool: """ 判断当前进程是否运行在容器内。 这个判断主要用于 Ollama 默认地址选择: - 普通本机运行时,`localhost` 指向用户机器本身; - Docker 容器内,`localhost` 指向容器自己,访问宿主机 Ollama 通常需要使用 `host.docker.internal`。 不能只判断 `/proc/1/cgroup` 是否存在,因为普通 Linux 也会有这个文件。 这里只在检测到明确的容器标记时返回 True,避免误伤非 Docker Linux 用户。 参数保留为可注入路径,便于单元测试覆盖不同运行环境。 """ if os.path.isfile(dockerenv_path) or os.path.isfile(containerenv_path): return True try: with open(cgroup_path, mode="r", encoding="utf-8") as fp: cgroup_content = fp.read().lower() except OSError: return False return any(marker in cgroup_content for marker in _CONTAINER_CGROUP_MARKERS) def _can_resolve_hostname(hostname: str) -> bool: try: socket.gethostbyname(hostname) except OSError: return False return True def _decode_linux_route_gateway(hex_gateway: str) -> str: # /proc/net/route 里的 Gateway 是 16 进制小端序,例如 010011AC 表示 # 172.17.0.1。这里单独解析,是为了在原生 Linux Docker 没有 # host.docker.internal DNS 记录时,还能尝试访问容器默认网关上的宿主机。 if len(hex_gateway) != 8: raise ValueError("invalid gateway length") octets = [ str(int(hex_gateway[index : index + 2], 16)) for index in range(6, -1, -2) ] return ".".join(octets) def get_container_default_gateway_ip(route_path: str = "/proc/net/route") -> str: """ 读取 Linux 容器里的默认网关 IP。 Docker Desktop 通常提供 `host.docker.internal`,但原生 Linux Docker 默认不一定提供这个 DNS 名称。默认网关通常可以作为访问宿主机服务的 兜底地址;如果用户的 Ollama 只监听 127.0.0.1,则仍需要用户让 Ollama 监听宿主机网卡或手动配置 `ollama_base_url`。 """ try: with open(route_path, mode="r", encoding="utf-8") as fp: route_lines = fp.readlines() except OSError: return "" for line in route_lines[1:]: fields = line.strip().split() if len(fields) < 3: continue destination = fields[1] gateway = fields[2] if destination != "00000000" or gateway == "00000000": continue try: return _decode_linux_route_gateway(gateway) except ValueError: logger.warning(f"invalid container gateway route entry: {line.strip()}") return "" return "" def get_default_ollama_base_url() -> str: """ 返回 Ollama 的默认 OpenAI-compatible base_url。 用户显式配置 `ollama_base_url` 时不会走这里;这里只处理“未配置时的 最佳默认值”。容器内默认指向宿主机,普通本机运行默认指向 localhost。 """ if not is_running_in_container(): return "http://localhost:11434/v1" if _can_resolve_hostname(_DOCKER_HOST_GATEWAY_NAME): return f"http://{_DOCKER_HOST_GATEWAY_NAME}:11434/v1" gateway_ip = get_container_default_gateway_ip() if gateway_ip: logger.info( "host.docker.internal is not resolvable, fallback to container " f"default gateway for Ollama: {gateway_ip}" ) return f"http://{gateway_ip}:11434/v1" logger.warning( "failed to resolve host.docker.internal and container default gateway; " "fallback to host.docker.internal for Ollama" ) return f"http://{_DOCKER_HOST_GATEWAY_NAME}:11434/v1" def load_config(): # fix: IsADirectoryError: [Errno 21] Is a directory: '/MoneyPrinterTurbo/config.toml' if os.path.isdir(config_file): shutil.rmtree(config_file) if not os.path.isfile(config_file): example_file = f"{root_dir}/config.example.toml" if os.path.isfile(example_file): shutil.copyfile(example_file, config_file) logger.info("copy config.example.toml to config.toml") logger.info(f"load config from file: {config_file}") try: _config_ = toml.load(config_file) except Exception as e: logger.warning(f"load config failed: {str(e)}, try to load as utf-8-sig") with open(config_file, mode="r", encoding="utf-8-sig") as fp: _cfg_content = fp.read() _config_ = toml.loads(_cfg_content) return _config_ def save_config(): with open(config_file, "w", encoding="utf-8") as f: _cfg["app"] = app _cfg["azure"] = azure _cfg["siliconflow"] = siliconflow _cfg["ui"] = ui f.write(toml.dumps(_cfg)) _cfg = load_config() app = _cfg.get("app", {}) whisper = _cfg.get("whisper", {}) proxy = _cfg.get("proxy", {}) azure = _cfg.get("azure", {}) siliconflow = _cfg.get("siliconflow", {}) ui = _cfg.get( "ui", { "hide_log": False, }, ) hostname = socket.gethostname() log_level = _cfg.get("log_level", "DEBUG") listen_host = _cfg.get("listen_host", "0.0.0.0") listen_port = _cfg.get("listen_port", 8099) project_name = _cfg.get("project_name", "MoneyPrinterTurbo") project_description = _cfg.get( "project_description", "https://github.com/harry0703/MoneyPrinterTurbo", ) project_version = _cfg.get("project_version", "1.2.9") reload_debug = False app["redis_host"] = os.getenv( "MPT_APP_REDIS_HOST", os.getenv("REDIS_HOST", app.get("redis_host", "localhost")), ) imagemagick_path = app.get("imagemagick_path", "") if imagemagick_path and os.path.isfile(imagemagick_path): os.environ["IMAGEMAGICK_BINARY"] = imagemagick_path ffmpeg_path = app.get("ffmpeg_path", "") if ffmpeg_path and os.path.isfile(ffmpeg_path): os.environ["IMAGEIO_FFMPEG_EXE"] = ffmpeg_path logger.info(f"{project_name} v{project_version}")