Files
abot/admin/dashboard/templates/system_status.html

367 lines
18 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}资源监控 - 机器人管理后台{% endblock %}
{% block content %}
<div class="page-shell system-page">
<div class="page-hero">
<div class="page-hero-copy">
<div class="page-eyebrow">System Workspace</div>
<h1>系统控制台</h1>
<p>把监控和全局 LLM 配置集中到同一页,减少来回切换。</p>
</div>
<div class="page-hero-actions">
<el-button type="danger" @click="confirmRestart"><i class="el-icon-refresh-left"></i> 重启服务</el-button>
</div>
</div>
<el-tabs v-model="activeTab" class="system-tabs">
<el-tab-pane label="监控面板" name="monitor">
<el-card class="iframe-shell-card" shadow="hover">
<div slot="header" class="workspace-header">
<div>
<h3>监控面板</h3>
<p>直接在控制台内查看系统资源变化与运行状态。</p>
</div>
<div class="page-hero-actions">
<div class="iframe-url">{{ src_url }}</div>
<el-button type="primary" plain size="mini" @click="reloadIframe"><i class="el-icon-refresh"></i> 刷新</el-button>
<el-button type="primary" size="mini" @click="openInNewTab"><i class="el-icon-top-right"></i> 新窗口打开</el-button>
</div>
</div>
<div class="iframe-shell">
<iframe ref="monitorFrame" src="{{ src_url }}" frameborder="0"></iframe>
</div>
</el-card>
</el-tab-pane>
<el-tab-pane label="全局配置" name="llm">
<el-card class="workspace-card" shadow="hover">
<div slot="header" class="workspace-header">
<div>
<h3>全局 LLM 配置</h3>
<p>用表单统一管理 `config.yaml` 中的 `llm.backends`,插件只引用后端名。</p>
</div>
<div class="page-hero-actions">
<el-button size="mini" plain @click="loadLlmConfig">刷新</el-button>
<el-button size="mini" @click="addBackend">新增后端</el-button>
<el-button size="mini" type="success" @click="saveLlmConfig">保存配置</el-button>
</div>
</div>
<div class="config-meta" v-if="configPath">
<span>配置文件:{% raw %}{{ configPath }}{% endraw %}</span>
<span>后端数量:{% raw %}{{ llmForm.backends.length }}{% endraw %}</span>
</div>
<el-form label-width="110px" class="llm-form">
<el-form-item label="默认后端">
<el-select v-model="llmForm.default_backend" placeholder="请选择默认后端" filterable clearable style="width: 360px;">
<el-option
v-for="item in backendNameOptions"
:key="item"
:label="item"
:value="item">
</el-option>
</el-select>
</el-form-item>
</el-form>
<div class="backend-list" v-if="llmForm.backends.length">
<el-card
v-for="(backend, index) in llmForm.backends"
:key="backend.uid"
class="backend-card"
shadow="never">
<div slot="header" class="backend-card-header">
<div>
<strong>{% raw %}{{ backend.name || `后端 ${index + 1}` }}{% endraw %}</strong>
</div>
<el-button type="text" class="danger-text" @click="removeBackend(index)">删除</el-button>
</div>
<el-form label-width="110px" class="backend-form">
<div class="backend-grid">
<el-form-item label="后端名称">
<el-input v-model="backend.name" placeholder="例如dify_workflow_chat"></el-input>
</el-form-item>
<el-form-item label="Provider">
<el-select v-model="backend.provider" placeholder="请选择">
<el-option label="dify" value="dify"></el-option>
<el-option label="openai_compatible" value="openai_compatible"></el-option>
</el-select>
</el-form-item>
<el-form-item label="Mode">
<el-select v-model="backend.mode" placeholder="请选择" clearable>
<el-option label="workflow" value="workflow"></el-option>
<el-option label="chat" value="chat"></el-option>
<el-option label="completion" value="completion"></el-option>
</el-select>
</el-form-item>
<el-form-item label="模型">
<el-input v-model="backend.model" placeholder="例如gpt-5.4"></el-input>
</el-form-item>
<el-form-item label="API Base URL">
<el-input v-model="backend.api_base_url" placeholder="例如http://127.0.0.1:3000/v1"></el-input>
</el-form-item>
<el-form-item label="API URL">
<el-input v-model="backend.api_url" placeholder="完整地址,和 Base URL 二选一"></el-input>
</el-form-item>
<el-form-item label="Endpoint">
<el-input v-model="backend.endpoint" placeholder="例如chat/completions"></el-input>
</el-form-item>
<el-form-item label="API Key">
<el-input v-model="backend.api_key" placeholder="请输入 API Key" show-password></el-input>
</el-form-item>
<el-form-item label="响应模式">
<el-select v-model="backend.response_mode" placeholder="请选择" clearable>
<el-option label="blocking" value="blocking"></el-option>
<el-option label="streaming" value="streaming"></el-option>
</el-select>
</el-form-item>
<el-form-item label="输出字段">
<el-input v-model="backend.workflow_output_key" placeholder="例如text"></el-input>
</el-form-item>
<el-form-item label="超时(秒)">
<el-input-number v-model="backend.timeout_seconds" :min="1" :step="1"></el-input-number>
</el-form-item>
<el-form-item label="请求超时">
<el-input-number v-model="backend.request_timeout" :min="1" :step="1"></el-input-number>
</el-form-item>
<el-form-item label="温度">
<el-input-number v-model="backend.temperature" :min="0" :max="2" :step="0.05"></el-input-number>
</el-form-item>
<el-form-item label="Max Tokens">
<el-input-number v-model="backend.max_tokens" :min="1" :step="1"></el-input-number>
</el-form-item>
<el-form-item label="最大重试">
<el-input-number v-model="backend.max_retries" :min="1" :step="1"></el-input-number>
</el-form-item>
<el-form-item label="重试间隔">
<el-input-number v-model="backend.retry_delay_seconds" :min="0" :step="0.5"></el-input-number>
</el-form-item>
</div>
<div class="backend-switches">
<el-switch v-model="backend.stream" active-text="Stream"></el-switch>
</div>
</el-form>
</el-card>
</div>
<el-empty v-else description="暂无后端配置,先新增一个"></el-empty>
</el-card>
</el-tab-pane>
</el-tabs>
</div>
{% endblock %}
{% block scripts %}
<script>
new Vue({
el: '#app',
mixins: [baseApp],
data() {
return {
currentView: '14',
activeTab: 'monitor',
frameUrl: '{{ src_url }}',
restarting: false,
configPath: '',
llmForm: {
default_backend: '',
backends: []
}
}
},
computed: {
backendNameOptions() {
return (this.llmForm.backends || [])
.map(item => item.name)
.filter(Boolean);
}
},
mounted() {
this.currentView = '14';
this.loadLlmConfig();
},
methods: {
reloadIframe() {
if (this.$refs.monitorFrame) {
this.$refs.monitorFrame.src = this.frameUrl;
}
},
openInNewTab() {
window.open(this.frameUrl, '_blank');
},
confirmRestart() {
this.$confirm('确认执行 ./restart.sh 重启服务吗?这会中断当前服务几秒钟。', '重启确认', {
confirmButtonText: '确认重启',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.triggerRestart();
}).catch(() => {});
},
async triggerRestart() {
if (this.restarting) return;
this.restarting = true;
try {
const response = await axios.post('/api/restart_service');
if (response.data.success) {
this.$message.success(response.data.message || '已触发重启');
} else {
this.$message.error(response.data.message || '重启失败');
}
} catch (error) {
this.$message.error(error.response?.data?.message || '触发重启失败');
} finally {
this.restarting = false;
}
},
newBackend() {
return {
uid: `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
name: '',
provider: 'dify',
mode: '',
model: '',
api_base_url: '',
api_url: '',
endpoint: '',
api_key: '',
response_mode: '',
workflow_output_key: '',
timeout_seconds: 60,
request_timeout: 60,
temperature: 0.7,
max_tokens: 1024,
max_retries: 3,
retry_delay_seconds: 1.0,
stream: false
};
},
normalizeBackend(item) {
return {
uid: `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
name: item.name || '',
provider: item.provider || 'dify',
mode: item.mode || '',
model: item.model || '',
api_base_url: item.api_base_url || '',
api_url: item.api_url || '',
endpoint: item.endpoint || '',
api_key: item.api_key || '',
response_mode: item.response_mode || '',
workflow_output_key: item.workflow_output_key || '',
timeout_seconds: item.timeout_seconds ?? 60,
request_timeout: item.request_timeout ?? 60,
temperature: item.temperature ?? 0.7,
max_tokens: item.max_tokens ?? 1024,
max_retries: item.max_retries ?? 3,
retry_delay_seconds: item.retry_delay_seconds ?? 1.0,
stream: !!item.stream
};
},
addBackend() {
this.llmForm.backends.push(this.newBackend());
},
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 = '';
}
},
async loadLlmConfig() {
try {
const response = await axios.get('/api/system/llm_config');
if (response.data.success) {
const data = response.data.data || {};
this.configPath = data.config_path || '';
this.llmForm.default_backend = data.default_backend || '';
this.llmForm.backends = (data.backends || []).map(item => this.normalizeBackend(item));
} else {
this.$message.error(response.data.message || '读取全局 LLM 配置失败');
}
} catch (error) {
this.$message.error(error.response?.data?.message || '读取全局 LLM 配置失败');
}
},
async saveLlmConfig() {
const payload = {
default_backend: this.llmForm.default_backend || '',
backends: (this.llmForm.backends || []).map(item => {
const cleaned = { ...item };
delete cleaned.uid;
return cleaned;
})
};
try {
const response = await axios.post('/api/system/llm_config', payload);
if (response.data.success) {
this.$message.success(response.data.message || '保存成功');
this.loadLlmConfig();
} else {
this.$message.error(response.data.message || '保存失败');
}
} catch (error) {
this.$message.error(error.response?.data?.message || '保存失败');
}
}
}
});
</script>
<style>
.page-shell { display: flex; flex-direction: column; gap: 16px; }
.page-hero {
display: flex; align-items: flex-end; justify-content: space-between; gap: 18px; padding: 24px 26px; border-radius: 24px;
background: linear-gradient(135deg, rgba(79,70,229,0.10), rgba(59,130,246,0.08), rgba(255,255,255,0.9));
border: 1px solid rgba(148, 163, 184, 0.16); box-shadow: 0 18px 40px rgba(15, 23, 42, 0.06);
}
.page-hero-actions { display: flex; align-items: center; gap: 12px; }
.page-eyebrow { font-size: 12px; text-transform: uppercase; letter-spacing: .08em; color: #6366f1; font-weight: 700; margin-bottom: 8px; }
.page-hero-copy h1 { font-size: 30px; line-height: 1.1; margin-bottom: 10px; color: #0f172a; }
.page-hero-copy p { color: #64748b; font-size: 14px; }
.system-tabs { width: 100%; }
.workspace-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
.workspace-header h3 { font-size: 18px; margin-bottom: 4px; }
.workspace-header p { font-size: 13px; color: #64748b; }
.iframe-url {
max-width: 40%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px;
color: #94a3b8; padding: 8px 12px; border-radius: 999px; background: rgba(248,250,252,0.9); border: 1px solid rgba(148,163,184,0.12);
}
.iframe-shell-card { height: calc(100vh - 280px); }
.iframe-shell-card .el-card__body { height: calc(100% - 73px); }
.iframe-shell { height: 100%; border-radius: 18px; overflow: hidden; border: 1px solid rgba(148,163,184,0.12); background: rgba(248,250,252,0.82); }
.iframe-shell iframe { width: 100%; height: 100%; border: none; display: block; background: #fff; }
.workspace-card .el-card__body { display: flex; flex-direction: column; gap: 16px; }
.config-meta { display: flex; justify-content: space-between; gap: 12px; color: #64748b; font-size: 12px; }
.backend-list { display: flex; flex-direction: column; gap: 16px; }
.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 {
display: grid;
grid-template-columns: repeat(2, minmax(260px, 1fr));
gap: 8px 16px;
}
.backend-switches { display: flex; align-items: center; gap: 16px; padding: 0 12px 8px; }
.danger-text { color: #dc2626; }
@media (max-width: 960px) {
.page-hero { flex-direction: column; align-items: flex-start; }
.workspace-header { flex-direction: column; align-items: flex-start; }
.page-hero-actions { flex-wrap: wrap; }
.iframe-url { max-width: 100%; }
.backend-grid { grid-template-columns: 1fr; }
}
</style>
{% endblock %}