Initial upload for secondary development

This commit is contained in:
2026-06-08 19:00:03 +08:00
commit b913b8c78c
81 changed files with 27139 additions and 0 deletions

View File

@@ -0,0 +1,174 @@
from __future__ import annotations
import logging
import sqlite3
from dataclasses import dataclass
from pathlib import Path
import httpx
from fastapi import HTTPException
from config import settings
from services.chatlog_context import get_chatlog_context
log = logging.getLogger(__name__)
@dataclass
class ResolvedMedia:
bytes: bytes
content_type: str
url: str
def _media_url(kind: str, key: str, thumb: bool = False) -> str:
url = f"{settings.chatlog_base_url}/{kind}/{key}"
if thumb:
url += "?thumb=1"
return url
def _read_voice_resource_status(key: str) -> dict:
ctx = get_chatlog_context()
work_dir = ctx.get("work_dir") or ""
if not work_dir:
return {"checked": False, "reason": "missing_work_dir"}
db_path = Path(work_dir) / "db_storage" / "message" / "message_resource.db"
if not db_path.exists():
return {"checked": False, "reason": "message_resource_db_missing", "path": str(db_path)}
try:
conn = sqlite3.connect(f"file:{db_path.as_posix()}?mode=ro", uri=True)
conn.row_factory = sqlite3.Row
try:
info = conn.execute(
"SELECT * FROM MessageResourceInfo WHERE message_svr_id=?",
(int(key),),
).fetchone()
if not info:
return {
"checked": True,
"found": False,
"path": str(db_path),
"message": "当前已解密资源库里没有这条语音的媒体资源记录",
}
details = conn.execute(
"SELECT type,size,status,data_index FROM MessageResourceDetail WHERE message_id=?",
(info["message_id"],),
).fetchall()
return {
"checked": True,
"found": True,
"path": str(db_path),
"message_id": info["message_id"],
"resources": [dict(row) for row in details],
}
finally:
conn.close()
except Exception as exc:
return {"checked": False, "reason": "resource_db_read_failed", "error": str(exc), "path": str(db_path)}
def _download_failure_message(kind: str, key: str, status_code: int | None, body: str = "") -> str:
if kind == "voice":
base = "底层语音文件未读取成功"
if status_code:
base += f"chatlog /voice 返回 HTTP {status_code}"
return (
f"{base}。请先确认已安装新版程序并重新识别当前微信账号;"
"如果仍失败,说明当前 chatlog 版本还不能解析该 WeChat 4.x 语音资源。"
)
if status_code:
return f"从 chatlog 下载媒体失败: HTTP {status_code}"
return f"从 chatlog 下载媒体失败: {body or 'unknown error'}"
async def diagnose_media(kind: str, key: str) -> dict:
if kind not in {"voice", "image", "video"}:
raise HTTPException(400, "不支持的媒体类型")
if not key:
raise HTTPException(400, "媒体 key 不能为空")
url = _media_url(kind, key, thumb=kind in {"image", "video"})
result = {
"ok": False,
"kind": kind,
"key": key,
"url": url,
"chatlog_base_url": settings.chatlog_base_url,
"chatlog_context": get_chatlog_context(),
}
async with httpx.AsyncClient(timeout=20, trust_env=False, follow_redirects=True) as client:
try:
resp = await client.get(url)
content_type = resp.headers.get("content-type", "")
result.update(
{
"status_code": resp.status_code,
"content_type": content_type,
"content_length": len(resp.content or b""),
"ok": resp.status_code < 400 and bool(resp.content),
}
)
if resp.status_code >= 400:
result["error"] = _download_failure_message(kind, key, resp.status_code, resp.text[:500])
result["response_preview"] = resp.text[:500]
elif not resp.content:
result["error"] = "chatlog 返回了空媒体文件"
except Exception as exc:
result.update({"error": f"无法连接 chatlog 媒体接口: {exc}", "exception": str(exc)})
if kind == "voice":
result["resource_db"] = _read_voice_resource_status(key)
return result
async def resolve_media(kind: str, key: str) -> ResolvedMedia:
if kind not in {"voice", "image", "video"}:
raise HTTPException(400, "不支持的媒体类型")
if not key:
raise HTTPException(400, "媒体 key 不能为空")
url = _media_url(kind, key, thumb=kind in {"image", "video"})
async with httpx.AsyncClient(timeout=60, trust_env=False, follow_redirects=True) as client:
try:
resp = await client.get(url)
resp.raise_for_status()
except httpx.HTTPStatusError as exc:
diagnostics = await diagnose_media(kind, key)
log.warning("[media_resolver] media download failed: %s", diagnostics)
raise HTTPException(
502,
{
"message": _download_failure_message(kind, key, exc.response.status_code, exc.response.text[:500]),
"diagnostics": diagnostics,
},
)
except Exception as exc:
diagnostics = await diagnose_media(kind, key)
log.warning("[media_resolver] media download exception: %s", diagnostics)
raise HTTPException(
502,
{
"message": _download_failure_message(kind, key, None, str(exc)),
"diagnostics": diagnostics,
},
)
if not resp.content:
diagnostics = await diagnose_media(kind, key)
raise HTTPException(
502,
{
"message": "chatlog 返回了空媒体文件",
"diagnostics": diagnostics,
},
)
return ResolvedMedia(
bytes=resp.content,
content_type=resp.headers.get("content-type", "application/octet-stream"),
url=url,
)