Spaces:
Runtime error
Runtime error
File size: 11,794 Bytes
7e94d17 feef6eb 7e94d17 feef6eb 7e94d17 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 | """
配置管理模块
支持三层配置来源(优先级从低到高):
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()
|