事故根因(2026-07-21): hermes gateway /v1/chat/completions 非透传, 每个请求创建带工具的agent会话。一次603288重评螺旋35分钟/44次 terminal调用/输入153k token, 客户端超时后服务端空转, 重试叠加 新会话自我DDoS。 - llm_client 重写: OCG直连为主(key运行时从hermes config.yaml ocg-key6读取, 不落盘), gateway兜底(agent模式仅应急) - REASSESS_MODEL: pro -> flash (A/B实测新prompt下质量差距微弱, flash快40%) - prompt输出纪律: 【建议仓位】不可省略(非买入写'不新建仓'), 禁止structured_data/XML/JSON块, 禁止寒暄开场白 - batch main 双通道预检(全挂才退出)
490 lines
20 KiB
Python
490 lines
20 KiB
Python
#!/usr/bin/env python3
|
||
"""batch_reassess.py — 批量补全12维(九维矩阵)LLM分析(逐只处理,间隔防限流)
|
||
|
||
用法:
|
||
python3 batch_reassess.py # 所有缺分析/过期的 active 策略
|
||
python3 batch_reassess.py --type holding # 只处理持仓策略
|
||
python3 batch_reassess.py --type watchlist # 只处理自选策略
|
||
python3 batch_reassess.py --type holding --today # 持仓每日刷新(今早未评过的强制重评)
|
||
python3 batch_reassess.py --code XXXXXX # 单只
|
||
|
||
流程:收集最新数据 → 调LLM(gateway)写12维分析+策略 → 保存到DB
|
||
"""
|
||
import sys, json, subprocess, sqlite3, re, time, os
|
||
from datetime import datetime
|
||
|
||
# ── 共享 LLM 客户端 + DB 工具(profile-scripts 硬链到同目录)──
|
||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||
sys.path.insert(0, "/home/hmo/MoFin")
|
||
from llm_client import call_llm, REASSESS_MODEL, gateway_alive, ocg_alive
|
||
from mofin_db import snapshot_strategy_history
|
||
|
||
DB = "/home/hmo/MoFin/data/mofin.db"
|
||
COOLDOWN_HOURS = 1
|
||
STALE_HOURS = 20 # 分析超过20小时视为过期,需要重评
|
||
|
||
def has_llm_analysis(code):
|
||
"""检查是否为LLM生成的12维分析(>500字)"""
|
||
conn = sqlite3.connect(DB)
|
||
r = conn.execute("SELECT LENGTH(full_analysis) FROM holding_strategies WHERE code=? AND status='active'", (code,)).fetchone()
|
||
conn.close()
|
||
return r and r[0] and r[0] > 500
|
||
|
||
def in_cooldown(code):
|
||
"""冷却期检查"""
|
||
conn = sqlite3.connect(DB)
|
||
r = conn.execute("SELECT reassessed_at FROM holding_strategies WHERE code=? AND status='active'", (code,)).fetchone()
|
||
conn.close()
|
||
if not r or not r[0]:
|
||
return False
|
||
try:
|
||
last = datetime.fromisoformat(r[0])
|
||
diff = (datetime.now() - last).total_seconds() / 3600
|
||
return diff < COOLDOWN_HOURS
|
||
except:
|
||
return False
|
||
|
||
def analysis_stale(code, force_today=False):
|
||
"""分析是否过期(>STALE_HOURS 或 force_today 时今早4点前未重评)"""
|
||
conn = sqlite3.connect(DB)
|
||
r = conn.execute("SELECT reassessed_at FROM holding_strategies WHERE code=? AND status='active'", (code,)).fetchone()
|
||
conn.close()
|
||
if not r or not r[0]:
|
||
return True
|
||
try:
|
||
last = datetime.fromisoformat(r[0])
|
||
if force_today:
|
||
today4am = datetime.now().replace(hour=4, minute=0, second=0, microsecond=0)
|
||
return last < today4am
|
||
return (datetime.now() - last).total_seconds() / 3600 > STALE_HOURS
|
||
except:
|
||
return True
|
||
|
||
def get_portfolio():
|
||
"""从 portfolio_summary 读实时现金/总资产(不再硬编码)"""
|
||
try:
|
||
conn = sqlite3.connect(DB)
|
||
r = conn.execute("SELECT cash, total_assets FROM portfolio_summary WHERE id=1").fetchone()
|
||
conn.close()
|
||
if r and r[1]:
|
||
return int(r[0] or 0), int(r[1])
|
||
except Exception:
|
||
pass
|
||
return 0, 0
|
||
|
||
def collect_data(code):
|
||
"""收集最新数据(含完整策略原文)"""
|
||
data = {"code": code}
|
||
|
||
# 从DB读策略(含 full_analysis / changelog_json / position_advice)
|
||
conn = sqlite3.connect(DB)
|
||
r = conn.execute("SELECT name, entry_low, entry_high, stop_loss, take_profit, timing_signal, action, rr_ratio, tech_snapshot, sector_context, stock_category, full_analysis, changelog_json, reassessed_at, position_advice FROM holding_strategies WHERE code=? AND status='active'", (code,)).fetchone()
|
||
if r:
|
||
data["name"] = r[0]
|
||
data["entry_low"] = r[1] or 0
|
||
data["entry_high"] = r[2] or 0
|
||
data["stop_loss"] = r[3] or 0
|
||
data["take_profit"] = r[4] or 0
|
||
data["timing_signal"] = r[5] or ""
|
||
data["action"] = r[6] or ""
|
||
data["rr_ratio"] = r[7] or 0
|
||
data["tech_snapshot"] = r[8] or ""
|
||
data["sector_context"] = r[9] or ""
|
||
data["stock_category"] = r[10] or ""
|
||
data["full_analysis"] = r[11] or ""
|
||
data["changelog_json"] = r[12] or ""
|
||
data["reassessed_at"] = r[13] or ""
|
||
data["position_advice"] = r[14] or ""
|
||
conn.close()
|
||
|
||
# 从腾讯API拉最新价和基本面
|
||
# 代码前缀:5位=港股(hk),6/9开头=沪(sh),其他=深(sz)
|
||
_c = str(code)
|
||
if len(_c) == 5:
|
||
prefix = "hk"
|
||
elif _c.startswith(("6", "9")):
|
||
prefix = "sh"
|
||
else:
|
||
prefix = "sz"
|
||
try:
|
||
r = subprocess.run(["curl", "-s", f"http://qt.gtimg.cn/q={prefix}{code}"], capture_output=True, timeout=10)
|
||
parts = r.stdout.decode("gbk", errors="ignore").split("~")
|
||
data["price"] = float(parts[3]) if len(parts) > 3 and parts[3] else 0
|
||
data["pe"] = parts[39] if len(parts) > 39 and parts[39] else ""
|
||
data["mcap"] = parts[44] if len(parts) > 44 and parts[44] else ""
|
||
data["change_pct"] = parts[32] if len(parts) > 32 and parts[32] else "0"
|
||
except:
|
||
data["price"] = 0
|
||
|
||
# 大盘
|
||
try:
|
||
conn = sqlite3.connect(DB)
|
||
mr = conn.execute("SELECT structure FROM macro_context_log ORDER BY id DESC LIMIT 1").fetchone()
|
||
if mr and mr[0]:
|
||
s = json.loads(mr[0])
|
||
data["macro"] = s.get("description", "大盘震荡")
|
||
conn.close()
|
||
except:
|
||
data["macro"] = "大盘震荡"
|
||
|
||
return data
|
||
|
||
def build_prompt(data):
|
||
"""构建LLM prompt,先审阅原策略再结合实时数据输出修改判断+九维矩阵分析"""
|
||
cash, total = get_portfolio()
|
||
if not total:
|
||
cash, total = 241330, 929727 # 兜底(DB读不到时)
|
||
|
||
# 拉取资金流数据
|
||
_flow_note = "暂无资金流数据"
|
||
try:
|
||
import sqlite3 as _sq, json as _j
|
||
_db = _sq.connect("/home/hmo/MoFin/data/mofin.db")
|
||
_fr = _db.execute("SELECT cache_json FROM capital_flow_cache ORDER BY id DESC LIMIT 1").fetchone()
|
||
if _fr and _fr[0]:
|
||
_fc = _j.loads(_fr[0])
|
||
_stocks = _fc.get("stocks", {})
|
||
_s = _stocks.get(data['code'], {})
|
||
if _s and _s.get("analysis"):
|
||
_a = _s["analysis"]
|
||
_net = _a.get("net_flow", 0)
|
||
_main = _a.get("main_force", 0)
|
||
_retail = _a.get("retail_flow", 0)
|
||
_trend = _a.get("trend", "中性")
|
||
_flow_note = f"净流入{_net:.0f}万 主力{_main:.0f}万 散户{_retail:.0f}万 趋势{_trend}"
|
||
_db.close()
|
||
except:
|
||
pass
|
||
|
||
# 拉取近期消息面
|
||
_news_note = "暂无近期消息"
|
||
try:
|
||
import sqlite3 as _sq
|
||
_db = _sq.connect("/home/hmo/MoFin/data/mofin.db")
|
||
_nr = _db.execute(
|
||
"SELECT summary, overall_sentiment, created_at FROM signal_news "
|
||
"WHERE (code=? OR sector LIKE ?) AND overall_sentiment IN ('利好','利空') "
|
||
"ORDER BY id DESC LIMIT 3",
|
||
(data['code'], f'%{data.get("name","")[:4]}%')
|
||
).fetchall()
|
||
if _nr:
|
||
_news_note = " | ".join([f"{r[2][:10]} {r[1]} {r[0][:40]}" for r in _nr])
|
||
_db.close()
|
||
except:
|
||
pass
|
||
|
||
# ── 构建【原策略全文】section ──
|
||
_params_parts = []
|
||
if data.get('action'): _params_parts.append(f"当前策略: {data['action']}")
|
||
if data.get('timing_signal'): _params_parts.append(f"信号: {data['timing_signal']}")
|
||
if data.get('entry_low') or data.get('entry_high'):
|
||
_params_parts.append(f"买入区间: {data.get('entry_low',0)}~{data.get('entry_high',0)}")
|
||
if data.get('stop_loss'): _params_parts.append(f"止损: {data['stop_loss']}")
|
||
if data.get('take_profit'): _params_parts.append(f"止盈: {data['take_profit']}")
|
||
if data.get('position_advice'): _params_parts.append(f"仓位: {data['position_advice']}")
|
||
_params_str = " | ".join(_params_parts) if _params_parts else "无策略参数"
|
||
|
||
# 最近3条变更记录
|
||
_changelog_str = "无变更记录"
|
||
try:
|
||
_cl_raw = data.get('changelog_json', '')
|
||
if _cl_raw:
|
||
_cl = json.loads(_cl_raw) if isinstance(_cl_raw, str) else _cl_raw
|
||
if isinstance(_cl, list) and _cl:
|
||
_recent = _cl[-3:] if len(_cl) > 3 else _cl
|
||
_cl_lines = []
|
||
for i, c in enumerate(_recent):
|
||
_act = c.get('action', c.get('reason', '')) if isinstance(c, dict) else str(c)
|
||
_ts = c.get('timestamp', '') if isinstance(c, dict) else ''
|
||
_cl_lines.append(f" {i+1}. {_ts[:16]} {_act[:80]}")
|
||
if _cl_lines:
|
||
_changelog_str = "\n".join(_cl_lines)
|
||
except:
|
||
pass
|
||
|
||
# 完整分析原文(不截断)
|
||
_full_analysis = data.get('full_analysis', '') or ''
|
||
_fa_display = _full_analysis if _full_analysis else '(首次分析,无历史)'
|
||
|
||
_orig_strategy_section = f"""当前策略参数: {_params_str}
|
||
|
||
变更记录(最近3条):
|
||
{_changelog_str}
|
||
|
||
完整分析原文:
|
||
{_fa_display}"""
|
||
|
||
return f"""你是一个资深A股分析师。请先审阅以下【原策略全文】,判断是否需要修改策略,然后做出完整的九维矩阵分析。
|
||
|
||
【原策略全文】
|
||
{_orig_strategy_section}
|
||
|
||
── 以上是已有的策略,以下是当前实时数据,请结合两者做出判断 ──
|
||
|
||
⚠️ 重要:以下9个维度不是独立分析的,你必须交叉对比后给出综合结论。
|
||
例如:如果消息面利好但资金流在流出,说明利好可能是出货;如果基本面强但技术面破位,说明估值可能还没到底。
|
||
|
||
当前数据(以下数据均来自实时API,每条标注时间窗口,禁止使用模型内部训练数据):
|
||
大盘:{data.get('macro','震荡')}(当日实时)
|
||
最新价:{data.get('price',0)} 涨跌:{data.get('change_pct','0')}%(当日实时)
|
||
PE={data.get('pe','?')}(最新财报) 市值={data.get('mcap','?')}亿
|
||
行业:{data.get('sector_context','?')}(当日实时)
|
||
技术面:{data.get('tech_snapshot','')[:300]}(MA=5/10/20/60日 支撑阻力=近20日 量价=当日+近5日趋势)
|
||
资金流:{_flow_note}(近5日累计)
|
||
消息面:{_news_note}(最近3条,自动标注抓取时间)
|
||
当前信号:{data.get('timing_signal','?')} 分类:{data.get('stock_category','?')}
|
||
|
||
我的总资产={total}元,可用现金={cash}元。
|
||
|
||
请严格按以下格式输出(注意节标题不可省略):
|
||
|
||
【维持或修改】明确二选一判断:维持原策略 / 需要修改策略
|
||
【修改点及理由】
|
||
如果维持原策略 → 写"无需修改"
|
||
如果需要修改 → 逐条列出(每条格式:"- 修改点名称:理由说明")
|
||
【最终新策略】
|
||
用自然语言输出完整的最终策略全文(200-400字),自包含核心交易逻辑、买入区间价格、止损价、止盈价、仓位比例、风险提示。
|
||
⚠️ 本段不要使用【综合结论】【买入区间】等标签——用自然语言描述即可。
|
||
|
||
【交叉分析】用2-3句话说明哪些维度出现矛盾/共振,最关键的信号是什么
|
||
① 大盘×基本面 [一句话,说明矛盾关系]
|
||
② 大盘×消息面 [一句话]
|
||
③ 大盘×技术面 [一句话]
|
||
④ 大盘×资金面 [一句话]
|
||
⑤ 行业×基本面 [一句话]
|
||
⑥ 行业×消息面 [一句话]
|
||
⑦ 行业×技术面 [一句话]
|
||
⑧ 行业×资金面 [一句话]
|
||
⑨ 个股×基本面 [一句话]
|
||
⑩ 个股×消息面 [一句话]
|
||
⑪ 个股×技术面 [一句话]
|
||
⑫ 个股×资金面 [一句话]
|
||
|
||
【综合结论】(买入/关注/观望/卖出)
|
||
【操作建议】具体操作建议
|
||
【买入区间】最低价~最高价
|
||
【建议止损】数字
|
||
【建议止盈】数字
|
||
|
||
【建议仓位】⚠️不可省略。综合结论非"买入"时写"不新建仓";为"买入"时按以下公式:
|
||
基础仓位按RR确定:RR<1.5→不推荐,RR1.5~3→8%,RR3~5→12%,RR5+→15%
|
||
大盘偏弱×0.8,大盘偏强×1.15
|
||
蓝筹/白马×1.2,成长×0.85,题材/短线×0.6
|
||
最终仓位范围:5%~20%
|
||
同时考虑:现金{cash}元足够买多少手。
|
||
输出格式:"X%(理由:一句话说明为什么这个仓位)"
|
||
|
||
⚠️ 输出纪律(必须遵守):
|
||
1. 直接以【维持或修改】开头,禁止任何寒暄、开场白、分隔线
|
||
2. 禁止输出 <structured_data> 或任何 XML/JSON/代码块
|
||
3. 所有【】节标题一个都不能少"""
|
||
def parse_response(text):
|
||
"""从LLM回复中提取策略参数"""
|
||
result = {"signal": "", "entry_low": 0, "entry_high": 0, "stop_loss": 0, "take_profit": 0, "position": ""}
|
||
|
||
# 信号
|
||
sl = [l for l in text.split("\n") if "综合结论" in l]
|
||
if sl:
|
||
for kw in ["买入","关注","观望","卖出"]:
|
||
if kw in sl[0]:
|
||
result["signal"] = kw
|
||
break
|
||
|
||
# 买入区间
|
||
zl = [l for l in text.split("\n") if "买入区间" in l]
|
||
if zl:
|
||
nums = re.findall(r'[\d.]+', zl[0])
|
||
if len(nums) >= 2:
|
||
result["entry_low"] = float(nums[0])
|
||
result["entry_high"] = float(nums[1])
|
||
|
||
# 止损
|
||
for l in text.split("\n"):
|
||
if "建议止损" in l:
|
||
nums = re.findall(r'[\d.]+', l)
|
||
if nums: result["stop_loss"] = float(nums[0])
|
||
|
||
# 止盈
|
||
for l in text.split("\n"):
|
||
if "建议止盈" in l:
|
||
nums = re.findall(r'[\d.]+', l)
|
||
if nums: result["take_profit"] = float(nums[0])
|
||
|
||
# 仓位:只有买入信号才需要,提取百分比数字
|
||
result["position"] = ""
|
||
if result["signal"] == "买入":
|
||
for l in text.split("\n"):
|
||
if "建议仓位" in l:
|
||
nums = re.findall(r'[\d.]+', l)
|
||
for n in nums:
|
||
f = float(n)
|
||
if 1 <= f <= 30: # 合理的仓位范围
|
||
result["position"] = f"{f:.0f}%"
|
||
break
|
||
break
|
||
|
||
return result
|
||
|
||
def save_result(code, full_text, parsed):
|
||
"""保存LLM结果到DB(先快照再UPDATE)"""
|
||
conn = sqlite3.connect(DB)
|
||
now = datetime.now().isoformat()
|
||
|
||
# ── 修改前快照 ──
|
||
snapshot_strategy_history(conn, code, 'batch_12d')
|
||
|
||
updates = ["full_analysis=?", "reassessed_at=?"]
|
||
params = [full_text, now]
|
||
|
||
if parsed["signal"]:
|
||
updates.append("timing_signal=?")
|
||
params.append(parsed["signal"])
|
||
if parsed["entry_low"] > 0:
|
||
updates.append("entry_low=?")
|
||
params.append(parsed["entry_low"])
|
||
if parsed["entry_high"] > 0:
|
||
updates.append("entry_high=?")
|
||
params.append(parsed["entry_high"])
|
||
if parsed["stop_loss"] > 0:
|
||
updates.append("stop_loss=?")
|
||
params.append(parsed["stop_loss"])
|
||
if parsed["take_profit"] > 0:
|
||
updates.append("take_profit=?")
|
||
params.append(parsed["take_profit"])
|
||
if parsed["position"]:
|
||
updates.append("position_advice=?")
|
||
params.append(parsed["position"])
|
||
|
||
params.append(code)
|
||
sql = f"UPDATE holding_strategies SET {', '.join(updates)} WHERE code=? AND status='active'"
|
||
conn.execute(sql, params)
|
||
conn.commit()
|
||
|
||
# 买入信号→推XMPP通知(在conn close前执行)
|
||
if parsed.get("signal") == "买入":
|
||
try:
|
||
_nr = conn.execute("SELECT name, price FROM holding_strategies WHERE code=? AND status='active'", (code,)).fetchone()
|
||
_name = _nr[0] if _nr else code
|
||
_p = _nr[1] if _nr else 0
|
||
_el = parsed.get("entry_low", 0)
|
||
_eh = parsed.get("entry_high", 0)
|
||
_sl = parsed.get("stop_loss", 0)
|
||
_tp = parsed.get("take_profit", 0)
|
||
_pos = parsed.get("position", "")
|
||
_msg = f"\U0001f4c8 {_name}({code}) 价{_p}\u219212维分析生成买入信号!区间{_el}~{_eh} 损{_sl} 盈{_tp} 仓位{_pos}"
|
||
import urllib.request, json as _jj
|
||
_req = urllib.request.Request("http://127.0.0.1:5805/",
|
||
data=_jj.dumps({"body": _msg, "to": "hmo@yoin.fun", "type": "chat"}).encode(),
|
||
headers={"Content-Type": "application/json"})
|
||
urllib.request.urlopen(_req, timeout=5)
|
||
print(f" \U0001f4e8 XMPP推送成功: {_msg[:60]}")
|
||
except Exception as _e:
|
||
print(f" \u26a0\ufe0f XMPP推送失败: {_e}")
|
||
|
||
conn.close()
|
||
|
||
def process_stock(code, force_today=False):
|
||
"""处理单只股票"""
|
||
print(f"\n{'='*50}")
|
||
print(f"处理: {code}")
|
||
print(f"{'='*50}")
|
||
|
||
if in_cooldown(code):
|
||
print(f" \u23ed 冷却期内,跳过")
|
||
return False
|
||
|
||
# 有分析且未过期 \u2192 跳过(除非 force_today 且今早未评)
|
||
if has_llm_analysis(code) and not analysis_stale(code, force_today):
|
||
print(f" \u23ed 已有12维分析且未过期,跳过")
|
||
return False
|
||
|
||
print(f" 收集数据...", flush=True)
|
||
data = collect_data(code)
|
||
if not data.get("price"):
|
||
print(f" \u26a0\ufe0f 无价格数据,跳过")
|
||
return False
|
||
|
||
print(f" 调LLM生成九维分析...", flush=True)
|
||
prompt = build_prompt(data)
|
||
|
||
# ── 使用共享 LLM 客户端(替代 curl subprocess)──
|
||
result = call_llm(prompt, model=REASSESS_MODEL, max_tokens=4096)
|
||
|
||
if not result["ok"]:
|
||
print(f" \u274c LLM调用失败: {result.get('error','未知错误')}")
|
||
return False
|
||
|
||
full_text = result["content"]
|
||
print(f" \u2705 LLM返回({len(full_text)}字, {result['elapsed']:.1f}s, 尝试{result['attempts']}次)", flush=True)
|
||
|
||
parsed = parse_response(full_text)
|
||
print(f" 信号={parsed['signal']} 区间={parsed['entry_low']}~{parsed['entry_high']} 损={parsed['stop_loss']} 盈={parsed['take_profit']} 仓位={parsed['position']}")
|
||
|
||
save_result(code, full_text, parsed)
|
||
print(f" \u2705 已保存到DB")
|
||
return True
|
||
|
||
def main():
|
||
# ── 双通道预检:OCG直连 + hermes gateway 兜底,全挂才退出 ──
|
||
_ocg_ok = ocg_alive()
|
||
_gw_ok = gateway_alive()
|
||
if not _ocg_ok and not _gw_ok:
|
||
print("[FATAL] OCG上游与hermes gateway均不可用,退出")
|
||
sys.exit(1)
|
||
if not _ocg_ok:
|
||
print("[WARN] OCG直连不可用,将使用gateway兜底(agent运行时,较慢)")
|
||
if not _gw_ok:
|
||
print("[WARN] hermes gateway不可用,仅使用OCG直连")
|
||
|
||
codes = []
|
||
force_today = "--today" in sys.argv
|
||
dtype = None
|
||
if "--type" in sys.argv:
|
||
idx = sys.argv.index("--type")
|
||
dtype = sys.argv[idx + 1] # holding | watchlist | all
|
||
if "--code" in sys.argv:
|
||
idx = sys.argv.index("--code")
|
||
codes = [sys.argv[idx+1]]
|
||
else:
|
||
# 按类型筛选 active 策略
|
||
type_map = {"holding": "持仓策略", "watchlist": "自选策略"}
|
||
conn = sqlite3.connect(DB)
|
||
if dtype in type_map:
|
||
rows = conn.execute(
|
||
"SELECT code FROM holding_strategies WHERE status='active' AND decision_type=? ORDER BY code",
|
||
(type_map[dtype],)).fetchall()
|
||
else:
|
||
rows = conn.execute(
|
||
"SELECT code FROM holding_strategies WHERE status='active' ORDER BY decision_type, code").fetchall()
|
||
conn.close()
|
||
codes = [r[0] for r in rows]
|
||
|
||
print(f"待处理: {len(codes)}只 (type={dtype or 'all'}, force_today={force_today})")
|
||
|
||
ok = 0
|
||
fail = 0
|
||
skip = 0
|
||
for i, code in enumerate(codes):
|
||
if has_llm_analysis(code) and not analysis_stale(code, force_today):
|
||
print(f" [{i+1}/{len(codes)}] \u23ed {code} 已有12维分析且未过期")
|
||
skip += 1
|
||
continue
|
||
|
||
print(f" [{i+1}/{len(codes)}] ", end="", flush=True)
|
||
if process_stock(code, force_today):
|
||
ok += 1
|
||
else:
|
||
fail += 1
|
||
|
||
# 间隔8秒(pro model较重但gateway可承受;retry逻辑吸收瞬断)
|
||
if i < len(codes) - 1:
|
||
print(f" 等待8秒...", flush=True)
|
||
time.sleep(8)
|
||
|
||
print(f"\n{'='*50}")
|
||
print(f"完成: {ok}成功, {fail}失败, {skip}跳过")
|
||
print(f"{'='*50}")
|
||
|
||
if __name__ == "__main__":
|
||
main()
|