From ce81297159b4f287e314b72696af44cec231bda5 Mon Sep 17 00:00:00 2001 From: QihuAI Design Date: Thu, 16 Jul 2026 15:10:50 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=8A=80=E8=83=BD?= =?UTF-8?q?=E5=B8=82=E5=9C=BA=E5=88=86=E9=A1=B5=E3=80=81=E5=90=8E=E5=8F=B0?= =?UTF-8?q?Skills=E7=AE=A1=E7=90=86=E3=80=81=E5=BE=AE=E4=BF=A1Bot=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E7=94=9F=E6=88=90=E8=B6=85=E6=97=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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图片生成无限期挂住的问题 --- backend/python/app/api/admin/skills_admin.py | 113 +++++++++++++------ backend/python/app/api/im_llm.py | 18 ++- frontend/src/pages/front/MarketPage.tsx | 2 +- 3 files changed, 94 insertions(+), 39 deletions(-) diff --git a/backend/python/app/api/admin/skills_admin.py b/backend/python/app/api/admin/skills_admin.py index f715b40..1e371db 100644 --- a/backend/python/app/api/admin/skills_admin.py +++ b/backend/python/app/api/admin/skills_admin.py @@ -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: """将技能ORM对象转换为字典""" result = { @@ -118,59 +151,71 @@ async def list_skills( admin_user=Depends(get_current_admin), db: Session = Depends(get_db), ): - """获取全平台技能列表(支持分页/过滤/搜索)""" + """获取全平台技能列表(支持分页/过滤/搜索)—— 同时查询 EvolutionSkill + MarketApp(type=skill)""" 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: - query = query.filter(EvolutionSkill.state == state) + evo_query = evo_query.filter(EvolutionSkill.state == state) if category: - query = query.filter(EvolutionSkill.category == category) + evo_query = evo_query.filter(EvolutionSkill.category == category) if keyword: - query = query.filter( - or_( - EvolutionSkill.skill_name.contains(keyword), - EvolutionSkill.description.contains(keyword) - ) + evo_query = evo_query.filter( + or_(EvolutionSkill.skill_name.contains(keyword), EvolutionSkill.description.contains(keyword)) ) if user_id: - query = query.filter(EvolutionSkill.user_id == user_id) + evo_query = evo_query.filter(EvolutionSkill.user_id == user_id) - # 总数 - total = query.count() - - # 排序 - 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: + evo_skills = evo_query.all() + 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: item = skill_to_dict(s) item["owner_username"] = users_map.get(s.user_id, s.user_id) + item["source"] = "evolution" 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 { "code": 0, "data": { - "items": result, + "items": paged, "total": total, "page": page, "page_size": page_size, - "pages": (total + page_size - 1) // page_size, + "pages": (total + page_size - 1) // page_size if total else 0, } } diff --git a/backend/python/app/api/im_llm.py b/backend/python/app/api/im_llm.py index 0665705..0b1d392 100644 --- a/backend/python/app/api/im_llm.py +++ b/backend/python/app/api/im_llm.py @@ -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" -def _run_async(coro): +def _run_async(coro, timeout=None): """在同步上下文中运行协程,兼容「无事件循环」与「已有事件循环」两种场景。 IM 个人微信在独立线程中调用(无事件循环)→ asyncio.run; IM 多平台 Webhook 可能在 FastAPI 协程内调用(已有事件循环)→ 丢到线程池执行。 + + timeout: 可选,秒数;超时抛出 asyncio.TimeoutError(由调用方捕获)。 """ try: loop = asyncio.get_running_loop() except RuntimeError: 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(): import concurrent.futures with concurrent.futures.ThreadPoolExecutor(1) as ex: - return ex.submit(lambda: asyncio.run(coro)).result() - return asyncio.run(coro) + return ex.submit(lambda: asyncio.run(target)).result(timeout=(timeout + 10) if timeout else None) + return asyncio.run(target) 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) if ai_model: 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: # 任何未预期异常(如 AGNES 连接重置)统一兜底为友好提示, # 绝不直接把原始异常(如 "Server disconnected")甩给用户 diff --git a/frontend/src/pages/front/MarketPage.tsx b/frontend/src/pages/front/MarketPage.tsx index d36bb7b..ed00428 100644 --- a/frontend/src/pages/front/MarketPage.tsx +++ b/frontend/src/pages/front/MarketPage.tsx @@ -1340,7 +1340,7 @@ const MarketPage: React.FC = () => { // 从后端加载应用列表 const loadApps = (search?: string) => { 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) { const items = (res.data.items || []).map((item: any) => ({ id: item.id,