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).
This commit is contained in:
1 parent
a1fd8b074d
commit
26075272d6
1 file changed
+67
-18
@@ -4,13 +4,16 @@ IM Bot 真实 LLM 调用。
|
|||||||
原实现位于 channels.py(通用频道管理模块),该模块已移除。
|
原实现位于 channels.py(通用频道管理模块),该模块已移除。
|
||||||
为保持 IM Bot(微信机器人等)正常生成回复,将 _call_real_llm 迁移至此独立模块。
|
为保持 IM Bot(微信机器人等)正常生成回复,将 _call_real_llm 迁移至此独立模块。
|
||||||
|
|
||||||
行为(对齐 AI 聊天):
|
行为(对齐 AI 聊天 — 2026-07-23 同步至最新):
|
||||||
- Issue 7:图像/视频/语音意图优先走平台媒体模型(与 AI 聊天同一套 Auto 路由),
|
- Issue 7:图像/视频/语音意图优先走平台媒体模型(与 AI 聊天同一套 Auto 路由),
|
||||||
不再对"制作图片"这类请求只回文本。
|
不再对"制作图片"这类请求只回文本。
|
||||||
- Issue 8:默认 Auto 模型;每轮先自动发现本账号已安装且平台已部署的技能,
|
- Issue 8:默认 Auto 模型;每轮先自动发现本账号已安装且平台已部署的技能,
|
||||||
多个适用时由模型自行判断选 1 个最优,通过 function calling 执行。
|
多个适用时由模型自行判断选 1 个最优,通过 function calling 执行。
|
||||||
- 2026-07-15:修复 trust_env=False + proxy 处理,避免容器坏代理导致调用失败;
|
- 2026-07-15:修复 trust_env=False + proxy 处理,避免容器坏代理导致调用失败;
|
||||||
媒体生成前先经 _optimize_media_prompt 精炼提示词,提升 AGNES 生成质量。
|
媒体生成前先经 _optimize_media_prompt 精炼提示词,提升 AGNES 生成质量。
|
||||||
|
- 2026-07-23:对齐 Web Chat 最新能力:
|
||||||
|
* 模型 failover(Auto 模式自动切换备选模型)
|
||||||
|
* 平台知识库注入(get_platform_knowledge,技能清单实时同步)
|
||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
@@ -83,7 +86,7 @@ def _run_async(coro, timeout=None):
|
|||||||
def _log_media_usage(db: Session, user_id: str, ai_model, media_type: str, prompt: str):
|
def _log_media_usage(db: Session, user_id: str, ai_model, media_type: str, prompt: str):
|
||||||
"""IM Bot 媒体生成记账:把图片/视频消耗写入 TokenUsageLog,供驾驶舱统计。
|
"""IM Bot 媒体生成记账:把图片/视频消耗写入 TokenUsageLog,供驾驶舱统计。
|
||||||
|
|
||||||
此前 IM Bot 路径完全不写 TokenUsageLog,导致微信生成的图片/视频在驾驶舱里“查无此项”。
|
此前 IM Bot 路径完全不写 TokenUsageLog,导致微信生成的图片/视频在驾驶舱里"查无此项"。
|
||||||
媒体 API 不返回 token 用量,这里用与 chat.py _generate_media_task 一致的估算。
|
媒体 API 不返回 token 用量,这里用与 chat.py _generate_media_task 一致的估算。
|
||||||
"""
|
"""
|
||||||
if not user_id or not ai_model:
|
if not user_id or not ai_model:
|
||||||
@@ -112,7 +115,7 @@ def _log_media_usage(db: Session, user_id: str, ai_model, media_type: str, promp
|
|||||||
|
|
||||||
|
|
||||||
def _call_real_llm(db: Session, user=None, prompt: str = "", platform: str = "im", user_id: str = None) -> "str | dict":
|
def _call_real_llm(db: Session, user=None, prompt: str = "", platform: str = "im", user_id: str = None) -> "str | dict":
|
||||||
"""调用七虎AI 生成回复,供 IM Bot 复用。支持媒体意图路由与已安装技能自动发现。"""
|
"""调用七虎AI 生成回复,供 IM Bot 复用。支持媒体意图路由、已安装技能自动发现、模型 failover。"""
|
||||||
try:
|
try:
|
||||||
from app.api.chat import (
|
from app.api.chat import (
|
||||||
_generate_media,
|
_generate_media,
|
||||||
@@ -151,19 +154,23 @@ def _call_real_llm(db: Session, user=None, prompt: str = "", platform: str = "im
|
|||||||
return s
|
return s
|
||||||
return "(媒体生成未能返回有效内容,请重试)"
|
return "(媒体生成未能返回有效内容,请重试)"
|
||||||
|
|
||||||
# ===== 2) 文本回复 + 技能自动发现(Issue 8)=====
|
# ===== 2) 文本回复:模型 failover + 技能自动发现 + 平台知识库注入 =====
|
||||||
ai_model = resolve_auto_model("text", db)
|
from ..platform_knowledge import get_platform_knowledge
|
||||||
if not ai_model:
|
|
||||||
return "(未配置默认文本模型,无法生成回复)"
|
|
||||||
provider = db.query(ModelProvider).filter(ModelProvider.id == ai_model.provider_id).first()
|
|
||||||
api_key = ai_model.api_key or (provider.api_key if provider else None)
|
|
||||||
api_url = ai_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 "(模型未配置 API Key 或地址,无法生成回复)"
|
|
||||||
|
|
||||||
# 自动发现本账号已安装且平台已部署的技能
|
# 构建候选模型列表(对齐 Web Chat failover)
|
||||||
|
ai_model = resolve_auto_model("text", db)
|
||||||
|
_try_models = [ai_model] if (ai_model and ai_model.is_enabled) else []
|
||||||
|
# Auto 模式附加备选(IM Bot 始终 Auto)
|
||||||
|
_fallbacks = db.query(AIModel).filter(
|
||||||
|
AIModel.is_enabled == True,
|
||||||
|
AIModel.id != (ai_model.id if ai_model else ""),
|
||||||
|
).order_by(AIModel.is_default.desc()).limit(4).all()
|
||||||
|
_try_models.extend([m for m in _fallbacks if m not in _try_models])
|
||||||
|
|
||||||
|
if not _try_models:
|
||||||
|
return "(未配置可用文本模型,无法生成回复)"
|
||||||
|
|
||||||
|
# 技能自动发现(提前执行一次,不必每个候选模型重复做)
|
||||||
skills = []
|
skills = []
|
||||||
if user_id:
|
if user_id:
|
||||||
try:
|
try:
|
||||||
@@ -171,10 +178,33 @@ def _call_real_llm(db: Session, user=None, prompt: str = "", platform: str = "im
|
|||||||
except Exception:
|
except Exception:
|
||||||
skills = []
|
skills = []
|
||||||
|
|
||||||
|
# 平台知识库注入(对齐 Web Chat——微信用户提问"平台有哪些技能"也能得到完整清单)
|
||||||
|
kb_context = ""
|
||||||
|
try:
|
||||||
|
pk = get_platform_knowledge(db=db)
|
||||||
|
if pk:
|
||||||
|
kb_context = f"【平台知识库——优先使用以下知识回答用户问题】\n{pk}"
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 尝试每个候选模型
|
||||||
|
_retry_log = []
|
||||||
|
for _try_model in _try_models:
|
||||||
|
ai_model = _try_model
|
||||||
|
|
||||||
|
provider = db.query(ModelProvider).filter(ModelProvider.id == ai_model.provider_id).first()
|
||||||
|
api_key = ai_model.api_key or (provider.api_key if provider else None)
|
||||||
|
api_url = ai_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):
|
||||||
|
_retry_log.append(f"{ai_model.name}: 缺少密钥或端点")
|
||||||
|
continue
|
||||||
|
|
||||||
system_prompt, function_definitions = _build_system_prompt(
|
system_prompt, function_definitions = _build_system_prompt(
|
||||||
skills=skills,
|
skills=skills,
|
||||||
agents=[],
|
agents=[],
|
||||||
kb_context="",
|
kb_context=kb_context,
|
||||||
user_content=prompt,
|
user_content=prompt,
|
||||||
mcp=[],
|
mcp=[],
|
||||||
auto_discovered=bool(skills),
|
auto_discovered=bool(skills),
|
||||||
@@ -184,9 +214,12 @@ def _call_real_llm(db: Session, user=None, prompt: str = "", platform: str = "im
|
|||||||
{"role": "user", "content": prompt},
|
{"role": "user", "content": prompt},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# 尝试调用
|
||||||
|
try:
|
||||||
max_rounds = 3
|
max_rounds = 3
|
||||||
ai_content = ""
|
ai_content = ""
|
||||||
for _ in range(max_rounds):
|
_http_ok = True
|
||||||
|
for round_num in range(max_rounds):
|
||||||
request_body = {"model": ai_model.name, "messages": messages, "max_tokens": 2048}
|
request_body = {"model": ai_model.name, "messages": messages, "max_tokens": 2048}
|
||||||
if function_definitions and skills:
|
if function_definitions and skills:
|
||||||
request_body["tools"] = [
|
request_body["tools"] = [
|
||||||
@@ -204,7 +237,9 @@ def _call_real_llm(db: Session, user=None, prompt: str = "", platform: str = "im
|
|||||||
json=request_body,
|
json=request_body,
|
||||||
)
|
)
|
||||||
if resp.status_code != 200:
|
if resp.status_code != 200:
|
||||||
return f"(AI 模型调用失败 HTTP {resp.status_code})"
|
_retry_log.append(f"{ai_model.name}: HTTP {resp.status_code}")
|
||||||
|
_http_ok = False
|
||||||
|
break
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
message = data.get("choices", [{}])[0].get("message", {})
|
message = data.get("choices", [{}])[0].get("message", {})
|
||||||
tool_calls = message.get("tool_calls", [])
|
tool_calls = message.get("tool_calls", [])
|
||||||
@@ -229,6 +264,20 @@ def _call_real_llm(db: Session, user=None, prompt: str = "", platform: str = "im
|
|||||||
continue
|
continue
|
||||||
ai_content = message.get("content", "")
|
ai_content = message.get("content", "")
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# 检查本轮调用是否成功
|
||||||
|
if _http_ok and ai_content:
|
||||||
return ai_content or "(AI 回复为空)"
|
return ai_content or "(AI 回复为空)"
|
||||||
|
# 本轮失败:记录日志,切换备选
|
||||||
|
logger.warning(f"[im_llm] 模型 {ai_model.name} 失败,切换备选...")
|
||||||
|
continue
|
||||||
|
except Exception as exc:
|
||||||
|
_retry_log.append(f"{ai_model.name}: {str(exc)[:60]}")
|
||||||
|
logger.warning(f"[im_llm] 模型 {ai_model.name} 异常: {exc}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 全部失败
|
||||||
|
_summary = "; ".join(_retry_log[-3:])
|
||||||
|
return f"(已尝试 {len(_try_models)} 个模型均调用失败。最后错误: {_summary}。请稍后重试。)"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"(AI 回复生成失败:{e})"
|
return f"(AI 回复生成失败:{e})"
|
||||||
Reference in new issue
Block a user