feat: 引入LLM场景路由与后台拓扑管理能力
变更项: 1. 新增 llm.scenes 场景路由层,支持 scene->backend 统一映射,并补充默认场景配置。 2. 扩展 LLMRegistry,新增 scene 解析逻辑;当声明 scene 时强制按场景路由结果生效,保持旧 backend 配置兼容。 3. 扩展后台 /api/system/llm_config 读写能力,支持 scenes 配置保存;新增插件 LLM 依赖扫描与拓扑数据输出。 4. 升级 system_llm 页面:新增场景路由管理区、插件依赖拓扑表,支持可视化查看 插件->scene->backend->provider。 5. 迁移核心插件配置到 scene 模式(保留兼容字段):dify/global_news/game_task/message_summary/ai_auto_response/member_context/douyu。 6. 调整部分插件初始化默认 llm_config,补充 scene 字段,确保后台场景切换可直接生效。
This commit is contained in:
@@ -11,6 +11,7 @@ from collections import deque
|
||||
import gzip
|
||||
import json
|
||||
import yaml
|
||||
import toml
|
||||
from utils.markdown_to_image import get_md2img_health_snapshot, warmup_md2img_browser_sync
|
||||
|
||||
# 创建系统信息蓝图
|
||||
@@ -38,6 +39,113 @@ def _save_system_yaml(config_obj: dict) -> None:
|
||||
yaml.safe_dump(config_obj, f, allow_unicode=True, sort_keys=False)
|
||||
|
||||
|
||||
def _plugins_root_path() -> str:
|
||||
"""返回插件根目录绝对路径。"""
|
||||
return os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', 'plugins'))
|
||||
|
||||
|
||||
def _scan_plugin_llm_usage() -> list:
|
||||
"""扫描各插件 config.toml,提取插件与 LLM 后端/场景的引用关系。
|
||||
|
||||
说明:
|
||||
1. 该扫描仅用于后台可视化,不会改写插件配置;
|
||||
2. 兼容两种写法:顶层 section 下直接写 backend/scene,或嵌套在 llm/api/report_api 等节点;
|
||||
3. 返回结果用于“插件 -> scene -> backend”依赖拓扑展示。
|
||||
"""
|
||||
plugins_root = _plugins_root_path()
|
||||
if not os.path.isdir(plugins_root):
|
||||
return []
|
||||
|
||||
usages = []
|
||||
|
||||
def _collect_refs(plugin_name: str, section_name: str, payload: dict) -> None:
|
||||
"""从单个配置节点收集 backend/scene 引用。"""
|
||||
if not isinstance(payload, dict):
|
||||
return
|
||||
backend_name = str(payload.get("backend") or "").strip()
|
||||
scene_name = str(payload.get("scene") or payload.get("scene_name") or "").strip()
|
||||
if not backend_name and not scene_name:
|
||||
return
|
||||
usages.append({
|
||||
"plugin": plugin_name,
|
||||
"section": section_name,
|
||||
"backend": backend_name,
|
||||
"scene": scene_name,
|
||||
})
|
||||
|
||||
for item in sorted(os.listdir(plugins_root)):
|
||||
plugin_dir = os.path.join(plugins_root, item)
|
||||
if not os.path.isdir(plugin_dir):
|
||||
continue
|
||||
config_path = os.path.join(plugin_dir, "config.toml")
|
||||
if not os.path.exists(config_path):
|
||||
continue
|
||||
try:
|
||||
config_obj = toml.load(config_path) or {}
|
||||
except Exception as e:
|
||||
logger.warning(f"扫描插件 LLM 依赖失败: plugin={item}, path={config_path}, error={e}")
|
||||
continue
|
||||
|
||||
# 优先扫描每个 section:兼容 [Dify] / [api] / [Douyu.report_api] 等写法。
|
||||
for section_name, section_value in config_obj.items():
|
||||
if isinstance(section_value, dict):
|
||||
_collect_refs(item, str(section_name), section_value)
|
||||
# 二层兜底:处理 llm/api/report_api 等嵌套节点。
|
||||
for nested_name, nested_value in section_value.items():
|
||||
if isinstance(nested_value, dict):
|
||||
_collect_refs(item, f"{section_name}.{nested_name}", nested_value)
|
||||
# 顶层兜底:兼容极少数直接写在根节点的 backend/scene。
|
||||
_collect_refs(item, "__root__", config_obj if isinstance(config_obj, dict) else {})
|
||||
|
||||
# 去重:同插件同 section 仅保留一条记录,避免前后兜底重复。
|
||||
unique = {}
|
||||
for row in usages:
|
||||
key = f"{row.get('plugin')}::{row.get('section')}::{row.get('scene')}::{row.get('backend')}"
|
||||
unique[key] = row
|
||||
return sorted(unique.values(), key=lambda x: (x.get("plugin", ""), x.get("section", "")))
|
||||
|
||||
|
||||
def _build_llm_topology() -> dict:
|
||||
"""构建 LLM 拓扑视图(供后台页面直观展示依赖关系)。"""
|
||||
config_obj = _load_system_yaml()
|
||||
llm_config = config_obj.get("llm", {}) or {}
|
||||
scenes = llm_config.get("scenes", {}) or {}
|
||||
backends = llm_config.get("backends", {}) or {}
|
||||
default_backend = str(llm_config.get("default_backend", "") or "").strip()
|
||||
|
||||
plugin_usages = _scan_plugin_llm_usage()
|
||||
topology_rows = []
|
||||
for usage in plugin_usages:
|
||||
scene_name = str(usage.get("scene") or "").strip()
|
||||
backend_name = str(usage.get("backend") or "").strip()
|
||||
resolved_backend = backend_name
|
||||
# 若插件只写了 scene,则由 scenes 路由解析后端。
|
||||
if not resolved_backend and scene_name:
|
||||
resolved_backend = str(scenes.get(scene_name) or "").strip()
|
||||
# 若既没有 scene 也没有 backend,则兜底 default_backend(仅展示,不改配置)。
|
||||
if not resolved_backend:
|
||||
resolved_backend = default_backend
|
||||
|
||||
topology_rows.append({
|
||||
"plugin": usage.get("plugin", ""),
|
||||
"section": usage.get("section", ""),
|
||||
"scene": scene_name,
|
||||
"backend": backend_name,
|
||||
"resolved_backend": resolved_backend,
|
||||
"provider": str((backends.get(resolved_backend) or {}).get("provider", "") or "").strip(),
|
||||
"valid_scene": bool((not scene_name) or scene_name in scenes),
|
||||
"valid_backend": bool((not resolved_backend) or resolved_backend in backends),
|
||||
})
|
||||
|
||||
return {
|
||||
"default_backend": default_backend,
|
||||
"scenes": scenes if isinstance(scenes, dict) else {},
|
||||
"backends": backends if isinstance(backends, dict) else {},
|
||||
"plugin_usages": plugin_usages,
|
||||
"topology_rows": topology_rows,
|
||||
}
|
||||
|
||||
|
||||
@system_bp.route('/api_docs')
|
||||
@login_required
|
||||
def api_docs():
|
||||
@@ -235,6 +343,7 @@ def get_system_llm_config():
|
||||
config_obj = _load_system_yaml()
|
||||
llm_config = config_obj.get("llm", {}) or {}
|
||||
backends = llm_config.get("backends", {}) or {}
|
||||
scenes = llm_config.get("scenes", {}) or {}
|
||||
backend_list = []
|
||||
for name, backend in backends.items():
|
||||
if not isinstance(backend, dict):
|
||||
@@ -243,11 +352,29 @@ def get_system_llm_config():
|
||||
item["name"] = name
|
||||
backend_list.append(item)
|
||||
backend_list.sort(key=lambda item: item.get("name", ""))
|
||||
|
||||
scene_list = []
|
||||
if isinstance(scenes, dict):
|
||||
for scene_name, backend_name in scenes.items():
|
||||
scene_name = str(scene_name or "").strip()
|
||||
backend_name = str(backend_name or "").strip()
|
||||
if not scene_name:
|
||||
continue
|
||||
scene_list.append({
|
||||
"name": scene_name,
|
||||
"backend": backend_name,
|
||||
})
|
||||
scene_list.sort(key=lambda item: item.get("name", ""))
|
||||
|
||||
topology = _build_llm_topology()
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"data": {
|
||||
"default_backend": llm_config.get("default_backend", ""),
|
||||
"backends": backend_list,
|
||||
"scenes": scene_list,
|
||||
"topology_rows": topology.get("topology_rows", []),
|
||||
"plugin_usages": topology.get("plugin_usages", []),
|
||||
"config_path": _system_config_path(),
|
||||
}
|
||||
})
|
||||
@@ -264,8 +391,11 @@ def update_system_llm_config():
|
||||
data = request.get_json() or {}
|
||||
default_backend = str(data.get("default_backend") or "").strip()
|
||||
backend_list = data.get("backends", []) or []
|
||||
scene_list = data.get("scenes", []) or []
|
||||
if not isinstance(backend_list, list):
|
||||
return jsonify({"success": False, "message": "backends 格式不正确"}), 400
|
||||
if not isinstance(scene_list, list):
|
||||
return jsonify({"success": False, "message": "scenes 格式不正确"}), 400
|
||||
|
||||
normalized_backends = {}
|
||||
for raw in backend_list:
|
||||
@@ -290,10 +420,24 @@ def update_system_llm_config():
|
||||
if default_backend and default_backend not in normalized_backends:
|
||||
return jsonify({"success": False, "message": "默认后端不存在"}), 400
|
||||
|
||||
normalized_scenes = {}
|
||||
for raw in scene_list:
|
||||
if not isinstance(raw, dict):
|
||||
continue
|
||||
scene_name = str(raw.get("name") or "").strip()
|
||||
backend_name = str(raw.get("backend") or "").strip()
|
||||
if not scene_name:
|
||||
continue
|
||||
if backend_name and backend_name not in normalized_backends:
|
||||
return jsonify({"success": False, "message": f"场景 {scene_name} 绑定的后端不存在"}), 400
|
||||
# 允许场景先占位但暂不绑定后端,方便后台分步配置。
|
||||
normalized_scenes[scene_name] = backend_name
|
||||
|
||||
config_obj = _load_system_yaml()
|
||||
config_obj["llm"] = {
|
||||
"default_backend": default_backend,
|
||||
"backends": normalized_backends,
|
||||
"scenes": normalized_scenes,
|
||||
}
|
||||
_save_system_yaml(config_obj)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user