diff --git a/backend/python/app/api/market.py b/backend/python/app/api/market.py index c185bcf..c38a6ea 100644 --- a/backend/python/app/api/market.py +++ b/backend/python/app/api/market.py @@ -18,7 +18,7 @@ from ..models import ( ) from ..auth import get_current_user, get_current_user_id, get_current_user_id_optional from ..middleware.log_service import LogService -from ..capability_registry import CAPABILITY_TYPES, resolve_auto_model as _resolve_auto_model +from ..capability_registry import resolve_auto_model as _resolve_auto_model from app.api.admin.mcp_admin import _test_mcp_server, parse_mcp_config router = APIRouter() @@ -207,9 +207,48 @@ def verify_agent_connectivity(platform: str, endpoint: str, api_key: str) -> dic return {"ok": False, "verified": False, "detail": f"地址不可达:{_short_err(e)}"} -# ===== 能力分类(AI 自动归类,任何失败都返回 None,不阻断导入) ===== -def _classify_capability(name: str, description: Optional[str], content: Optional[str], db: Session) -> Optional[str]: - """用平台文本模型把应用归类到 11 种能力之一;失败返回 None(不阻断导入)。""" +# ===== 业务场景分类(面向用户的技能/MCP 分类,与后端模型路由的 capability_registry 解耦) ===== +# 注意:MarketApp.capability 列在此存放「业务场景 id」(见 MARKET_SCENES),不再是 text/vision 等模型能力。 +MARKET_SCENES: list = [ + # 自由职业者 + {"id": "freelance_content", "label": "内容创作/自媒体", "color": "#F59E0B", "group": "freelance"}, + {"id": "freelance_client", "label": "客户沟通/提案", "color": "#F97316", "group": "freelance"}, + {"id": "freelance_finance", "label": "报价合同/记账", "color": "#FBBF24", "group": "freelance"}, + # 上班从业者 + {"id": "office_doc", "label": "文档/会议纪要", "color": "#3B82F6", "group": "office"}, + {"id": "office_data", "label": "数据/报表分析", "color": "#6366F1", "group": "office"}, + {"id": "office_email", "label": "邮件/协作", "color": "#0EA5E9", "group": "office"}, + {"id": "office_pm", "label": "项目/周报", "color": "#8B5CF6", "group": "office"}, + # 育人教育者 + {"id": "edu_prepare", "label": "备课/课件", "color": "#10B981", "group": "educator"}, + {"id": "edu_grade", "label": "作业批改/测评", "color": "#14B8A6", "group": "educator"}, + {"id": "edu_tutor", "label": "学情辅导/答疑", "color": "#22C55E", "group": "educator"}, + {"id": "edu_home", "label": "家校沟通", "color": "#84CC16", "group": "educator"}, + # 电商创业 + {"id": "biz_product", "label": "商品/营销文案", "color": "#EC4899", "group": "commerce"}, + {"id": "biz_service", "label": "客服/转化", "color": "#F43F5E", "group": "commerce"}, + {"id": "biz_ops", "label": "运营/投放", "color": "#D946EF", "group": "commerce"}, + # 通用提效 + {"id": "general_write", "label": "通用写作", "color": "#64748B", "group": "general"}, + {"id": "general_research", "label": "资料/调研", "color": "#06B6D4", "group": "general"}, + {"id": "general_agent", "label": "智能体/自动化", "color": "#A855F7", "group": "general"}, +] +SCENE_GROUPS: list = [ + {"id": "freelance", "label": "自由职业者"}, + {"id": "office", "label": "上班从业者"}, + {"id": "educator", "label": "育人教育者"}, + {"id": "commerce", "label": "电商创业"}, + {"id": "general", "label": "通用提效"}, +] +_VALID_SCENE_IDS = {s["id"] for s in MARKET_SCENES} + + +def _scene_label(scene_id): + return next((s["label"] for s in MARKET_SCENES if s["id"] == scene_id), None) + + +def _classify_scene(name: str, description: Optional[str], content: Optional[str], db: Session) -> Optional[str]: + """用平台文本模型把应用归类到面向用户的业务场景之一;失败返回 None(不阻断导入)。""" try: text_model = _resolve_auto_model("text", db) if not text_model: @@ -225,9 +264,10 @@ def _classify_capability(name: str, description: Optional[str], content: Optiona ) if not api_url: return None - options = "、".join(f"{c['id']}({c['label']})" for c in CAPABILITY_TYPES) + options = "、".join(f"{s['id']}({s['label']})" for s in MARKET_SCENES) sys_prompt = ( - "你是应用市场的能力分类器。根据应用名称、描述与内容,把它精确归类到下列能力之一:" + "你是 AI 应用市场的『业务场景分类器』。请站在使用者视角,根据应用名称、描述与内容," + "判断它最常被哪一类【人群 / 岗位 / 业务场景】的用户使用,并归类到下列场景之一:" f"{options}。\n只返回一个 JSON 对象:{{\"capability\": \"\"}},不要任何额外解释或 Markdown。" ) user_text = f"名称:{name}\n描述:{description or ''}\n内容摘要:{(content or '')[:2000]}" @@ -247,7 +287,7 @@ def _classify_capability(name: str, description: Optional[str], content: Optiona data = r.json() text_out = data.get("choices", [{}])[0].get("message", {}).get("content", "") m = re.search(r'"capability"\s*:\s*"([a-z_]+)"', text_out) - if m and m.group(1) in {c["id"] for c in CAPABILITY_TYPES}: + if m and m.group(1) in _VALID_SCENE_IDS: return m.group(1) return None except Exception: @@ -283,9 +323,9 @@ def detect_capability( db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): - """导入前 AI 验证阶段:根据名称/描述/内容识别能力分类,供前台预填并允许用户修改。""" - cap = _classify_capability(req.name, req.description, req.skill_content, db) - label = next((c["label"] for c in CAPABILITY_TYPES if c["id"] == cap), None) + """导入前 AI 验证阶段:根据名称/描述/内容识别业务场景分类,供前台预填并允许用户修改。""" + cap = _classify_scene(req.name, req.description, req.skill_content, db) + label = _scene_label(cap) return {"code": 0, "data": {"capability": cap, "label": label}, "message": "ok"} @@ -528,7 +568,7 @@ def upload_app( ) # 能力分类:前端显式指定优先;否则导入阶段由 AI 自动归类;都不行则留空(不阻断导入) _classify_content = req.skill_content or req.download_url or "" - app.capability = req.capability or _classify_capability(req.name, req.description, _classify_content, db) or None + app.capability = req.capability or _classify_scene(req.name, req.description, _classify_content, db) or None db.add(app) db.commit() db.refresh(app) diff --git a/frontend/src/pages/front/MarketPage.css b/frontend/src/pages/front/MarketPage.css index 907fcbf..a166998 100644 --- a/frontend/src/pages/front/MarketPage.css +++ b/frontend/src/pages/front/MarketPage.css @@ -1744,10 +1744,26 @@ .capability-filter { display: flex; flex-wrap: wrap; + align-items: center; gap: var(--space-xs); padding: var(--space-sm) 0 0; } +.cap-group-label { + display: inline-flex; + align-items: center; + margin-left: var(--space-sm); + padding: 0 4px; + font-size: 11px; + font-weight: 700; + color: var(--color-text-tertiary); + letter-spacing: 0.02em; +} + +.cap-group-label:first-of-type { + margin-left: 0; +} + .cap-chip { padding: 4px 12px; font-size: var(--text-xs); diff --git a/frontend/src/pages/front/MarketPage.tsx b/frontend/src/pages/front/MarketPage.tsx index cb27304..b89d968 100644 --- a/frontend/src/pages/front/MarketPage.tsx +++ b/frontend/src/pages/front/MarketPage.tsx @@ -182,30 +182,53 @@ const typeIcons: Record = { mcp: , }; -// ===== 能力分类(与后端 11 种能力保持一致) ===== -const CAPABILITIES: { id: string; label: string; color: string }[] = [ - { id: 'text', label: '文本对话', color: '#5B5FED' }, - { id: 'thinking', label: '推理思考', color: '#8B5CF6' }, - { id: 'vision', label: '视觉理解', color: '#219B5E' }, - { id: 'tool', label: '工具调用', color: '#3B82F6' }, - { id: 'image', label: '图像生成', color: '#D9383A' }, - { id: 'audio', label: '语音合成', color: '#E8922C' }, - { id: 'asr', label: '语音识别', color: '#0EA5E9' }, - { id: 'ocr', label: 'OCR识别', color: '#F59E0B' }, - { id: 'embedding', label: '嵌入向量', color: '#10B981' }, - { id: 'rerank', label: '重排序', color: '#EC4899' }, - { id: 'video', label: '视频生成', color: '#EF4444' }, +// ===== 业务场景分类(面向用户的技能/MCP 分类,与后端模型路由能力解耦) ===== +// 两级:人群大类(SCENE_GROUPS) -> 业务场景(SCENES)。id 与后端 MARKET_SCENES 保持一致。 +const SCENES: { id: string; label: string; color: string; group: string }[] = [ + // 自由职业者 + { id: 'freelance_content', label: '内容创作/自媒体', color: '#F59E0B', group: 'freelance' }, + { id: 'freelance_client', label: '客户沟通/提案', color: '#F97316', group: 'freelance' }, + { id: 'freelance_finance', label: '报价合同/记账', color: '#FBBF24', group: 'freelance' }, + // 上班从业者 + { id: 'office_doc', label: '文档/会议纪要', color: '#3B82F6', group: 'office' }, + { id: 'office_data', label: '数据/报表分析', color: '#6366F1', group: 'office' }, + { id: 'office_email', label: '邮件/协作', color: '#0EA5E9', group: 'office' }, + { id: 'office_pm', label: '项目/周报', color: '#8B5CF6', group: 'office' }, + // 育人教育者 + { id: 'edu_prepare', label: '备课/课件', color: '#10B981', group: 'educator' }, + { id: 'edu_grade', label: '作业批改/测评', color: '#14B8A6', group: 'educator' }, + { id: 'edu_tutor', label: '学情辅导/答疑', color: '#22C55E', group: 'educator' }, + { id: 'edu_home', label: '家校沟通', color: '#84CC16', group: 'educator' }, + // 电商创业 + { id: 'biz_product', label: '商品/营销文案', color: '#EC4899', group: 'commerce' }, + { id: 'biz_service', label: '客服/转化', color: '#F43F5E', group: 'commerce' }, + { id: 'biz_ops', label: '运营/投放', color: '#D946EF', group: 'commerce' }, + // 通用提效 + { id: 'general_write', label: '通用写作', color: '#64748B', group: 'general' }, + { id: 'general_research', label: '资料/调研', color: '#06B6D4', group: 'general' }, + { id: 'general_agent', label: '智能体/自动化', color: '#A855F7', group: 'general' }, ]; +const SCENE_GROUPS: { id: string; label: string }[] = [ + { id: 'freelance', label: '自由职业者' }, + { id: 'office', label: '上班从业者' }, + { id: 'educator', label: '育人教育者' }, + { id: 'commerce', label: '电商创业' }, + { id: 'general', label: '通用提效' }, +]; +const SCENE_BY_GROUP: Record = SCENE_GROUPS.reduce((acc, g) => { + acc[g.id] = SCENES.filter(s => s.group === g.id); + return acc; +}, {} as Record); function CapabilityBadge({ capability }: { capability?: string | null }) { if (!capability) return null; - const cap = CAPABILITIES.find(c => c.id === capability); + const cap = SCENES.find(c => c.id === capability); if (!cap) return null; return ( {cap.label} @@ -751,7 +774,7 @@ const CreateToolModal: React.FC = ({ appType, onClose, onS @@ -1134,7 +1157,7 @@ const HubImportModal: React.FC = ({ appType, platforms, onC @@ -1460,7 +1483,7 @@ const AgentConnectModal: React.FC = ({ onClose, onConnec @@ -1799,19 +1822,24 @@ const MarketPage: React.FC = () => { )} - {/* 能力分类筛选 */} + {/* 业务场景筛选(按人群分组) */}
- {CAPABILITIES.map(c => ( - + >全部场景 + {SCENE_GROUPS.map(g => ( + + {g.label} + {SCENE_BY_GROUP[g.id].map(c => ( + + ))} + ))}
diff --git a/migrations/018_normalize_marketapp_scene.sql b/migrations/018_normalize_marketapp_scene.sql new file mode 100644 index 0000000..7f3982d --- /dev/null +++ b/migrations/018_normalize_marketapp_scene.sql @@ -0,0 +1,14 @@ +-- 018: 将 market_apps.capability 从旧的「AI 技术能力」取值规范为「业务场景」取值 +-- 旧的 text/thinking/vision/tool/image/audio/asr/ocr/embedding/rerank/video 已不再使用, +-- 统一置空,后续由 AI 验证 / 导入阶段按业务场景重新归类。 +-- 幂等:执行一次后不再有匹配行,可安全每次重启重跑。 +UPDATE market_apps +SET capability = NULL +WHERE capability IS NOT NULL + AND capability NOT IN ( + 'freelance_content', 'freelance_client', 'freelance_finance', + 'office_doc', 'office_data', 'office_email', 'office_pm', + 'edu_prepare', 'edu_grade', 'edu_tutor', 'edu_home', + 'biz_product', 'biz_service', 'biz_ops', + 'general_write', 'general_research', 'general_agent' + );