重构: aiohttp 全异步 + chessdb 云库 + uv 管理

This commit is contained in:
AwesomeXiangQi committed 2026-09-13 13:50:11 +08:00
commit 8d996b8c5f
13 files changed
+1726

No files matched your search

+88
View File
@@ -0,0 +1,88 @@
# -*- coding: utf-8 -*-
"""chessdb.cn 云库客户端(aiohttp 异步, 带缓存)。"""
import asyncio
import logging
import time
import urllib.parse
import aiohttp
log = logging.getLogger('xiangqi.cloud')
API_URL = 'https://www.chessdb.cn/chessdb.php'
CACHE_TTL = 7 * 24 * 3600 # 已评局面基本不变, 缓存一周
class CloudBook:
"""chessdb 云库查询: 返回 (move, score_from_mover_perspective) 或 None。"""
def __init__(self, timeout=2.5, min_score=15, cache_size=4096):
self.min_score = min_score # 云库着法采纳的最低分(红方视角, 厘兵)
self._cache = {} # fen -> (expire_ts, result|None)
self._cache_size = cache_size
self._sem = asyncio.Semaphore(2) # 礼貌并发限制
self.timeout = aiohttp.ClientTimeout(total=timeout)
self._session = None
self.ok_count = 0
self.miss_count = 0
async def _get_session(self):
if self._session is None or self._session.closed:
self._session = aiohttp.ClientSession(timeout=self.timeout)
return self._session
async def close(self):
if self._session and not self._session.closed:
await self._session.close()
async def query_all(self, fen):
"""返回 [(move, score_red_perspective)];云库无局面/出错返回 []。"""
now = time.time()
hit = self._cache.get(fen)
if hit and hit[0] > now:
return hit[1]
url = f'{API_URL}?action=queryall&board={urllib.parse.quote(fen)}&learn=1'
try:
async with self._sem:
session = await self._get_session()
async with session.get(url) as resp:
text = (await resp.text()).strip()
except Exception as e:
log.warning('cloud request failed: %s', e)
return []
result = []
if text.startswith('move:'):
for item in text.split('|'):
fields = dict(kv.split(':', 1) for kv in item.split(',') if ':' in kv)
try:
result.append((fields['move'], int(fields.get('score', '-30000'))))
except (KeyError, ValueError):
continue
self.ok_count += 1
else:
# unknown / nobestmove / checkmate / stalemate / invalid board
self.miss_count += 1
if len(self._cache) >= self._cache_size:
self._cache.clear()
self._cache[fen] = (now + CACHE_TTL, result)
return result
async def best(self, fen, moves_count=0):
"""返回 (move, score_mover, depth_note)。
云库分数为红方视角; 轮黑时取负。分数不足 min_score 时放弃(交给引擎)。
moves_count 用于开局阶段放宽采纳阈值(前 8 回合云库几乎必赢)。
"""
entries = await self.query_all(fen)
if not entries:
return None
side_is_red = (fen.split()[1] == 'w')
sign = 1 if side_is_red else -1
scored = [(mv, sign * sc) for mv, sc in entries]
scored.sort(key=lambda x: -x[1])
mv, sc = scored[0]
# 开局阈值低(云库碾压引擎), 中后局要求云库分足够好
threshold = self.min_score if moves_count >= 16 else 0
if sc < threshold:
return None
return mv, sc, f'{len(scored)}候选'