From ad659c4ff0e5f42a1f0cb9b34dd7be6bf4258f75 Mon Sep 17 00:00:00 2001
From: zhaojunlong <5482498@qq.com>
Date: Fri, 6 Mar 2026 15:37:56 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BD=BF=E7=94=A8firecrawl=20=E5=AE=9E?=
=?UTF-8?q?=E7=8E=B0=E5=85=AC=E5=91=8A=E6=8A=93=E5=8F=96=E4=B8=8E=E5=88=86?=
=?UTF-8?q?=E6=9E=90=E5=B7=A5=E5=85=B7=E7=9A=84=E7=BD=91=E9=A1=B5=E7=95=8C?=
=?UTF-8?q?=E9=9D=A2=EF=BC=8C=E5=8C=85=E6=8B=AC=E6=8A=A5=E5=91=8A=E7=94=9F?=
=?UTF-8?q?=E6=88=90=E3=80=81=E5=AF=BC=E5=87=BA=E5=92=8C=E9=82=AE=E4=BB=B6?=
=?UTF-8?q?=E5=8F=91=E9=80=81=E5=8A=9F=E8=83=BD=E3=80=82?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.env.example | 8 +-
config.json | 28 +-
package-lock.json | 64 ++-
package.json | 9 +-
public/app.js | 87 +--
public/index.html | 881 +++++++++++++++++++++++++-----
public/results.html | 1273 +++++++++++++++++++++++++++++++++++++++++++
results.json | 432 +++++++++++++++
src/emailService.js | 163 ++++++
src/scheduler.js | 669 ++++++-----------------
src/server.js | 1066 +++++++++++-------------------------
11 files changed, 3190 insertions(+), 1490 deletions(-)
create mode 100644 public/results.html
create mode 100644 results.json
diff --git a/.env.example b/.env.example
index 55192bd..1251b42 100644
--- a/.env.example
+++ b/.env.example
@@ -1,11 +1,15 @@
# 服务器端口配置
PORT=5000
+# Firecrawl API Key(在 https://www.firecrawl.dev/app/api-keys 获取)
+FIRECRAWL_API_KEY=fc-your-api-key-here
+
# 环境说明:
# - 开发环境:通常使用 5000
# - 生产环境:可以使用 80、8080 等
#
# 使用方法:
# 1. 复制此文件为 .env
-# 2. 修改端口号
-# 3. 启动服务时会自动读取
+# 2. 填写 FIRECRAWL_API_KEY
+# 3. 修改端口号(可选)
+# 4. 启动服务时会自动读取
diff --git a/config.json b/config.json
index 903e816..9661468 100644
--- a/config.json
+++ b/config.json
@@ -5,7 +5,7 @@
"winningThreshold": 0,
"bidThreshold": 0,
"description": "每天9点采集当日项目",
- "timeRange": "thisMonth"
+ "timeRange": "today"
},
"email": {
"smtpHost": "smtp.qq.com",
@@ -13,5 +13,29 @@
"smtpUser": "1076597680@qq.com",
"smtpPass": "nfrjdiraqddsjeeh",
"recipients": "5482498@qq.com"
- }
+ },
+ "scrapers": [
+ {
+ "id": "scraper-1772762354799",
+ "city": "无锡市",
+ "url": "https://ggzyjy.wuxi.gov.cn/wxsggzyjyzxzl/jyxx/slgc/index.shtml",
+ "section": "水利工程",
+ "subsection": "",
+ "type": "招标公告",
+ "prompt": "提取页面上今天的招标公告信息,包括:标题、项目金额(可能为合同预估价/最高投标限价等等)、发布日期(YYYY-MM-DD格式)、详情页完整URL",
+ "enabled": true,
+ "model": "spark-1-mini"
+ },
+ {
+ "id": "scraper-1772762494299",
+ "city": "南京市",
+ "url": "https://njggzy.nanjing.gov.cn/njweb/fjsz/buildService1.html",
+ "section": "房建市政",
+ "subsection": "工程类",
+ "type": "招标公告",
+ "prompt": "提取页面上今日的招标公告信息,包括:标题、项目金额(可能为合同预估价/最高投标限价等等)、发布日期(YYYY-MM-DD格式)、详情页完整URL",
+ "enabled": false,
+ "model": "spark-1-mini"
+ }
+ ]
}
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 2f6840a..a019c4d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,14 +8,38 @@
"name": "njggzy-scraper",
"version": "2.0.0",
"dependencies": {
- "axios": "^1.6.8",
+ "@mendable/firecrawl-js": "^4.15.2",
"cheerio": "^1.0.0-rc.12",
"cors": "^2.8.5",
"dotenv": "^17.2.3",
"express": "^5.2.1",
- "iconv-lite": "^0.6.3",
"node-cron": "^4.2.1",
- "nodemailer": "^7.0.11"
+ "nodemailer": "^7.0.11",
+ "zod": "^4.3.6"
+ }
+ },
+ "node_modules/@mendable/firecrawl-js": {
+ "version": "4.15.2",
+ "resolved": "https://registry.npmmirror.com/@mendable/firecrawl-js/-/firecrawl-js-4.15.2.tgz",
+ "integrity": "sha512-J+lfnJpd00irDhy5ZJE58lsdqbc1fC1d7X6/UyF4VFASEGy1GDpR0FuVweasEpFfOhEGS5DZ+dq8Ui21zIFrOw==",
+ "license": "MIT",
+ "dependencies": {
+ "axios": "^1.13.5",
+ "typescript-event-target": "^1.1.1",
+ "zod": "^3.23.8",
+ "zod-to-json-schema": "^3.23.0"
+ },
+ "engines": {
+ "node": ">=22.0.0"
+ }
+ },
+ "node_modules/@mendable/firecrawl-js/node_modules/zod": {
+ "version": "3.25.76",
+ "resolved": "https://registry.npmmirror.com/zod/-/zod-3.25.76.tgz",
+ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/accepts": {
@@ -63,13 +87,13 @@
"license": "MIT"
},
"node_modules/axios": {
- "version": "1.13.2",
- "resolved": "https://registry.npmmirror.com/axios/-/axios-1.13.2.tgz",
- "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
+ "version": "1.13.6",
+ "resolved": "https://registry.npmmirror.com/axios/-/axios-1.13.6.tgz",
+ "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
"license": "MIT",
"dependencies": {
- "follow-redirects": "^1.15.6",
- "form-data": "^4.0.4",
+ "follow-redirects": "^1.15.11",
+ "form-data": "^4.0.5",
"proxy-from-env": "^1.1.0"
}
},
@@ -1331,6 +1355,12 @@
"url": "https://opencollective.com/express"
}
},
+ "node_modules/typescript-event-target": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmmirror.com/typescript-event-target/-/typescript-event-target-1.1.2.tgz",
+ "integrity": "sha512-TvkrTUpv7gCPlcnSoEwUVUBwsdheKm+HF5u2tPAKubkIGMfovdSizCTaZRY/NhR8+Ijy8iZZUapbVQAsNrkFrw==",
+ "license": "MIT"
+ },
"node_modules/undici": {
"version": "7.16.0",
"resolved": "https://registry.npmmirror.com/undici/-/undici-7.16.0.tgz",
@@ -1384,6 +1414,24 @@
"resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
+ },
+ "node_modules/zod": {
+ "version": "4.3.6",
+ "resolved": "https://registry.npmmirror.com/zod/-/zod-4.3.6.tgz",
+ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "node_modules/zod-to-json-schema": {
+ "version": "3.25.1",
+ "resolved": "https://registry.npmmirror.com/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz",
+ "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==",
+ "license": "ISC",
+ "peerDependencies": {
+ "zod": "^3.25 || ^4"
+ }
}
}
}
diff --git a/package.json b/package.json
index 1a7ef37..c218076 100644
--- a/package.json
+++ b/package.json
@@ -8,13 +8,12 @@
"start": "node src/server.js"
},
"dependencies": {
- "axios": "^1.6.8",
- "cheerio": "^1.0.0-rc.12",
+ "@mendable/firecrawl-js": "latest",
"cors": "^2.8.5",
"dotenv": "^17.2.3",
"express": "^5.2.1",
- "iconv-lite": "^0.6.3",
"node-cron": "^4.2.1",
- "nodemailer": "^7.0.11"
+ "nodemailer": "^7.0.11",
+ "zod": "^3.24.2"
}
-}
+}
\ No newline at end of file
diff --git a/public/app.js b/public/app.js
index 7e63407..dc5d909 100644
--- a/public/app.js
+++ b/public/app.js
@@ -687,51 +687,34 @@ function cronToFriendlyText(cronTime) {
// 加载定时任务配置
async function loadSchedulerConfig() {
try {
- // 从服务器获取配置
const response = await fetch(`${API_BASE}/config`);
const data = await response.json();
if (data.success && data.data) {
const config = data.data;
- // 填充表单
if (config.scheduler) {
document.getElementById('schedulerEnabled').checked = config.scheduler.enabled || false;
const cronTime = config.scheduler.cronTime || '0 9 * * *';
document.getElementById('schedulerCronInput').value = cronTime;
- document.getElementById('schedulerWinningThresholdInput').value = config.scheduler.winningThreshold !== undefined ? config.scheduler.winningThreshold : 10000;
- document.getElementById('schedulerBidThresholdInput').value = config.scheduler.bidThreshold !== undefined ? config.scheduler.bidThreshold : 0;
+ document.getElementById('schedulerThresholdInput').value = config.scheduler.threshold ?? 0;
document.getElementById('schedulerDescription').value = config.scheduler.description || '';
- // 时间段配置
- document.getElementById('schedulerTimeRange').value = config.scheduler.timeRange || 'thisMonth';
-
// 反向映射Cron表达式到预设选择器
const presetSelector = document.getElementById('schedulerCronPreset');
const customGroup = document.getElementById('customCronGroup');
- // 预设值列表
const presets = [
- '0 9 * * *',
- '0 6 * * *',
- '0 12 * * *',
- '0 18 * * *',
- '0 9,18 * * *',
- '0 */6 * * *',
- '0 */12 * * *',
- '0 9 * * 1',
- '0 9 1 * *'
+ '0 9 * * *', '0 6 * * *', '0 12 * * *', '0 18 * * *',
+ '0 9,18 * * *', '0 */6 * * *', '0 */12 * * *', '0 9 * * 1', '0 9 1 * *'
];
- // 检查是否匹配预设值
if (presets.includes(cronTime)) {
presetSelector.value = cronTime;
customGroup.style.display = 'none';
} else {
- // 自定义时间 - 尝试解析为 "分 时 * * *" 格式
presetSelector.value = 'custom';
customGroup.style.display = 'block';
-
const cronParts = cronTime.split(/\s+/);
if (cronParts.length >= 2) {
document.getElementById('customMinute').value = cronParts[0];
@@ -740,7 +723,6 @@ async function loadSchedulerConfig() {
}
}
- // 更新状态显示
await updateSchedulerStatus();
}
} catch (error) {
@@ -776,7 +758,7 @@ function updateCustomCron() {
cronInput.value = `${minute} ${hour} * * *`;
}
-document.addEventListener('DOMContentLoaded', function() {
+document.addEventListener('DOMContentLoaded', function () {
// 并行加载配置,提高加载速度
Promise.all([
loadEmailConfig().catch(err => console.error('加载邮件配置失败:', err)),
@@ -813,20 +795,12 @@ async function updateSchedulerStatus() {
// 更新执行计划
if (status.config) {
document.getElementById('schedulerCronTime').textContent = cronToFriendlyText(status.config.cronTime);
- const winningThreshold = status.config.winningThreshold;
- if (winningThreshold === 0) {
- document.getElementById('schedulerWinningThreshold').textContent = '不筛选';
- } else {
- const winningBillion = (winningThreshold / 10000).toFixed(1);
- document.getElementById('schedulerWinningThreshold').textContent = `${winningThreshold}万元 (${winningBillion}亿)`;
- }
- const bidThreshold = status.config.bidThreshold;
- if (bidThreshold === 0) {
- document.getElementById('schedulerBidThreshold').textContent = '不筛选';
- } else {
- const bidBillion = (bidThreshold / 10000).toFixed(1);
- document.getElementById('schedulerBidThreshold').textContent = `${bidThreshold}万元 (${bidBillion}亿)`;
- }
+ }
+
+ // 更新已启用来源数
+ const enabledCountEl = document.getElementById('schedulerEnabledCount');
+ if (enabledCountEl) {
+ enabledCountEl.textContent = `${status.enabledScrapers ?? '-'} 个`;
}
}
} catch (error) {
@@ -839,10 +813,8 @@ async function saveSchedulerConfig() {
const schedulerConfig = {
enabled: document.getElementById('schedulerEnabled').checked,
cronTime: document.getElementById('schedulerCronInput').value,
- winningThreshold: parseInt(document.getElementById('schedulerWinningThresholdInput').value),
- bidThreshold: parseInt(document.getElementById('schedulerBidThresholdInput').value),
+ threshold: parseInt(document.getElementById('schedulerThresholdInput').value) || 0,
description: document.getElementById('schedulerDescription').value,
- timeRange: document.getElementById('schedulerTimeRange').value
};
// 验证Cron表达式格式(简单验证)
@@ -852,36 +824,16 @@ async function saveSchedulerConfig() {
return;
}
- // 从localStorage获取邮件配置
- const emailConfigStr = localStorage.getItem('emailConfig');
- let emailConfig = {};
-
- if (emailConfigStr) {
- try {
- emailConfig = JSON.parse(emailConfigStr);
- } catch (e) {
- console.error('解析邮件配置失败:', e);
- }
- }
-
- // 如果邮件配置为空,提示用户
- if (!emailConfig.smtpHost || !emailConfig.smtpUser) {
- if (confirm('检测到邮件配置未完成,定时任务需要邮件配置才能发送报告。\n\n是否继续保存定时任务配置(不保存邮件配置)?')) {
- // 继续保存,但不包含邮件配置
- } else {
- return;
- }
- }
-
- // 构建完整配置对象
- const fullConfig = {
- scheduler: schedulerConfig,
- email: emailConfig
- };
-
showSchedulerStatus('正在保存配置...', 'info');
try {
+ // 先获取当前服务器配置(保留 email/scrapers 等字段)
+ const getResponse = await fetch(`${API_BASE}/config`);
+ const getData = await getResponse.json();
+ const currentCfg = (getData.success && getData.data) ? getData.data : {};
+
+ const fullConfig = { ...currentCfg, scheduler: schedulerConfig };
+
const response = await fetch(`${API_BASE}/config`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -892,7 +844,6 @@ async function saveSchedulerConfig() {
if (data.success) {
showSchedulerStatus('配置已保存,定时任务已重新加载!', 'success');
- // 刷新状态显示
await updateSchedulerStatus();
} else {
showSchedulerStatus(`保存失败: ${data.error}`, 'error');
@@ -1255,6 +1206,6 @@ async function sendCombinedReportByEmail() {
}
// 页面加载时初始化报告日期
-document.addEventListener('DOMContentLoaded', function() {
+document.addEventListener('DOMContentLoaded', function () {
initReportDates();
});
diff --git a/public/index.html b/public/index.html
index 51953c0..487f1be 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1,5 +1,6 @@
+
@@ -95,7 +96,8 @@
color: #333;
}
- .form-group input, .form-group select {
+ .form-group input,
+ .form-group select {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
@@ -104,7 +106,8 @@
transition: border 0.3s;
}
- .form-group input:focus, .form-group select:focus {
+ .form-group input:focus,
+ .form-group select:focus {
outline: none;
border-color: #667eea;
}
@@ -153,8 +156,13 @@
}
@keyframes spin {
- 0% { transform: rotate(0deg); }
- 100% { transform: rotate(360deg); }
+ 0% {
+ transform: rotate(0deg);
+ }
+
+ 100% {
+ transform: rotate(360deg);
+ }
}
.results {
@@ -323,8 +331,375 @@
font-size: 14px;
margin: 0 10px;
}
+
+ /* ===== 抓取来源配置页样式 ===== */
+ .scrapers-toolbar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 20px;
+ flex-wrap: wrap;
+ gap: 12px;
+ }
+
+ .scrapers-toolbar h2 {
+ margin: 0;
+ color: #667eea;
+ font-size: 20px;
+ }
+
+ .btn-add {
+ background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
+ color: white;
+ border: none;
+ padding: 10px 22px;
+ border-radius: 8px;
+ font-size: 15px;
+ cursor: pointer;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ transition: all 0.2s;
+ box-shadow: 0 2px 8px rgba(17, 153, 142, 0.3);
+ }
+
+ .btn-add:hover {
+ transform: translateY(-1px);
+ box-shadow: 0 4px 14px rgba(17, 153, 142, 0.4);
+ }
+
+ .scrapers-table-wrap {
+ overflow-x: auto;
+ border-radius: 12px;
+ border: 1px solid #e8eaf0;
+ box-shadow: 0 2px 12px rgba(102, 126, 234, 0.06);
+ }
+
+ .scrapers-table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 14px;
+ min-width: 800px;
+ }
+
+ .scrapers-table thead tr {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+ }
+
+ .scrapers-table th {
+ padding: 14px 14px;
+ text-align: left;
+ font-weight: 600;
+ white-space: nowrap;
+ }
+
+ .scrapers-table tbody tr {
+ border-bottom: 1px solid #f0f0f5;
+ transition: background 0.15s;
+ }
+
+ .scrapers-table tbody tr:last-child {
+ border-bottom: none;
+ }
+
+ .scrapers-table tbody tr:hover {
+ background: #f5f7ff;
+ }
+
+ .scrapers-table td {
+ padding: 12px 14px;
+ vertical-align: top;
+ color: #333;
+ }
+
+ .scrapers-table td.prompt-cell {
+ max-width: 220px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ color: #666;
+ font-size: 13px;
+ }
+
+ .tag {
+ display: inline-block;
+ padding: 2px 10px;
+ border-radius: 20px;
+ font-size: 12px;
+ font-weight: 600;
+ white-space: nowrap;
+ }
+
+ .tag-type {
+ background: #e8f4fd;
+ color: #1a73c8;
+ }
+
+ .tag-enabled {
+ background: #e4f9ee;
+ color: #1a8a4a;
+ }
+
+ .tag-disabled {
+ background: #feeaea;
+ color: #c0392b;
+ }
+
+ .url-cell a {
+ color: #667eea;
+ text-decoration: none;
+ font-size: 12px;
+ word-break: break-all;
+ }
+
+ .url-cell a:hover {
+ text-decoration: underline;
+ }
+
+ .action-btns {
+ display: flex;
+ gap: 6px;
+ flex-wrap: wrap;
+ }
+
+ .btn-sm {
+ padding: 5px 12px;
+ border-radius: 6px;
+ border: none;
+ font-size: 12px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.15s;
+ white-space: nowrap;
+ }
+
+ .btn-edit {
+ background: #fff3cd;
+ color: #856404;
+ }
+
+ .btn-edit:hover {
+ background: #ffc107;
+ color: #fff;
+ }
+
+ .btn-delete {
+ background: #fdeaea;
+ color: #c0392b;
+ }
+
+ .btn-delete:hover {
+ background: #e74c3c;
+ color: #fff;
+ }
+
+ .btn-run {
+ background: #e8f4fd;
+ color: #1a73c8;
+ }
+
+ .btn-run:hover {
+ background: #667eea;
+ color: #fff;
+ }
+
+ .btn-toggle-on {
+ background: #e4f9ee;
+ color: #1a8a4a;
+ }
+
+ .btn-toggle-on:hover {
+ background: #27ae60;
+ color: #fff;
+ }
+
+ .btn-toggle-off {
+ background: #feeaea;
+ color: #c0392b;
+ }
+
+ .btn-toggle-off:hover {
+ background: #e74c3c;
+ color: #fff;
+ }
+
+ .empty-state {
+ text-align: center;
+ padding: 60px 20px;
+ color: #aaa;
+ }
+
+ .empty-state svg {
+ margin-bottom: 12px;
+ opacity: 0.4;
+ }
+
+ /* 弹窗 */
+ .modal-overlay {
+ display: none;
+ position: fixed;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.45);
+ z-index: 1000;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .modal-overlay.show {
+ display: flex;
+ }
+
+ .modal-box {
+ background: white;
+ border-radius: 16px;
+ padding: 32px;
+ width: 600px;
+ max-width: 95vw;
+ max-height: 90vh;
+ overflow-y: auto;
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
+ animation: modalIn 0.2s ease;
+ }
+
+ @keyframes modalIn {
+ from {
+ opacity: 0;
+ transform: scale(0.95) translateY(-10px);
+ }
+
+ to {
+ opacity: 1;
+ transform: scale(1) translateY(0);
+ }
+ }
+
+ .modal-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 24px;
+ }
+
+ .modal-header h3 {
+ margin: 0;
+ color: #333;
+ font-size: 18px;
+ }
+
+ .modal-close {
+ background: none;
+ border: none;
+ font-size: 24px;
+ cursor: pointer;
+ color: #999;
+ line-height: 1;
+ padding: 0;
+ }
+
+ .modal-close:hover {
+ color: #333;
+ }
+
+ .modal-form .form-row {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 14px;
+ }
+
+ .modal-form .form-group {
+ margin-bottom: 16px;
+ }
+
+ .modal-form .form-group label {
+ display: block;
+ font-size: 13px;
+ font-weight: 600;
+ color: #555;
+ margin-bottom: 6px;
+ }
+
+ .modal-form .form-group input,
+ .modal-form .form-group select,
+ .modal-form .form-group textarea {
+ width: 100%;
+ padding: 10px 12px;
+ border: 1.5px solid #e0e0e0;
+ border-radius: 8px;
+ font-size: 14px;
+ font-family: inherit;
+ transition: border 0.2s;
+ box-sizing: border-box;
+ }
+
+ .modal-form .form-group input:focus,
+ .modal-form .form-group select:focus,
+ .modal-form .form-group textarea:focus {
+ outline: none;
+ border-color: #667eea;
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.12);
+ }
+
+ .modal-form .form-group textarea {
+ resize: vertical;
+ min-height: 90px;
+ }
+
+ .modal-footer {
+ display: flex;
+ justify-content: flex-end;
+ gap: 10px;
+ margin-top: 20px;
+ }
+
+ .btn-cancel {
+ background: #f0f0f0;
+ color: #555;
+ border: none;
+ padding: 10px 24px;
+ border-radius: 8px;
+ font-size: 14px;
+ cursor: pointer;
+ font-weight: 600;
+ }
+
+ .btn-cancel:hover {
+ background: #e0e0e0;
+ }
+
+ .btn-save {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+ border: none;
+ padding: 10px 28px;
+ border-radius: 8px;
+ font-size: 14px;
+ cursor: pointer;
+ font-weight: 600;
+ transition: all 0.2s;
+ }
+
+ .btn-save:hover {
+ box-shadow: 0 4px 14px rgba(102, 126, 234, 0.4);
+ }
+
+ .run-result {
+ margin-top: 16px;
+ padding: 14px;
+ background: #f7f8ff;
+ border-radius: 8px;
+ border: 1px solid #e0e5ff;
+ font-size: 13px;
+ max-height: 300px;
+ overflow-y: auto;
+ white-space: pre-wrap;
+ word-break: break-all;
+ color: #333;
+ }
+
-
-
-
-
+
+
+
📊 抓取结果
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
交通水务招标公告
-
浏览招标公告列表
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
生成综合报告
-
同时采集中标公示和招标公告,生成综合报告
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
定时任务配置
配置定时任务自动采集大于指定金额的项目并发送邮件报告
-
-
+
-
+
常用邮箱配置参考
- QQ邮箱: smtp.qq.com, 端口 587 或 465, 需要使用授权码
@@ -599,9 +872,345 @@
+
+
+
+
+
+
通过配置 URL 和提示词,使用 Firecrawl Agent
+ 抓取任意网页数据。结果会自动保存,可在「抓取结果」页查看历史。
+
+
+
+
+
+ | 城市 |
+ 板块 |
+ 子板块 |
+ 类型 |
+ 链接地址 |
+ 提示词 |
+ AI模型 |
+ 状态 |
+ 操作 |
+
+
+
+
+ |
+
+ 暂无配置,点击「新增来源」添加抓取任务
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+