Spaces:
Runtime error
Runtime error
| """ | |
| 配置管理模块 | |
| 支持三层配置来源(优先级从低到高): | |
| 1. .env 文件(默认值) | |
| 2. ~/.wellfound/config.json(界面保存的配置,持久化) | |
| 3. 运行时内存(界面实时修改) | |
| 导入方式: | |
| import src.config as cfg # 推荐:每次访问当前值 | |
| cfg.get_llm_api_key() # 总是返回最新值 | |
| """ | |
| import json | |
| import os | |
| import logging | |
| from pathlib import Path | |
| from typing import Optional | |
| from dotenv import load_dotenv | |
| logger = logging.getLogger(__name__) | |
| # ---- 持久化文件路径 ---- | |
| CONFIG_DIR = Path.home() / ".wellfound" | |
| CONFIG_FILE = CONFIG_DIR / "config.json" | |
| # ---- 模块级变量(可被运行时更新)---- | |
| _llm_provider = "deepseek" | |
| _openai_api_key = "" | |
| _openai_model = "gpt-4o-mini" | |
| _deepseek_api_key = "" | |
| _deepseek_model = "deepseek-chat" | |
| _google_sheet_id = "" | |
| _google_sheet_name = "West" | |
| # 所有区域工作表名称(按优先级顺序查询) | |
| _google_sheet_names = ["West", "South", "Northeast", "Midwest"] | |
| _google_service_account_json = "" | |
| _sheet_company_col = "C" | |
| _sheet_email_type_col = "AA" | |
| _sheet_email_body_col = "AB" | |
| _sheet_email_date_col = "AC" | |
| _sheet_timestamp_col = "AD" | |
| _fuzzy_match_threshold = 85 | |
| _fuzzy_ambiguous_gap = 3.0 | |
| _max_concurrent = 5 | |
| _email_body_max_chars = 500 | |
| # 列索引缓存(配置变更时重建) | |
| _indices: dict = {} | |
| _config_loaded = False | |
| def _col_letter_to_index(col: str) -> int: | |
| col = col.upper().strip() | |
| result = 0 | |
| for ch in col: | |
| result = result * 26 + (ord(ch) - ord("A") + 1) | |
| return result - 1 | |
| def _rebuild_indices(): | |
| global _indices | |
| _indices = { | |
| "company": _col_letter_to_index(_sheet_company_col), | |
| "email_type": _col_letter_to_index(_sheet_email_type_col), | |
| "email_body": _col_letter_to_index(_sheet_email_body_col), | |
| "email_date": _col_letter_to_index(_sheet_email_date_col), | |
| "timestamp": _col_letter_to_index(_sheet_timestamp_col), | |
| } | |
| # ---- 初始化:加载配置 ---- | |
| def _load_env(): | |
| """用 .env 文件值填充模块变量(仅在首次调用时作为默认值)""" | |
| load_dotenv() | |
| # 优先从环境变量读取(HF Spaces Secrets 或本地 .env) | |
| # 如果环境变量不存在,则保持当前值(可能来自 config.json) | |
| env_provider = os.getenv("LLM_PROVIDER") | |
| if env_provider: | |
| global _llm_provider | |
| _llm_provider = env_provider | |
| env_openai_key = os.getenv("OPENAI_API_KEY") | |
| if env_openai_key: | |
| global _openai_api_key | |
| _openai_api_key = env_openai_key | |
| env_openai_model = os.getenv("OPENAI_MODEL") | |
| if env_openai_model: | |
| global _openai_model | |
| _openai_model = env_openai_model | |
| env_deepseek_key = os.getenv("DEEPSEEK_API_KEY") | |
| if env_deepseek_key: | |
| global _deepseek_api_key | |
| _deepseek_api_key = env_deepseek_key | |
| env_deepseek_model = os.getenv("DEEPSEEK_MODEL") | |
| if env_deepseek_model: | |
| global _deepseek_model | |
| _deepseek_model = env_deepseek_model | |
| env_sheet_id = os.getenv("GOOGLE_SHEET_ID") | |
| if env_sheet_id: | |
| global _google_sheet_id | |
| _google_sheet_id = env_sheet_id | |
| env_sheet_name = os.getenv("GOOGLE_SHEET_NAME") | |
| if env_sheet_name: | |
| global _google_sheet_name | |
| _google_sheet_name = env_sheet_name | |
| env_sa_json = os.getenv("GOOGLE_SERVICE_ACCOUNT_JSON") | |
| if env_sa_json: | |
| global _google_service_account_json | |
| _google_service_account_json = env_sa_json | |
| # 可选配置 | |
| env_company_col = os.getenv("SHEET_COMPANY_COL") | |
| if env_company_col: | |
| global _sheet_company_col | |
| _sheet_company_col = env_company_col | |
| env_type_col = os.getenv("SHEET_EMAIL_TYPE_COL") | |
| if env_type_col: | |
| global _sheet_email_type_col | |
| _sheet_email_type_col = env_type_col | |
| env_body_col = os.getenv("SHEET_EMAIL_BODY_COL") | |
| if env_body_col: | |
| global _sheet_email_body_col | |
| _sheet_email_body_col = env_body_col | |
| env_date_col = os.getenv("SHEET_EMAIL_DATE_COL") | |
| if env_date_col: | |
| global _sheet_email_date_col | |
| _sheet_email_date_col = env_date_col | |
| env_ts_col = os.getenv("SHEET_TIMESTAMP_COL") | |
| if env_ts_col: | |
| global _sheet_timestamp_col | |
| _sheet_timestamp_col = env_ts_col | |
| env_threshold = os.getenv("FUZZY_MATCH_THRESHOLD") | |
| if env_threshold: | |
| global _fuzzy_match_threshold | |
| _fuzzy_match_threshold = int(env_threshold) | |
| env_gap = os.getenv("FUZZY_AMBIGUOUS_GAP") | |
| if env_gap: | |
| global _fuzzy_ambiguous_gap | |
| _fuzzy_ambiguous_gap = float(env_gap) | |
| _rebuild_indices() | |
| def _apply_dict(d: dict): | |
| """用字典更新模块变量""" | |
| global _llm_provider, _openai_api_key, _openai_model | |
| global _deepseek_api_key, _deepseek_model | |
| global _google_sheet_id, _google_sheet_name, _google_service_account_json | |
| global _sheet_company_col, _sheet_email_type_col, _sheet_email_body_col | |
| global _sheet_email_date_col, _sheet_timestamp_col | |
| global _fuzzy_match_threshold, _fuzzy_ambiguous_gap | |
| global _max_concurrent, _email_body_max_chars | |
| _llm_provider = d.get("llm_provider", _llm_provider) | |
| _openai_api_key = d.get("openai_api_key", _openai_api_key) | |
| _openai_model = d.get("openai_model", _openai_model) | |
| _deepseek_api_key = d.get("deepseek_api_key", _deepseek_api_key) | |
| _deepseek_model = d.get("deepseek_model", _deepseek_model) | |
| _google_sheet_id = d.get("google_sheet_id", _google_sheet_id) | |
| _google_sheet_name = d.get("google_sheet_name", _google_sheet_name) | |
| _google_service_account_json = d.get("google_service_account_json", _google_service_account_json) | |
| _sheet_company_col = d.get("sheet_company_col", _sheet_company_col) | |
| _sheet_email_type_col = d.get("sheet_email_type_col", _sheet_email_type_col) | |
| _sheet_email_body_col = d.get("sheet_email_body_col", _sheet_email_body_col) | |
| _sheet_email_date_col = d.get("sheet_email_date_col", _sheet_email_date_col) | |
| _sheet_timestamp_col = d.get("sheet_timestamp_col", _sheet_timestamp_col) | |
| _fuzzy_match_threshold = d.get("fuzzy_match_threshold", _fuzzy_match_threshold) | |
| _fuzzy_ambiguous_gap = d.get("fuzzy_ambiguous_gap", _fuzzy_ambiguous_gap) | |
| _max_concurrent = d.get("max_concurrent", _max_concurrent) | |
| _email_body_max_chars = d.get("email_body_max_chars", _email_body_max_chars) | |
| _rebuild_indices() | |
| def load_config() -> dict: | |
| """ | |
| 加载配置(优先级:config.json > 环境变量 > 内置默认值) | |
| 在应用启动时调用一次即可。 | |
| Returns: 当前配置字典 | |
| """ | |
| global _config_loaded | |
| # 先加载环境变量(作为基础值) | |
| _load_env() | |
| # 如果在 HF Spaces 环境中,不加载 config.json(优先使用 Secrets) | |
| # 但可以提供一个选项:如果 HF_SPACES_LOAD_CONFIG=true,则仍然加载 | |
| if os.getenv("SPACE_ID") or os.getenv("HF_SPACE"): | |
| if not os.getenv("HF_SPACES_LOAD_CONFIG"): | |
| logger.info("🌐 检测到 HF Spaces 环境,优先使用环境变量(Secrets)") | |
| _config_loaded = True | |
| return get_config_dict() | |
| # 本地环境或显式要求:尝试从 config.json 加载 | |
| if CONFIG_FILE.exists(): | |
| try: | |
| with open(CONFIG_FILE, "r", encoding="utf-8") as f: | |
| file_cfg = json.load(f) | |
| _apply_dict(file_cfg) | |
| logger.info(f"✅ 已从 {CONFIG_FILE} 加载持久化配置") | |
| except Exception as e: | |
| logger.warning(f"读取配置文件失败: {e},使用环境变量 / 默认值") | |
| _config_loaded = True | |
| return get_config_dict() | |
| def save_config(cfg: dict) -> tuple[bool, str]: | |
| """ | |
| 将配置保存到 ~/.wellfound/config.json | |
| Args: | |
| cfg: 完整配置字典(来自界面输入) | |
| Returns: | |
| (success, message) | |
| """ | |
| try: | |
| CONFIG_DIR.mkdir(parents=True, exist_ok=True) | |
| # 写入文件 | |
| with open(CONFIG_FILE, "w", encoding="utf-8") as f: | |
| json.dump(cfg, f, indent=2, ensure_ascii=False) | |
| # 同步到内存 | |
| _apply_dict(cfg) | |
| logger.info(f"✅ 配置已保存到 {CONFIG_FILE}") | |
| return True, f"✅ 配置已保存至 {CONFIG_FILE.name}" | |
| except Exception as e: | |
| logger.error(f"保存配置失败: {e}") | |
| return False, f"❌ 保存失败: {str(e)}" | |
| def apply_config(cfg: dict): | |
| """仅更新内存中的配置(不写文件),用于界面实时预览""" | |
| _apply_dict(cfg) | |
| def get_config_dict() -> dict: | |
| """返回当前所有配置的字典(用于界面回填)""" | |
| return { | |
| "llm_provider": _llm_provider, | |
| "openai_api_key": _openai_api_key, | |
| "openai_model": _openai_model, | |
| "deepseek_api_key": _deepseek_api_key, | |
| "deepseek_model": _deepseek_model, | |
| "google_sheet_id": _google_sheet_id, | |
| "google_sheet_name": _google_sheet_name, | |
| "google_service_account_json": _google_service_account_json, | |
| "sheet_company_col": _sheet_company_col, | |
| "sheet_email_type_col": _sheet_email_type_col, | |
| "sheet_email_body_col": _sheet_email_body_col, | |
| "sheet_email_date_col": _sheet_email_date_col, | |
| "sheet_timestamp_col": _sheet_timestamp_col, | |
| "fuzzy_match_threshold": _fuzzy_match_threshold, | |
| "fuzzy_ambiguous_gap": _fuzzy_ambiguous_gap, | |
| "max_concurrent": _max_concurrent, | |
| "email_body_max_chars": _email_body_max_chars, | |
| } | |
| # ---- 便捷读取函数(各模块调用这些函数,总是拿到最新值)---- | |
| def get_llm_provider() -> str: | |
| return _llm_provider | |
| def get_llm_api_key() -> str: | |
| return _deepseek_api_key if _llm_provider == "deepseek" else _openai_api_key | |
| def get_llm_model() -> str: | |
| return _deepseek_model if _llm_provider == "deepseek" else _openai_model | |
| def get_llm_base_url() -> Optional[str]: | |
| if _llm_provider == "deepseek": | |
| return "https://api.deepseek.com" | |
| return None | |
| def get_google_sheet_id() -> str: | |
| return _google_sheet_id | |
| def get_google_sheet_name() -> str: | |
| return _google_sheet_name | |
| def get_google_sheet_names() -> list[str]: | |
| """返回所有需要查询的区域工作表名称列表""" | |
| return list(_google_sheet_names) | |
| def get_service_account_info() -> Optional[dict]: | |
| """解析 Service Account JSON(支持 str 或 dict)""" | |
| raw = _google_service_account_json | |
| if not raw: | |
| return None | |
| if isinstance(raw, dict): | |
| return raw | |
| if isinstance(raw, str): | |
| raw = raw.strip() | |
| if not raw: | |
| return None | |
| # 尝试解析 JSON 字符串 | |
| try: | |
| return json.loads(raw) | |
| except json.JSONDecodeError: | |
| # 尝试作为文件路径 | |
| if os.path.exists(raw): | |
| with open(raw, "r", encoding="utf-8") as f: | |
| return json.load(f) | |
| return None | |
| return None | |
| def get_sheet_col_indices() -> dict: | |
| """返回所有列 0-based 索引""" | |
| return _indices | |
| def get_fuzzy_threshold() -> int: | |
| return int(_fuzzy_match_threshold) | |
| def get_fuzzy_ambiguous_gap() -> float: | |
| return float(_fuzzy_ambiguous_gap) | |
| def get_max_concurrent() -> int: | |
| return int(_max_concurrent) | |
| def get_email_body_max_chars() -> int: | |
| return int(_email_body_max_chars) | |
| # ---- 常量 ---- | |
| HIGH_PRIORITY_TYPES = {"Forward", "not fit", "considered", | |
| "connect more", "career page", "not hiring"} | |
| LOW_PRIORITY_TYPES = {"Auto reply", "do not exist"} | |
| ALL_CATEGORIES = [ | |
| "Auto reply", "career page", "connect more", | |
| "considered", "not hiring", "do not exist", "not fit", "Forward", | |
| ] | |
| # ---- 启动时自动加载 ---- | |
| load_config() | |