Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5ff90c46a | ||
|
|
2a9f187ce8 | ||
|
|
f7f14d7114 | ||
|
|
ef4d26f11b | ||
|
|
fc43e6e806 | ||
|
|
5783feb60d |
@@ -31,6 +31,9 @@ captures/
|
|||||||
*.png
|
*.png
|
||||||
*.html
|
*.html
|
||||||
|
|
||||||
|
# 运行时数据(历史记录数据库 + 截图文件)
|
||||||
|
data/
|
||||||
|
|
||||||
# 环境变量
|
# 环境变量
|
||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# 手动 Chrome 捕获指南
|
||||||
|
|
||||||
|
## 方案三完整流程
|
||||||
|
|
||||||
|
### 1️⃣ 启动真实 Chrome(带调试端口)
|
||||||
|
|
||||||
|
在您的本地机器或服务器上运行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Linux
|
||||||
|
google-chrome --remote-debugging-port=9222 https://www.techpowerup.com/gpu-specs/rtx-pro-6000-blackwell.c4272
|
||||||
|
|
||||||
|
# 或者使用已有的 Chrome 进程(需要先关闭所有 Chrome)
|
||||||
|
google-chrome-stable --remote-debugging-port=9222 --user-data-dir=/tmp/chrome-debug
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2️⃣ 手动验证(如 Cloudflare)
|
||||||
|
|
||||||
|
- 在浏览器中手动完成验证过程
|
||||||
|
- 等待页面完全加载
|
||||||
|
- 确认可以看到正常内容
|
||||||
|
|
||||||
|
### 3️⃣ 连接到 Chrome 并截图
|
||||||
|
|
||||||
|
验证完成后,运行脚本:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 连接到 Chrome 并截图
|
||||||
|
python3.12 /tmp/connect_chrome.py techpowerup
|
||||||
|
|
||||||
|
# 或者捕获当前所有页面
|
||||||
|
python3.12 /tmp/connect_chrome.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4️⃣ 查看结果
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看截图
|
||||||
|
ls -lh /tmp/chrome_capture_*.png
|
||||||
|
|
||||||
|
# 查看HTML
|
||||||
|
head -100 /tmp/chrome_html_0.html
|
||||||
|
```
|
||||||
|
|
||||||
|
## API 集成方案
|
||||||
|
|
||||||
|
如果需要通过 API 获取内容,可以:
|
||||||
|
|
||||||
|
1. 在服务代码中添加 CDP 连接选项
|
||||||
|
2. 用户先手动验证
|
||||||
|
3. API 连接到已验证的浏览器实例
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
⚠️ **重要**:
|
||||||
|
- Chrome 必须带 `--remote-debugging-port=9222` 启动
|
||||||
|
- 所有 Chrome 进程必须先关闭(或使用新的 user-data-dir)
|
||||||
|
- 验证完成后才能运行脚本
|
||||||
|
- 截图后浏览器保持打开,可以继续使用
|
||||||
|
|
||||||
|
## 一键脚本
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 启动 Chrome 并等待用户验证
|
||||||
|
google-chrome --remote-debugging-port=9222 "https://www.techpowerup.com/gpu-specs/rtx-pro-6000-blackwell.c4272" &
|
||||||
|
echo "请在浏览器中完成验证,然后按 Enter 继续截图..."
|
||||||
|
read
|
||||||
|
python3.12 /tmp/connect_chrome.py techpowerup
|
||||||
|
```
|
||||||
+808
-24
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Web Capture - 网页截图与代码提取</title>
|
<title>Web Capture - 网页截图/HTML/文本提取</title>
|
||||||
<style>
|
<style>
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -77,6 +77,7 @@
|
|||||||
.radio-group {
|
.radio-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.radio-label {
|
.radio-label {
|
||||||
@@ -201,16 +202,62 @@
|
|||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.result-text {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
color: #222;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: auto;
|
||||||
|
max-height: 600px;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.8;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary {
|
.btn-secondary {
|
||||||
background: #6c757d;
|
background: #6c757d;
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
width: auto;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-small {
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 6px 14px;
|
||||||
|
font-size: 13px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-small:hover {
|
||||||
|
background: #5568d8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-small.danger {
|
||||||
|
background: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-small.danger:hover {
|
||||||
|
background: #c82333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-small:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
@@ -227,6 +274,256 @@
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== 历史记录区域 ===== */
|
||||||
|
.history-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-header h3 {
|
||||||
|
font-size: 1.4em;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-toolbar {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-toolbar select,
|
||||||
|
.history-toolbar input[type="text"] {
|
||||||
|
width: auto;
|
||||||
|
min-width: 120px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-toolbar .search-input {
|
||||||
|
min-width: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-count {
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-table th,
|
||||||
|
.history-table td {
|
||||||
|
padding: 12px 10px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-table th {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-table td {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-table tr:hover td {
|
||||||
|
background: #f8f9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-screenshot {
|
||||||
|
background: #e3f2fd;
|
||||||
|
color: #1565c0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-html {
|
||||||
|
background: #fff3e0;
|
||||||
|
color: #e65100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-text {
|
||||||
|
background: #e8f5e9;
|
||||||
|
color: #2e7d32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-failed {
|
||||||
|
background: #fdecea;
|
||||||
|
color: #c62828;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-url {
|
||||||
|
max-width: 280px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
color: #1565c0;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-title {
|
||||||
|
max-width: 220px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
color: #333;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-empty {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
color: #999;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-btn {
|
||||||
|
min-width: 36px;
|
||||||
|
padding: 7px 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
background: white;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-btn:hover:not(:disabled) {
|
||||||
|
border-color: #667eea;
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-btn.active {
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-btn:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-info {
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 查看详情弹窗 ===== */
|
||||||
|
.modal-overlay {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
z-index: 1000;
|
||||||
|
padding: 30px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-overlay.active {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
background: white;
|
||||||
|
border-radius: 14px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 900px;
|
||||||
|
margin: auto;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 18px 24px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
gap: 15px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header h3 {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #333;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-meta {
|
||||||
|
padding: 10px 24px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 20px 24px;
|
||||||
|
max-height: 70vh;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body .result-image {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-size: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
line-height: 1;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close:hover {
|
||||||
|
background: #545b62;
|
||||||
|
}
|
||||||
|
|
||||||
.api-docs {
|
.api-docs {
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
@@ -254,13 +551,22 @@
|
|||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 700px) {
|
||||||
|
.history-table th:nth-child(2),
|
||||||
|
.history-table td:nth-child(2),
|
||||||
|
.history-table th:nth-child(3),
|
||||||
|
.history-table td:nth-child(3) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h1>🌐 Web Capture</h1>
|
<h1>🌐 Web Capture</h1>
|
||||||
<p>网页截图与代码提取服务</p>
|
<p>网页截图 / HTML / 可读文本 提取服务</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
@@ -281,8 +587,20 @@
|
|||||||
<input type="radio" name="action" value="html">
|
<input type="radio" name="action" value="html">
|
||||||
📄 提取HTML
|
📄 提取HTML
|
||||||
</label>
|
</label>
|
||||||
|
<label class="radio-label">
|
||||||
|
<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>
|
</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">
|
<div class="form-group">
|
||||||
<label>高级选项</label>
|
<label>高级选项</label>
|
||||||
@@ -295,20 +613,21 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>滚动设置(用于加载动态内容)</label>
|
<label>等待设置(重要!)</label>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div>
|
<div>
|
||||||
<label for="scrollTimes">滚动次数</label>
|
<label for="waitTime">页面加载等待(ms) ⏱️</label>
|
||||||
|
<input type="number" id="waitTime" value="2000" min="0" max="60000" step="500">
|
||||||
|
<small style="color: #666; display: block; margin-top: 4px;">某些网站需要较长验证过程,可设置为 10000-30000</small>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="scrollTimes">滚动次数(可选)</label>
|
||||||
<input type="number" id="scrollTimes" value="0" min="0" max="20">
|
<input type="number" id="scrollTimes" value="0" min="0" max="20">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="scrollDelay">滚动间隔(ms)</label>
|
<label for="scrollDelay">滚动间隔(ms)</label>
|
||||||
<input type="number" id="scrollDelay" value="1000" min="500" max="5000" step="100">
|
<input type="number" id="scrollDelay" value="1000" min="500" max="5000" step="100">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<label for="waitTime">页面加载等待(ms)</label>
|
|
||||||
<input type="number" id="waitTime" value="2000" min="0" max="10000" step="500">
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -349,18 +668,148 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="history-header">
|
||||||
|
<h3>📜 提取历史</h3>
|
||||||
|
<div class="history-toolbar">
|
||||||
|
<select id="historyActionFilter" onchange="loadHistory(1)">
|
||||||
|
<option value="all">全部类型</option>
|
||||||
|
<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>
|
||||||
|
<button class="btn-small" onclick="refreshHistory()">🔄 刷新</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="history-count" id="historyCount"></div>
|
||||||
|
|
||||||
|
<div style="overflow-x: auto; margin-top: 10px;">
|
||||||
|
<table class="history-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>时间</th>
|
||||||
|
<th>类型</th>
|
||||||
|
<th>网址</th>
|
||||||
|
<th>标题</th>
|
||||||
|
<th>状态</th>
|
||||||
|
<th style="text-align: right;">操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="historyBody"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="history-empty" id="historyEmpty" style="display: none;">暂无提取记录,先去上方提取一个网页吧 🚀</div>
|
||||||
|
|
||||||
|
<div class="pagination" id="pagination"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card api-docs">
|
<div class="card api-docs">
|
||||||
<h3>📚 API 文档</h3>
|
<h3>📚 使用说明</h3>
|
||||||
<p>POST /api/capture</p>
|
<p><strong>重要提示:</strong></p>
|
||||||
<pre><code>{
|
<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>
|
||||||
|
<li>🎯 <strong>全页截图</strong>:滚动加载后建议开启此选项</li>
|
||||||
|
<li>📜 <strong>提取历史</strong>:每次提取自动入库,支持分页浏览、按类型筛选、搜索与删除</li>
|
||||||
|
</ul>
|
||||||
|
<p style="margin-top: 15px;"><strong>API 调用:</strong></p>
|
||||||
|
<pre><code>POST /api/capture
|
||||||
|
{
|
||||||
"url": "https://example.com",
|
"url": "https://example.com",
|
||||||
"action": "screenshot", // 或 "html"
|
"action": "smart", // "screenshot" | "html" | "text" | "smart"
|
||||||
"scroll_times": 3,
|
"wait_time": 15000, // 重要!验证网站需设置较长等待
|
||||||
"scroll_delay": 1000,
|
"scroll_times": 3, // 可选:加载动态内容
|
||||||
"full_page": true,
|
"scroll_delay": 1000, // 可选:滚动间隔
|
||||||
"viewport": {"width": 1920, "height": 1080},
|
"full_page": true, // 可选:全页截图
|
||||||
"wait_time": 2000
|
"backend": "playwright", // 可选:playwright / agent-browser
|
||||||
}</code></pre>
|
"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/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">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3 id="modalTitle">记录详情</h3>
|
||||||
|
<button class="modal-close" onclick="closeModal()">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-meta" id="modalMeta"></div>
|
||||||
|
<div class="modal-body" id="modalBody"></div>
|
||||||
|
<div class="actions" style="padding: 0 24px 20px;">
|
||||||
|
<button class="btn btn-secondary" onclick="downloadHistoryContent()">💾 下载内容</button>
|
||||||
|
<button class="btn btn-secondary" onclick="copyHistoryContent()">📋 复制内容</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -372,6 +821,105 @@
|
|||||||
const submitBtn = document.getElementById('submitBtn');
|
const submitBtn = document.getElementById('submitBtn');
|
||||||
let currentData = null;
|
let currentData = null;
|
||||||
let currentBlob = null;
|
let currentBlob = null;
|
||||||
|
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);
|
||||||
|
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) => {
|
form.addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -395,7 +943,8 @@
|
|||||||
viewport: {
|
viewport: {
|
||||||
width: viewportWidth,
|
width: viewportWidth,
|
||||||
height: viewportHeight
|
height: viewportHeight
|
||||||
}
|
},
|
||||||
|
smart_config: action === 'smart' ? (smartConfig || {}) : undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
// 显示加载
|
// 显示加载
|
||||||
@@ -421,16 +970,37 @@
|
|||||||
if (action === 'screenshot') {
|
if (action === 'screenshot') {
|
||||||
currentBlob = await response.blob();
|
currentBlob = await response.blob();
|
||||||
const imageUrl = URL.createObjectURL(currentBlob);
|
const imageUrl = URL.createObjectURL(currentBlob);
|
||||||
document.getElementById('resultContent').innerHTML =
|
document.getElementById('resultContent').innerHTML =
|
||||||
`<img src="${imageUrl}" class="result-image" alt="截图结果">`;
|
`<img src="${imageUrl}" class="result-image" alt="截图结果">`;
|
||||||
} else {
|
} else {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
currentBlob = new Blob([data.html], { type: 'text/html' });
|
if (action === 'html') {
|
||||||
document.getElementById('resultContent').innerHTML =
|
currentBlob = new Blob([data.html], { type: 'text/html' });
|
||||||
`<div class="result-code"><pre>${escapeHtml(data.html)}</pre></div>`;
|
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;
|
||||||
|
document.getElementById('resultContent').innerHTML =
|
||||||
|
`<div style="margin-bottom:10px;color:#666;font-size:13px;">页面标题:${escapeHtml(data.title || '无')} | 共 ${lines} 行,${(data.text || '').length} 字符(已剔除标签与无效字符)</div>` +
|
||||||
|
`<div class="result-text">${escapeHtml(data.text || '')}</div>`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result.classList.add('active');
|
result.classList.add('active');
|
||||||
|
// 捕获成功后刷新历史(回到第一页看最新)
|
||||||
|
loadHistory(1);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.textContent = '❌ ' + err.message;
|
error.textContent = '❌ ' + err.message;
|
||||||
error.classList.add('active');
|
error.classList.add('active');
|
||||||
@@ -447,12 +1017,24 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function downloadResult() {
|
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;
|
if (!currentBlob) return;
|
||||||
|
|
||||||
const url = URL.createObjectURL(currentBlob);
|
const url = URL.createObjectURL(currentBlob);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = url;
|
a.href = url;
|
||||||
a.download = currentData.action === 'screenshot' ? 'screenshot.png' : 'page.html';
|
const name = currentData.action === 'screenshot' ? 'screenshot.png'
|
||||||
|
: currentData.action === 'html' ? 'page.html' : 'page.txt';
|
||||||
|
a.download = name;
|
||||||
a.click();
|
a.click();
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
}
|
}
|
||||||
@@ -474,6 +1056,208 @@
|
|||||||
error.classList.remove('active');
|
error.classList.remove('active');
|
||||||
currentData = null;
|
currentData = null;
|
||||||
currentBlob = null;
|
currentBlob = null;
|
||||||
|
lastSmartHistoryId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 历史记录 ===== */
|
||||||
|
async function loadHistory(page) {
|
||||||
|
const action = document.getElementById('historyActionFilter').value;
|
||||||
|
const search = document.getElementById('historySearch').value.trim();
|
||||||
|
const qs = new URLSearchParams({ page: page, page_size: 15, action, search });
|
||||||
|
const res = await fetch('/api/history?' + qs.toString());
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (!data.success) {
|
||||||
|
document.getElementById('historyEmpty').style.display = 'block';
|
||||||
|
document.getElementById('historyEmpty').textContent = '❌ 加载历史失败:' + (data.error || '未知错误');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPage = page;
|
||||||
|
currentHistory = data;
|
||||||
|
renderHistory(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshHistory() {
|
||||||
|
loadHistory(currentPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderHistory(data) {
|
||||||
|
const body = document.getElementById('historyBody');
|
||||||
|
const empty = document.getElementById('historyEmpty');
|
||||||
|
const count = document.getElementById('historyCount');
|
||||||
|
|
||||||
|
count.textContent = `共 ${data.total} 条记录`;
|
||||||
|
|
||||||
|
if (!data.records.length) {
|
||||||
|
body.innerHTML = '';
|
||||||
|
empty.style.display = 'block';
|
||||||
|
empty.textContent = '暂无提取记录,先去上方提取一个网页吧 🚀';
|
||||||
|
renderPagination(data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
empty.style.display = 'none';
|
||||||
|
|
||||||
|
const badges = {
|
||||||
|
screenshot: '📸 截图',
|
||||||
|
html: '📄 HTML',
|
||||||
|
text: '📝 文本',
|
||||||
|
smart: '🧠 按需截图'
|
||||||
|
};
|
||||||
|
|
||||||
|
body.innerHTML = data.records.map(r => {
|
||||||
|
const badgeClass = r.status === 'failed' ? 'badge-failed' : ('badge-' + r.action);
|
||||||
|
const badgeText = r.status === 'failed' ? '❌ 失败' : (badges[r.action] || r.action);
|
||||||
|
const statusText = r.status === 'failed'
|
||||||
|
? `<span style="color:#c62828;" title="${escapeHtml(r.error || '')}">失败</span>`
|
||||||
|
: '<span style="color:#2e7d32;">成功</span>';
|
||||||
|
return `<tr>
|
||||||
|
<td style="white-space:nowrap;">${escapeHtml(r.created_at)}</td>
|
||||||
|
<td><span class="badge ${escapeHtml(badgeClass)}">${badgeText}</span></td>
|
||||||
|
<td><a class="history-url" href="${escapeHtml(r.url)}" target="_blank" rel="noopener">${escapeHtml(r.url)}</a></td>
|
||||||
|
<td><span class="history-title" title="${escapeHtml(r.title || '')}">${escapeHtml(r.title || '—')}</span></td>
|
||||||
|
<td>${statusText}</td>
|
||||||
|
<td style="text-align:right; white-space:nowrap;">
|
||||||
|
<button class="btn-small" onclick="viewRecord(${r.id})">👁 查看</button>
|
||||||
|
<button class="btn-small danger" onclick="deleteRecord(${r.id})">🗑</button>
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
renderPagination(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPagination(data) {
|
||||||
|
const el = document.getElementById('pagination');
|
||||||
|
if (!data.records.length) {
|
||||||
|
el.innerHTML = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
html += `<button class="page-btn" onclick="loadHistory(${data.page - 1})" ${data.page <= 1 ? 'disabled' : ''}>‹ 上一页</button>`;
|
||||||
|
|
||||||
|
const total = data.total_pages;
|
||||||
|
const cur = data.page;
|
||||||
|
let start = Math.max(1, cur - 2);
|
||||||
|
let end = Math.min(total, cur + 2);
|
||||||
|
if (end - start < 4) {
|
||||||
|
start = Math.max(1, end - 4);
|
||||||
|
end = Math.min(total, start + 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (start > 1) html += `<button class="page-btn" onclick="loadHistory(1)">1</button>`;
|
||||||
|
if (start > 2) html += `<span style="color:#999;">…</span>`;
|
||||||
|
for (let p = start; p <= end; p++) {
|
||||||
|
html += `<button class="page-btn ${p === cur ? 'active' : ''}" onclick="loadHistory(${p})">${p}</button>`;
|
||||||
|
}
|
||||||
|
if (end < total - 1) html += `<span style="color:#999;">…</span>`;
|
||||||
|
if (end < total) html += `<button class="page-btn" onclick="loadHistory(${total})">${total}</button>`;
|
||||||
|
|
||||||
|
html += `<button class="page-btn" onclick="loadHistory(${data.page + 1})" ${data.page >= total ? 'disabled' : ''}>下一页 ›</button>`;
|
||||||
|
html += `<span class="page-info">第 ${data.page} / ${total} 页</span>`;
|
||||||
|
|
||||||
|
el.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function viewRecord(id) {
|
||||||
|
const res = await fetch('/api/history/' + id);
|
||||||
|
const data = await res.json();
|
||||||
|
if (!data.success) {
|
||||||
|
alert('加载失败:' + (data.error || '未知错误'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalRecord = data.record;
|
||||||
|
const r = modalRecord;
|
||||||
|
|
||||||
|
document.getElementById('modalTitle').textContent = (r.title || r.url || '记录详情');
|
||||||
|
const badgeClass = r.status === 'failed' ? 'badge-failed' : ('badge-' + r.action);
|
||||||
|
const badgeText = r.status === 'failed' ? '❌ 失败' : ({screenshot: '📸 截图', html: '📄 HTML', text: '📝 文本'}[r.action] || r.action);
|
||||||
|
document.getElementById('modalMeta').innerHTML =
|
||||||
|
`<span>类型:<span class="badge ${badgeClass}">${badgeText}</span></span>` +
|
||||||
|
`<span>时间:${escapeHtml(r.created_at)}</span>` +
|
||||||
|
`<span>后端:${escapeHtml(r.backend || '—')}</span>` +
|
||||||
|
`<span>状态:${r.status === 'failed' ? '<span style="color:#c62828;">失败</span>' : '<span style="color:#2e7d32;">成功</span>'}</span>`;
|
||||||
|
|
||||||
|
const body = document.getElementById('modalBody');
|
||||||
|
if (r.status === 'failed') {
|
||||||
|
body.innerHTML = `<div class="result-text" style="color:#c62828;">❌ 提取失败:${escapeHtml(r.error || '')}</div>`;
|
||||||
|
} else if (r.action === 'screenshot') {
|
||||||
|
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>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('modalOverlay').classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
document.getElementById('modalOverlay').classList.remove('active');
|
||||||
|
modalRecord = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteRecord(id) {
|
||||||
|
if (!confirm('确定删除这条记录吗?截图文件将一并删除。')) return;
|
||||||
|
const res = await fetch('/api/history/' + id, { method: 'DELETE' });
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success) {
|
||||||
|
// 若当前页删空了则回退一页
|
||||||
|
if (currentHistory.records.length === 1 && currentPage > 1) {
|
||||||
|
loadHistory(currentPage - 1);
|
||||||
|
} else {
|
||||||
|
loadHistory(currentPage);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert('删除失败:' + (data.error || '未知错误'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentModalBlob() {
|
||||||
|
if (!modalRecord) return null;
|
||||||
|
if (modalRecord.action === 'screenshot') return null; // 图片用链接下载
|
||||||
|
const type = modalRecord.action === 'html' ? 'text/html' : 'text/plain;charset=utf-8';
|
||||||
|
return new Blob([modalRecord.content || ''], { type });
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadHistoryContent() {
|
||||||
|
if (!modalRecord) return;
|
||||||
|
if (modalRecord.action === 'screenshot' || modalRecord.action === 'smart') {
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = `/api/history/${modalRecord.id}/file`;
|
||||||
|
a.download = modalRecord.action === 'smart' ? `smart_${modalRecord.id}.png` : `capture_${modalRecord.id}.png`;
|
||||||
|
a.click();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const blob = currentModalBlob();
|
||||||
|
if (!blob) return;
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = modalRecord.action === 'html' ? `page_${modalRecord.id}.html` : `page_${modalRecord.id}.txt`;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyHistoryContent() {
|
||||||
|
if (!modalRecord || !modalRecord.content) return;
|
||||||
|
navigator.clipboard.writeText(modalRecord.content).then(() => {
|
||||||
|
alert('已复制到剪贴板');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user