diff --git a/admin/dashboard/blueprints/system.py b/admin/dashboard/blueprints/system.py
index 39b44f3..dc7edf7 100644
--- a/admin/dashboard/blueprints/system.py
+++ b/admin/dashboard/blueprints/system.py
@@ -45,11 +45,11 @@ def _plugins_root_path() -> str:
def _scan_plugin_llm_usage() -> list:
- """扫描各插件 config.toml,提取插件与 LLM 后端/场景的引用关系。
+ """扫描各插件 config.toml,提取插件与 LLM 场景的引用关系。
说明:
1. 该扫描仅用于后台可视化,不会改写插件配置;
- 2. 兼容两种写法:顶层 section 下直接写 backend/scene,或嵌套在 llm/api/report_api 等节点;
+ 2. 严格模式只采集 scene:顶层 section 写法,或嵌套在 llm/api/report_api 等节点;
3. 返回结果用于“插件 -> scene -> backend”依赖拓扑展示。
"""
plugins_root = _plugins_root_path()
@@ -59,17 +59,15 @@ def _scan_plugin_llm_usage() -> list:
usages = []
def _collect_refs(plugin_name: str, section_name: str, payload: dict) -> None:
- """从单个配置节点收集 backend/scene 引用。"""
+ """从单个配置节点收集 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:
+ scene_name = str(payload.get("scene") or "").strip()
+ if not scene_name:
return
usages.append({
"plugin": plugin_name,
"section": section_name,
- "backend": backend_name,
"scene": scene_name,
})
@@ -94,13 +92,13 @@ def _scan_plugin_llm_usage() -> list:
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。
+ # 顶层兜底:兼容极少数直接写在根节点的 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')}"
+ key = f"{row.get('plugin')}::{row.get('section')}::{row.get('scene')}"
unique[key] = row
return sorted(unique.values(), key=lambda x: (x.get("plugin", ""), x.get("section", "")))
@@ -117,12 +115,8 @@ def _build_llm_topology() -> dict:
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(仅展示,不改配置)。
+ # 严格模式:插件必须声明 scene,后端统一由 scenes 映射解析。
+ resolved_backend = str(scenes.get(scene_name) or "").strip()
if not resolved_backend:
resolved_backend = default_backend
@@ -130,10 +124,9 @@ def _build_llm_topology() -> dict:
"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_scene": bool(scene_name in scenes),
"valid_backend": bool((not resolved_backend) or resolved_backend in backends),
})
@@ -428,9 +421,11 @@ def update_system_llm_config():
backend_name = str(raw.get("backend") or "").strip()
if not scene_name:
continue
- if backend_name and backend_name not in normalized_backends:
+ # 严格模式:每个 scene 必须绑定一个有效 backend,避免“空绑定”导致运行时不确定性。
+ if not backend_name:
+ return jsonify({"success": False, "message": f"场景 {scene_name} 未绑定后端"}), 400
+ if 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()
diff --git a/admin/dashboard/templates/system_llm.html b/admin/dashboard/templates/system_llm.html
index 42986c0..0c5638a 100644
--- a/admin/dashboard/templates/system_llm.html
+++ b/admin/dashboard/templates/system_llm.html
@@ -54,7 +54,7 @@
-
+
删除
@@ -172,11 +172,6 @@
-
-
-
- {% raw %}{{ scope.row.backend || '-' }}{% endraw %}
-
-
@@ -327,6 +322,14 @@
}
},
async saveLlmConfig() {
+ // 前端先做一次严格校验,避免把空场景名或未绑定后端的记录提交到后端。
+ const invalidScene = (this.llmForm.scenes || []).find(item => {
+ return !String(item.name || '').trim() || !String(item.backend || '').trim();
+ });
+ if (invalidScene) {
+ this.$message.error('场景配置不完整:请确保每一行都填写场景名并绑定后端');
+ return;
+ }
const payload = {
default_backend: this.llmForm.default_backend || '',
backends: (this.llmForm.backends || []).map(item => {
diff --git a/plugins/dify/config.toml b/plugins/dify/config.toml
index 6dd37d7..9c3af3a 100644
--- a/plugins/dify/config.toml
+++ b/plugins/dify/config.toml
@@ -2,8 +2,6 @@
enable = true
# 业务场景优先:聊天插件只关心 chat.main,由全局 llm.scenes 决定具体后端。
scene = "chat.main"
-# 兼容字段保留:旧版本仍可读取 backend,新版本会优先走 scene 路由。
-backend = "dify_workflow_chat"
commands = ["聊天"]
command-tip = """
diff --git a/plugins/dify/main.py b/plugins/dify/main.py
index 7dbae14..f56d36f 100644
--- a/plugins/dify/main.py
+++ b/plugins/dify/main.py
@@ -98,25 +98,15 @@ class DifyPlugin(MessagePluginInterface):
self._commands = dify_config.get("commands", ["ai", "dify", "聊天", "AI"])
self.command_format = dify_config.get("command-tip", "聊天 请求内容")
self.enable = dify_config.get("enable", True)
- raw_api_key = dify_config.get("api-key", "")
- raw_base_url = dify_config.get("base-url", "")
self.price = dify_config.get("price", 0)
self.admin_ignore = dify_config.get("admin_ignore", False)
self.whitelist_ignore = dify_config.get("whitelist_ignore", False)
self.http_proxy = dify_config.get("http-proxy", "")
llm_config = dify_config.get("llm", {}) or {}
if not llm_config:
+ # 严格场景路由:插件初始化只传 scene,不再拼接 backend 兼容字段。
llm_config = {
- # 优先支持场景路由:后台改 scene 绑定即可切换供应商/模型。
"scene": dify_config.get("scene", ""),
- "backend": dify_config.get("backend", ""),
- "provider": "dify",
- "mode": "workflow",
- "api-key": raw_api_key,
- "base-url": raw_base_url,
- "endpoint": "workflows/run",
- "response_mode": "blocking",
- "request_timeout": 40,
}
self.llm_client = UnifiedLLMClient(llm_config)
self.api_key = self.llm_client.api_key
diff --git a/plugins/douyu/config.toml b/plugins/douyu/config.toml
index 5aca029..cd39b1b 100644
--- a/plugins/douyu/config.toml
+++ b/plugins/douyu/config.toml
@@ -30,8 +30,6 @@ audience_stats_sample_interval_seconds = 0
# 切换到“场景路由”模式:日报插件只关心 douyu.daily_report,
# 具体绑定哪个后端由根目录 config.yaml 的 llm.scenes 统一维护。
scene = "douyu.daily_report"
-# 兼容旧配置:保留 backend 作为回退字段。
-backend = "dify_workflow_douyu_daily_report"
# 是否把完整结构化 payload(JSON 大对象)作为输入传给 Dify。
# 某些 Workflow 对复杂输入类型校验严格,会导致 400,默认关闭以保证可用性。
include_structured_inputs = false
diff --git a/plugins/game_task/config.toml b/plugins/game_task/config.toml
index 3f7e3d4..2c8a3c7 100644
--- a/plugins/game_task/config.toml
+++ b/plugins/game_task/config.toml
@@ -2,7 +2,6 @@
enable = true
# 通过 scene 绑定后端,便于后台统一切换百科出题与判分模型。
scene = "game.task"
-backend = "openai_compatible_game_task"
command = ["/t", "/a", "/s", "/r", "/l", "/h"]
command-format = """
🎮 百科问答指令:
diff --git a/plugins/game_task/main.py b/plugins/game_task/main.py
index c538f96..cb8e997 100644
--- a/plugins/game_task/main.py
+++ b/plugins/game_task/main.py
@@ -80,22 +80,11 @@ class GameTaskPlugin(MessagePluginInterface):
/h - 查看未完成任务
""")
plugin_config = self._config.get("GameTask", {})
- self.authorization = plugin_config.get("authorization", "")
- self.url = plugin_config.get("url", "")
- self.model = plugin_config.get("model", "")
llm_config = plugin_config.get("llm", {}) or {}
if not llm_config:
+ # 严格场景路由:仅通过 scene 映射具体后端与模型参数。
llm_config = {
- # 场景路由优先,后台改 scene 即可替换出题/判分模型。
"scene": plugin_config.get("scene", ""),
- "backend": plugin_config.get("backend", ""),
- "provider": "openai_compatible",
- "authorization": self.authorization,
- "url": self.url,
- "model": self.model,
- "stream": False,
- "temperature": 0.2,
- "max_tokens": 1000,
}
self.llm_client = UnifiedLLMClient(llm_config)
diff --git a/plugins/global_news/config.toml b/plugins/global_news/config.toml
index a273573..e3612b3 100644
--- a/plugins/global_news/config.toml
+++ b/plugins/global_news/config.toml
@@ -3,7 +3,6 @@ enable = true
command = ["全球新闻", "国际新闻", "环球新闻", "政经新闻", "政治经济新闻"]
# 通过 scene 绑定后端,便于后台统一切换新闻分析模型。
scene = "news.global"
-backend = "dify_chat_global_news"
command-format = """
🌍全球新闻指令:
全球新闻 - 获取最新的全球政治经济新闻
diff --git a/plugins/global_news/main.py b/plugins/global_news/main.py
index b59cc9f..90a1c51 100644
--- a/plugins/global_news/main.py
+++ b/plugins/global_news/main.py
@@ -78,15 +78,9 @@ class GlobalNewsPlugin(MessagePluginInterface):
self.enable = plugin_config.get("enable", True)
llm_config = plugin_config.get("llm", {}) or {}
if not llm_config:
+ # 严格场景路由:仅由 scene 决定使用哪个后端。
llm_config = {
- # 场景路由优先,便于后台统一切换新闻分析后端。
"scene": plugin_config.get("scene", ""),
- "backend": plugin_config.get("backend", ""),
- "provider": "dify",
- "mode": "chat",
- "authorization": plugin_config.get("authorization", ""),
- "url": plugin_config.get("url", ""),
- "response_mode": "blocking",
}
self.llm_client = UnifiedLLMClient(llm_config)
self.LOG.debug(f"[{self.name}] 插件初始化完成,指令:{self._commands}")
diff --git a/utils/ai/llm_registry.py b/utils/ai/llm_registry.py
index ac3f5ad..772fccb 100644
--- a/utils/ai/llm_registry.py
+++ b/utils/ai/llm_registry.py
@@ -94,15 +94,10 @@ class LLMRegistry:
@classmethod
def resolve(cls, local_config: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
local = dict(local_config or {})
- # 兼容说明:
- # 1. 新链路优先支持 scene 字段,便于“业务场景 -> 后端”集中路由;
- # 2. 若未配置 scene,再按旧逻辑读取 backend,保持历史插件零改动可运行。
- scene_name = (
- local.get("scene")
- or local.get("scene_name")
- or local.get("scene_ref")
- or ""
- )
+ # 严格模式说明:
+ # 1. 统一只认 scene 作为路由入口,避免 backend/backend_ref 等多入口并存;
+ # 2. 若未声明 scene,则视为“调用方直接给出完整连接参数”,原样返回 local。
+ scene_name = local.get("scene") or ""
scene_name = str(scene_name).strip()
if scene_name:
merged = cls.resolve_by_scene(scene_name)
@@ -113,16 +108,4 @@ class LLMRegistry:
merged["scene"] = scene_name
return merged
- backend_name = (
- local.get("backend")
- or local.get("backend_name")
- or local.get("backend_ref")
- or ""
- )
- if not backend_name:
- return local
-
- merged = cls.get_backend(str(backend_name).strip())
- merged.update(local)
- merged["backend"] = backend_name
- return merged
+ return local