v1.1.0 新增大模型配置管理:页面可配置分析网页的LLM接口(名称/BaseURL/Key/模型/温度/超时/默认),支持视觉标记——视觉模型走截图视觉分析路线,非视觉模型走DOM快照文本分析路线;任务创建可选模型并记录使用情况
This commit is contained in:
@@ -78,3 +78,23 @@ a.report-link:hover { text-decoration: underline; }
|
||||
.step-shot img { max-width: 320px; border-radius: 8px; border: 1px solid #e5e7eb; display: block; margin-top: 6px; cursor: zoom-in; }
|
||||
.step-detail { font-size: 12px; color: #374151; }
|
||||
.step-time { font-size: 11px; color: #9ca3af; margin-left: 8px; }
|
||||
|
||||
/* 大模型配置 */
|
||||
select { padding: 10px 12px; border: 1px solid #d1d5db; border-radius: 8px; font-size: 14px; background: #fff; }
|
||||
select:focus { outline: none; border-color: #2563eb; box-shadow: 0 0 0 3px rgba(37,99,235,.1); }
|
||||
.mode-badge { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 11px; font-weight: 600; white-space: nowrap; }
|
||||
.mode-badge.vision { background: #ede9fe; color: #6d28d9; }
|
||||
.mode-badge.text { background: #f3f4f6; color: #4b5563; }
|
||||
.tag { font-size: 11px; font-weight: 400; margin-left: 6px; padding: 1px 8px; border-radius: 999px; }
|
||||
.tag-vision { background: #ede9fe; color: #6d28d9; }
|
||||
.tag-text { background: #f3f4f6; color: #4b5563; }
|
||||
.url-cell { max-width: 240px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.model-cell { font-weight: 600; }
|
||||
.ops { white-space: nowrap; }
|
||||
.llm-modal-body { width: 560px; height: auto; max-height: 92vh; }
|
||||
.llm-form { padding: 18px 20px; display: grid; grid-template-columns: 1fr 1fr; gap: 14px; overflow-y: auto; }
|
||||
.llm-opts { display: flex; flex-direction: row; gap: 20px; align-items: center; }
|
||||
.check { display: flex; align-items: center; gap: 6px; font-size: 13px; color: #374151; cursor: pointer; }
|
||||
.check input { width: auto; }
|
||||
.llm-actions { grid-column: span 2; display: flex; align-items: center; gap: 12px; }
|
||||
.llm-test-result { font-size: 12px; color: #6b7280; word-break: break-all; }
|
||||
+59
-1
@@ -28,13 +28,27 @@
|
||||
<label>最大步数</label>
|
||||
<input id="max_steps" type="number" value="30" min="1" max="100">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>分析模型 <span class="tag" id="model-tag"></span></label>
|
||||
<select id="llm_config_id"></select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>超时(秒)</label>
|
||||
<input id="timeout" type="number" value="600" min="30" max="3600">
|
||||
</div>
|
||||
</div>
|
||||
<button id="submit" class="btn primary">🚀 开始测试</button>
|
||||
<div class="hint">测试由 AI 自动驱动浏览器执行:打开页面 → 逐步操作 → 断言验证 → 生成报告(含截图)</div>
|
||||
<div class="hint">测试由 AI 自动驱动浏览器执行:打开页面 → 逐步操作 → 断言验证 → 生成报告(含截图)。
|
||||
<b>🖼️ 视觉模型</b>:通过截图直接观察页面视觉状态分析;<b>📄 文本模型</b>:通过 DOM 元素快照分析。</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>🔌 大模型配置 <button id="add-llm" class="btn small primary">+ 新增配置</button></h2>
|
||||
<table id="llm-table">
|
||||
<thead><tr><th>名称</th><th>模型</th><th>Base URL</th><th>分析方式</th><th>温度</th><th>超时</th><th>默认</th><th>操作</th></tr></thead>
|
||||
<tbody><tr><td colspan="8" class="empty">加载中...</td></tr></tbody>
|
||||
</table>
|
||||
<div class="hint">配置 OpenAI 兼容接口的大模型。支持视觉的模型(如 GPT-4o、豆包视觉版)用截图分析网页,不支持视觉的模型用 DOM 快照分析。</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
@@ -80,6 +94,50 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="llm-modal" class="modal hidden">
|
||||
<div class="modal-body llm-modal-body">
|
||||
<div class="modal-head"><span id="llm-modal-title">新增模型配置</span><button id="llm-modal-close" class="btn small">关闭</button></div>
|
||||
<div class="llm-form">
|
||||
<div class="field">
|
||||
<label>配置名称</label>
|
||||
<input id="llm-name" type="text" placeholder="例如:火山引擎豆包视觉版">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Base URL(OpenAI 兼容接口地址)</label>
|
||||
<input id="llm-base-url" type="text" placeholder="https://ark.cn-beijing.volces.com/api/plan/v3">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>API Key</label>
|
||||
<input id="llm-api-key" type="password" placeholder="sk-...">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>模型名</label>
|
||||
<input id="llm-model" type="text" placeholder="例如:doubao-seed-evolving">
|
||||
</div>
|
||||
<div class="field span2 llm-opts">
|
||||
<label class="check"><input id="llm-vision" type="checkbox"> 🖼️ 支持视觉分析(通过页面截图观察分析)</label>
|
||||
<label class="check"><input id="llm-default" type="checkbox"> ⭐ 设为默认配置</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>温度(0~1)</label>
|
||||
<input id="llm-temperature" type="number" value="0.2" min="0" max="1" step="0.1">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>超时(秒)</label>
|
||||
<input id="llm-timeout" type="number" value="120" min="10" max="600">
|
||||
</div>
|
||||
<div class="llm-actions">
|
||||
<button id="llm-test" class="btn small">🔌 测试连接</button>
|
||||
<span id="llm-test-result" class="llm-test-result"></span>
|
||||
</div>
|
||||
<div class="llm-actions">
|
||||
<button id="llm-save" class="btn primary">💾 保存</button>
|
||||
<button id="llm-cancel" class="btn small">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+181
-2
@@ -6,7 +6,8 @@ async function refreshHealth() {
|
||||
try {
|
||||
const r = await fetch(API + '/health');
|
||||
const d = await r.json();
|
||||
$('#health').textContent = `服务正常 | 模型: ${d.llm_model} | 运行中任务: ${d.concurrent}`;
|
||||
const mode = d.llm_vision ? '🖼️视觉' : '📄文本';
|
||||
$('#health').textContent = `服务正常 | 默认模型: ${d.llm_name || d.llm_model || '-'}(${mode}) | 运行中任务: ${d.concurrent}`;
|
||||
$('#health').classList.add('ok');
|
||||
} catch (e) {
|
||||
$('#health').textContent = '服务异常';
|
||||
@@ -21,6 +22,180 @@ const RESULT_MAP = {
|
||||
pending: '待定', pass: '✅ 通过', fail: '❌ 失败', error: '⚠️ 错误', stopped: '⏹️ 停止'
|
||||
};
|
||||
|
||||
/* ========== 大模型配置 ========== */
|
||||
let llmConfigs = [];
|
||||
let editingLlmId = null;
|
||||
|
||||
function llmBadge(cfg) {
|
||||
return cfg.vision ? '🖼️ 视觉' : '📄 文本';
|
||||
}
|
||||
|
||||
async function loadLlmConfigs() {
|
||||
try {
|
||||
const r = await fetch(API + '/api/llm-configs');
|
||||
const d = await r.json();
|
||||
llmConfigs = d.configs || [];
|
||||
renderLlmTable();
|
||||
renderLlmSelect();
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
function renderLlmTable() {
|
||||
const tb = $('#llm-table tbody');
|
||||
if (!llmConfigs.length) {
|
||||
tb.innerHTML = '<tr><td colspan="8" class="empty">暂无配置,点右上角「新增配置」添加</td></tr>';
|
||||
return;
|
||||
}
|
||||
tb.innerHTML = llmConfigs.map(c => `
|
||||
<tr>
|
||||
<td>${esc(c.name)}</td>
|
||||
<td>${esc(c.model)}</td>
|
||||
<td class="url-cell" title="${esc(c.base_url)}">${esc(c.base_url)}</td>
|
||||
<td><span class="mode-badge ${c.vision ? 'vision' : 'text'}">${llmBadge(c)}</span></td>
|
||||
<td>${c.temperature}</td>
|
||||
<td>${c.timeout}s</td>
|
||||
<td>${c.is_default ? '⭐' : ''}</td>
|
||||
<td class="ops">
|
||||
<button class="btn small" onclick="openLlmModal(${c.id})">编辑</button>
|
||||
<button class="btn small" onclick="testLlm(${c.id})">测试</button>
|
||||
${c.is_default ? '' : `<button class="btn small" onclick="setDefaultLlm(${c.id})">设默认</button>`}
|
||||
${c.is_default ? '' : `<button class="btn small danger" onclick="deleteLlm(${c.id})">删除</button>`}
|
||||
</td>
|
||||
</tr>`).join('');
|
||||
}
|
||||
|
||||
function renderLlmSelect() {
|
||||
const sel = $('#llm_config_id');
|
||||
if (!sel) return;
|
||||
if (!llmConfigs.length) {
|
||||
sel.innerHTML = '<option value="">(请先添加模型配置)</option>';
|
||||
$('#model-tag').textContent = '';
|
||||
return;
|
||||
}
|
||||
sel.innerHTML = llmConfigs.map(c =>
|
||||
`<option value="${c.id}" ${c.is_default ? 'selected' : ''}>${esc(c.name)}(${llmBadge(c)})</option>`
|
||||
).join('');
|
||||
updateModelTag();
|
||||
}
|
||||
|
||||
function updateModelTag() {
|
||||
const id = parseInt($('#llm_config_id').value);
|
||||
const cfg = llmConfigs.find(c => c.id === id);
|
||||
$('#model-tag').textContent = cfg ? llmBadge(cfg) : '';
|
||||
$('#model-tag').className = 'tag ' + (cfg && cfg.vision ? 'tag-vision' : 'tag-text');
|
||||
}
|
||||
|
||||
function openLlmModal(id) {
|
||||
editingLlmId = id || null;
|
||||
const c = id ? llmConfigs.find(x => x.id === id) : null;
|
||||
$('#llm-modal-title').textContent = c ? `编辑配置 - ${c.name}` : '新增模型配置';
|
||||
$('#llm-name').value = c ? c.name : '';
|
||||
$('#llm-base-url').value = c ? c.base_url : '';
|
||||
$('#llm-api-key').value = c ? c.api_key : '';
|
||||
$('#llm-model').value = c ? c.model : '';
|
||||
$('#llm-vision').checked = c ? !!c.vision : false;
|
||||
$('#llm-default').checked = c ? !!c.is_default : false;
|
||||
$('#llm-temperature').value = c ? c.temperature : 0.2;
|
||||
$('#llm-timeout').value = c ? c.timeout : 120;
|
||||
$('#llm-test-result').textContent = '';
|
||||
$('#llm-modal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeLlmModal() {
|
||||
$('#llm-modal').classList.add('hidden');
|
||||
editingLlmId = null;
|
||||
}
|
||||
|
||||
async function saveLlm() {
|
||||
const body = {
|
||||
name: $('#llm-name').value.trim(),
|
||||
base_url: $('#llm-base-url').value.trim(),
|
||||
api_key: $('#llm-api-key').value.trim(),
|
||||
model: $('#llm-model').value.trim(),
|
||||
vision: $('#llm-vision').checked,
|
||||
is_default: $('#llm-default').checked,
|
||||
temperature: parseFloat($('#llm-temperature').value) || 0.2,
|
||||
timeout: parseInt($('#llm-timeout').value) || 120
|
||||
};
|
||||
if (!body.name || !body.base_url || !body.api_key || !body.model) {
|
||||
alert('请填写名称、Base URL、API Key、模型名');
|
||||
return;
|
||||
}
|
||||
const url = editingLlmId ? `${API}/api/llm-configs/${editingLlmId}` : `${API}/api/llm-configs`;
|
||||
const method = editingLlmId ? 'PUT' : 'POST';
|
||||
try {
|
||||
const r = await fetch(url, {
|
||||
method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body)
|
||||
});
|
||||
const d = await r.json();
|
||||
if (d.error) { alert('保存失败: ' + d.error); return; }
|
||||
closeLlmModal();
|
||||
loadLlmConfigs();
|
||||
} catch (e) { alert('保存失败: ' + e); }
|
||||
}
|
||||
|
||||
async function deleteLlm(id) {
|
||||
const c = llmConfigs.find(x => x.id === id);
|
||||
if (!confirm(`确定删除配置「${c.name}」?`)) return;
|
||||
const r = await fetch(API + `/api/llm-configs/${id}`, { method: 'DELETE' });
|
||||
const d = await r.json();
|
||||
if (d.error) { alert('删除失败: ' + d.error); return; }
|
||||
loadLlmConfigs();
|
||||
}
|
||||
|
||||
async function setDefaultLlm(id) {
|
||||
await fetch(API + `/api/llm-configs/${id}/default`, { method: 'POST' });
|
||||
loadLlmConfigs();
|
||||
}
|
||||
|
||||
async function testLlm(id) {
|
||||
const c = llmConfigs.find(x => x.id === id);
|
||||
const el = $('#llm-test-result');
|
||||
if (el) el.textContent = '';
|
||||
try {
|
||||
const r = await fetch(API + `/api/llm-configs/${id}/test`, { method: 'POST' });
|
||||
const d = await r.json();
|
||||
if (d.ok) {
|
||||
alert(`✅ 连接成功!模型回复: ${d.reply}`);
|
||||
} else {
|
||||
alert(`❌ 连接失败: ${d.error}`);
|
||||
}
|
||||
} catch (e) { alert('测试请求失败: ' + e); }
|
||||
}
|
||||
|
||||
$('#add-llm').onclick = () => openLlmModal(null);
|
||||
$('#llm-modal-close').onclick = closeLlmModal;
|
||||
$('#llm-cancel').onclick = closeLlmModal;
|
||||
$('#llm-save').onclick = saveLlm;
|
||||
$('#llm-modal').onclick = e => { if (e.target === $('#llm-modal')) closeLlmModal(); };
|
||||
$('#llm-test').onclick = async () => {
|
||||
// 用表单当前内容测试(未保存也能测)
|
||||
const body = {
|
||||
name: $('#llm-name').value.trim() || '测试',
|
||||
base_url: $('#llm-base-url').value.trim(),
|
||||
api_key: $('#llm-api-key').value.trim(),
|
||||
model: $('#llm-model').value.trim(),
|
||||
vision: $('#llm-vision').checked,
|
||||
temperature: parseFloat($('#llm-temperature').value) || 0.2,
|
||||
timeout: parseInt($('#llm-timeout').value) || 120
|
||||
};
|
||||
if (!body.base_url || !body.api_key || !body.model) {
|
||||
alert('请先填写 Base URL、API Key、模型名再测试');
|
||||
return;
|
||||
}
|
||||
const el = $('#llm-test-result');
|
||||
el.textContent = '测试中...';
|
||||
try {
|
||||
const r = await fetch(API + '/api/llm-configs/test-form', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body)
|
||||
});
|
||||
const d = await r.json();
|
||||
el.textContent = d.ok ? `✅ ${d.reply}` : `❌ ${d.error}`;
|
||||
} catch (e) { el.textContent = '❌ ' + e; }
|
||||
};
|
||||
$('#llm_config_id').onchange = updateModelTag;
|
||||
|
||||
|
||||
function esc(s) {
|
||||
return String(s ?? '').replace(/[&<>"']/g, c => (
|
||||
{'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||
@@ -46,6 +221,7 @@ async function loadTasks() {
|
||||
<td>${esc(t.id)}</td>
|
||||
<td>${esc(t.url)}</td>
|
||||
<td><div class="goal-cell" title="${esc(t.goal)}">${esc(t.goal)}</div></td>
|
||||
<td>${t.llm_name ? `<span class="model-cell" title="${esc(t.llm_name)}">${esc(t.llm_name)}</span> <span class="mode-badge ${t.vision ? 'vision' : 'text'}">${t.vision ? '🖼️' : '📄'}</span>` : '-'}</td>
|
||||
<td><span class="status ${esc(t.status)}">${STATUS_MAP[t.status] || esc(t.status)}</span></td>
|
||||
<td><span class="result ${esc(t.result)}">${RESULT_MAP[t.result] || esc(t.result)}</span></td>
|
||||
<td>${t.steps || 0}</td>
|
||||
@@ -105,6 +281,7 @@ async function refreshDetail() {
|
||||
$('#detail-meta').innerHTML = `
|
||||
<div class="m-item"><b>目标网址</b><span>${esc(t.url)}</span></div>
|
||||
<div class="m-item"><b>测试目标</b><span>${esc(t.goal)}</span></div>
|
||||
<div class="m-item"><b>分析模型</b><span>${t.llm_name ? esc(t.llm_name) + ' ' + (t.vision ? '🖼️' : '📄') : '-'}</span></div>
|
||||
<div class="m-item"><b>状态</b><span><span class="status ${esc(t.status)}">${STATUS_MAP[t.status] || esc(t.status)}</span> / <span class="result ${esc(t.result)}">${RESULT_MAP[t.result] || esc(t.result)}</span></span></div>
|
||||
<div class="m-item"><b>步骤</b><span>${t.steps || 0} / ${t.max_steps}</span></div>
|
||||
<div class="m-item"><b>创建时间</b><span>${esc(t.created)}</span></div>
|
||||
@@ -185,7 +362,8 @@ $('#submit').onclick = async () => {
|
||||
body: JSON.stringify({
|
||||
url, goal,
|
||||
max_steps: parseInt($('#max_steps').value) || 30,
|
||||
timeout: parseInt($('#timeout').value) || 600
|
||||
timeout: parseInt($('#timeout').value) || 600,
|
||||
llm_config_id: parseInt($('#llm_config_id').value) || null
|
||||
})
|
||||
});
|
||||
const d = await r.json();
|
||||
@@ -217,5 +395,6 @@ $('#refresh').onclick = loadTasks;
|
||||
|
||||
refreshHealth();
|
||||
loadTasks();
|
||||
loadLlmConfigs();
|
||||
setInterval(refreshHealth, 30000);
|
||||
setInterval(loadTasks, 5000);
|
||||
Reference in New Issue
Block a user