diff --git a/backend/python/app/api/admin/skills_admin.py b/backend/python/app/api/admin/skills_admin.py index 855759a..f715b40 100644 --- a/backend/python/app/api/admin/skills_admin.py +++ b/backend/python/app/api/admin/skills_admin.py @@ -16,7 +16,8 @@ from sqlalchemy import desc, func, or_, and_ from ...database import get_db from ...models import ( User, EvolutionSkill, SkillVersion, SkillPublishRecord, - EvolutionEvent, TeamSharedSkill + EvolutionEvent, TeamSharedSkill, MarketApp, AppInstallation, + AppPurchase, AppReview ) from ...auth import get_current_admin from ...middleware.log_service import LogService @@ -429,7 +430,55 @@ async def update_skill( # ============================================================ -# 删除技能 +# 批量删除技能(用于一次性清空技能市场) +# ============================================================ + + +class BatchDeleteSkillsRequest(BaseModel): + ids: list[str] = [] # 要删除的技能 ID 列表;传空列表则删除所有 + + +@router.post("/batch-delete") +async def batch_delete_skills( + req: BatchDeleteSkillsRequest, + admin_user=Depends(get_current_admin), + db: Session = Depends(get_db), +): + """批量删除技能。ids 为空则删除全部技能。""" + if req.ids: + skills = db.query(EvolutionSkill).filter(EvolutionSkill.id.in_(req.ids)).all() + else: + skills = db.query(EvolutionSkill).all() + + deleted_count = 0 + for skill in skills: + skill_name = skill.skill_name + # 删除版本历史 + db.query(SkillVersion).filter(SkillVersion.skill_name == skill_name).delete() + # 删除发布记录 + db.query(SkillPublishRecord).filter(SkillPublishRecord.skill_id == skill.id).delete() + # 删除团队共享 + db.query(TeamSharedSkill).filter(TeamSharedSkill.skill_id == skill.id).delete() + # 同步清理市场应用表中的技能 + market_app = db.query(MarketApp).filter( + MarketApp.type == "skill", + MarketApp.name == skill_name, + ).first() + if market_app: + # 清理关联的安装和购买记录 + db.query(AppInstallation).filter(AppInstallation.app_id == market_app.id).delete() + db.query(AppPurchase).filter(AppPurchase.app_id == market_app.id).delete() + db.query(AppReview).filter(AppReview.app_id == market_app.id).delete() + db.delete(market_app) + db.delete(skill) + deleted_count += 1 + + db.commit() + return {"code": 0, "data": {"deleted": deleted_count}, "message": f"已删除 {deleted_count} 个技能"} + + +# ============================================================ +# 删除技能(单个) # ============================================================ @router.delete("/{skill_id}") diff --git a/backend/python/app/api/chat.py b/backend/python/app/api/chat.py index b123e88..98b78b5 100644 --- a/backend/python/app/api/chat.py +++ b/backend/python/app/api/chat.py @@ -48,14 +48,21 @@ def detect_media_intent(text: str) -> str | None: if t.endswith(("?", "?")) or any(q in t for q in ("吗", "呢", "怎么", "如何", "能否", "是否可以", "可以吗", "会不会", "why", "how")): return None video_kw = ("视频", "短片", "影片", "vide", "动画视频", "一段视频", "做个视频", "生成视频", - "视频生成", "来段视频", "拍个视频", "做段视频", "视频剪辑") + "视频生成", "来段视频", "拍个视频", "做段视频", "视频剪辑", "做视频", + "生成一个视频", "制作一个视频", "帮我做视频", "制作视频", + "生成短片", "做短片", "制作短片", "一段短片") image_kw = ("图片", "图像", "插画", "海报", "照片", "头像", "壁纸", "漫画", "表情包", "logo", "图标", "imag", "画一张", "画张", "画一只", "画只", "画个", "画幅", "来张", "来一张", "来幅", "发张图", "发个图", "生成图片", "生成一张", "生成一幅", "生成图", "图像生成", "做一张", "做张图", "做张", "出张图", "出一张图", "帮我画", "给我画", "请画", "绘制", "作画", "ai绘画", "ai画图", - "画个图", "画张图", "画一幅图", "一张图", "一幅画", "生成一幅") + "画个图", "画张图", "画一幅图", "一张图", "一幅画", "生成一幅", + "画画", "画一下", "画出来", "生成个图", "生成一张图", "生成个图片", + "做图片", "做个图", "制图", "整张图", "整张图出来", "搞张图", + "帮我生成图片", "帮我生成图像", "给我生成图片", "给我生成图像", + "生成一张图片", "生成一个图片", "制作一张图", "制作一张图片", + "来一张图片", "画一张图片", "画一幅", "画一副") audio_kw = ("语音", "配音", "朗读", "播报", "念一段", "有声", "tts", "音频", "生成语音", "读一下", "念一下", "语音合成") if any(k in t for k in video_kw): @@ -152,6 +159,86 @@ def _proxy_for_url(url: str) -> str | None: return _outbound_proxy() +async def _optimize_media_prompt(raw_prompt: str, media_type: str, db=None) -> str: + """使用平台文本模型对用户原始输入做「媒体提示词增强优化」。 + + AGNES 图像/视频模型对 prompt 质量高度敏感: + - 用户在多轮对话中说"帮我生成一张刚才讨论的产品图"包含大量会话噪音 + - AGNES 需要的是**详细的、描述性的、英文优先或中英混合**的视觉提示词 + - 缺乏构图/风格/光影/质量等指导词时,生成结果与预期差异巨大 + + 本函数把用户原文交给文本 LLM 精炼为符合 AGNES 预期的优质 prompt。 + 失败时回退到原始输入(不做额外引导,保证必达)。 + """ + if not raw_prompt or len(raw_prompt.strip()) < 2: + return raw_prompt + # 如果原始输入已经很长(>200 字)且英文为主,可能本身就是一个不错的 prompt,直接返回 + stripped = raw_prompt.strip() + ascii_chars = sum(1 for c in stripped if ord(c) < 128) + if len(stripped) > 200 and ascii_chars / max(len(stripped), 1) > 0.6: + return stripped + # 用文本 LLM 做翻译增强 + try: + from .capability_registry import resolve_auto_model as _resolve_auto + from ..models import ModelProvider as MP + from ..database import SessionLocal as SL + import httpx as _httpx + + # 独立数据库会话,避免与主流程竞争 + _db = db + _should_close = False + if _db is None: + _db = SL() + _should_close = True + try: + text_model = _resolve_auto("text", _db) + if not text_model: + return stripped + provider = _db.query(MP).filter(MP.id == text_model.provider_id).first() + api_key = text_model.api_key or (provider.api_key if provider else None) + api_url = text_model.endpoint + if not api_url and provider and provider.api_base_url: + api_url = f"{provider.api_base_url.rstrip('/')}/v1/chat/completions" + if not (api_key and api_url): + return stripped + + prompt_zh = ( + "你是一个专业的AI图像/视频提示词工程师。请将以下用户需求提炼成" + f"一段高质量的{'图像' if media_type == 'image' else '视频'}生成" + "提示词。要求:\n" + "1. 用英文书写(AGNES模型对英文响应最佳),核心关键词可保留中文;\n" + "2. 添加构图/风格/光影/色彩/质量描述(如 highly detailed, 8K, " + "professional lighting, masterpiece, cinematic等);\n" + "3. 去掉与视觉无关的对话噪音;\n" + "4. 只输出最终prompt文本,不要任何解释、不要引号包裹、不要\"Prompt:\"前缀。\n\n" + f"用户需求:{stripped}" + ) + messages = [ + {"role": "system", "content": "You are a professional prompt engineer. Output ONLY the final prompt text, no explanations."}, + {"role": "user", "content": prompt_zh}, + ] + body = {"model": text_model.name, "messages": messages, "max_tokens": 512, "temperature": 0.7} + async with _httpx.AsyncClient( + timeout=30.0, proxy=_proxy_for_url(api_url), trust_env=False + ) as client: + resp = await client.post( + api_url, + headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}, + json=body, + ) + if resp.status_code == 200: + data = resp.json() + opt = data.get("choices", [{}])[0].get("message", {}).get("content", "").strip() + if opt and len(opt) > 5: + return opt + finally: + if _should_close: + _db.close() + except Exception: + pass + return stripped + + async def _generate_media(db, ai_model, prompt: str, user_id: str = None) -> str: """图像/视频生成模型统一入口:按 capabilities 调用 AGNES 等媒体生成 API。 返回可直接在 AI 聊天中渲染的富文本(含 /