fix: 修复技能市场分页、后台Skills管理、微信Bot图片生成超时

1. MarketPage.tsx: getApps 加 page_size=100,修复只显示20条
2. skills_admin.py: list_skills 同时查询 market_apps(type=skill)
   解决前台导入50个技能后台看不到(前后台用不同表)
3. im_llm.py: _run_async 加 timeout 参数,_call_real_llm 媒体生成
   5分钟超时保护,解决微信Bot图片生成无限期挂住的问题
This commit is contained in:
QihuAI Design committed 2026-07-16 15:10:50 +08:00
1 parent b3866f770c
commit ce81297159
3 files changed
+94 -39

No files matched your search

+79 -34
View File
@@ -54,6 +54,39 @@ SKILL_CATEGORIES = [
# 辅助函数 # 辅助函数
# ============================================================ # ============================================================
def market_app_to_skill_dict(app: "MarketApp") -> dict:
"""将市场应用(MarketApp type=skill)转换为与 EvolutionSkill 一致的字典格式"""
status = (getattr(app, "status", None) or "pending").strip()
state = "main" if status == "published" else "baby"
raw_tags = getattr(app, "tags", None)
tags_list = []
if raw_tags:
try:
tags_list = json.loads(raw_tags) if isinstance(raw_tags, str) else list(raw_tags)
except Exception:
tags_list = [t.strip() for t in str(raw_tags).split(",")]
category = getattr(app, "category", None) or "other"
return {
"id": app.id,
"user_id": getattr(app, "publisher_id", None) or "system",
"skill_name": app.name,
"description": getattr(app, "description", None) or "",
"tags": tags_list,
"category": category,
"category_label": next((c["label"] for c in SKILL_CATEGORIES if c["id"] == category), category or "其他"),
"state": state,
"state_info": SKILL_STATES.get(state, SKILL_STATES["baby"]),
"use_count": getattr(app, "install_count", None) or 0,
"version": 1,
"skill_md_path": None,
"git_commit_sha": None,
"last_used_at": None,
"created_at": app.created_at.isoformat() if getattr(app, "created_at", None) else None,
"updated_at": getattr(app, "updated_at", None).isoformat() if getattr(app, "updated_at", None) else None,
"source": "market",
}
def skill_to_dict(s: EvolutionSkill, include_md: bool = False) -> dict: def skill_to_dict(s: EvolutionSkill, include_md: bool = False) -> dict:
"""将技能ORM对象转换为字典""" """将技能ORM对象转换为字典"""
result = { result = {
@@ -118,59 +151,71 @@ async def list_skills(
admin_user=Depends(get_current_admin), admin_user=Depends(get_current_admin),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
"""获取全平台技能列表(支持分页/过滤/搜索)""" """获取全平台技能列表(支持分页/过滤/搜索)—— 同时查询 EvolutionSkill + MarketApp(type=skill)"""
logger.info(f"[Admin Skills] {admin_user.username} 列出技能: state={state}, keyword={keyword}") logger.info(f"[Admin Skills] {admin_user.username} 列出技能: state={state}, keyword={keyword}")
query = db.query(EvolutionSkill) result: list = []
# ===== 1) 查询 EvolutionSkill =====
evo_query = db.query(EvolutionSkill)
if state: if state:
query = query.filter(EvolutionSkill.state == state) evo_query = evo_query.filter(EvolutionSkill.state == state)
if category: if category:
query = query.filter(EvolutionSkill.category == category) evo_query = evo_query.filter(EvolutionSkill.category == category)
if keyword: if keyword:
query = query.filter( evo_query = evo_query.filter(
or_( or_(EvolutionSkill.skill_name.contains(keyword), EvolutionSkill.description.contains(keyword))
EvolutionSkill.skill_name.contains(keyword),
EvolutionSkill.description.contains(keyword)
)
) )
if user_id: if user_id:
query = query.filter(EvolutionSkill.user_id == user_id) evo_query = evo_query.filter(EvolutionSkill.user_id == user_id)
# 总数 evo_skills = evo_query.all()
total = query.count() user_ids = list(set(s.user_id for s in evo_skills))
users_map = {u.id: u.username for u in db.query(User.id, User.username).filter(User.id.in_(user_ids)).all()}
# 排序 for s in evo_skills:
sort_field = getattr(EvolutionSkill, sort_by, EvolutionSkill.updated_at)
if order == "desc":
query = query.order_by(desc(sort_field))
else:
query = query.order_by(sort_field)
# 分页
skills = query.offset((page - 1) * page_size).limit(page_size).all()
# 补充用户名
user_ids = list(set(s.user_id for s in skills))
users_map = {
u.id: u.username
for u in db.query(User.id, User.username).filter(User.id.in_(user_ids)).all()
}
result = []
for s in skills:
item = skill_to_dict(s) item = skill_to_dict(s)
item["owner_username"] = users_map.get(s.user_id, s.user_id) item["owner_username"] = users_map.get(s.user_id, s.user_id)
item["source"] = "evolution"
result.append(item) result.append(item)
# ===== 2) 查询 MarketApp (type=skill) =====
mkt_query = db.query(MarketApp).filter(MarketApp.type == "skill")
if keyword:
mkt_query = mkt_query.filter(
or_(MarketApp.name.contains(keyword), MarketApp.description.contains(keyword))
)
if category:
mkt_query = mkt_query.filter(MarketApp.category == category)
# 状态映射:主面板只展示已发布 → state='main';草稿/移除 → 'baby'
if state == "main":
mkt_query = mkt_query.filter(MarketApp.status == "published")
elif state:
mkt_query = mkt_query.filter(MarketApp.status != "published") # 非发布=草稿等
market_apps_rows = mkt_query.all()
for app in market_apps_rows:
item = market_app_to_skill_dict(app)
item["owner_username"] = app.publisher_name or "system"
result.append(item)
# ===== 3) 合并排序 & 分页 =====
total = len(result)
reverse = order == "desc"
key_fn = lambda x: x.get(sort_by) or ""
result.sort(key=key_fn, reverse=reverse)
start = (page - 1) * page_size
end = start + page_size
paged = result[start:end]
return { return {
"code": 0, "code": 0,
"data": { "data": {
"items": result, "items": paged,
"total": total, "total": total,
"page": page, "page": page,
"page_size": page_size, "page_size": page_size,
"pages": (total + page_size - 1) // page_size, "pages": (total + page_size - 1) // page_size if total else 0,
} }
} }
+14 -4
View File
@@ -52,21 +52,31 @@ def _proxy_for_url(url: str) -> str | None:
return os.environ.get("HTTPS_PROXY") or os.environ.get("HTTP_PROXY") or "http://172.18.0.1:7890" return os.environ.get("HTTPS_PROXY") or os.environ.get("HTTP_PROXY") or "http://172.18.0.1:7890"
def _run_async(coro): def _run_async(coro, timeout=None):
"""在同步上下文中运行协程,兼容「无事件循环」与「已有事件循环」两种场景。 """在同步上下文中运行协程,兼容「无事件循环」与「已有事件循环」两种场景。
IM 个人微信在独立线程中调用(无事件循环)→ asyncio.run; IM 个人微信在独立线程中调用(无事件循环)→ asyncio.run;
IM 多平台 Webhook 可能在 FastAPI 协程内调用(已有事件循环)→ 丢到线程池执行。 IM 多平台 Webhook 可能在 FastAPI 协程内调用(已有事件循环)→ 丢到线程池执行。
timeout: 可选,秒数;超时抛出 asyncio.TimeoutError(由调用方捕获)。
""" """
try: try:
loop = asyncio.get_running_loop() loop = asyncio.get_running_loop()
except RuntimeError: except RuntimeError:
loop = None loop = None
if timeout is not None:
async def _timed():
return await asyncio.wait_for(coro, timeout=timeout)
target = _timed()
else:
target = coro
if loop is not None and loop.is_running(): if loop is not None and loop.is_running():
import concurrent.futures import concurrent.futures
with concurrent.futures.ThreadPoolExecutor(1) as ex: with concurrent.futures.ThreadPoolExecutor(1) as ex:
return ex.submit(lambda: asyncio.run(coro)).result() return ex.submit(lambda: asyncio.run(target)).result(timeout=(timeout + 10) if timeout else None)
return asyncio.run(coro) return asyncio.run(target)
def _log_media_usage(db: Session, user_id: str, ai_model, media_type: str, prompt: str): def _log_media_usage(db: Session, user_id: str, ai_model, media_type: str, prompt: str):
@@ -116,7 +126,7 @@ def _call_real_llm(db: Session, user=None, prompt: str = "", platform: str = "im
ai_model = resolve_auto_model(intent, db) ai_model = resolve_auto_model(intent, db)
if ai_model: if ai_model:
try: try:
result = _run_async(_generate_media(db, ai_model, prompt, user_id)) result = _run_async(_generate_media(db, ai_model, prompt, user_id), timeout=300)
except Exception: except Exception:
# 任何未预期异常(如 AGNES 连接重置)统一兜底为友好提示, # 任何未预期异常(如 AGNES 连接重置)统一兜底为友好提示,
# 绝不直接把原始异常(如 "Server disconnected")甩给用户 # 绝不直接把原始异常(如 "Server disconnected")甩给用户
+1 -1
View File
@@ -1340,7 +1340,7 @@ const MarketPage: React.FC = () => {
// 从后端加载应用列表 // 从后端加载应用列表
const loadApps = (search?: string) => { const loadApps = (search?: string) => {
setLoading(true); setLoading(true);
marketAPI.getApps(search ? { search } : {}).then((res: any) => { marketAPI.getApps(search ? { search, page_size: 100 } : { page_size: 100 }).then((res: any) => {
if (res.code === 0 && res.data) { if (res.code === 0 && res.data) {
const items = (res.data.items || []).map((item: any) => ({ const items = (res.data.items || []).map((item: any) => ({
id: item.id, id: item.id,