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:
1 parent
b3866f770c
commit
ce81297159
3 files changed
+94
-39
No files matched your search
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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")甩给用户
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user