Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/trader/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class TraderConfig:
account_name: Optional[str] = None
paired_at: Optional[str] = None
xiadan_path_manual: Optional[str] = None # 用户手动指定的 xiadan.exe 路径
tesseract_path_manual: Optional[str] = None # 用户手动指定的 tesseract.exe 路径
ws_endpoint: Optional[str] = None # 自定义中转地址:只填域名或 IP[:端口],协议和路径自动补全
enable_ths_plugin: bool = True # 是否启用同花顺实盘交易插件,默认启用
order_watch_idle_secs: int = 300 # order_watch 空闲轮询周期(秒):默认 5 分钟
Expand Down Expand Up @@ -74,6 +75,7 @@ def load() -> TraderConfig:
account_name=data.get("account_name"),
paired_at=data.get("paired_at"),
xiadan_path_manual=data.get("xiadan_path_manual"),
tesseract_path_manual=data.get("tesseract_path_manual"),
ws_endpoint=data.get("ws_endpoint"),
enable_ths_plugin=data.get("enable_ths_plugin", True),
order_watch_idle_secs=data.get("order_watch_idle_secs", 300),
Expand All @@ -95,6 +97,7 @@ def save(config: TraderConfig) -> None:
"account_name": config.account_name,
"paired_at": config.paired_at,
"xiadan_path_manual": config.xiadan_path_manual,
"tesseract_path_manual": config.tesseract_path_manual,
"ws_endpoint": config.ws_endpoint,
"enable_ths_plugin": config.enable_ths_plugin,
"order_watch_idle_secs": config.order_watch_idle_secs,
Expand All @@ -106,4 +109,3 @@ def save(config: TraderConfig) -> None:
with open(config_path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)


6 changes: 3 additions & 3 deletions src/trader/installer/auto_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ async def ensure_xiadan(
确保 xiadan.exe 可用——纯检测,不自动下载。

返回 None 时,调用方应该让用户:
- 点「下载同花顺」按钮 → 看 README 手动装
- 或点「指定路径...」→ 选 xiadan.exe 写到 config
- 选择本机已安装的 xiadan.exe 写到 config
- 或从同花顺官方渠道安装后重新检测

历史:早期版本 (v0.2.0-v0.2.6) 这里走 ensure_private_install 自动下载 214MB
+ silent install。实测 HTTP 403(CDN 屏蔽)+ 同花顺 EULA 不允许第三方分发,
Expand All @@ -83,7 +83,7 @@ async def ensure_xiadan(
))
return detected

logger.info("未检测到 xiadan——等用户在 UI 点「下载同花顺」或「指定路径」")
logger.info("未检测到 xiadan——等待用户选择路径或完成官方安装后重新检测")
return None


Expand Down
30 changes: 22 additions & 8 deletions src/trader/installer/tesseract.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ def detect_tesseract() -> Optional[str]:
- 具体路径 → 在固定安装目录命中
- ``None`` → 未安装
"""
try:
from .. import config

manual = config.load().tesseract_path_manual
if manual and Path(manual).is_file():
return manual
except Exception:
logger.debug("读取手动 Tesseract 路径失败", exc_info=True)

if shutil.which("tesseract"):
return ""
for p in TESSERACT_COMMON_PATHS:
Expand Down Expand Up @@ -81,11 +90,11 @@ def log(msg: str) -> None:
return existing

if not shutil.which("winget"):
log("⚠ 未找到 winget,无法自动安装 Tesseract(手动:winget install "
f"{WINGET_PACKAGE_ID})")
log("⚠ 未找到 winget,无法自动安装 Tesseract。请选择本地 tesseract.exe,"
f"或手动运行:winget install -e --id {WINGET_PACKAGE_ID}")
return None

log("正在通过 winget 安装 Tesseract OCR(首次约 1-2 分钟)...")
log("正在通过 winget 后台尝试安装 Tesseract OCR;不影响连接和窗口检测...")
cmd = [
"winget", "install", "-e", "--id", WINGET_PACKAGE_ID,
"--silent",
Expand Down Expand Up @@ -117,9 +126,14 @@ def log(msg: str) -> None:
return found
await asyncio.sleep(1.5)

# 仍没有:多半是 winget 静默安装需要管理员权限(UB-Mannheim 是 per-machine 装),
# 无提权时静默失败。明确引导手动安装。
log(f"⚠ Tesseract 自动安装未成功(winget rc={rc})。请用【管理员】PowerShell 运行:"
f"winget install -e --id {WINGET_PACKAGE_ID} 装好后重启 trader。"
"(在此之前下单验证码需手动输入)")
unsigned_rc = (rc or 0) & 0xFFFFFFFF
code = f"0x{unsigned_rc:08X}"
if "InternetOpenUrl() failed" in out or unsigned_rc == 0x80072EE2:
reason = "下载 GitHub 安装包超时,请检查虚拟机网络,或选择本地 tesseract.exe"
elif unsigned_rc == 0xC000013A:
reason = "安装进程已中断"
else:
reason = "winget 未完成安装"
log(f"⚠ Tesseract 自动安装失败({code}):{reason}。"
"OCR 已跳过,不影响连接和交易窗口检测。")
return None
87 changes: 73 additions & 14 deletions src/trader/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,30 +285,27 @@ async def _async_main(
if platform.system() == "Windows" and bootstrap_result.found_xiadan_path:
_check_xiadan_conflict(state)

# Step 3.5: 确保 Tesseract OCR 就绪(缺则 winget 静默安装),再喂给 OCR 后端。
# 旧架构在 server.py 启动时调 thsauto_setup(window_title, tesseract_cmd);PH-061
# 迁到本仓库时这步丢了 → pytesseract 只认 PATH,验证码识别静默失效。这里恢复该
# setup 调用,并补上旧版没有的"自动安装"。None=没装→装;""=在 PATH;具体路径=已应用。
# Step 3.5: 先接通已有 OCR;缺失时的 winget 安装转为后台任务,绝不能阻塞
# xiadan 窗口检测或网关连接。None=未装;""=PATH;具体路径=已应用。
if platform.system() == "Windows":
try:
from .installer.tesseract import ensure_tesseract
from .installer.tesseract import verify_ocr_runnable
from .ths.win import setup as ths_setup
tesseract_cmd = bootstrap_result.found_tesseract_cmd
if tesseract_cmd is None:
tesseract_cmd = await ensure_tesseract(on_log=state.log)
bootstrap_result.found_tesseract_cmd = tesseract_cmd
ths_setup("网上股票交易系统5.0", tesseract_cmd or "", str(trader_config.tmp_dir()))
if tesseract_cmd is None:
state.log("⚠ Tesseract 仍不可用,下单验证码无法自动识别")
state.update(ocr_status="installing", ocr_message="正在后台尝试安装")
state.log("OCR 未安装,正在后台尝试安装;不影响连接和窗口检测")
else:
# 启动自检:确认 OCR 真能跑,而不只是文件在。
from .installer.tesseract import verify_ocr_runnable
ocr_ok, ocr_info = verify_ocr_runnable()
if ocr_ok:
state.update(ocr_status="ready", ocr_message=str(ocr_info))
state.log(f"✓ OCR 就绪(Tesseract {ocr_info})")
else:
state.update(ocr_status="unavailable", ocr_message="OCR 自检失败")
state.log(f"⚠ OCR 自检失败:{ocr_info}(下单验证码可能无法识别)")
except Exception as e:
state.update(ocr_status="unavailable", ocr_message="OCR 初始化失败")
logger.warning("Tesseract 准备失败: %s", e)

# Step 4: WS 连接
Expand Down Expand Up @@ -372,6 +369,37 @@ def on_pair_pending(code, expires_at) -> None:
order_event_task = asyncio.create_task(order_watch.order_watch_task(state, client))
from . import watchlist_watch
watchlist_task = asyncio.create_task(watchlist_watch.watchlist_watch_task(state, client))
ocr_task = None
if platform.system() == "Windows" and bootstrap_result.found_tesseract_cmd is None:
async def _install_ocr_in_background() -> None:
try:
from .installer.tesseract import ensure_tesseract, verify_ocr_runnable
from .ths.win import setup as ths_setup

installed = await ensure_tesseract(on_log=state.log)
# 用户在安装期间已选择有效的离线程序时,保留其选择和当前状态。
if trader_config.load().tesseract_path_manual:
return
if installed is None:
state.update(ocr_status="unavailable", ocr_message="自动安装失败,可选择本地 tesseract.exe")
return
ths_setup("网上股票交易系统5.0", installed, str(trader_config.tmp_dir()))
ok, info = verify_ocr_runnable()
if ok:
bootstrap_result.found_tesseract_cmd = installed
state.update(ocr_status="ready", ocr_message=str(info))
state.log(f"✓ OCR 就绪(Tesseract {info})")
else:
state.update(ocr_status="unavailable", ocr_message="安装后自检失败")
state.log(f"⚠ Tesseract 安装后自检失败:{info}")
except asyncio.CancelledError:
raise
except Exception as e:
state.update(ocr_status="unavailable", ocr_message="自动安装异常,可选择本地 tesseract.exe")
state.log(f"⚠ Tesseract 后台安装异常:{e}")
logger.exception("Tesseract 后台安装失败")

ocr_task = asyncio.create_task(_install_ocr_in_background())
try:
await client.run()
except asyncio.CancelledError:
Expand All @@ -385,7 +413,11 @@ def on_pair_pending(code, expires_at) -> None:
order_event_task.cancel()
watchlist_task.cancel()
try:
await asyncio.gather(polling_task, refresh_task, order_event_task, watchlist_task, return_exceptions=True)
tasks = [polling_task, refresh_task, order_event_task, watchlist_task]
if ocr_task is not None:
ocr_task.cancel()
tasks.append(ocr_task)
await asyncio.gather(*tasks, return_exceptions=True)
except Exception:
pass

Expand Down Expand Up @@ -597,7 +629,7 @@ def on_open_xiadan() -> None:
except Exception as e:
state.log(f"⚠ 启动同花顺失败: {e}")
else:
state.log("⚠ xiadan 路径未知。先点「指定路径...」选 xiadan.exe,或「下载同花顺」装一份")
state.log("⚠ xiadan 路径未知。请选择 xiadan.exe,或从同花顺官方渠道安装后重新检测。")

def on_redetect_xiadan() -> None:
"""重新检测 xiadan 路径"""
Expand All @@ -611,7 +643,7 @@ def on_redetect_xiadan() -> None:
state.log(f"✓ 重新检测命中:{found}")
else:
state.update(xiadan_path=None)
state.log("⚠ 重新检测未找到 xiadan。点「下载同花顺」或「指定路径...」")
state.log("⚠ 重新检测未找到 xiadan。请选择 xiadan.exe,或确认已从同花顺官方渠道安装。")
except Exception as e:
state.log(f"⚠ 检测异常: {e}")

Expand All @@ -637,6 +669,32 @@ def on_set_xiadan_path(path: str) -> None:
except Exception as e:
state.log(f"⚠ 设置路径失败: {e}")

def on_set_tesseract_path(path: str) -> None:
"""接入用户离线提供的 tesseract.exe,并立即验证。"""
try:
from pathlib import Path as _P
from .installer.tesseract import verify_ocr_runnable
from .ths.win import setup as ths_setup

p = _P(path)
if not p.is_file():
state.log(f"⚠ 指定的 Tesseract 路径不是文件:{path}")
return
ths_setup("网上股票交易系统5.0", str(p), str(trader_config.tmp_dir()))
ok, info = verify_ocr_runnable()
if not ok:
state.update(ocr_status="unavailable", ocr_message="所选程序无法运行")
state.log(f"⚠ 所选 Tesseract 无法运行:{info}")
return
result.config.tesseract_path_manual = str(p)
trader_config.save(result.config)
bootstrap_result.found_tesseract_cmd = str(p)
state.update(ocr_status="ready", ocr_message=str(info))
state.log(f"✓ 已设置本地 Tesseract:{p}")
except Exception as e:
state.update(ocr_status="unavailable", ocr_message="设置本地 OCR 失败")
state.log(f"⚠ 设置 Tesseract 路径失败:{e}")

def on_reset_pair() -> None:
try:
# 1. 清 config 中的 pairing 字段
Expand Down Expand Up @@ -722,6 +780,7 @@ def on_tray_exit() -> None:
on_exit=on_main_exit,
on_redetect_xiadan=on_redetect_xiadan,
on_set_xiadan_path=on_set_xiadan_path,
on_set_tesseract_path=on_set_tesseract_path,
on_apply_self_update=on_apply_self_update,
minimize_to_tray=(platform.system() == "Windows"),
)
Expand Down
Loading
Loading