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:
@@ -42,6 +42,27 @@
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-card class="scene-card" shadow="never">
|
||||
<div slot="header" class="workspace-header">
|
||||
<div>
|
||||
<h3>场景路由(Scene Binding)</h3>
|
||||
<p>用业务场景绑定后端。插件优先引用 scene,再由 scene 统一路由到 backend。</p>
|
||||
</div>
|
||||
<el-button size="mini" @click="addScene">新增场景</el-button>
|
||||
</div>
|
||||
|
||||
<div class="scene-list" v-if="llmForm.scenes.length">
|
||||
<div class="scene-row" v-for="(scene, index) in llmForm.scenes" :key="scene.uid">
|
||||
<el-input v-model="scene.name" placeholder="例如:chat.main"></el-input>
|
||||
<el-select v-model="scene.backend" placeholder="绑定后端" filterable clearable>
|
||||
<el-option v-for="item in backendNameOptions" :key="item" :label="item" :value="item"></el-option>
|
||||
</el-select>
|
||||
<el-button type="text" class="danger-text" @click="removeScene(index)">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty v-else description="暂无场景配置,可先新增 chat.main / summary.daily 等"></el-empty>
|
||||
</el-card>
|
||||
|
||||
<div class="backend-list" v-if="llmForm.backends.length">
|
||||
<el-card
|
||||
v-for="(backend, index) in llmForm.backends"
|
||||
@@ -132,6 +153,45 @@
|
||||
</div>
|
||||
|
||||
<el-empty v-else description="暂无后端配置,先新增一个"></el-empty>
|
||||
|
||||
<el-card class="topology-card" shadow="never">
|
||||
<div slot="header" class="workspace-header">
|
||||
<div>
|
||||
<h3>插件依赖拓扑</h3>
|
||||
<p>展示 插件 -> scene -> backend -> provider 的实际映射,便于定位切换影响范围。</p>
|
||||
</div>
|
||||
</div>
|
||||
<el-table :data="topologyRows" size="mini" style="width: 100%">
|
||||
<el-table-column prop="plugin" label="插件" min-width="120"></el-table-column>
|
||||
<el-table-column prop="section" label="配置段" min-width="120"></el-table-column>
|
||||
<el-table-column prop="scene" label="Scene" min-width="140">
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.scene" size="mini" :type="scope.row.valid_scene ? 'success' : 'danger'">
|
||||
{% raw %}{{ scope.row.scene }}{% endraw %}
|
||||
</el-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="backend" label="配置Backend" min-width="160">
|
||||
<template slot-scope="scope">
|
||||
<span>{% raw %}{{ scope.row.backend || '-' }}{% endraw %}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="resolved_backend" label="实际Backend" min-width="160">
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.resolved_backend" size="mini" :type="scope.row.valid_backend ? 'success' : 'danger'">
|
||||
{% raw %}{{ scope.row.resolved_backend }}{% endraw %}
|
||||
</el-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="provider" label="Provider" min-width="120">
|
||||
<template slot-scope="scope">
|
||||
<span>{% raw %}{{ scope.row.provider || '-' }}{% endraw %}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-card>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -145,9 +205,11 @@
|
||||
return {
|
||||
currentView: '17',
|
||||
configPath: '',
|
||||
topologyRows: [],
|
||||
llmForm: {
|
||||
default_backend: '',
|
||||
backends: []
|
||||
backends: [],
|
||||
scenes: []
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -164,6 +226,7 @@
|
||||
},
|
||||
methods: {
|
||||
newBackend() {
|
||||
// 统一后端对象结构,保证新增/编辑时字段完整,避免后端清洗时丢键。
|
||||
return {
|
||||
uid: `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
||||
name: '',
|
||||
@@ -185,6 +248,14 @@
|
||||
stream: false
|
||||
};
|
||||
},
|
||||
newScene() {
|
||||
// Scene 是“业务场景 -> 后端”的路由单元,插件建议只依赖 scene。
|
||||
return {
|
||||
uid: `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
||||
name: '',
|
||||
backend: ''
|
||||
};
|
||||
},
|
||||
normalizeBackend(item) {
|
||||
return {
|
||||
uid: `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
||||
@@ -207,15 +278,36 @@
|
||||
stream: !!item.stream
|
||||
};
|
||||
},
|
||||
normalizeScene(item) {
|
||||
return {
|
||||
uid: `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
||||
name: item.name || '',
|
||||
backend: item.backend || ''
|
||||
};
|
||||
},
|
||||
addBackend() {
|
||||
this.llmForm.backends.push(this.newBackend());
|
||||
},
|
||||
addScene() {
|
||||
this.llmForm.scenes.push(this.newScene());
|
||||
},
|
||||
removeBackend(index) {
|
||||
const removed = this.llmForm.backends[index];
|
||||
this.llmForm.backends.splice(index, 1);
|
||||
if (removed && removed.name && this.llmForm.default_backend === removed.name) {
|
||||
this.llmForm.default_backend = '';
|
||||
}
|
||||
// 后端被删除后,自动清理引用它的 scene,避免保存时出现无效绑定。
|
||||
if (removed && removed.name) {
|
||||
(this.llmForm.scenes || []).forEach(scene => {
|
||||
if (scene.backend === removed.name) {
|
||||
scene.backend = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
removeScene(index) {
|
||||
this.llmForm.scenes.splice(index, 1);
|
||||
},
|
||||
async loadLlmConfig() {
|
||||
try {
|
||||
@@ -225,6 +317,8 @@
|
||||
this.configPath = data.config_path || '';
|
||||
this.llmForm.default_backend = data.default_backend || '';
|
||||
this.llmForm.backends = (data.backends || []).map(item => this.normalizeBackend(item));
|
||||
this.llmForm.scenes = (data.scenes || []).map(item => this.normalizeScene(item));
|
||||
this.topologyRows = data.topology_rows || [];
|
||||
} else {
|
||||
this.$message.error(response.data.message || '读取全局 LLM 配置失败');
|
||||
}
|
||||
@@ -239,6 +333,11 @@
|
||||
const cleaned = { ...item };
|
||||
delete cleaned.uid;
|
||||
return cleaned;
|
||||
}),
|
||||
scenes: (this.llmForm.scenes || []).map(item => {
|
||||
const cleaned = { ...item };
|
||||
delete cleaned.uid;
|
||||
return cleaned;
|
||||
})
|
||||
};
|
||||
try {
|
||||
@@ -273,6 +372,14 @@
|
||||
.workspace-header p { font-size: 13px; color: #64748b; }
|
||||
.config-meta { display: flex; gap: 12px; color: #64748b; font-size: 12px; flex-wrap: wrap; }
|
||||
.backend-list { display: flex; flex-direction: column; gap: 16px; }
|
||||
.scene-card, .topology-card { border-radius: 18px; border: 1px solid rgba(148,163,184,0.16); }
|
||||
.scene-list { display: flex; flex-direction: column; gap: 10px; }
|
||||
.scene-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(260px, 1fr) minmax(260px, 1fr) auto;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
.backend-card { border-radius: 18px; border: 1px solid rgba(148,163,184,0.16); }
|
||||
.backend-card-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
||||
.backend-grid {
|
||||
@@ -286,6 +393,7 @@
|
||||
.page-hero { flex-direction: column; align-items: flex-start; }
|
||||
.workspace-header { flex-direction: column; align-items: flex-start; }
|
||||
.page-hero-actions { flex-wrap: wrap; }
|
||||
.scene-row { grid-template-columns: 1fr; }
|
||||
.backend-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user