From 26075272d6d75a63f1d73f956c61213f168ba707 Mon Sep 17 00:00:00 2001 From: QihuAI Design Date: Thu, 23 Jul 2026 22:42:46 +0800 Subject: [PATCH] chore(align): sync Git with running container as authoritative source One-time alignment to close drift between the running container (qihu-aiflow / image qihuaiflow:with-deps) and this Git repository. Container hotfixes previously persisted only via `docker commit` are now captured in Git so Git becomes the single source of truth. Backend: - chat_service.py: accumulated fixes incl. EigenFlux README guidance - chat_context.py / chat_media.py: URL/web-repo context injection - api/url_reader.py: NEW SSRF-safe web/README reader (Issue #725) - skills_admin.py & Skills.tsx: 14-scene capability unification - pgvector_search.py, orchestrator.py, platform_knowledge.py, market.py, admin/models.py: drift sync Frontend: - ChatPage.tsx: collapsible AI think/act block (Issue #724) - MarketPage / EvolutionDashboard / TokenDashboard styling sync Migrations: - 018_normalize_marketapp_scene.sql: fixed capability allowlist (prevents capability wipe on restart) Alignment performed 2026-07-23 22:42:43. No deletions; secrets/venv/node_modules/ caches were not touched (tracked-source-only copy). --- backend/python/app/api/im_llm.py | 517 +++++++++++++++++-------------- 1 file changed, 283 insertions(+), 234 deletions(-) diff --git a/backend/python/app/api/im_llm.py b/backend/python/app/api/im_llm.py index 5a0d1cd..f528dcc 100644 --- a/backend/python/app/api/im_llm.py +++ b/backend/python/app/api/im_llm.py @@ -1,234 +1,283 @@ -""" -IM Bot 真实 LLM 调用。 - -原实现位于 channels.py(通用频道管理模块),该模块已移除。 -为保持 IM Bot(微信机器人等)正常生成回复,将 _call_real_llm 迁移至此独立模块。 - -行为(对齐 AI 聊天): -- Issue 7:图像/视频/语音意图优先走平台媒体模型(与 AI 聊天同一套 Auto 路由), - 不再对"制作图片"这类请求只回文本。 -- Issue 8:默认 Auto 模型;每轮先自动发现本账号已安装且平台已部署的技能, - 多个适用时由模型自行判断选 1 个最优,通过 function calling 执行。 -- 2026-07-15:修复 trust_env=False + proxy 处理,避免容器坏代理导致调用失败; - 媒体生成前先经 _optimize_media_prompt 精炼提示词,提升 AGNES 生成质量。 -""" -import asyncio -import json -import logging -import re as _re -from datetime import datetime, timezone, timedelta -from typing import Optional -from urllib.parse import urlparse as _urlparse - -import httpx -from sqlalchemy.orm import Session - -from ..capability_registry import resolve_auto_model -from ..models import AIModel, ModelProvider, TokenUsageLog, gen_uuid -from app.core.observability import get_trace_id - -logger = logging.getLogger(__name__) - - -# 香港出网代理(实测对 AGNES 延迟更低,2026-07-15 用户实测),与 chat.py 同值。 -_HK_PROXY = "http://qyyfw:XHR521yhf@103.242.14.208:11495" - - -def _proxy_for_url(url: str) -> str | None: - """按目标域名选择是否走代理(与 chat.py 同逻辑)。 - - 2026-07-15 调整:AGNES 改走香港代理(实测延迟更低),不再直连。 - 国内域名(尊重 NO_PROXY)直连,更稳定。 - """ - import os - host = (_urlparse(url).hostname or "").lower() - if "agnes-ai.com" in host or "agnes-ai.space" in host: - return _HK_PROXY - if "qihuyun.com" in host: - return None - _no_proxy = [h.strip().lower() for h in os.environ.get("NO_PROXY", "").split(",") if h.strip()] - for _np in _no_proxy: - if host == _np or host.endswith("." + _np) or _np in host: - return None - return os.environ.get("HTTPS_PROXY") or os.environ.get("HTTP_PROXY") or "http://172.18.0.1:7890" - - -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(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): - """IM Bot 媒体生成记账:把图片/视频消耗写入 TokenUsageLog,供驾驶舱统计。 - - 此前 IM Bot 路径完全不写 TokenUsageLog,导致微信生成的图片/视频在驾驶舱里“查无此项”。 - 媒体 API 不返回 token 用量,这里用与 chat.py _generate_media_task 一致的估算。 - """ - if not user_id or not ai_model: - return - try: - base = 1200 if media_type == "video" else 400 - est = base + (len(prompt or "") // 4) - db.add(TokenUsageLog( - id=gen_uuid(), - user_id=user_id, - model_id=ai_model.id, - model_name=ai_model.name, - trace_id=get_trace_id(), - prompt_tokens=est, - completion_tokens=0, - total_tokens=est, - created_at=datetime.now(timezone(timedelta(hours=8))).replace(tzinfo=None), - )) - db.commit() - except Exception as e: - try: - db.rollback() - except Exception: - pass - logger.warning(f"[im_llm] 媒体 Token 记账失败(忽略): {e}") - - -def _call_real_llm(db: Session, user=None, prompt: str = "", platform: str = "im", user_id: str = None) -> "str | dict": - """调用七虎AI 生成回复,供 IM Bot 复用。支持媒体意图路由与已安装技能自动发现。""" - try: - from app.api.chat import ( - _generate_media, - detect_media_intent, - _discover_installed_skills, - _build_system_prompt, - _execute_skill_tool, - ) - - # ===== 1) 媒体意图路由(Issue 7)===== - intent = detect_media_intent(prompt) - if intent: - ai_model = resolve_auto_model(intent, db) - if ai_model: - try: - result = _run_async(_generate_media(db, ai_model, prompt, user_id), timeout=300) - except Exception: - # 任何未预期异常(如 AGNES 连接重置)统一兜底为友好提示, - # 绝不直接把原始异常(如 "Server disconnected")甩给用户 - result = "⚠️ 媒体生成暂时失败(连接或服务波动),请稍后重试。" - if result: - # 媒体生成成功:返回结构化对象,交由 im_bot_user 在微信内联发送图片/视频 - s = result.strip() - if s.startswith(" str | None: + """按目标域名选择是否走代理(与 chat.py 同逻辑)。 + + 2026-07-15 调整:AGNES 改走香港代理(实测延迟更低),不再直连。 + 国内域名(尊重 NO_PROXY)直连,更稳定。 + """ + import os + host = (_urlparse(url).hostname or "").lower() + if "agnes-ai.com" in host or "agnes-ai.space" in host: + return _HK_PROXY + if "qihuyun.com" in host: + return None + _no_proxy = [h.strip().lower() for h in os.environ.get("NO_PROXY", "").split(",") if h.strip()] + for _np in _no_proxy: + if host == _np or host.endswith("." + _np) or _np in host: + return None + return os.environ.get("HTTPS_PROXY") or os.environ.get("HTTP_PROXY") or "http://172.18.0.1:7890" + + +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(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): + """IM Bot 媒体生成记账:把图片/视频消耗写入 TokenUsageLog,供驾驶舱统计。 + + 此前 IM Bot 路径完全不写 TokenUsageLog,导致微信生成的图片/视频在驾驶舱里"查无此项"。 + 媒体 API 不返回 token 用量,这里用与 chat.py _generate_media_task 一致的估算。 + """ + if not user_id or not ai_model: + return + try: + base = 1200 if media_type == "video" else 400 + est = base + (len(prompt or "") // 4) + db.add(TokenUsageLog( + id=gen_uuid(), + user_id=user_id, + model_id=ai_model.id, + model_name=ai_model.name, + trace_id=get_trace_id(), + prompt_tokens=est, + completion_tokens=0, + total_tokens=est, + created_at=datetime.now(timezone(timedelta(hours=8))).replace(tzinfo=None), + )) + db.commit() + except Exception as e: + try: + db.rollback() + except Exception: + pass + logger.warning(f"[im_llm] 媒体 Token 记账失败(忽略): {e}") + + +def _call_real_llm(db: Session, user=None, prompt: str = "", platform: str = "im", user_id: str = None) -> "str | dict": + """调用七虎AI 生成回复,供 IM Bot 复用。支持媒体意图路由、已安装技能自动发现、模型 failover。""" + try: + from app.api.chat import ( + _generate_media, + detect_media_intent, + _discover_installed_skills, + _build_system_prompt, + _execute_skill_tool, + ) + + # ===== 1) 媒体意图路由(Issue 7)===== + intent = detect_media_intent(prompt) + if intent: + ai_model = resolve_auto_model(intent, db) + if ai_model: + try: + result = _run_async(_generate_media(db, ai_model, prompt, user_id), timeout=300) + except Exception: + # 任何未预期异常(如 AGNES 连接重置)统一兜底为友好提示, + # 绝不直接把原始异常(如 "Server disconnected")甩给用户 + result = "⚠️ 媒体生成暂时失败(连接或服务波动),请稍后重试。" + if result: + # 媒体生成成功:返回结构化对象,交由 im_bot_user 在微信内联发送图片/视频 + s = result.strip() + if s.startswith("