fix: 更新当前界面,支持多公帐号切换
This commit is contained in:
@@ -64,17 +64,34 @@ def _detail_for_draft_error(data: dict) -> str:
|
||||
|
||||
class WechatPublisher:
|
||||
def __init__(self) -> None:
|
||||
self._access_token = None
|
||||
self._expires_at = 0
|
||||
self._token_cache: dict[str, dict[str, int | str]] = {}
|
||||
self._runtime_thumb_media_id: str | None = None
|
||||
|
||||
async def publish_draft(self, req: WechatPublishRequest, request_id: str = "") -> PublishResponse:
|
||||
def _resolve_account(self, account: dict | None = None) -> dict[str, str]:
|
||||
src = account or {}
|
||||
appid = (src.get("appid") or settings.wechat_appid or "").strip()
|
||||
secret = (src.get("secret") or settings.wechat_secret or "").strip()
|
||||
author = (src.get("author") or settings.wechat_author or "").strip()
|
||||
thumb_media_id = (src.get("thumb_media_id") or settings.wechat_thumb_media_id or "").strip()
|
||||
thumb_image_path = (src.get("thumb_image_path") or settings.wechat_thumb_image_path or "").strip()
|
||||
return {
|
||||
"appid": appid,
|
||||
"secret": secret,
|
||||
"author": author,
|
||||
"thumb_media_id": thumb_media_id,
|
||||
"thumb_image_path": thumb_image_path,
|
||||
}
|
||||
|
||||
async def publish_draft(
|
||||
self, req: WechatPublishRequest, request_id: str = "", account: dict | None = None
|
||||
) -> PublishResponse:
|
||||
rid = request_id or "-"
|
||||
if not settings.wechat_appid or not settings.wechat_secret:
|
||||
acct = self._resolve_account(account)
|
||||
if not acct["appid"] or not acct["secret"]:
|
||||
logger.warning("wechat skipped rid=%s reason=missing_appid_or_secret", rid)
|
||||
return PublishResponse(ok=False, detail="缺少 WECHAT_APPID / WECHAT_SECRET 配置")
|
||||
|
||||
token, token_from_cache, token_err_body = await self._get_access_token()
|
||||
token, token_from_cache, token_err_body = await self._get_access_token(acct["appid"], acct["secret"])
|
||||
if not token:
|
||||
detail = _detail_for_token_error(token_err_body)
|
||||
logger.error("wechat access_token_unavailable rid=%s detail=%s", rid, detail[:200])
|
||||
@@ -91,7 +108,7 @@ class WechatPublisher:
|
||||
logger.info("wechat_thumb rid=%s source=request_media_id", rid)
|
||||
thumb_id = req_thumb
|
||||
else:
|
||||
thumb_id = await self._resolve_thumb_media_id(token, rid)
|
||||
thumb_id = await self._resolve_thumb_media_id(token, rid, account=acct)
|
||||
if not thumb_id:
|
||||
return PublishResponse(
|
||||
ok=False,
|
||||
@@ -115,7 +132,7 @@ class WechatPublisher:
|
||||
{
|
||||
"article_type": "news",
|
||||
"title": req.title[:32] if len(req.title) > 32 else req.title,
|
||||
"author": (req.author or settings.wechat_author)[:16],
|
||||
"author": (req.author or acct["author"] or settings.wechat_author)[:16],
|
||||
"digest": (req.summary or "")[:128],
|
||||
"content": html,
|
||||
"content_source_url": "",
|
||||
@@ -126,7 +143,7 @@ class WechatPublisher:
|
||||
]
|
||||
}
|
||||
|
||||
explicit_used = bool((settings.wechat_thumb_media_id or "").strip())
|
||||
explicit_used = bool((acct.get("thumb_media_id") or "").strip())
|
||||
draft_url = f"https://api.weixin.qq.com/cgi-bin/draft/add?access_token={token}"
|
||||
async with httpx.AsyncClient(timeout=25) as client:
|
||||
logger.info(
|
||||
@@ -142,7 +159,9 @@ class WechatPublisher:
|
||||
rid,
|
||||
)
|
||||
self._runtime_thumb_media_id = None
|
||||
thumb_alt = await self._resolve_thumb_media_id(token, rid, force_skip_explicit=True)
|
||||
thumb_alt = await self._resolve_thumb_media_id(
|
||||
token, rid, force_skip_explicit=True, account=acct
|
||||
)
|
||||
if thumb_alt:
|
||||
payload["articles"][0]["thumb_media_id"] = thumb_alt
|
||||
async with httpx.AsyncClient(timeout=25) as client:
|
||||
@@ -167,33 +186,69 @@ class WechatPublisher:
|
||||
)
|
||||
return PublishResponse(ok=True, detail="已发布到公众号草稿箱", data=data)
|
||||
|
||||
async def upload_cover(self, filename: str, content: bytes, request_id: str = "") -> PublishResponse:
|
||||
async def upload_cover(
|
||||
self, filename: str, content: bytes, request_id: str = "", account: dict | None = None
|
||||
) -> PublishResponse:
|
||||
"""上传封面到微信永久素材,返回 thumb_media_id。"""
|
||||
rid = request_id or "-"
|
||||
if not settings.wechat_appid or not settings.wechat_secret:
|
||||
acct = self._resolve_account(account)
|
||||
if not acct["appid"] or not acct["secret"]:
|
||||
return PublishResponse(ok=False, detail="缺少 WECHAT_APPID / WECHAT_SECRET 配置")
|
||||
if not content:
|
||||
return PublishResponse(ok=False, detail="封面文件为空")
|
||||
|
||||
token, _, token_err_body = await self._get_access_token()
|
||||
token, _, token_err_body = await self._get_access_token(acct["appid"], acct["secret"])
|
||||
if not token:
|
||||
return PublishResponse(ok=False, detail=_detail_for_token_error(token_err_body), data=token_err_body)
|
||||
|
||||
async with httpx.AsyncClient(timeout=60) as client:
|
||||
mid = await self._upload_permanent_image(client, token, content, filename)
|
||||
if not mid:
|
||||
material = await self._upload_permanent_image(client, token, content, filename)
|
||||
if not material:
|
||||
return PublishResponse(
|
||||
ok=False,
|
||||
detail="封面上传失败:请检查图片格式/大小,或查看日志中的 wechat_material_add_failed",
|
||||
)
|
||||
|
||||
mid = material["media_id"]
|
||||
self._runtime_thumb_media_id = mid
|
||||
logger.info("wechat_cover_upload_ok rid=%s filename=%s media_id=%s", rid, filename, mid)
|
||||
return PublishResponse(ok=True, detail="封面上传成功", data={"thumb_media_id": mid, "filename": filename})
|
||||
|
||||
async def upload_body_material(
|
||||
self, filename: str, content: bytes, request_id: str = "", account: dict | None = None
|
||||
) -> PublishResponse:
|
||||
"""上传正文图片到微信永久素材库,返回 media_id 与可插入正文的 URL。"""
|
||||
rid = request_id or "-"
|
||||
acct = self._resolve_account(account)
|
||||
if not acct["appid"] or not acct["secret"]:
|
||||
return PublishResponse(ok=False, detail="缺少 WECHAT_APPID / WECHAT_SECRET 配置")
|
||||
if not content:
|
||||
return PublishResponse(ok=False, detail="素材文件为空")
|
||||
|
||||
token, _, token_err_body = await self._get_access_token(acct["appid"], acct["secret"])
|
||||
if not token:
|
||||
return PublishResponse(ok=False, detail=_detail_for_token_error(token_err_body), data=token_err_body)
|
||||
|
||||
async with httpx.AsyncClient(timeout=60) as client:
|
||||
material = await self._upload_permanent_image(client, token, content, filename)
|
||||
if not material:
|
||||
return PublishResponse(
|
||||
ok=False,
|
||||
detail="素材上传失败:请检查图片格式/大小,或查看日志中的 wechat_material_add_failed",
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"wechat_body_material_upload_ok rid=%s filename=%s media_id=%s url=%s",
|
||||
rid,
|
||||
filename,
|
||||
material.get("media_id"),
|
||||
material.get("url"),
|
||||
)
|
||||
return PublishResponse(ok=True, detail="素材上传成功", data=material)
|
||||
|
||||
async def _upload_permanent_image(
|
||||
self, client: httpx.AsyncClient, token: str, content: bytes, filename: str
|
||||
) -> str | None:
|
||||
) -> dict[str, str] | None:
|
||||
url = f"https://api.weixin.qq.com/cgi-bin/material/add_material?access_token={token}&type=image"
|
||||
ctype = "image/png" if filename.lower().endswith(".png") else "image/jpeg"
|
||||
files = {"media": (filename, content, ctype)}
|
||||
@@ -206,11 +261,14 @@ class WechatPublisher:
|
||||
if not mid:
|
||||
logger.warning("wechat_material_add_no_media_id body=%s", data)
|
||||
return None
|
||||
return mid
|
||||
return {"media_id": mid, "url": data.get("url") or ""}
|
||||
|
||||
async def _resolve_thumb_media_id(self, token: str, rid: str, *, force_skip_explicit: bool = False) -> str | None:
|
||||
async def _resolve_thumb_media_id(
|
||||
self, token: str, rid: str, *, force_skip_explicit: bool = False, account: dict | None = None
|
||||
) -> str | None:
|
||||
"""draft/add 要求 thumb_media_id 为永久图片素材;优先用配置,否则上传文件或内置图。"""
|
||||
explicit = (settings.wechat_thumb_media_id or "").strip()
|
||||
acct = self._resolve_account(account)
|
||||
explicit = (acct.get("thumb_media_id") or "").strip()
|
||||
if explicit and not force_skip_explicit:
|
||||
logger.info("wechat_thumb rid=%s source=config_media_id", rid)
|
||||
return explicit
|
||||
@@ -219,35 +277,40 @@ class WechatPublisher:
|
||||
logger.info("wechat_thumb rid=%s source=runtime_cache", rid)
|
||||
return self._runtime_thumb_media_id
|
||||
|
||||
path = (settings.wechat_thumb_image_path or "").strip()
|
||||
path = (acct.get("thumb_image_path") or "").strip()
|
||||
async with httpx.AsyncClient(timeout=60) as client:
|
||||
if path:
|
||||
p = Path(path)
|
||||
if p.is_file():
|
||||
content = p.read_bytes()
|
||||
mid = await self._upload_permanent_image(client, token, content, p.name)
|
||||
if mid:
|
||||
self._runtime_thumb_media_id = mid
|
||||
material = await self._upload_permanent_image(client, token, content, p.name)
|
||||
if material:
|
||||
self._runtime_thumb_media_id = material["media_id"]
|
||||
logger.info("wechat_thumb rid=%s source=path_upload ok=1", rid)
|
||||
return mid
|
||||
return material["media_id"]
|
||||
logger.warning("wechat_thumb rid=%s source=path_upload ok=0 path=%s", rid, path)
|
||||
else:
|
||||
logger.warning("wechat_thumb rid=%s path_not_found=%s", rid, path)
|
||||
|
||||
content, fname = _build_default_cover_jpeg()
|
||||
mid = await self._upload_permanent_image(client, token, content, fname)
|
||||
if mid:
|
||||
self._runtime_thumb_media_id = mid
|
||||
material = await self._upload_permanent_image(client, token, content, fname)
|
||||
if material:
|
||||
self._runtime_thumb_media_id = material["media_id"]
|
||||
logger.info("wechat_thumb rid=%s source=default_jpeg_upload ok=1", rid)
|
||||
return mid
|
||||
return material["media_id"]
|
||||
logger.error("wechat_thumb rid=%s source=default_jpeg_upload ok=0", rid)
|
||||
return None
|
||||
|
||||
async def _get_access_token(self) -> tuple[str | None, bool, dict | None]:
|
||||
async def _get_access_token(self, appid: str, secret: str) -> tuple[str | None, bool, dict | None]:
|
||||
"""成功时第三项为 None;失败时为微信返回的 JSON(含 errcode/errmsg)。"""
|
||||
now = int(time.time())
|
||||
if self._access_token and now < self._expires_at - 60:
|
||||
return self._access_token, True, None
|
||||
key = appid.strip()
|
||||
cached = self._token_cache.get(key)
|
||||
if cached:
|
||||
token = str(cached.get("token") or "")
|
||||
exp = int(cached.get("expires_at") or 0)
|
||||
if token and now < exp - 60:
|
||||
return token, True, None
|
||||
|
||||
logger.info("wechat_http_get endpoint=cgi-bin/token reason=refresh_access_token")
|
||||
async with httpx.AsyncClient(timeout=20) as client:
|
||||
@@ -255,8 +318,8 @@ class WechatPublisher:
|
||||
"https://api.weixin.qq.com/cgi-bin/token",
|
||||
params={
|
||||
"grant_type": "client_credential",
|
||||
"appid": settings.wechat_appid,
|
||||
"secret": settings.wechat_secret,
|
||||
"appid": appid,
|
||||
"secret": secret,
|
||||
},
|
||||
)
|
||||
data = r.json() if r.content else {}
|
||||
@@ -270,6 +333,5 @@ class WechatPublisher:
|
||||
)
|
||||
return None, False, data if isinstance(data, dict) else None
|
||||
|
||||
self._access_token = token
|
||||
self._expires_at = now + int(data.get("expires_in", 7200))
|
||||
self._token_cache[key] = {"token": token, "expires_at": now + int(data.get("expires_in", 7200))}
|
||||
return token, False, None
|
||||
|
||||
Reference in New Issue
Block a user