Compare commits

...
5 Commits
Author SHA1 Message Date
hz4th_coder 20f3ec1f18 修复页面抓取功能,使用 agent-browser 替代 requests
- 改用浏览器方式抓取页面内容,绑过反爬虫机制
- 从 accessibility tree snapshot 中提取文本内容
- 增加等待时间让页面完全加载
- 可抓取知乎等有反爬措施的网站
2026-07-13 12:14:21 +08:00
hz4th_coder 7bb5a250c8 前端增加人工互联网搜索功能
- 新增互联网搜索面板,支持手动输入关键词搜索
- 搜索结果展示为卡片形式,显示标题和链接
- 支持抓取单个结果的详细内容
- 可一键保存搜索结果到内容库
- 新增搜索 API 端点:/api/articles/internet-search
- 搜索结果详情模态框展示
- 响应式设计,搜索结果自适应布局
2026-07-13 12:04:20 +08:00
hz4th_coder 4f02a6a0ea 实现浏览器方式互联网搜索功能
- 使用 agent-browser 浏览器自动化进行 Bing 搜索
- 解析 accessibility tree 提取搜索结果
- 自动获取每个结果的 URL
- 支持设置最大结果数量
2026-07-13 11:55:33 +08:00
hz4th_coder 187d5037b8 添加Git推送说明文档 2026-07-12 01:24:46 +08:00
hz4th_coder 47729a52da 添加部署文档和说明 2026-07-12 01:24:19 +08:00
7 changed files with 991 additions and 27 deletions
+302
View File
@@ -0,0 +1,302 @@
# Param Auto Manager 部署文档
## 📦 系统概览
**参数数据自动化管理系统** - 自动管理参数数据网站的系统,支持从内容库和互联网搜索数据,自动处理产品并提交到后台管理待审核区。
---
## 🚀 快速部署
### 1. 系统要求
- Python 3.10+
- Flask
- SQLite3
- Git
### 2. 安装依赖
```bash
cd works/param-auto-manager
pip install -r requirements.txt
```
### 3. 启动服务
```bash
# 方式1: 使用启动脚本
./start.sh
# 方式2: 直接运行
export PYTHONPATH="$HOME/.local/lib/python3.12/site-packages:$PYTHONPATH"
python3 app.py
```
### 4. 访问系统
- **前端界面**: http://localhost:16043 或 http://192.168.0.101:16043
- **API接口**: http://localhost:16043/api
---
## 🌐 前端界面功能
### 主要功能模块
1. **统计面板**
- 内容库文章数量
- 待处理产品数量
- 处理中产品数量
- 最近成功/失败数量
2. **待处理产品列表**
- 查看所有待处理产品
- 添加单个/批量产品
- 手动触发处理
- 移除产品
- 批量处理按钮
3. **内容库文章**
- 查看所有文章
- 搜索文章(关键词)
- 添加新文章
- 查看文章详情
- 删除文章
4. **处理历史**
- 查看处理历史记录
- 状态显示(已提交/失败/错误)
- 查看处理详情
5. **系统配置**
- 自动处理开关
- 处理间隔设置
- 批量处理数量
---
## 🔌 API接口
### 文章内容库 API
```bash
# 获取文章列表
GET /api/articles
# 搜索文章
GET /api/articles/search?q=关键词
# 创建文章
POST /api/articles
{
"product_names": ["产品1", "产品2"],
"category": "分类",
"keywords": ["关键词"],
"summary": "摘要",
"content": "内容",
"source": "来源"
}
# 删除文章
DELETE /api/articles/{id}
```
### 产品处理 API
```bash
# 待处理产品列表
GET /api/products/pending
# 添加产品
POST /api/products/pending
{
"product_name": "产品名称",
"category": "分类",
"priority": 10
}
# 处理单个产品
POST /api/products/process
{
"product_name": "产品名称"
}
# 批量处理
POST /api/products/process/batch
{
"limit": 5
}
# 处理历史
GET /api/products/history
```
### 系统管理 API
```bash
# 系统统计
GET /api/system/stats
# 系统配置
GET /api/system/config
PUT /api/system/config
# 健康检查
GET /api/system/health
```
---
## ⚙️ Git仓库推送
### ⚠️ 注意事项
当前Git服务器不允许用户直接推送创建仓库(Push to create),需要管理员预先创建仓库。
### 推送步骤
**方式1: 管理员预先创建仓库**
1. 在Git服务器创建仓库:
- 地址: `http://121.40.164.32:12007/hz4th_coder/param-auto-manager.git`
- 账号: `hz4th_coder`
2. 推送代码:
```bash
cd works/param-auto-manager
git remote add origin http://hz4th_coder:262e7dbfce09c8cc21fbacff2b450cc3f1c3e265@121.40.164.32:12007/hz4th_coder/param-auto-manager.git
git push -u origin master
git push origin v1.1.0
```
**方式2: 请求管理员推送权限**
联系Git服务器管理员,开通用户的"Push to create"权限。
---
## 📊 使用示例
### 1. 通过前端界面操作
1. **添加待处理产品**
- 点击"添加产品"按钮
- 输入产品名称(如: GPT-4o
- 设置分类和优先级
- 点击"添加"
2. **添加文章到内容库**
- 点击"添加文章"按钮
- 填写产品名称、摘要、内容等
- 点击"添加"
3. **手动触发处理**
- 在待处理产品列表找到产品
- 点击"处理"按钮
- 查看处理结果
### 2. 通过API操作
```bash
# 添加产品
curl -X POST http://localhost:16043/api/products/pending \
-H "Content-Type: application/json" \
-d '{"product_name":"GPT-4o","category":"AI模型","priority":10}'
# 添加文章
curl -X POST http://localhost:16043/api/articles \
-H "Content-Type: application/json" \
-d '{
"product_names": ["GPT-4o"],
"category": "AI模型",
"keywords": ["大模型","多模态"],
"summary": "OpenAI多模态大模型",
"content": "详细内容...",
"source": "官方网站"
}'
# 触发处理
curl -X POST http://localhost:16043/api/products/process \
-H "Content-Type: application/json" \
-d '{"product_name":"GPT-4o"}'
```
---
## 🔧 系统配置
配置文件: `config.py`
```python
PORT = 16043 # 服务端口
PARAMHUB_BASE_URL = 'http://localhost:16041' # ParamHub API地址
PROCESS_INTERVAL = 300 # 自动处理间隔(秒)
BATCH_SIZE = 5 # 批量处理数量
```
---
## 📁 项目结构
```
param-auto-manager/
├── app.py # 主应用
├── config.py # 配置文件
├── models/
│ └── database.py # 数据库模型
├── routes/
│ ├── articles.py # 文章API
│ ├── products.py # 产品API
│ └── system.py # 系统API
├── services/
│ ├── search_service.py # 搜索服务
│ ├── process_service.py # 处理服务
│ └── paramhub_client.py # ParamHub客户端
├── templates/
│ └── index.html # 前端页面
├── static/
│ ├── css/style.css # 样式文件
│ └── js/app.js # JavaScript文件
├── utils/
│ └── scheduler.py # 定时任务
├── data/ # 数据库文件
└── logs/ # 日志文件
```
---
## ✅ 当前部署状态
- **端口**: 16043 ✅
- **状态**: 运行中
- **PID**: 2710498
- **前端**: http://192.168.0.101:16043 ✅
- **API**: http://192.168.0.101:16043/api ✅
- **数据库**: SQLite (param_auto.db) ✅
- **定时任务**: 每5分钟自动处理 ✅
---
## 📝 版本历史
- **v1.1.0** (2026-07-12): 添加前端界面和操作页面
- **v1.0.0** (2026-07-12): 初始版本,核心功能实现
---
## 🆘 常见问题
### Q: Git推送失败怎么办?
A: 需要在Git服务器上预先创建仓库,或联系管理员开通推送权限。
### Q: 如何查看日志?
A: 日志文件位于 `logs/app.log`
### Q: 如何停止服务?
A: 运行 `./stop.sh` 或手动杀掉进程
### Q: 前端页面打不开?
A: 检查服务是否启动,查看日志是否有错误
---
## 📞 支持
如有问题,请查看日志文件或联系系统管理员。
+88
View File
@@ -0,0 +1,88 @@
# Git推送说明
## 📌 当前状态
代码已准备推送,但Git服务器返回403错误:
```
remote: Push to create is not enabled for users.
```
这表示Git服务器不允许用户直接推送创建仓库。
---
## ✅ 解决方案
### 方案1: 管理员预先创建仓库
请在Git服务器上手动创建以下仓库:
**仓库信息:**
- **地址**: http://121.40.164.32:12007/hz4th_coder/param-auto-manager.git
- **账号**: hz4th_coder
- **组织**: hz4th_coder
**创建后推送代码:**
```bash
cd /home/openclaw/.openclaw/workspace-hz4th_coder/works/param-auto-manager
# 添加远程仓库(如果还没有)
git remote add origin http://hz4th_coder:262e7dbfce09c8cc21fbacff2b450cc3f1c3e265@121.40.164.32:12007/hz4th_coder/param-auto-manager.git
# 推送代码和标签
git push -u origin master
git push origin v1.0.0
git push origin v1.1.0
git push origin v1.2.0
```
---
### 方案2: 开通推送权限
联系Git服务器管理员,为 `hz4th_coder` 用户开通"Push to create"权限。
---
## 📊 当前Git状态
```bash
cd works/param-auto-manager
git log --oneline -5
```
输出:
```
47729a5 添加部署文档和说明
8ad0246 添加前端界面和操作页面
e3bc883 添加.gitignore文件,排除缓存和临时文件
b7b0925 初始化参数数据自动化管理系统
```
标签:
```
v1.0.0 - 初始化版本
v1.1.0 - 添加前端界面
v1.2.0 - 添加部署文档
```
---
## 🔐 Git认证信息
- **账号**: hz4th_coder
- **邮箱**: hz4th_coder@tphai.com
- **Token**: 262e7dbfce09c8cc21fbacff2b450cc3f1c3e265
---
## 📝 待推送文件统计
- 总文件数: 26个源文件
- 总代码行数: 约5000行
- 版本标签: 3个
---
**建议**: 请在Git服务器创建仓库后,执行推送命令即可完成部署。
+56
View File
@@ -133,3 +133,59 @@ def fetch_article():
}) })
else: else:
return jsonify({'error': '抓取失败'}), 500 return jsonify({'error': '抓取失败'}), 500
@bp.route('/internet-search', methods=['POST'])
def internet_search():
"""互联网搜索(浏览器方式)"""
data = request.get_json()
keyword = data.get('keyword', '')
max_results = data.get('max_results', 10)
save_to_library = data.get('save_to_library', False)
if not keyword:
return jsonify({'error': '请提供搜索关键词'}), 400
# 执行互联网搜索
results = search_service.search_internet(keyword, max_results)
return jsonify({
'success': True,
'keyword': keyword,
'results': results,
'count': len(results)
})
@bp.route('/internet-search-and-fetch', methods=['POST'])
def internet_search_and_fetch():
"""互联网搜索并抓取内容"""
data = request.get_json()
keyword = data.get('keyword', '')
max_results = data.get('max_results', 5)
category = data.get('category')
if not keyword:
return jsonify({'error': '请提供搜索关键词'}), 400
# 执行互联网搜索
results = search_service.search_internet(keyword, max_results)
# 抓取每个结果的详细内容
fetched_results = []
for r in results:
url = r.get('url')
if url:
content = search_service.fetch_url_content(url)
if content:
fetched_results.append({
'title': r['title'],
'url': url,
'source': r['source'],
'fetched_content': content
})
return jsonify({
'success': True,
'keyword': keyword,
'results': fetched_results,
'count': len(fetched_results)
})
+182 -26
View File
@@ -4,6 +4,10 @@
import requests import requests
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
import json import json
import subprocess
import os
import re
import urllib.parse
from datetime import datetime from datetime import datetime
from config import Config from config import Config
from models.database import db from models.database import db
@@ -13,57 +17,209 @@ class SearchService:
self.timeout = Config.SEARCH_TIMEOUT self.timeout = Config.SEARCH_TIMEOUT
self.max_results = Config.SEARCH_MAX_RESULTS self.max_results = Config.SEARCH_MAX_RESULTS
def _run_browser(self, *args, timeout=30000):
"""运行 agent-browser 命令"""
env = os.environ.copy()
env['XDG_RUNTIME_DIR'] = '/tmp/agent-browser-runtime'
os.makedirs(env['XDG_RUNTIME_DIR'], exist_ok=True)
cmd = ['agent-browser'] + list(args)
result = subprocess.run(
cmd,
capture_output=True,
text=True,
env=env,
timeout=timeout // 1000 + 5
)
return result.stdout, result.stderr, result.returncode
def search_internet(self, keyword, max_results=None): def search_internet(self, keyword, max_results=None):
""" """
从互联网搜索(使用搜索API或爬虫 从互联网搜索(使用 agent-browser 浏览器自动化
这里暂时使用简单的搜索模拟
""" """
max_results = max_results or self.max_results max_results = max_results or self.max_results
# TODO: 接入真实的搜索API(如Google Custom Search、Bing等)
# 这里先返回空列表,等待后续接入真实API
results = [] results = []
try:
# 1. 打开 Bing 搜索
encoded_keyword = urllib.parse.quote(keyword)
search_url = f"https://www.bing.com/search?q={encoded_keyword}"
stdout, stderr, code = self._run_browser('open', search_url, '--timeout', '20000')
if code != 0:
print(f"打开搜索页面失败: {stderr}")
return results
# 等待页面加载
stdout, stderr, code = self._run_browser('wait', '5000')
# 2. 获取搜索结果页面结构 (JSON 格式)
stdout, stderr, code = self._run_browser('snapshot', '--json', '--timeout', '30000')
if code != 0:
print(f"获取页面结构失败: {stderr}")
return results
# 3. 解析 JSON 提取搜索结果
try:
data = json.loads(stdout)
except json.JSONDecodeError:
print(f"解析 JSON 失败: {stdout[:500]}")
return results
# 4. 从 accessibility tree 中提取搜索结果
# Bing 搜索结果在 main[aria-label="搜索结果"] 区域内
results = self._parse_bing_results(data, max_results)
# 5. 关闭浏览器
self._run_browser('close')
except subprocess.TimeoutExpired:
print(f"搜索超时: {keyword}")
except Exception as e:
print(f"搜索出错: {str(e)}")
# 尝试关闭浏览器
try:
self._run_browser('close')
except:
pass
return results return results
def fetch_url_content(self, url): def _parse_bing_results(self, snapshot_data, max_results=10):
"""抓取网页内容""" """
从 Bing 搜索结果的 snapshot 中解析出标题和链接
snapshot_data 是 agent-browser snapshot --json 的输出
结构: {success, data: {snapshot: "文本格式的 accessibility tree"}, error}
"""
results = []
# 获取 snapshot 文本
snapshot = snapshot_data.get('data', {}).get('snapshot', '')
if not snapshot:
return results
# 解析 accessibility tree 文本
in_results = False
refs = [] # 存储 (title, ref) 元组
lines = snapshot.split('\n')
for i, line in enumerate(lines):
line = line.strip()
# 进入搜索结果区域
if 'main "搜索结果"' in line:
in_results = True
continue
# 离开搜索结果区域
if in_results and line.startswith('- ') and 'main' in line and '搜索结果' not in line:
break
if not in_results:
continue
# 匹配标题链接:link "标题文字" [ref=eXX]
# 需要过滤域名链接(如 "zhihu.com")和短链接
if 'link "' in line and '[ref=' in line:
match = re.search(r'link "([^"]+)" \[ref=(e\d+)\]', line)
if match:
title = match.group(1)
ref = match.group(2)
# 过滤短标题(域名链接如 "zhihu.com"
if len(title) > 20 and '.' not in title[:10]: # 不是域名格式
refs.append((title, ref))
# 获取每个结果的 URL
for title, ref in refs[:max_results]:
url = self._get_link_url(ref)
if url and 'bing.com/search' not in url: # 过滤搜索结果页本身的链接
results.append({
'title': title,
'url': url,
'snippet': '',
'source': 'bing'
})
return results
def _get_link_url(self, ref):
"""通过 agent-browser 获取链接的 URL"""
try: try:
headers = { stdout, stderr, code = self._run_browser('get', 'attr', f'@{ref}', 'href', '--json', '--timeout', '5000')
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' if code == 0 and stdout:
} data = json.loads(stdout)
response = requests.get(url, headers=headers, timeout=self.timeout) return data.get('data', {}).get('value', '')
response.raise_for_status() except Exception as e:
print(f"获取 URL 失败 (ref={ref}): {e}")
return None
soup = BeautifulSoup(response.text, 'lxml') def fetch_url_content(self, url):
"""抓取网页内容(使用 agent-browser 浏览器方式,绑过反爬虫)"""
try:
# 使用浏览器方式抓取
stdout, stderr, code = self._run_browser('open', url, '--timeout', '20000')
if code != 0:
print(f"打开页面失败: {stderr}")
return None
# 提取标题 # 等待页面加载
title = soup.find('title') self._run_browser('wait', '5000')
title = title.text.strip() if title else ''
# 提取正文(简单提取,可优化) # 获取页面标题
# 移除脚本和样式 stdout, stderr, code = self._run_browser('get', 'title', '--timeout', '5000')
for script in soup(['script', 'style']): title = stdout.strip().replace('[agent-browser] ', '').strip() if code == 0 else ''
script.decompose()
# 提取文本 # 获取页面内容(通过 snapshot 获取 accessibility tree
text = soup.get_text(separator='\n', strip=True) stdout, stderr, code = self._run_browser('snapshot', '--json', '--timeout', '15000')
text = ''
if code == 0 and stdout:
try:
data = json.loads(stdout)
snapshot = data.get('data', {}).get('snapshot', '')
# 从 snapshot 中提取所有 StaticText
text = self._extract_text_from_snapshot(snapshot)
except:
pass
# 提取元数据 # 获取 URL(可能被重定向)
meta_desc = soup.find('meta', attrs={'name': 'description'}) stdout, stderr, code = self._run_browser('get', 'url', '--timeout', '5000')
description = meta_desc['content'] if meta_desc else '' actual_url = stdout.strip() if code == 0 else url
# 关闭浏览器
self._run_browser('close')
# 提取描述(从页面内容的前200字符)
description = text[:200].strip() if text else ''
return { return {
'title': title, 'title': title,
'description': description, 'description': description,
'content': text, 'content': text,
'url': url, 'url': actual_url,
'fetch_date': datetime.now().isoformat() 'fetch_date': datetime.now().isoformat()
} }
except Exception as e: except Exception as e:
print(f"抓取URL失败: {url}, 错误: {str(e)}") print(f"抓取URL失败: {url}, 错误: {str(e)}")
# 尝试关闭浏览器
try:
self._run_browser('close')
except:
pass
return None return None
def _extract_text_from_snapshot(self, snapshot):
"""从 accessibility tree snapshot 中提取文本内容"""
# 提取所有 StaticText 行
texts = []
for line in snapshot.split('\n'):
if 'StaticText' in line:
# 格式: - StaticText "文本内容"
match = re.search(r'StaticText "([^"]+)"', line)
if match:
texts.append(match.group(1))
return '\n'.join(texts)
def search_articles(self, keyword, category=None): def search_articles(self, keyword, category=None):
"""从内容库搜索""" """从内容库搜索"""
return db.search_articles(keyword, category) return db.search_articles(keyword, category)
+93
View File
@@ -643,3 +643,96 @@ body {
.priority-low { .priority-low {
color: #10b981; color: #10b981;
} }
/* 搜索结果卡片 */
.search-results-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 15px;
}
.search-result-card {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 15px;
display: flex;
gap: 15px;
transition: all 0.2s;
}
.search-result-card:hover {
border-color: #667eea;
background: white;
}
.result-number {
width: 30px;
height: 30px;
background: #667eea;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 14px;
}
.result-content {
flex: 1;
min-width: 0;
}
.result-title {
font-size: 15px;
color: #333;
cursor: pointer;
margin-bottom: 8px;
word-break: break-word;
}
.result-title:hover {
color: #667eea;
}
.result-url {
font-size: 12px;
color: #666;
margin-bottom: 6px;
overflow: hidden;
}
.result-url a {
color: #3b82f6;
text-decoration: none;
display: flex;
align-items: center;
gap: 4px;
}
.result-url a:hover {
text-decoration: underline;
}
.result-source {
font-size: 12px;
color: #999;
}
.result-actions {
display: flex;
flex-direction: column;
gap: 8px;
}
/* 搜索状态 */
#internet-search-status {
display: flex;
align-items: center;
gap: 8px;
}
#internet-search-status i {
font-size: 16px;
}
+229
View File
@@ -613,3 +613,232 @@ function getStatusText(status) {
}; };
return statusMap[status] || status; return statusMap[status] || status;
} }
// ========== 互联网搜索功能 ==========
let currentSearchResult = null;
// 执行互联网搜索
async function doInternetSearch() {
const keyword = document.getElementById('internet-search-keyword').value.trim();
const maxResults = parseInt(document.getElementById('internet-search-count').value) || 10;
if (!keyword) {
showToast('请输入搜索关键词', 'error');
return;
}
// 显示搜索状态
const statusDiv = document.getElementById('internet-search-status');
const resultsDiv = document.getElementById('internet-search-results');
const searchBtn = document.getElementById('search-btn');
statusDiv.innerHTML = '<i class="ri-loader-4-line"></i> 正在搜索...';
resultsDiv.innerHTML = '<div class="empty-text">搜索中...</div>';
searchBtn.disabled = true;
try {
const response = await fetch(`${API_BASE}/api/articles/internet-search`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ keyword, max_results: maxResults })
});
const data = await response.json();
if (data.success) {
statusDiv.innerHTML = `<i class="ri-check-line"></i> 搜索完成,找到 ${data.count} 条结果`;
if (data.results.length > 0) {
resultsDiv.innerHTML = data.results.map((r, i) => `
<div class="search-result-card">
<div class="result-number">${i + 1}</div>
<div class="result-content">
<div class="result-title" onclick="showSearchResultDetail(${i})">${escapeHtml(r.title)}</div>
<div class="result-url">
<a href="${escapeHtml(r.url)}" target="_blank">
<i class="ri-external-link-line"></i> ${escapeHtml(r.url.substring(0, 60))}${r.url.length > 60 ? '...' : ''}
</a>
</div>
<div class="result-source">来源: ${escapeHtml(r.source)}</div>
</div>
<div class="result-actions">
<button onclick="fetchAndShowResult(${i})" class="btn btn-sm btn-secondary">
<i class="ri-download-line"></i> 抓取内容
</button>
<button onclick="quickSaveResult(${i})" class="btn btn-sm btn-success">
<i class="ri-save-line"></i> 保存
</button>
</div>
</div>
`).join('');
// 存储搜索结果供后续使用
window.lastSearchResults = data.results;
} else {
resultsDiv.innerHTML = '<div class="empty-text">未找到相关结果</div>';
}
} else {
statusDiv.innerHTML = `<i class="ri-error-warning-line"></i> 搜索失败: ${escapeHtml(data.error)}`;
resultsDiv.innerHTML = '<div class="empty-text">搜索失败</div>';
}
} catch (error) {
statusDiv.innerHTML = '<i class="ri-error-warning-line"></i> 搜索出错';
resultsDiv.innerHTML = '<div class="empty-text">搜索出错,请稍后重试</div>';
console.error('搜索错误:', error);
}
searchBtn.disabled = false;
}
// 显示搜索结果详情
function showSearchResultDetail(index) {
if (!window.lastSearchResults || !window.lastSearchResults[index]) {
return;
}
currentSearchResult = window.lastSearchResults[index];
const body = document.getElementById('search-result-body');
body.innerHTML = `
<div class="detail-meta">
<div class="detail-meta-item">
<label>标题</label>
<span>${escapeHtml(currentSearchResult.title)}</span>
</div>
<div class="detail-meta-item">
<label>URL</label>
<a href="${escapeHtml(currentSearchResult.url)}" target="_blank">${escapeHtml(currentSearchResult.url)}</a>
</div>
<div class="detail-meta-item">
<label>来源</label>
<span>${escapeHtml(currentSearchResult.source)}</span>
</div>
</div>
<div class="detail-section">
<h4><i class="ri-information-line"></i> 提示</h4>
<p>点击"抓取内容"按钮可以获取页面详细内容,然后保存到内容库。</p>
</div>
`;
document.getElementById('search-result-modal').classList.add('active');
}
// 抓取并显示搜索结果内容
async function fetchAndShowResult(index) {
if (!window.lastSearchResults || !window.lastSearchResults[index]) {
return;
}
const result = window.lastSearchResults[index];
currentSearchResult = result;
showToast('正在抓取页面内容...', '');
try {
const response = await fetch(`${API_BASE}/api/articles/fetch`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url: result.url,
product_names: [result.title],
category: ''
})
});
const data = await response.json();
if (data.success) {
// 更新当前搜索结果,添加抓取的内容
currentSearchResult = {
...result,
fetched: true,
fetchedContent: data.data
};
const body = document.getElementById('search-result-body');
body.innerHTML = `
<div class="detail-meta">
<div class="detail-meta-item">
<label>标题</label>
<span>${escapeHtml(data.data.title)}</span>
</div>
<div class="detail-meta-item">
<label>URL</label>
<a href="${escapeHtml(result.url)}" target="_blank">${escapeHtml(result.url)}</a>
</div>
<div class="detail-meta-item">
<label>描述</label>
<span>${escapeHtml(data.data.description || '无')}</span>
</div>
</div>
<div class="detail-section">
<h4><i class="ri-file-text-line"></i> 页面内容</h4>
<pre style="white-space: pre-wrap; max-height: 400px; overflow-y: auto; background: #f8f9fa; padding: 15px; border-radius: 8px;">${escapeHtml(data.data.content.substring(0, 2000))}${data.data.content.length > 2000 ? '\n... (内容过长,已截断)' : ''}</pre>
</div>
`;
document.getElementById('search-result-modal').classList.add('active');
showToast('内容抓取成功', 'success');
} else {
showToast('抓取失败: ' + data.error, 'error');
}
} catch (error) {
showToast('抓取出错', 'error');
console.error('抓取错误:', error);
}
}
// 快速保存搜索结果到内容库
async function quickSaveResult(index) {
if (!window.lastSearchResults || !window.lastSearchResults[index]) {
return;
}
const result = window.lastSearchResults[index];
// 先抓取内容再保存
showToast('正在抓取并保存...', '');
try {
const response = await fetch(`${API_BASE}/api/articles/fetch`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url: result.url,
product_names: [result.title],
category: ''
})
});
const data = await response.json();
if (data.success) {
showToast('已保存到内容库', 'success');
loadArticles();
loadStats();
} else {
showToast('保存失败: ' + data.error, 'error');
}
} catch (error) {
showToast('保存出错', 'error');
}
}
// 从详情模态框保存到内容库
async function saveSearchResultToLibrary() {
if (!currentSearchResult) {
return;
}
// 如果还没有抓取内容,先抓取
if (!currentSearchResult.fetched) {
await fetchAndShowResult(window.lastSearchResults.findIndex(r => r.url === currentSearchResult.url));
return;
}
showToast('已保存到内容库', 'success');
closeModal('search-result-modal');
loadArticles();
loadStats();
}
+40
View File
@@ -116,6 +116,26 @@
</div> </div>
</div> </div>
<!-- 互联网搜索区域 -->
<div class="panel full-width">
<div class="panel-header">
<h2><i class="ri-search-line"></i> 互联网搜索</h2>
<div class="panel-actions">
<input type="text" id="internet-search-keyword" placeholder="输入关键词搜索..." class="search-input" style="width: 300px;">
<input type="number" id="internet-search-count" value="10" min="1" max="20" style="width: 80px;" title="结果数量">
<button onclick="doInternetSearch()" class="btn btn-primary" id="search-btn">
<i class="ri-search-line"></i> 搜索
</button>
</div>
</div>
<div class="panel-body">
<div id="internet-search-status" style="margin-bottom: 10px; color: #666; font-size: 14px;"></div>
<div class="search-results-grid" id="internet-search-results">
<div class="empty-text">输入关键词进行互联网搜索</div>
</div>
</div>
</div>
<!-- 处理历史 --> <!-- 处理历史 -->
<div class="panel full-width"> <div class="panel full-width">
<div class="panel-header"> <div class="panel-header">
@@ -267,6 +287,26 @@
</div> </div>
</div> </div>
<!-- 搜索结果详情模态框 -->
<div id="search-result-modal" class="modal">
<div class="modal-content large">
<div class="modal-header">
<h3><i class="ri-external-link-line"></i> 搜索结果详情</h3>
<button onclick="closeModal('search-result-modal')" class="close-btn">
<i class="ri-close-line"></i>
</button>
</div>
<div class="modal-body" id="search-result-body">
</div>
<div class="modal-footer">
<button onclick="closeModal('search-result-modal')" class="btn btn-secondary">关闭</button>
<button onclick="saveSearchResultToLibrary()" class="btn btn-success">
<i class="ri-save-line"></i> 保存到内容库
</button>
</div>
</div>
</div>
<!-- 文章详情模态框 --> <!-- 文章详情模态框 -->
<div id="article-detail-modal" class="modal"> <div id="article-detail-modal" class="modal">
<div class="modal-content large"> <div class="modal-content large">