优化日报脚本网络与路径配置

新增 network.use_environment_proxy 配置项,默认禁用 urllib 对 HTTP_PROXY/HTTPS_PROXY 等环境代理的自动读取,避免代理 CONNECT 连接被重置时影响 Nasdaq、FRED 和 QQBot 请求。

将示例配置中的 state、delivery log 和 runtime log 路径改为相对路径,由脚本按 NdxDailyReport 所在目录解析,方便项目迁移和定时任务部署。

保留真实 config.yaml、state 和投递日志为本地文件,不纳入提交。
This commit is contained in:
2026-06-06 00:26:04 +08:00
parent 0e2863ed47
commit 4a592cf457
2 changed files with 33 additions and 8 deletions
+8 -3
View File
@@ -1,13 +1,18 @@
paths:
state_file: D:/project/Python-tools/NdxDailyReport/ndx-daily-report-state.json
delivery_log_file: D:/project/Python-tools/NdxDailyReport/ndx-daily-report-delivery-log.jsonl
runtime_log_file: D:/project/Python-tools/NdxDailyReport/ndx-daily-report-runtime.log
state_file: ndx-daily-report-state.json
delivery_log_file: ndx-daily-report-delivery-log.jsonl
runtime_log_file: ndx-daily-report-runtime.log
nasdaq:
quote_api: https://api.nasdaq.com/api/quote/NDX/info?assetclass=index
fred_csv: https://fred.stlouisfed.org/graph/fredgraph.csv?id=NASDAQ100
request_timeout_seconds: 15
network:
# false 表示 urllib 不读取 HTTP_PROXY/HTTPS_PROXY 等环境代理。
# 某些代理会重置 CONNECT 连接,定时脚本默认直连更稳定。
use_environment_proxy: false
qqbot:
appid: "用户填写"
appkey: "用户填写"
+25 -5
View File
@@ -264,7 +264,7 @@ def fetch_nasdaq_quote(config: dict[str, Any], logger: logging.Logger) -> Market
url = str(config["nasdaq"]["quote_api"])
timeout = int(config["nasdaq"].get("request_timeout_seconds", 15))
logger.info("Nasdaq quote API 请求开始")
payload = http_json("GET", url, timeout=timeout)
payload = http_json("GET", url, timeout=timeout, use_environment_proxy=use_environment_proxy(config))
data = payload.get("data", {})
primary = data.get("primaryData", {})
key_stats = data.get("keyStats", {})
@@ -308,7 +308,8 @@ def fetch_fred_csv(config: dict[str, Any], logger: logging.Logger) -> MarketData
timeout = int(config["nasdaq"].get("request_timeout_seconds", 15))
logger.info("FRED CSV 请求开始")
request = urllib.request.Request(url, headers={"User-Agent": DEFAULT_HEADERS["User-Agent"]})
with urllib.request.urlopen(request, timeout=timeout) as response:
opener = build_url_opener(use_environment_proxy(config))
with opener.open(request, timeout=timeout) as response:
text = response.read().decode("utf-8-sig")
rows = [row for row in csv.DictReader(text.splitlines()) if row.get("NASDAQ100") not in {"", "."}]
if len(rows) < 2:
@@ -436,7 +437,14 @@ def send_qqbot_message(config: dict[str, Any], content: str, logger: logging.Log
url = f"{base_api}/v2/users/{target_openid}/messages"
logger.info("QQBot 投递开始: target_type=%s", target_type)
body = {"content": content, "msg_type": 0}
http_json("POST", url, body=body, timeout=15, headers={"Authorization": f"QQBot {token}"})
http_json(
"POST",
url,
body=body,
timeout=15,
headers={"Authorization": f"QQBot {token}"},
use_environment_proxy=use_environment_proxy(config),
)
logger.info("QQBot 投递成功")
@@ -444,7 +452,7 @@ def get_qqbot_token(config: dict[str, Any], logger: logging.Logger) -> str:
qqbot = config["qqbot"]
url = str(qqbot.get("token_api", "https://bots.qq.com/app/getAppAccessToken"))
body = {"appId": str(qqbot["appid"]), "clientSecret": str(qqbot["appkey"])}
payload = http_json("POST", url, body=body, timeout=15)
payload = http_json("POST", url, body=body, timeout=15, use_environment_proxy=use_environment_proxy(config))
token = payload.get("access_token") or payload.get("accessToken")
if not token:
raise ValueError("QQBot token 响应缺少 access_token")
@@ -486,6 +494,7 @@ def http_json(
body: dict[str, Any] | None = None,
timeout: int = 15,
headers: dict[str, str] | None = None,
use_environment_proxy: bool = False,
) -> dict[str, Any]:
request_headers = dict(DEFAULT_HEADERS)
if headers:
@@ -495,8 +504,9 @@ def http_json(
data = json.dumps(body, ensure_ascii=False).encode("utf-8")
request_headers["Content-Type"] = "application/json"
request = urllib.request.Request(url, data=data, headers=request_headers, method=method)
opener = build_url_opener(use_environment_proxy)
try:
with urllib.request.urlopen(request, timeout=timeout) as response:
with opener.open(request, timeout=timeout) as response:
text = response.read().decode("utf-8")
except urllib.error.HTTPError as exc:
detail = exc.read().decode("utf-8", errors="replace")[:300]
@@ -507,6 +517,16 @@ def http_json(
return parsed
def build_url_opener(use_proxy: bool) -> urllib.request.OpenerDirector:
if use_proxy:
return urllib.request.build_opener()
return urllib.request.build_opener(urllib.request.ProxyHandler({}))
def use_environment_proxy(config: dict[str, Any]) -> bool:
return bool(config.get("network", {}).get("use_environment_proxy", False))
def serialize_market_data(data: MarketData, now: datetime) -> dict[str, str]:
return {
"tradeDate": data.trade_date,