- 核心改造:不再使用 openclaw 智能体执行处理步骤,改为直接调用大模型接口 - 新增 services/llm_client.py:OpenAI 兼容接口客户端,支持多模型配置管理 - 步骤4/5 由大模型直接完成(提取产品数据、填充字段) - 步骤6 改为直接调用 ParamHub API 提交审核 - 新增 llm_configs 数据库表,默认配置 unsloth/Qwen3.6-27B-Q4_K_M (262144上下文) - 新增 /api/llm 配置管理 API:增删改查、切换激活、测试连接 - 前端首页新增「大模型配置」面板,可随时新增/切换模型 - 处理步骤名称更新为「大模型」版
1189 lines
42 KiB
JavaScript
1189 lines
42 KiB
JavaScript
// API基础地址
|
|
const API_BASE = '';
|
|
|
|
// 当前选中的文章ID
|
|
let currentArticleId = null;
|
|
|
|
// 页面加载时初始化
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
refreshData();
|
|
loadConfig();
|
|
loadLlmConfigs();
|
|
|
|
// 搜索框事件
|
|
document.getElementById('article-search').addEventListener('input', (e) => {
|
|
searchArticles(e.target.value);
|
|
});
|
|
|
|
// 自动刷新(每30秒)
|
|
setInterval(refreshData, 30000);
|
|
});
|
|
|
|
// 刷新所有数据
|
|
async function refreshData() {
|
|
try {
|
|
await Promise.all([
|
|
loadStats(),
|
|
loadPendingProducts(),
|
|
loadHistory(),
|
|
loadQuickStats()
|
|
]);
|
|
updateSystemStatus(true);
|
|
} catch (error) {
|
|
console.error('刷新数据失败:', error);
|
|
updateSystemStatus(false);
|
|
}
|
|
}
|
|
|
|
// 更新系统状态显示
|
|
function updateSystemStatus(isHealthy) {
|
|
const statusDot = document.getElementById('system-status');
|
|
const statusText = document.getElementById('status-text');
|
|
|
|
if (isHealthy) {
|
|
statusDot.classList.remove('error');
|
|
statusText.textContent = '系统正常';
|
|
} else {
|
|
statusDot.classList.add('error');
|
|
statusText.textContent = '系统异常';
|
|
}
|
|
}
|
|
|
|
// 加载统计数据
|
|
async function loadStats() {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/api/system/stats`);
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
document.getElementById('articles-count').textContent = data.stats.total_articles;
|
|
document.getElementById('pending-count').textContent = data.stats.pending_products;
|
|
document.getElementById('processing-count').textContent = data.stats.processing_products;
|
|
document.getElementById('success-count').textContent = data.stats.recent_success;
|
|
document.getElementById('failed-count').textContent = data.stats.recent_failed;
|
|
}
|
|
} catch (error) {
|
|
console.error('加载统计失败:', error);
|
|
}
|
|
}
|
|
|
|
// 加载快速统计信息
|
|
async function loadQuickStats() {
|
|
try {
|
|
// 加载文章统计
|
|
const articlesResponse = await fetch(`${API_BASE}/api/articles?limit=1000`);
|
|
const articlesData = await articlesResponse.json();
|
|
|
|
if (articlesData.success) {
|
|
document.getElementById('quick-articles-count').textContent = articlesData.count;
|
|
|
|
// 计算分类数
|
|
const categories = new Set(articlesData.articles.map(a => a.category).filter(c => c));
|
|
document.getElementById('quick-categories-count').textContent = categories.size;
|
|
|
|
// 计算今日新增
|
|
const today = new Date().toISOString().split('T')[0];
|
|
const todayCount = articlesData.articles.filter(a => a.fetch_date && a.fetch_date.startsWith(today)).length;
|
|
document.getElementById('quick-today-count').textContent = todayCount;
|
|
}
|
|
|
|
// 加载失败URL数
|
|
const failedResponse = await fetch(`${API_BASE}/api/articles/failed-urls`);
|
|
const failedData = await failedResponse.json();
|
|
|
|
if (failedData.success) {
|
|
document.getElementById('quick-failed-count').textContent = failedData.count;
|
|
}
|
|
} catch (error) {
|
|
console.error('加载快速统计失败:', error);
|
|
}
|
|
}
|
|
|
|
// 加载待处理产品
|
|
async function loadPendingProducts() {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/api/products/pending`);
|
|
const data = await response.json();
|
|
|
|
const tbody = document.getElementById('pending-table');
|
|
|
|
if (data.success && data.products.length > 0) {
|
|
tbody.innerHTML = data.products.map(product => `
|
|
<tr>
|
|
<td><strong>${escapeHtml(product.product_name)}</strong></td>
|
|
<td>${escapeHtml(product.category || '-')}</td>
|
|
<td>${escapeHtml(product.subcategory || '-')}</td>
|
|
<td><span class="${getPriorityClass(product.priority)}">${product.priority}</span></td>
|
|
<td>${escapeHtml(product.source)}</td>
|
|
<td>${formatDate(product.created_at)}</td>
|
|
<td>
|
|
<button onclick="processProduct('${escapeHtml(product.product_name)}', '${escapeHtml(product.category || '')}', '${escapeHtml(product.subcategory || '')}')"
|
|
class="btn btn-success btn-sm">
|
|
<i class="ri-play-line"></i> 处理
|
|
</button>
|
|
<button onclick="removeProduct('${escapeHtml(product.product_name)}')"
|
|
class="btn btn-danger btn-sm">
|
|
<i class="ri-delete-bin-line"></i>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
`).join('');
|
|
} else {
|
|
tbody.innerHTML = '<tr><td colspan="7" class="empty-text">暂无待处理产品</td></tr>';
|
|
}
|
|
} catch (error) {
|
|
console.error('加载待处理产品失败:', error);
|
|
}
|
|
}
|
|
|
|
// 加载文章列表
|
|
async function loadArticles() {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/api/articles?limit=50`);
|
|
const data = await response.json();
|
|
|
|
const container = document.getElementById('articles-list');
|
|
|
|
if (data.success && data.articles.length > 0) {
|
|
container.innerHTML = data.articles.map(article => `
|
|
<div class="article-item" onclick="showArticleDetail(${article.id})">
|
|
<div class="article-title">
|
|
<i class="ri-file-text-line"></i>
|
|
${escapeHtml(article.product_names.join(', '))}
|
|
</div>
|
|
<div class="article-meta">
|
|
<span><i class="ri-folder-line"></i> ${escapeHtml(article.category || '未分类')}</span>
|
|
<span><i class="ri-calendar-line"></i> ${formatDate(article.fetch_date)}</span>
|
|
<span><i class="ri-link"></i> ${escapeHtml(article.source)}</span>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
} else {
|
|
container.innerHTML = '<div class="empty-text">暂无文章</div>';
|
|
}
|
|
} catch (error) {
|
|
console.error('加载文章失败:', error);
|
|
}
|
|
}
|
|
|
|
// 搜索文章
|
|
async function searchArticles(keyword) {
|
|
if (!keyword.trim()) {
|
|
loadArticles();
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/api/articles/search?q=${encodeURIComponent(keyword)}`);
|
|
const data = await response.json();
|
|
|
|
const container = document.getElementById('articles-list');
|
|
|
|
if (data.success && data.articles.length > 0) {
|
|
container.innerHTML = data.articles.map(article => `
|
|
<div class="article-item" onclick="showArticleDetail(${article.id})">
|
|
<div class="article-title">
|
|
<i class="ri-file-text-line"></i>
|
|
${escapeHtml(article.product_names.join(', '))}
|
|
</div>
|
|
<div class="article-meta">
|
|
<span><i class="ri-folder-line"></i> ${escapeHtml(article.category || '未分类')}</span>
|
|
<span><i class="ri-calendar-line"></i> ${formatDate(article.fetch_date)}</span>
|
|
<span><i class="ri-link"></i> ${escapeHtml(article.source)}</span>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
} else {
|
|
container.innerHTML = '<div class="empty-text">未找到相关文章</div>';
|
|
}
|
|
} catch (error) {
|
|
console.error('搜索文章失败:', error);
|
|
}
|
|
}
|
|
|
|
// 加载处理历史
|
|
async function loadHistory() {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/api/products/history?limit=20`);
|
|
const data = await response.json();
|
|
|
|
const tbody = document.getElementById('history-table');
|
|
|
|
if (data.success && data.history.length > 0) {
|
|
tbody.innerHTML = data.history.map(item => `
|
|
<tr>
|
|
<td><strong>${escapeHtml(item.product_name)}</strong></td>
|
|
<td>${escapeHtml(item.category || '-')}</td>
|
|
<td>${escapeHtml(item.subcategory || '-')}</td>
|
|
<td><span class="status-badge-inline ${item.status}">${getStatusText(item.status)}</span></td>
|
|
<td>${escapeHtml(item.review_id || '-')}</td>
|
|
<td>${formatDate(item.submitted_at)}</td>
|
|
<td>
|
|
<button onclick="showProcessDetail(${JSON.stringify(item.details).replace(/"/g, '"')})"
|
|
class="btn btn-icon btn-sm">
|
|
<i class="ri-information-line"></i>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
`).join('');
|
|
} else {
|
|
tbody.innerHTML = '<tr><td colspan="7" class="empty-text">暂无处理历史</td></tr>';
|
|
}
|
|
} catch (error) {
|
|
console.error('加载处理历史失败:', error);
|
|
}
|
|
}
|
|
|
|
// 加载系统配置
|
|
async function loadConfig() {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/api/system/config`);
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
document.getElementById('auto-process-enabled').value = data.config.auto_process_enabled;
|
|
document.getElementById('process-interval').value = data.config.process_interval;
|
|
document.getElementById('batch-size').value = data.config.batch_size;
|
|
}
|
|
} catch (error) {
|
|
console.error('加载配置失败:', error);
|
|
}
|
|
}
|
|
|
|
// 保存配置
|
|
async function saveConfig() {
|
|
const config = {
|
|
auto_process_enabled: document.getElementById('auto-process-enabled').value,
|
|
process_interval: document.getElementById('process-interval').value,
|
|
batch_size: document.getElementById('batch-size').value
|
|
};
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/api/system/config`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(config)
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
showToast('配置已保存', 'success');
|
|
} else {
|
|
showToast('保存失败: ' + data.error, 'error');
|
|
}
|
|
} catch (error) {
|
|
showToast('保存配置失败', 'error');
|
|
}
|
|
}
|
|
|
|
// 显示添加产品模态框
|
|
function showAddProductModal() {
|
|
document.getElementById('add-product-modal').classList.add('active');
|
|
document.getElementById('add-product-form').reset();
|
|
}
|
|
|
|
// 显示添加文章模态框
|
|
function showAddArticleModal() {
|
|
document.getElementById('add-article-modal').classList.add('active');
|
|
document.getElementById('add-article-form').reset();
|
|
}
|
|
|
|
// 关闭模态框
|
|
function closeModal(modalId) {
|
|
document.getElementById(modalId).classList.remove('active');
|
|
}
|
|
|
|
// 添加产品
|
|
async function addProduct() {
|
|
const batchProducts = document.getElementById('batch-products').value.trim();
|
|
|
|
let productsData;
|
|
|
|
if (batchProducts) {
|
|
// 批量添加
|
|
try {
|
|
productsData = JSON.parse(batchProducts);
|
|
} catch (e) {
|
|
showToast('JSON格式错误', 'error');
|
|
return;
|
|
}
|
|
} else {
|
|
// 单个添加
|
|
const productName = document.getElementById('product-name').value.trim();
|
|
if (!productName) {
|
|
showToast('请输入产品名称', 'error');
|
|
return;
|
|
}
|
|
|
|
productsData = {
|
|
product_name: productName,
|
|
category: document.getElementById('product-category').value.trim(),
|
|
subcategory: document.getElementById('product-subcategory').value.trim(),
|
|
priority: parseInt(document.getElementById('product-priority').value),
|
|
source: 'manual'
|
|
};
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/api/products/pending`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(productsData)
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
showToast(`成功添加 ${data.added_count || 1} 个产品`, 'success');
|
|
closeModal('add-product-modal');
|
|
loadPendingProducts();
|
|
loadStats();
|
|
} else {
|
|
showToast('添加失败: ' + data.error, 'error');
|
|
}
|
|
} catch (error) {
|
|
showToast('添加产品失败', 'error');
|
|
}
|
|
}
|
|
|
|
// 添加文章
|
|
async function addArticle() {
|
|
const products = document.getElementById('article-products').value.trim();
|
|
const summary = document.getElementById('article-summary').value.trim();
|
|
const content = document.getElementById('article-content').value.trim();
|
|
const source = document.getElementById('article-source').value.trim();
|
|
|
|
if (!products || !summary || !content || !source) {
|
|
showToast('请填写必填字段', 'error');
|
|
return;
|
|
}
|
|
|
|
const articleData = {
|
|
product_names: products.split(',').map(p => p.trim()),
|
|
category: document.getElementById('article-category').value.trim(),
|
|
keywords: document.getElementById('article-keywords').value.split(',').map(k => k.trim()),
|
|
summary: summary,
|
|
content: content,
|
|
source: source,
|
|
url: document.getElementById('article-url').value.trim()
|
|
};
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/api/articles`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(articleData)
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
showToast('文章添加成功', 'success');
|
|
closeModal('add-article-modal');
|
|
loadArticles();
|
|
loadStats();
|
|
} else {
|
|
showToast('添加失败: ' + data.error, 'error');
|
|
}
|
|
} catch (error) {
|
|
showToast('添加文章失败', 'error');
|
|
}
|
|
}
|
|
|
|
// 处理单个产品
|
|
async function processProduct(productName, category, subcategory) {
|
|
if (!confirm(`确定要处理产品 "${productName}" 吗?`)) {
|
|
return;
|
|
}
|
|
|
|
showToast('正在启动处理...', '');
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/api/process/start`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
product_name: productName,
|
|
category: category || '',
|
|
subcategory: subcategory || ''
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
showToast('处理流程已启动', 'success');
|
|
// 跳转到处理监控页面
|
|
setTimeout(() => {
|
|
window.location.href = '/process';
|
|
}, 1000);
|
|
} else {
|
|
const errorMsg = data.error || data.message || '未知错误';
|
|
showToast('处理失败: ' + errorMsg, 'error');
|
|
}
|
|
|
|
refreshData();
|
|
} catch (error) {
|
|
console.error('处理产品失败:', error);
|
|
showToast('处理产品失败', 'error');
|
|
}
|
|
}
|
|
|
|
// 批量处理
|
|
async function processBatch() {
|
|
if (!confirm('确定要批量处理待处理产品吗?')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/api/products/process/batch`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ limit: 5 })
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
showToast(`已处理 ${data.processed} 个产品`, 'success');
|
|
} else {
|
|
showToast('批量处理失败', 'error');
|
|
}
|
|
|
|
refreshData();
|
|
} catch (error) {
|
|
showToast('批量处理失败', 'error');
|
|
}
|
|
}
|
|
|
|
// 移除待处理产品
|
|
async function removeProduct(productName) {
|
|
if (!confirm(`确定要从待处理列表移除 "${productName}" 吗?`)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/api/products/pending/${encodeURIComponent(productName)}`, {
|
|
method: 'DELETE'
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
showToast('产品已移除', 'success');
|
|
loadPendingProducts();
|
|
loadStats();
|
|
} else {
|
|
showToast('移除失败: ' + data.error, 'error');
|
|
}
|
|
} catch (error) {
|
|
showToast('移除产品失败', 'error');
|
|
}
|
|
}
|
|
|
|
// 显示文章详情
|
|
async function showArticleDetail(articleId) {
|
|
currentArticleId = articleId;
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/api/articles/${articleId}`);
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
const article = data.article;
|
|
const body = document.getElementById('article-detail-body');
|
|
|
|
body.innerHTML = `
|
|
<div class="detail-meta">
|
|
<div class="detail-meta-item">
|
|
<label>产品名称</label>
|
|
<span>${escapeHtml(article.product_names.join(', '))}</span>
|
|
</div>
|
|
<div class="detail-meta-item">
|
|
<label>分类</label>
|
|
<span>${escapeHtml(article.category || '未分类')}</span>
|
|
</div>
|
|
<div class="detail-meta-item">
|
|
<label>关键词</label>
|
|
<span>${escapeHtml(article.keywords.join(', ') || '无')}</span>
|
|
</div>
|
|
<div class="detail-meta-item">
|
|
<label>来源</label>
|
|
<span>${escapeHtml(article.source)}</span>
|
|
</div>
|
|
<div class="detail-meta-item">
|
|
<label>获取日期</label>
|
|
<span>${formatDate(article.fetch_date)}</span>
|
|
</div>
|
|
<div class="detail-meta-item">
|
|
<label>URL</label>
|
|
<span>${escapeHtml(article.url || '无')}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="detail-section">
|
|
<h4><i class="ri-file-text-line"></i> 摘要总结</h4>
|
|
<p>${escapeHtml(article.summary)}</p>
|
|
</div>
|
|
|
|
<div class="detail-section">
|
|
<h4><i class="ri-align-left"></i> 具体内容</h4>
|
|
<p style="white-space: pre-wrap;">${escapeHtml(article.content)}</p>
|
|
</div>
|
|
`;
|
|
|
|
document.getElementById('article-detail-modal').classList.add('active');
|
|
}
|
|
} catch (error) {
|
|
showToast('加载文章详情失败', 'error');
|
|
}
|
|
}
|
|
|
|
// 删除文章
|
|
async function deleteArticle() {
|
|
if (!currentArticleId) {
|
|
return;
|
|
}
|
|
|
|
if (!confirm('确定要删除这篇文章吗?')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/api/articles/${currentArticleId}`, {
|
|
method: 'DELETE'
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
showToast('文章已删除', 'success');
|
|
closeModal('article-detail-modal');
|
|
loadArticles();
|
|
loadStats();
|
|
} else {
|
|
showToast('删除失败: ' + data.error, 'error');
|
|
}
|
|
} catch (error) {
|
|
showToast('删除文章失败', 'error');
|
|
}
|
|
}
|
|
|
|
// 显示处理详情
|
|
function showProcessDetail(details) {
|
|
const body = document.getElementById('process-detail-body');
|
|
|
|
if (details) {
|
|
if (details.error) {
|
|
body.innerHTML = `
|
|
<div class="detail-section">
|
|
<h4><i class="ri-error-warning-line"></i> 错误信息</h4>
|
|
<p style="color: #ef4444;">${escapeHtml(details.error)}</p>
|
|
</div>
|
|
`;
|
|
} else if (details.data) {
|
|
body.innerHTML = `
|
|
<div class="detail-section">
|
|
<h4><i class="ri-check-line"></i> 提交数据</h4>
|
|
<pre style="background: #f8f9fa; padding: 15px; border-radius: 8px; overflow-x: auto;">
|
|
${JSON.stringify(details.data, null, 2)}
|
|
</pre>
|
|
</div>
|
|
`;
|
|
} else {
|
|
body.innerHTML = '<p class="empty-text">无详细信息</p>';
|
|
}
|
|
} else {
|
|
body.innerHTML = '<p class="empty-text">无详细信息</p>';
|
|
}
|
|
|
|
document.getElementById('process-detail-modal').classList.add('active');
|
|
}
|
|
|
|
// 显示提示消息
|
|
function showToast(message, type = '') {
|
|
const toast = document.getElementById('toast');
|
|
toast.textContent = message;
|
|
toast.className = `toast active ${type}`;
|
|
|
|
setTimeout(() => {
|
|
toast.classList.remove('active');
|
|
}, 3000);
|
|
}
|
|
|
|
// 辅助函数:HTML转义
|
|
function escapeHtml(text) {
|
|
if (!text) return '';
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
// 辅助函数:日期格式化
|
|
function formatDate(dateString) {
|
|
if (!dateString) return '-';
|
|
const date = new Date(dateString);
|
|
return date.toLocaleString('zh-CN', {
|
|
year: 'numeric',
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
});
|
|
}
|
|
|
|
// 辅助函数:获取优先级样式类
|
|
function getPriorityClass(priority) {
|
|
if (priority >= 8) return 'priority-high';
|
|
if (priority >= 5) return 'priority-medium';
|
|
return 'priority-low';
|
|
}
|
|
|
|
// 辅助函数:获取状态文本
|
|
function getStatusText(status) {
|
|
const statusMap = {
|
|
'submitted': '已提交',
|
|
'processing': '处理中',
|
|
'failed': '失败',
|
|
'error': '错误',
|
|
'completed': '已完成'
|
|
};
|
|
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();
|
|
}
|
|
// ========== 大模型配置管理 ==========
|
|
|
|
let llmEditId = null; // 当前编辑的配置ID
|
|
|
|
// 加载大模型配置列表
|
|
async function loadLlmConfigs() {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/api/llm/configs`);
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
displayLlmConfigs(data.configs);
|
|
updateActiveModelBadge(data.configs);
|
|
}
|
|
} catch (error) {
|
|
console.error('加载大模型配置失败:', error);
|
|
}
|
|
}
|
|
|
|
// 显示大模型配置列表
|
|
function displayLlmConfigs(configs) {
|
|
const container = document.getElementById('llm-config-table');
|
|
|
|
if (!configs || configs.length === 0) {
|
|
container.innerHTML = '<tr><td colspan="6" class="empty-text">暂无大模型配置</td></tr>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = configs.map(cfg => `
|
|
<tr>
|
|
<td>
|
|
<strong>${escapeHtml(cfg.name)}</strong>
|
|
${cfg.is_active ? ' <span class="badge badge-active">当前</span>' : ''}
|
|
</td>
|
|
<td><code>${escapeHtml(cfg.base_url)}</code></td>
|
|
<td><code>${escapeHtml(cfg.model_name)}</code></td>
|
|
<td>${cfg.max_context ? Number(cfg.max_context).toLocaleString() : '-'}</td>
|
|
<td>
|
|
${cfg.is_active
|
|
? '<span class="status-badge"><span class="status-dot green"></span> 使用中</span>'
|
|
: '<span class="status-badge"><span class="status-dot gray"></span> 未启用</span>'}
|
|
</td>
|
|
<td>
|
|
<div class="action-buttons">
|
|
${!cfg.is_active ? `
|
|
<button onclick="activateLlmConfig(${cfg.id})" class="btn btn-sm btn-success" title="切换为当前使用">
|
|
<i class="ri-switch-line"></i> 启用
|
|
</button>
|
|
` : ''}
|
|
<button onclick="editLlmConfig(${cfg.id})" class="btn btn-sm btn-secondary" title="编辑">
|
|
<i class="ri-edit-line"></i>
|
|
</button>
|
|
${!cfg.is_active ? `
|
|
<button onclick="deleteLlmConfig(${cfg.id})" class="btn btn-sm btn-danger" title="删除">
|
|
<i class="ri-delete-bin-line"></i>
|
|
</button>
|
|
` : ''}
|
|
<button onclick="testLlmConfigById(${cfg.id})" class="btn btn-sm btn-secondary" title="测试连接">
|
|
<i class="ri-connection-line"></i>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`).join('');
|
|
}
|
|
|
|
// 更新顶部激活模型徽章
|
|
function updateActiveModelBadge(configs) {
|
|
const active = (configs || []).find(c => c.is_active);
|
|
const nameEl = document.getElementById('active-model-name');
|
|
if (active) {
|
|
nameEl.textContent = `${active.name} (${active.model_name})`;
|
|
} else {
|
|
nameEl.textContent = '未配置大模型';
|
|
}
|
|
}
|
|
|
|
// 打开新增大模型模态框
|
|
function showAddLlmModal() {
|
|
llmEditId = null;
|
|
document.getElementById('add-llm-modal').classList.add('active');
|
|
|
|
// 清空表单
|
|
document.getElementById('llm-name').value = '';
|
|
document.getElementById('llm-base-url').value = 'http://192.168.2.7:18003/v1';
|
|
document.getElementById('llm-api-key').value = 'sk-xxxx';
|
|
document.getElementById('llm-model-name').value = 'unsloth/Qwen3.6-27B-Q4_K_M';
|
|
document.getElementById('llm-max-context').value = 262144;
|
|
|
|
// 重置按钮文字
|
|
const modalTitle = document.querySelector('#add-llm-modal .modal-header h3');
|
|
modalTitle.innerHTML = '<i class="ri-openai-line"></i> 新增大模型配置';
|
|
const saveBtn = document.querySelector('#add-llm-modal .modal-footer .btn-primary');
|
|
saveBtn.textContent = '添加';
|
|
saveBtn.onclick = addLlmConfig;
|
|
}
|
|
|
|
// 编辑大模型配置
|
|
async function editLlmConfig(configId) {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/api/llm/configs`);
|
|
const data = await response.json();
|
|
|
|
if (!data.success) return;
|
|
|
|
const cfg = data.configs.find(c => c.id === configId);
|
|
if (!cfg) return;
|
|
|
|
llmEditId = configId;
|
|
document.getElementById('llm-name').value = cfg.name || '';
|
|
document.getElementById('llm-base-url').value = cfg.base_url || '';
|
|
document.getElementById('llm-api-key').value = cfg.api_key || '';
|
|
document.getElementById('llm-model-name').value = cfg.model_name || '';
|
|
document.getElementById('llm-max-context').value = cfg.max_context || 262144;
|
|
|
|
const modalTitle = document.querySelector('#add-llm-modal .modal-header h3');
|
|
modalTitle.innerHTML = '<i class="ri-edit-line"></i> 编辑大模型配置';
|
|
const saveBtn = document.querySelector('#add-llm-modal .modal-footer .btn-primary');
|
|
saveBtn.textContent = '保存修改';
|
|
saveBtn.onclick = updateLlmConfig;
|
|
|
|
document.getElementById('add-llm-modal').classList.add('active');
|
|
} catch (error) {
|
|
showToast('加载配置失败', 'error');
|
|
}
|
|
}
|
|
|
|
// 添加大模型配置
|
|
async function addLlmConfig() {
|
|
const name = document.getElementById('llm-name').value.trim();
|
|
const baseUrl = document.getElementById('llm-base-url').value.trim();
|
|
const apiKey = document.getElementById('llm-api-key').value.trim();
|
|
const modelName = document.getElementById('llm-model-name').value.trim();
|
|
const maxContext = parseInt(document.getElementById('llm-max-context').value) || 262144;
|
|
|
|
if (!name || !baseUrl || !modelName) {
|
|
showToast('请填写必填字段(名称、接口地址、模型名称)', 'error');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/api/llm/configs`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ name, base_url: baseUrl, api_key: apiKey, model_name: modelName, max_context: maxContext })
|
|
});
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
showToast('大模型配置已添加', 'success');
|
|
closeModal('add-llm-modal');
|
|
loadLlmConfigs();
|
|
} else {
|
|
showToast('添加失败: ' + data.error, 'error');
|
|
}
|
|
} catch (error) {
|
|
showToast('添加失败', 'error');
|
|
}
|
|
}
|
|
|
|
// 更新大模型配置
|
|
async function updateLlmConfig() {
|
|
if (!llmEditId) return;
|
|
|
|
const name = document.getElementById('llm-name').value.trim();
|
|
const baseUrl = document.getElementById('llm-base-url').value.trim();
|
|
const apiKey = document.getElementById('llm-api-key').value.trim();
|
|
const modelName = document.getElementById('llm-model-name').value.trim();
|
|
const maxContext = parseInt(document.getElementById('llm-max-context').value) || 262144;
|
|
|
|
if (!name || !baseUrl || !modelName) {
|
|
showToast('请填写必填字段(名称、接口地址、模型名称)', 'error');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/api/llm/configs/${llmEditId}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ name, base_url: baseUrl, api_key: apiKey, model_name: modelName, max_context: maxContext })
|
|
});
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
showToast('配置已更新', 'success');
|
|
closeModal('add-llm-modal');
|
|
loadLlmConfigs();
|
|
} else {
|
|
showToast('更新失败: ' + data.error, 'error');
|
|
}
|
|
} catch (error) {
|
|
showToast('更新失败', 'error');
|
|
}
|
|
}
|
|
|
|
// 删除大模型配置
|
|
async function deleteLlmConfig(configId) {
|
|
if (!confirm('确定删除该大模型配置吗?')) return;
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/api/llm/configs/${configId}`, {
|
|
method: 'DELETE'
|
|
});
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
showToast('配置已删除', 'success');
|
|
loadLlmConfigs();
|
|
} else {
|
|
showToast('删除失败: ' + data.error, 'error');
|
|
}
|
|
} catch (error) {
|
|
showToast('删除失败', 'error');
|
|
}
|
|
}
|
|
|
|
// 切换激活的大模型配置
|
|
async function activateLlmConfig(configId) {
|
|
if (!confirm('确定切换使用该大模型吗?后续处理将使用它执行所有智能体任务。')) return;
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/api/llm/configs/${configId}/activate`, {
|
|
method: 'POST'
|
|
});
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
showToast(data.message, 'success');
|
|
loadLlmConfigs();
|
|
} else {
|
|
showToast('切换失败: ' + data.error, 'error');
|
|
}
|
|
} catch (error) {
|
|
showToast('切换失败', 'error');
|
|
}
|
|
}
|
|
|
|
// 测试当前表单中的连接
|
|
async function testLlmConnection() {
|
|
const name = document.getElementById('llm-name').value.trim();
|
|
const baseUrl = document.getElementById('llm-base-url').value.trim();
|
|
const apiKey = document.getElementById('llm-api-key').value.trim();
|
|
const modelName = document.getElementById('llm-model-name').value.trim();
|
|
|
|
if (!baseUrl || !modelName) {
|
|
showToast('请先填写接口地址和模型名称', 'error');
|
|
return;
|
|
}
|
|
|
|
const btn = document.getElementById('llm-test-btn');
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<i class="ri-loader-4-line"></i> 测试中...';
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/api/llm/test`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
config: { base_url: baseUrl, api_key: apiKey, model_name: modelName }
|
|
})
|
|
});
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
showToast('连接成功: ' + data.message, 'success');
|
|
} else {
|
|
showToast('连接失败: ' + data.message, 'error');
|
|
}
|
|
} catch (error) {
|
|
showToast('测试失败', 'error');
|
|
}
|
|
|
|
btn.disabled = false;
|
|
btn.innerHTML = '<i class="ri-connection-line"></i> 测试连接';
|
|
}
|
|
|
|
// 测试指定配置的连接
|
|
async function testLlmConfigById(configId) {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/api/llm/configs`);
|
|
const data = await response.json();
|
|
if (!data.success) return;
|
|
|
|
const cfg = data.configs.find(c => c.id === configId);
|
|
if (!cfg) return;
|
|
|
|
showToast('正在测试连接...', '');
|
|
|
|
const testResp = await fetch(`${API_BASE}/api/llm/test`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
config: { base_url: cfg.base_url, api_key: cfg.api_key, model_name: cfg.model_name }
|
|
})
|
|
});
|
|
const testData = await testResp.json();
|
|
|
|
if (testData.success) {
|
|
showToast('连接成功: ' + testData.message, 'success');
|
|
} else {
|
|
showToast('连接失败: ' + testData.message, 'error');
|
|
}
|
|
} catch (error) {
|
|
showToast('测试失败', 'error');
|
|
}
|
|
}
|