新增按需截图功能:滚动截图 + 视觉大模型实时判断
- 新 action=smart:每次截图后用视觉大模型判断主题内容是否已截全、是否还需向下滚动,自动滚动到判定完成,最后纵向拼接成长图
- 默认视觉模型 qwen3.6-plus(autodl),支持 agent-browser / playwright 双后端
- LLM 接口可网页配置:⚙️智能配置弹窗(base_url/api_key/model/提示词/最大滚动次数/滚动比例/超时) + 测试连接 + 持久化到 data/config.json
- 新增 API:GET/POST /api/smart/config、POST /api/smart/test
- 安全兜底:大模型判断失败保守停止、滚动位置不再变化判底部、最大滚动次数上限
- 历史新增 smart 类型:入库拼接长图+AI判断过程日志,详情弹窗可查看每一步截图判断
- 前端:智能模式提示条 + 结果展示拼接长图 + AI判断过程折叠查看 + 历史筛选/徽章/详情
This commit is contained in:
+211
-12
@@ -591,8 +591,16 @@
|
||||
<input type="radio" name="action" value="text">
|
||||
📝 提取文本
|
||||
</label>
|
||||
<label class="radio-label">
|
||||
<input type="radio" name="action" value="smart">
|
||||
🧠 按需截图(AI判断滚动)
|
||||
</label>
|
||||
<button type="button" class="btn-small" onclick="openSmartConfig()" style="margin-top:0;" title="配置视觉大模型接口">⚙️ 智能配置</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" id="smartHint" style="display:none; padding:12px 16px; background:#eef2ff; border-radius:8px; color:#333; font-size:14px; line-height:1.8;">
|
||||
🧠 <strong>按需截图模式</strong>:每次截图后由视觉大模型实时判断——是否已截全主题内容、还需不需要继续往下滚动,自动滚动直到大模型判定完成,最后拼成一张长图。大模型接口可在「⚙️ 智能配置」中随时修改(默认已配置 qwen3.6-plus)。
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>高级选项</label>
|
||||
@@ -669,6 +677,7 @@
|
||||
<option value="screenshot">📸 截图</option>
|
||||
<option value="html">📄 HTML</option>
|
||||
<option value="text">📝 文本</option>
|
||||
<option value="smart">🧠 按需截图</option>
|
||||
</select>
|
||||
<input type="text" class="search-input" id="historySearch" placeholder="🔍 搜索网址/标题..." onkeydown="if(event.key==='Enter')loadHistory(1)">
|
||||
<button class="btn-small" onclick="loadHistory(1)">搜索</button>
|
||||
@@ -703,6 +712,7 @@
|
||||
<h3>📚 使用说明</h3>
|
||||
<p><strong>重要提示:</strong></p>
|
||||
<ul style="margin: 10px 0; line-height: 1.8;">
|
||||
<li>🧠 <strong>按需截图</strong>:每次截图后用视觉大模型实时判断是否已截全主题内容、是否还需继续滚动,自动滚动到内容结束并拼接成长图(大模型接口可在「⚙️ 智能配置」修改)</li>
|
||||
<li>📝 <strong>提取文本</strong>:自动剔除脚本/样式/标签,得到干净的前端可读文本,适合直接用于分析/喂给大模型</li>
|
||||
<li>⏱️ <strong>页面加载等待</strong>:某些网站需要验证过程(如 Cloudflare),请设置较长时间(10000-30000ms)</li>
|
||||
<li>🔄 <strong>滚动次数</strong>:用于加载动态内容(如微博、推特等),建议 3-5 次</li>
|
||||
@@ -713,21 +723,80 @@
|
||||
<pre><code>POST /api/capture
|
||||
{
|
||||
"url": "https://example.com",
|
||||
"action": "text", // "screenshot" | "html" | "text"
|
||||
"action": "smart", // "screenshot" | "html" | "text" | "smart"
|
||||
"wait_time": 15000, // 重要!验证网站需设置较长等待
|
||||
"scroll_times": 3, // 可选:加载动态内容
|
||||
"scroll_delay": 1000, // 可选:滚动间隔
|
||||
"full_page": true, // 可选:全页截图
|
||||
"backend": "playwright" // 可选:playwright / agent-browser
|
||||
"backend": "playwright", // 可选:playwright / agent-browser
|
||||
"viewport": {"width":1280,"height":700},
|
||||
"smart_config": { // 可选:临时覆盖按需截图 LLM 配置
|
||||
"base_url": "https://www.autodl.art/api/v1",
|
||||
"api_key": "sk-xxx",
|
||||
"model": "qwen3.6-plus",
|
||||
"max_scrolls": 20,
|
||||
"scroll_ratio": 0.85
|
||||
}
|
||||
}
|
||||
|
||||
GET /api/history?page=1&page_size=15&action=all&search=关键词 // 历史分页
|
||||
GET /api/history/<id> // 历史详情
|
||||
GET /api/history/<id>/file // 历史截图文件
|
||||
DELETE /api/history/<id> // 删除历史</code></pre>
|
||||
GET /api/smart/config // 获取按需截图 LLM 配置
|
||||
POST /api/smart/config // 保存配置(body: {config:{...}})
|
||||
POST /api/smart/test // 测试连接(body: {config:{...}} 可选)
|
||||
GET /api/history?page=1&page_size=15&action=all&search=关键词 // 历史分页
|
||||
GET /api/history/<id> // 历史详情
|
||||
GET /api/history/<id>/file // 历史截图文件
|
||||
DELETE /api/history/<id> // 删除历史</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 智能截图配置弹窗 -->
|
||||
<div class="modal-overlay" id="smartCfgOverlay" onclick="if(event.target===this)closeSmartConfig()">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h3>⚙️ 智能截图 · 视觉大模型配置</h3>
|
||||
<button class="modal-close" onclick="closeSmartConfig()">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="scBaseUrl">接口地址 base_url</label>
|
||||
<input type="text" id="scBaseUrl" placeholder="https://.../api/v1">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="scApiKey">API Key</label>
|
||||
<input type="password" id="scApiKey" placeholder="sk-...">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="scModel">模型名称</label>
|
||||
<input type="text" id="scModel" placeholder="qwen3.6-plus">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="scPrompt">判断提示词(告诉大模型如何判断是否截全)</label>
|
||||
<textarea id="scPrompt" rows="7" style="width:100%; padding:12px; border:2px solid #e0e0e0; border-radius:8px; font-size:14px; font-family:inherit; line-height:1.6;"></textarea>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div>
|
||||
<label for="scMaxScrolls">最大滚动次数</label>
|
||||
<input type="number" id="scMaxScrolls" min="1" max="100" value="20">
|
||||
</div>
|
||||
<div>
|
||||
<label for="scRatio">单次滚动比例(视口高度%)</label>
|
||||
<input type="number" id="scRatio" min="10" max="100" value="85">
|
||||
</div>
|
||||
<div>
|
||||
<label for="scTimeout">LLM 超时(秒)</label>
|
||||
<input type="number" id="scTimeout" min="10" max="600" value="120">
|
||||
</div>
|
||||
</div>
|
||||
<div id="scTestResult" style="margin-top:12px; font-size:14px; line-height:1.8;"></div>
|
||||
</div>
|
||||
<div class="actions" style="padding: 0 24px 20px;">
|
||||
<button class="btn btn-secondary" onclick="testSmartConfig()">🧪 测试连接</button>
|
||||
<button class="btn btn-secondary" onclick="saveSmartConfig()">💾 保存配置</button>
|
||||
<button class="btn btn-secondary" onclick="closeSmartConfig()">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 历史详情弹窗 -->
|
||||
<div class="modal-overlay" id="modalOverlay" onclick="if(event.target===this)closeModal()">
|
||||
<div class="modal">
|
||||
@@ -755,9 +824,102 @@ DELETE /api/history/<id> // 删除
|
||||
let currentPage = 1;
|
||||
let currentHistory = { page: 1, total: 0, total_pages: 1 };
|
||||
let modalRecord = null;
|
||||
let smartConfig = null;
|
||||
let lastSmartHistoryId = null;
|
||||
|
||||
// 页面加载时读取历史
|
||||
window.addEventListener('load', () => loadHistory(1));
|
||||
// 页面加载时读取历史 + 智能配置
|
||||
window.addEventListener('load', () => {
|
||||
loadHistory(1);
|
||||
loadSmartConfig();
|
||||
});
|
||||
|
||||
// 操作类型切换:智能模式显示提示条
|
||||
document.querySelectorAll('input[name="action"]').forEach(r => {
|
||||
r.addEventListener('change', () => {
|
||||
const smart = document.querySelector('input[name="action"]:checked').value === 'smart';
|
||||
document.getElementById('smartHint').style.display = smart ? 'block' : 'none';
|
||||
});
|
||||
});
|
||||
|
||||
/* ===== 智能截图配置 ===== */
|
||||
async function loadSmartConfig() {
|
||||
try {
|
||||
const res = await fetch('/api/smart/config');
|
||||
const data = await res.json();
|
||||
if (data.success) smartConfig = data.config;
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function openSmartConfig() {
|
||||
if (!smartConfig) return;
|
||||
document.getElementById('scBaseUrl').value = smartConfig.base_url || '';
|
||||
document.getElementById('scApiKey').value = smartConfig.api_key || '';
|
||||
document.getElementById('scModel').value = smartConfig.model || '';
|
||||
document.getElementById('scPrompt').value = smartConfig.prompt || '';
|
||||
document.getElementById('scMaxScrolls').value = smartConfig.max_scrolls || 20;
|
||||
document.getElementById('scRatio').value = Math.round((smartConfig.scroll_ratio || 0.85) * 100);
|
||||
document.getElementById('scTimeout').value = smartConfig.timeout || 120;
|
||||
document.getElementById('scTestResult').innerHTML = '';
|
||||
document.getElementById('smartCfgOverlay').classList.add('active');
|
||||
}
|
||||
|
||||
function closeSmartConfig() {
|
||||
document.getElementById('smartCfgOverlay').classList.remove('active');
|
||||
}
|
||||
|
||||
function collectSmartConfigForm() {
|
||||
return {
|
||||
base_url: document.getElementById('scBaseUrl').value.trim(),
|
||||
api_key: document.getElementById('scApiKey').value.trim(),
|
||||
model: document.getElementById('scModel').value.trim(),
|
||||
prompt: document.getElementById('scPrompt').value.trim(),
|
||||
max_scrolls: parseInt(document.getElementById('scMaxScrolls').value) || 20,
|
||||
scroll_ratio: (parseInt(document.getElementById('scRatio').value) || 85) / 100,
|
||||
timeout: parseInt(document.getElementById('scTimeout').value) || 120
|
||||
};
|
||||
}
|
||||
|
||||
async function testSmartConfig() {
|
||||
const cfg = collectSmartConfigForm();
|
||||
const el = document.getElementById('scTestResult');
|
||||
el.innerHTML = '<span style="color:#667eea;">⏳ 正在测试连接...</span>';
|
||||
try {
|
||||
const res = await fetch('/api/smart/test', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ config: cfg })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
el.innerHTML = `<span style="color:#2e7d32;">✅ 连接成功!耗时 ${data.latency}s,模型 ${escapeHtml(data.model)} 回复:${escapeHtml(data.reply)}</span>`;
|
||||
} else {
|
||||
el.innerHTML = `<span style="color:#c62828;">❌ 测试失败:${escapeHtml(data.error || '未知错误')}</span>`;
|
||||
}
|
||||
} catch (err) {
|
||||
el.innerHTML = `<span style="color:#c62828;">❌ 测试失败:${escapeHtml(err.message)}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSmartConfig() {
|
||||
const cfg = collectSmartConfigForm();
|
||||
const el = document.getElementById('scTestResult');
|
||||
try {
|
||||
const res = await fetch('/api/smart/config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ config: cfg })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
smartConfig = data.config;
|
||||
el.innerHTML = '<span style="color:#2e7d32;">✅ 配置已保存并生效</span>';
|
||||
} else {
|
||||
el.innerHTML = `<span style="color:#c62828;">❌ 保存失败:${escapeHtml(data.error || '未知错误')}</span>`;
|
||||
}
|
||||
} catch (err) {
|
||||
el.innerHTML = `<span style="color:#c62828;">❌ 保存失败:${escapeHtml(err.message)}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
@@ -781,7 +943,8 @@ DELETE /api/history/<id> // 删除
|
||||
viewport: {
|
||||
width: viewportWidth,
|
||||
height: viewportHeight
|
||||
}
|
||||
},
|
||||
smart_config: action === 'smart' ? (smartConfig || {}) : undefined
|
||||
};
|
||||
|
||||
// 显示加载
|
||||
@@ -815,6 +978,17 @@ DELETE /api/history/<id> // 删除
|
||||
currentBlob = new Blob([data.html], { type: 'text/html' });
|
||||
document.getElementById('resultContent').innerHTML =
|
||||
`<div class="result-code"><pre>${escapeHtml(data.html)}</pre></div>`;
|
||||
} else if (action === 'smart') {
|
||||
lastSmartHistoryId = data.history_id || null;
|
||||
currentBlob = new Blob([JSON.stringify(data.steps || [], null, 2)], { type: 'application/json' });
|
||||
const stepsHtml = (data.steps || []).map(s =>
|
||||
`<li style="margin:4px 0;">第 <strong>${s.step}</strong> 次截图:${s.complete ? '✅ 已完整,停止滚动' : '⏳ 继续滚动'} —— ${escapeHtml(s.reason || '')}</li>`
|
||||
).join('');
|
||||
document.getElementById('resultContent').innerHTML =
|
||||
`<div style="margin-bottom:10px;color:#666;font-size:13px;">页面标题:${escapeHtml(data.title || '无')} | 🧠 AI 共滚动 <strong>${data.total_steps || 0}</strong> 次判定完成,已拼接为长图(下方展示)</div>` +
|
||||
`<img src="/api/history/${data.history_id}/file" class="result-image" alt="智能截图长图" onerror="this.style.display='none'">` +
|
||||
`<details style="margin-top:12px;"><summary style="cursor:pointer;color:#667eea;font-weight:600;">📋 查看 AI 判断过程(${(data.steps || []).length} 步)</summary>` +
|
||||
`<ul style="margin:10px 0 0 20px;line-height:1.8;color:#333;font-size:14px;">${stepsHtml}</ul></details>`;
|
||||
} else {
|
||||
currentBlob = new Blob([data.text], { type: 'text/plain;charset=utf-8' });
|
||||
const lines = (data.text || '').split('\n').length;
|
||||
@@ -843,6 +1017,16 @@ DELETE /api/history/<id> // 删除
|
||||
}
|
||||
|
||||
function downloadResult() {
|
||||
if (!currentData) return;
|
||||
if (currentData.action === 'smart') {
|
||||
if (lastSmartHistoryId) {
|
||||
const a = document.createElement('a');
|
||||
a.href = `/api/history/${lastSmartHistoryId}/file`;
|
||||
a.download = 'smart_capture.png';
|
||||
a.click();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!currentBlob) return;
|
||||
|
||||
const url = URL.createObjectURL(currentBlob);
|
||||
@@ -872,6 +1056,7 @@ DELETE /api/history/<id> // 删除
|
||||
error.classList.remove('active');
|
||||
currentData = null;
|
||||
currentBlob = null;
|
||||
lastSmartHistoryId = null;
|
||||
}
|
||||
|
||||
/* ===== 历史记录 ===== */
|
||||
@@ -916,7 +1101,8 @@ DELETE /api/history/<id> // 删除
|
||||
const badges = {
|
||||
screenshot: '📸 截图',
|
||||
html: '📄 HTML',
|
||||
text: '📝 文本'
|
||||
text: '📝 文本',
|
||||
smart: '🧠 按需截图'
|
||||
};
|
||||
|
||||
body.innerHTML = data.records.map(r => {
|
||||
@@ -1000,6 +1186,19 @@ DELETE /api/history/<id> // 删除
|
||||
body.innerHTML = `<img src="/api/history/${r.id}/file" class="result-image" alt="历史截图" onerror="this.parentElement.innerHTML='<p style="color:#c62828;">截图文件不存在或已删除</p>'">`;
|
||||
} else if (r.action === 'html') {
|
||||
body.innerHTML = `<div class="result-code"><pre>${escapeHtml(r.content || '')}</pre></div>`;
|
||||
} else if (r.action === 'smart') {
|
||||
let stepsHtml = '';
|
||||
let stepsCount = 0;
|
||||
try {
|
||||
const steps = JSON.parse(r.content || '[]');
|
||||
stepsCount = steps.length;
|
||||
stepsHtml = steps.map(s =>
|
||||
`<li style="margin:4px 0;">第 <strong>${s.step}</strong> 次截图:${s.complete ? '✅ 已完整,停止滚动' : '⏳ 继续滚动'} —— ${escapeHtml(s.reason || '')}</li>`
|
||||
).join('');
|
||||
} catch (e) {}
|
||||
body.innerHTML =
|
||||
`<img src="/api/history/${r.id}/file" class="result-image" alt="智能截图长图" onerror="this.style.display='none'">` +
|
||||
(stepsHtml ? `<details open style="margin-top:14px;"><summary style="cursor:pointer;color:#667eea;font-weight:600;">📋 AI 判断过程(${stepsCount} 步)</summary><ul style="margin:10px 0 0 20px;line-height:1.8;color:#333;font-size:14px;">${stepsHtml}</ul></details>` : '');
|
||||
} else {
|
||||
body.innerHTML = `<div class="result-text">${escapeHtml(r.content || '')}</div>`;
|
||||
}
|
||||
@@ -1037,10 +1236,10 @@ DELETE /api/history/<id> // 删除
|
||||
|
||||
function downloadHistoryContent() {
|
||||
if (!modalRecord) return;
|
||||
if (modalRecord.action === 'screenshot') {
|
||||
if (modalRecord.action === 'screenshot' || modalRecord.action === 'smart') {
|
||||
const a = document.createElement('a');
|
||||
a.href = `/api/history/${modalRecord.id}/file`;
|
||||
a.download = `capture_${modalRecord.id}.png`;
|
||||
a.download = modalRecord.action === 'smart' ? `smart_${modalRecord.id}.png` : `capture_${modalRecord.id}.png`;
|
||||
a.click();
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user