Files
tool-node/public/index.html
zhaojunlong 6fc9748009
All checks were successful
Deploy Vue App / build-and-deploy (push) Successful in 9s
```
feat(llm): 添加AI智能分析配置功能

新增LLM配置模块,支持通过阿里云DashScope API进行招标金额的智能提取。
配置包括API Key、Base URL、模型选择等,并提供启用开关。
前端界面增加“AI配置”标签页,包含状态展示、配置表单及测试连接功能。
后端增强parseDetailEnhanced方法,优先使用LLM提取金额,失败时降级至正则表达式。
同时实现LLM状态查询与连接测试接口,确保配置有效性。
配置文件中新增llm字段,默认关闭,支持安全存储API密钥。
```
2025-12-15 17:49:11 +08:00

740 lines
30 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>南京公共工程建设中心 - 公告采集工具</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
text-align: center;
}
.header h1 {
font-size: 28px;
margin-bottom: 10px;
}
.header p {
opacity: 0.9;
font-size: 14px;
}
.tabs {
display: flex;
background: #f5f5f5;
border-bottom: 2px solid #e0e0e0;
}
.tab {
flex: 1;
padding: 15px;
text-align: center;
cursor: pointer;
background: #f5f5f5;
border: none;
font-size: 16px;
transition: all 0.3s;
}
.tab:hover {
background: #e8e8e8;
}
.tab.active {
background: white;
color: #667eea;
font-weight: bold;
border-bottom: 3px solid #667eea;
}
.content {
padding: 30px;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #333;
}
.form-group input {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
transition: border 0.3s;
}
.form-group input:focus {
outline: none;
border-color: #667eea;
}
.btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 12px 30px;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
margin-right: 10px;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.loading {
display: none;
text-align: center;
padding: 20px;
color: #667eea;
}
.loading.active {
display: block;
}
.spinner {
border: 3px solid #f3f3f3;
border-top: 3px solid #667eea;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.results {
margin-top: 20px;
}
.list-item {
background: #f9f9f9;
padding: 15px;
margin-bottom: 10px;
border-radius: 8px;
border-left: 4px solid #667eea;
}
.list-item h3 {
font-size: 16px;
margin-bottom: 8px;
color: #333;
}
.list-item .meta {
font-size: 14px;
color: #666;
margin-bottom: 5px;
}
.list-item .budget {
display: inline-block;
background: #667eea;
color: white;
padding: 4px 12px;
border-radius: 4px;
font-size: 14px;
font-weight: bold;
margin-top: 8px;
}
.list-item a {
color: #667eea;
text-decoration: none;
font-size: 14px;
}
.list-item a:hover {
text-decoration: underline;
}
.summary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
}
.summary h2 {
margin-bottom: 15px;
font-size: 20px;
}
.summary .stat {
display: inline-block;
margin-right: 30px;
margin-bottom: 10px;
}
.summary .stat-label {
opacity: 0.9;
font-size: 14px;
}
.summary .stat-value {
font-size: 24px;
font-weight: bold;
margin-top: 5px;
}
.export-btn {
background: #28a745;
margin-top: 10px;
}
.export-btn:hover {
box-shadow: 0 5px 15px rgba(40, 167, 69, 0.4);
}
.simple-list {
max-height: 380px;
overflow-y: auto;
}
.error {
background: #f8d7da;
color: #721c24;
padding: 15px;
border-radius: 8px;
margin-top: 10px;
}
.checkbox-wrapper {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
background: #f8f9ff;
border: 2px solid #e0e5ff;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
user-select: none;
}
.checkbox-wrapper:hover {
background: #eef1ff;
border-color: #667eea;
}
.checkbox-wrapper input[type="checkbox"] {
width: 20px;
height: 20px;
cursor: pointer;
accent-color: #667eea;
}
.checkbox-wrapper label {
margin: 0;
cursor: pointer;
font-size: 15px;
color: #333;
font-weight: 500;
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
margin-top: 20px;
padding: 20px 0;
}
.pagination button {
background: white;
border: 2px solid #667eea;
color: #667eea;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
min-width: 40px;
}
.pagination button:hover:not(:disabled) {
background: #667eea;
color: white;
}
.pagination button:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.pagination button.active {
background: #667eea;
color: white;
font-weight: bold;
}
.pagination .page-info {
color: #666;
font-size: 14px;
margin: 0 10px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>南京公共工程建设中心</h1>
<p>公告采集与分析工具</p>
</div>
<div class="tabs">
<button class="tab active" onclick="switchTab('list')">公告列表</button>
<button class="tab" onclick="switchTab('detail')">详情采集</button>
<button class="tab" onclick="switchTab('report')">生成报告</button>
<button class="tab" onclick="switchTab('scheduler')">定时任务</button>
<button class="tab" onclick="switchTab('email')">邮件配置</button>
<button class="tab" onclick="switchTab('llm')">AI配置</button>
</div>
<div class="content">
<!-- 公告列表 -->
<div id="list" class="tab-content active">
<div class="form-group">
<label>列表页URL (可选)</label>
<input type="text" id="listUrl" placeholder="默认: https://gjzx.nanjing.gov.cn/gggs/">
</div>
<div class="form-group">
<label>页码 (第1页为最新公告)</label>
<input type="number" id="listPage" value="1" min="1" max="100">
</div>
<button class="btn" onclick="fetchList()">获取公告列表</button>
<div id="listLoading" class="loading">
<div class="spinner"></div>
<p>正在采集...</p>
</div>
<div id="listResults" class="results"></div>
<div id="listPagination" class="pagination" style="display:none;">
<button onclick="goToListPage(1)" id="listFirstPage">首页</button>
<button onclick="goToListPage(currentListPage - 1)" id="listPrevPage">上一页</button>
<span class="page-info"><span id="listCurrentPage">1</span></span>
<button onclick="goToListPage(currentListPage + 1)" id="listNextPage">下一页</button>
</div>
</div>
<!-- 详情采集 -->
<div id="detail" class="tab-content">
<div class="form-group">
<div class="checkbox-wrapper" onclick="document.getElementById('useDetailDateRange').click();">
<input type="checkbox" id="useDetailDateRange" onchange="toggleDetailDateRange()" onclick="event.stopPropagation();">
<label for="useDetailDateRange">按时间范围采集</label>
</div>
</div>
<div id="detailDateRangeFields" style="display:none;">
<div class="form-group">
<label>开始日期</label>
<input type="date" id="detailStartDate">
</div>
<div class="form-group">
<label>结束日期</label>
<input type="date" id="detailEndDate">
</div>
<div class="form-group">
<label>最大采集页数</label>
<input type="number" id="detailMaxPages" value="1" min="1">
</div>
</div>
<div id="detailNormalFields">
<div class="form-group">
<label>列表页URL (可选)</label>
<input type="text" id="detailUrl" placeholder="默认: https://gjzx.nanjing.gov.cn/gggs/">
</div>
<div class="form-group">
<label>采集数量</label>
<input type="number" id="detailLimit" value="5" min="1" max="50">
</div>
</div>
<button class="btn" onclick="fetchDetails()">开始采集</button>
<div id="detailLoading" class="loading">
<div class="spinner"></div>
<p>正在采集详情...</p>
</div>
<div id="detailResults" class="results"></div>
</div>
<!-- 生成报告 -->
<div id="report" class="tab-content">
<div class="form-group">
<div class="checkbox-wrapper" onclick="document.getElementById('useDateRange').click();">
<input type="checkbox" id="useDateRange" onchange="toggleDateRange()" onclick="event.stopPropagation();">
<label for="useDateRange">按时间范围采集</label>
</div>
</div>
<div id="dateRangeFields" style="display:none;">
<div class="form-group">
<label>开始日期</label>
<input type="date" id="startDate">
</div>
<div class="form-group">
<label>结束日期</label>
<input type="date" id="endDate">
</div>
<div class="form-group">
<label>最大采集页数</label>
<input type="number" id="maxPages" value="1" min="1" >
</div>
</div>
<div id="normalFields">
<div class="form-group">
<label>列表页URL (可选)</label>
<input type="text" id="reportUrl" placeholder="默认: https://gjzx.nanjing.gov.cn/gggs/">
</div>
<div class="form-group">
<label>采集数量</label>
<input type="number" id="reportLimit" value="15" min="1" max="50">
</div>
</div>
<div class="form-group">
<label>金额阈值 (万元)</label>
<input type="number" id="reportThreshold" value="50" min="0" step="0.1">
</div>
<button class="btn" onclick="generateReport()">生成报告</button>
<button class="btn export-btn" onclick="exportReport()" id="exportBtn" style="display:none;">导出Word</button>
<button class="btn" onclick="sendReportByEmail()" id="sendEmailBtn" style="display:none; background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);">发送邮件</button>
<div id="reportLoading" class="loading">
<div class="spinner"></div>
<p>正在生成报告...</p>
</div>
<div id="reportResults" class="results"></div>
</div>
<!-- 定时任务 -->
<div id="scheduler" class="tab-content">
<h2 style="margin-bottom: 20px; color: #667eea;">定时任务配置</h2>
<p style="color: #666; margin-bottom: 20px;">配置定时任务自动采集本月大于指定金额的项目并发送邮件报告</p>
<!-- 任务状态 -->
<div id="schedulerStatus" style="margin-bottom: 30px; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border-radius: 8px;">
<h3 style="margin-top: 0; margin-bottom: 15px;">任务状态</h3>
<div style="display: flex; gap: 30px; flex-wrap: wrap;">
<div>
<div style="opacity: 0.9; font-size: 14px;">运行状态</div>
<div style="font-size: 20px; font-weight: bold; margin-top: 5px;" id="schedulerRunningStatus">加载中...</div>
</div>
<div>
<div style="opacity: 0.9; font-size: 14px;">执行时间</div>
<div style="font-size: 20px; font-weight: bold; margin-top: 5px;" id="schedulerCronTime">-</div>
</div>
<div>
<div style="opacity: 0.9; font-size: 14px;">金额阈值</div>
<div style="font-size: 20px; font-weight: bold; margin-top: 5px;" id="schedulerThreshold">-</div>
</div>
</div>
</div>
<!-- 配置表单 -->
<div class="form-group">
<div class="checkbox-wrapper" onclick="document.getElementById('schedulerEnabled').click();">
<input type="checkbox" id="schedulerEnabled" onclick="event.stopPropagation();">
<label for="schedulerEnabled">启用定时任务</label>
</div>
</div>
<div class="form-group">
<label>执行计划</label>
<select id="schedulerCronPreset" style="width: 100%; padding: 12px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 14px;" onchange="handleCronPresetChange()">
<option value="0 9 * * *">每天上午9点</option>
<option value="0 6 * * *">每天上午6点</option>
<option value="0 12 * * *">每天中午12点</option>
<option value="0 18 * * *">每天下午18点</option>
<option value="0 9,18 * * *">每天9点和18点</option>
<option value="0 */6 * * *">每6小时</option>
<option value="0 */12 * * *">每12小时</option>
<option value="0 9 * * 1">每周一上午9点</option>
<option value="0 9 1 * *">每月1日上午9点</option>
<option value="custom">自定义时间...</option>
</select>
</div>
<!-- 自定义时间配置 -->
<div class="form-group" id="customCronGroup" style="display: none;">
<label>自定义执行时间</label>
<div style="display: flex; gap: 10px; align-items: center;">
<div style="flex: 1;">
<label style="font-size: 12px; color: #666;">小时 (0-23)</label>
<input type="number" id="customHour" min="0" max="23" value="9" style="width: 100%; padding: 12px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 14px;">
</div>
<div style="flex: 1;">
<label style="font-size: 12px; color: #666;">分钟 (0-59)</label>
<input type="number" id="customMinute" min="0" max="59" value="0" style="width: 100%; padding: 12px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 14px;">
</div>
</div>
<small style="color: #666; display: block; margin-top: 5px;">
将在每天指定的时间执行
</small>
</div>
<!-- 隐藏的Cron表达式字段 -->
<input type="hidden" id="schedulerCronInput" value="0 9 * * *">
<div class="form-group">
<label>采集时间段</label>
<select id="schedulerTimeRange" style="width: 100%; padding: 12px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 14px;">
<option value="today">今日</option>
<option value="thisWeek">本周</option>
<option value="thisMonth" selected>本月</option>
</select>
<small style="color: #666; display: block; margin-top: 5px;">
今日:今天 | 本周:本周一至今 | 本月:本月1日至今
</small>
</div>
<div class="form-group">
<label>金额阈值 (万元)</label>
<input type="number" id="schedulerThresholdInput" value="100000" min="0" step="1000">
<small style="color: #666; display: block; margin-top: 5px;">
10亿 = 100000万元 | 5亿 = 50000万元 | 1亿 = 10000万元
</small>
</div>
<div class="form-group">
<label>任务描述 (可选)</label>
<input type="text" id="schedulerDescription" placeholder="例如: 每天9点采集大于10亿的项目">
</div>
<button class="btn" onclick="saveSchedulerConfig()">保存配置</button>
<button class="btn" onclick="testSchedulerNow()" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);">立即测试运行</button>
<button class="btn" onclick="loadSchedulerConfig()" style="background: #6c757d;">刷新状态</button>
<div id="schedulerConfigStatus" style="margin-top: 20px;"></div>
<div style="margin-top: 30px; padding: 20px; background: #fff3cd; border-radius: 8px; border-left: 4px solid #ffc107;">
<h3 style="margin-top: 0; color: #856404;">使用说明</h3>
<ul style="line-height: 1.8; color: #856404;">
<li><strong>定时任务功能:</strong> 自动采集选中时间段的所有公告</li>
<li><strong>筛选条件:</strong> 只保留预算金额大于设定阈值的项目</li>
<li><strong>邮件发送:</strong> 自动将筛选结果生成HTML报告并发送到配置的邮箱</li>
<li><strong>执行时间:</strong> 通过下拉菜单或自定义时间设置定时执行时间</li>
<li><strong>注意事项:</strong> 保存配置后会自动重启定时任务,无需重启服务器</li>
</ul>
</div>
</div>
<!-- 邮件配置 -->
<div id="email" class="tab-content">
<h2 style="margin-bottom: 20px; color: #667eea;">邮件配置</h2>
<p style="color: #666; margin-bottom: 20px;">配置SMTP邮件服务器信息,用于发送报告到指定邮箱</p>
<div class="form-group">
<label>SMTP服务器地址 *</label>
<input type="text" id="smtpHost" placeholder="例如: smtp.qq.com, smtp.163.com, smtp.gmail.com">
</div>
<div class="form-group">
<label>SMTP端口 *</label>
<input type="number" id="smtpPort" value="587" placeholder="通常为 587 (TLS) 或 465 (SSL)">
</div>
<div class="form-group">
<label>发件人邮箱 (SMTP用户名) *</label>
<input type="email" id="smtpUser" placeholder="your-email@example.com">
</div>
<div class="form-group">
<label>SMTP密码/授权码 *</label>
<input type="password" id="smtpPass" placeholder="邮箱密码或授权码">
</div>
<div class="form-group">
<label>收件人邮箱 (多个用逗号分隔) *</label>
<input type="text" id="recipients" placeholder="email1@example.com, email2@example.com">
</div>
<button class="btn" onclick="saveEmailConfig()">保存配置</button>
<button class="btn" onclick="testEmailConfig()" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);">测试连接</button>
<div id="emailConfigStatus" style="margin-top: 20px;"></div>
<div style="margin-top: 30px; padding: 20px; background: #f0f8ff; border-radius: 8px; border-left: 4px solid #667eea;">
<h3 style="margin-top: 0; color: #667eea;">常用邮箱配置参考</h3>
<ul style="line-height: 1.8; color: #666;">
<li><strong>QQ邮箱:</strong> smtp.qq.com, 端口 587 或 465, 需要使用授权码</li>
<li><strong>163邮箱:</strong> smtp.163.com, 端口 465 或 25, 需要使用授权码</li>
<li><strong>Gmail:</strong> smtp.gmail.com, 端口 587 或 465, 需要开启"允许不够安全的应用"</li>
<li><strong>Outlook:</strong> smtp-mail.outlook.com, 端口 587</li>
<li><strong>企业邮箱:</strong> 请咨询您的IT管理员获取SMTP配置</li>
</ul>
<p style="margin: 10px 0 0 0; color: #999; font-size: 13px;">
提示: QQ和163邮箱需要在邮箱设置中开启SMTP服务并生成授权码,授权码不是邮箱密码。
</p>
</div>
</div>
<!-- AI配置 -->
<div id="llm" class="tab-content">
<h2 style="margin-bottom: 20px; color: #667eea;">AI 智能分析配置</h2>
<p style="color: #666; margin-bottom: 20px;">使用大语言模型智能提取招标金额,提高金额识别的准确性</p>
<!-- AI 状态 -->
<div id="llmStatus" style="margin-bottom: 30px; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border-radius: 8px;">
<h3 style="margin-top: 0; margin-bottom: 15px;">AI 服务状态</h3>
<div style="display: flex; gap: 30px; flex-wrap: wrap;">
<div>
<div style="opacity: 0.9; font-size: 14px;">服务状态</div>
<div style="font-size: 20px; font-weight: bold; margin-top: 5px;" id="llmRunningStatus">加载中...</div>
</div>
<div>
<div style="opacity: 0.9; font-size: 14px;">当前模型</div>
<div style="font-size: 20px; font-weight: bold; margin-top: 5px;" id="llmModelName">-</div>
</div>
</div>
</div>
<!-- 配置表单 -->
<div class="form-group">
<div class="checkbox-wrapper" onclick="document.getElementById('llmEnabled').click();">
<input type="checkbox" id="llmEnabled" onclick="event.stopPropagation();">
<label for="llmEnabled">启用 AI 金额提取</label>
</div>
</div>
<div class="form-group">
<label>API Key *</label>
<input type="password" id="llmApiKey" placeholder="请输入阿里云 DashScope API Key">
<small style="color: #666; display: block; margin-top: 5px;">
<a href="https://dashscope.console.aliyun.com/apiKey" target="_blank" style="color: #667eea;">点击这里获取 API Key</a>
</small>
</div>
<div class="form-group">
<label>API 地址</label>
<input type="text" id="llmBaseUrl" value="https://dashscope.aliyuncs.com/compatible-mode/v1" placeholder="https://dashscope.aliyuncs.com/compatible-mode/v1">
</div>
<div class="form-group">
<label>模型选择</label>
<select id="llmModel" style="width: 100%; padding: 12px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 14px;">
<option value="qwen-turbo">通义千问 Turbo (快速、低成本)</option>
<option value="qwen-plus">通义千问 Plus (更准确)</option>
<option value="qwen-max">通义千问 Max (最强)</option>
</select>
</div>
<button class="btn" onclick="saveLLMConfig()">保存配置</button>
<button class="btn" onclick="testLLMConnection()" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);">测试连接</button>
<button class="btn" onclick="loadLLMConfig()" style="background: #6c757d;">刷新状态</button>
<div id="llmConfigStatus" style="margin-top: 20px;"></div>
<div style="margin-top: 30px; padding: 20px; background: #e8f5e9; border-radius: 8px; border-left: 4px solid #4caf50;">
<h3 style="margin-top: 0; color: #2e7d32;">功能说明</h3>
<ul style="line-height: 1.8; color: #2e7d32;">
<li><strong>智能提取:</strong> 使用大语言模型理解公告内容,准确提取预算金额</li>
<li><strong>自动降级:</strong> 当 AI 服务不可用时,自动使用正则表达式提取</li>
<li><strong>支持模型:</strong> 阿里云通义千问系列模型qwen-turbo/plus/max</li>
<li><strong>计费说明:</strong> 按实际调用量计费qwen-turbo 约 0.0008元/千tokens</li>
</ul>
</div>
<div style="margin-top: 20px; padding: 20px; background: #fff3cd; border-radius: 8px; border-left: 4px solid #ffc107;">
<h3 style="margin-top: 0; color: #856404;">模型对比</h3>
<table style="width: 100%; border-collapse: collapse; margin-top: 10px;">
<tr style="border-bottom: 1px solid #ddd;">
<th style="text-align: left; padding: 8px; color: #856404;">模型</th>
<th style="text-align: left; padding: 8px; color: #856404;">速度</th>
<th style="text-align: left; padding: 8px; color: #856404;">准确度</th>
<th style="text-align: left; padding: 8px; color: #856404;">成本</th>
</tr>
<tr style="border-bottom: 1px solid #ddd;">
<td style="padding: 8px;">qwen-turbo</td>
<td style="padding: 8px;">最快</td>
<td style="padding: 8px;">良好</td>
<td style="padding: 8px;">最低</td>
</tr>
<tr style="border-bottom: 1px solid #ddd;">
<td style="padding: 8px;">qwen-plus</td>
<td style="padding: 8px;">较快</td>
<td style="padding: 8px;">优秀</td>
<td style="padding: 8px;">中等</td>
</tr>
<tr>
<td style="padding: 8px;">qwen-max</td>
<td style="padding: 8px;">较慢</td>
<td style="padding: 8px;">最佳</td>
<td style="padding: 8px;">较高</td>
</tr>
</table>
</div>
</div>
</div>
</div>
<script src="https://unpkg.com/docx@7.8.2/build/index.js"></script>
<script src="app.js"></script>
</body>
</html>