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 聊天中渲染的富文本(含
/