Compare commits
6 Commits
vue
...
agent_new3
| Author | SHA1 | Date | |
|---|---|---|---|
| 052f3a137b | |||
| 0d74cfe754 | |||
| 89d0abd44c | |||
| f8374f5e0d | |||
| f8dfad26a4 | |||
| 2a5fd99319 |
@@ -6,7 +6,7 @@
|
||||
},
|
||||
"scheduler": {
|
||||
"enabled": true,
|
||||
"cronTime": "0 9 * * *",
|
||||
"cronTime": "40 08 * * *",
|
||||
"threshold": 100000,
|
||||
"description": "每天9点采集当日项目",
|
||||
"timeRange": "thisMonth"
|
||||
@@ -16,6 +16,6 @@
|
||||
"smtpPort": 587,
|
||||
"smtpUser": "1076597680@qq.com",
|
||||
"smtpPass": "nfrjdiraqddsjeeh",
|
||||
"recipients": "1650243281@qq.com"
|
||||
"recipients": "5482498@qq.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
13
disable-all-tasks.bat
Normal file
13
disable-all-tasks.bat
Normal file
@@ -0,0 +1,13 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
cd /d "%~dp0"
|
||||
|
||||
echo ========================================
|
||||
echo 批量禁用所有任务
|
||||
echo ========================================
|
||||
echo.
|
||||
|
||||
node disable-all-tasks.js
|
||||
|
||||
echo.
|
||||
pause
|
||||
65
disable-all-tasks.js
Normal file
65
disable-all-tasks.js
Normal file
@@ -0,0 +1,65 @@
|
||||
// 批量禁用所有任务脚本
|
||||
// 用法: node disable-all-tasks.js
|
||||
|
||||
import Database from 'better-sqlite3';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
import { existsSync, mkdirSync } from 'fs';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const DEFAULT_DB_DIR = join(__dirname, 'data');
|
||||
const DEFAULT_DB_PATH = join(DEFAULT_DB_DIR, 'results.db');
|
||||
const LEGACY_DB_PATH = join(DEFAULT_DB_DIR, 'results.sqlite');
|
||||
|
||||
// 确定数据库路径
|
||||
let DB_PATH = process.env.APP_DB_PATH || process.env.RESULTS_DB_PATH;
|
||||
if (!DB_PATH) {
|
||||
if (existsSync(DEFAULT_DB_PATH)) {
|
||||
DB_PATH = DEFAULT_DB_PATH;
|
||||
} else if (existsSync(LEGACY_DB_PATH)) {
|
||||
DB_PATH = LEGACY_DB_PATH;
|
||||
} else {
|
||||
DB_PATH = DEFAULT_DB_PATH;
|
||||
}
|
||||
}
|
||||
|
||||
if (!existsSync(DB_PATH)) {
|
||||
console.error('数据库文件不存在:', DB_PATH);
|
||||
console.log('请确保项目已初始化并运行过至少一次');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
const db = new Database(DB_PATH);
|
||||
|
||||
// 先查询当前启用的任务数量
|
||||
const enabledCount = db.prepare('SELECT COUNT(*) as count FROM tasks WHERE enabled = 1').get();
|
||||
console.log(`当前启用的任务数量: ${enabledCount.count}`);
|
||||
|
||||
// 查询总任务数量
|
||||
const totalCount = db.prepare('SELECT COUNT(*) as count FROM tasks').get();
|
||||
console.log(`总任务数量: ${totalCount.count}`);
|
||||
|
||||
if (enabledCount.count === 0) {
|
||||
console.log('没有需要禁用的任务');
|
||||
db.close();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// 执行批量禁用
|
||||
const result = db.prepare('UPDATE tasks SET enabled = 0').run();
|
||||
console.log(`已禁用 ${result.changes} 个任务`);
|
||||
|
||||
// 验证结果
|
||||
const newEnabledCount = db.prepare('SELECT COUNT(*) as count FROM tasks WHERE enabled = 1').get();
|
||||
console.log(`禁用后启用的任务数量: ${newEnabledCount.count}`);
|
||||
|
||||
db.close();
|
||||
console.log('操作完成');
|
||||
|
||||
} catch (error) {
|
||||
console.error('操作失败:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
1
logs/server.pid
Normal file
1
logs/server.pid
Normal file
@@ -0,0 +1 @@
|
||||
15556
|
||||
@@ -622,6 +622,7 @@
|
||||
<div style="display:flex;gap:10px;align-items:center;">
|
||||
<button class="btn btn-primary btn-sm" onclick="searchProjectResults()">查询</button>
|
||||
<button class="btn btn-secondary btn-sm" onclick="resetProjectFilters()">重置</button>
|
||||
<button class="btn btn-success btn-sm" id="btnExportProjects" onclick="exportProjectResults(this)">导出</button>
|
||||
<!-- <button class="btn btn-danger btn-sm" onclick="clearResults()">清空全部</button> -->
|
||||
</div>
|
||||
</div>
|
||||
@@ -687,14 +688,51 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Cron 表达式</label>
|
||||
<input type="text" id="cfgCronTime" placeholder="0 9 * * *">
|
||||
<label>执行方式</label>
|
||||
<select id="cfgScheduleMode" onchange="handleSchedulerModeChange()">
|
||||
<option value="daily">每天</option>
|
||||
<option value="weekly">每周</option>
|
||||
<option value="monthly">每月</option>
|
||||
<option value="custom">高级 Cron</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>描述</label>
|
||||
<input type="text" id="cfgDescription" placeholder="每天9点采集">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>执行时间</label>
|
||||
<input type="time" id="cfgScheduleTime" value="09:00" onchange="updateSchedulerCronPreview()">
|
||||
</div>
|
||||
<div class="form-group" id="cfgWeekdayGroup" style="display:none;">
|
||||
<label>星期</label>
|
||||
<select id="cfgWeekday" onchange="updateSchedulerCronPreview()">
|
||||
<option value="1">周一</option>
|
||||
<option value="2">周二</option>
|
||||
<option value="3">周三</option>
|
||||
<option value="4">周四</option>
|
||||
<option value="5">周五</option>
|
||||
<option value="6">周六</option>
|
||||
<option value="0">周日</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" id="cfgMonthDayGroup" style="display:none;">
|
||||
<label>每月几号</label>
|
||||
<input type="number" id="cfgMonthDay" min="1" max="31" value="1" onchange="updateSchedulerCronPreview()">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group" id="cfgCustomCronGroup" style="display:none;">
|
||||
<label>高级 Cron 表达式</label>
|
||||
<input type="text" id="cfgCronTime" placeholder="0 9 * * *" oninput="updateSchedulerCronPreview()">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>计划预览</label>
|
||||
<input type="text" id="cfgCronPreview" readonly placeholder="每天 09:00">
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-secondary btn-sm" onclick="triggerScheduledTask()">立即执行定时任务</button>
|
||||
<span id="schedulerStatus" style="margin-left:12px;font-size:13px;color:#888;"></span>
|
||||
</div>
|
||||
@@ -1130,21 +1168,10 @@
|
||||
|
||||
async function loadProjectResults() {
|
||||
try {
|
||||
await loadProjectFilters();
|
||||
const params = new URLSearchParams({ page: currentProjectsPage, pageSize: 10 });
|
||||
const projectFilters = getProjectFilterValues();
|
||||
|
||||
if (projectFilters.minAmount && projectFilters.maxAmount && Number(projectFilters.minAmount) > Number(projectFilters.maxAmount)) {
|
||||
throw new Error('最小金额不能大于最大金额');
|
||||
}
|
||||
|
||||
if (projectFilters.startDate && projectFilters.endDate && projectFilters.startDate > projectFilters.endDate) {
|
||||
throw new Error('开始日期不能晚于结束日期');
|
||||
}
|
||||
|
||||
Object.entries(projectFilters).forEach(([key, value]) => {
|
||||
if (value !== '') params.set(key, value);
|
||||
});
|
||||
validateProjectFilters(projectFilters);
|
||||
await loadProjectFilters();
|
||||
const params = buildProjectQueryParams({ projectFilters, includePagination: true });
|
||||
|
||||
const res = await fetch('/api/projects?' + params);
|
||||
const json = await res.json();
|
||||
@@ -1198,6 +1225,31 @@
|
||||
};
|
||||
}
|
||||
|
||||
function validateProjectFilters(projectFilters) {
|
||||
if (projectFilters.minAmount && projectFilters.maxAmount && Number(projectFilters.minAmount) > Number(projectFilters.maxAmount)) {
|
||||
throw new Error('最小金额不能大于最大金额');
|
||||
}
|
||||
|
||||
if (projectFilters.startDate && projectFilters.endDate && projectFilters.startDate > projectFilters.endDate) {
|
||||
throw new Error('开始日期不能晚于结束日期');
|
||||
}
|
||||
}
|
||||
|
||||
function buildProjectQueryParams({ projectFilters = getProjectFilterValues(), includePagination = true } = {}) {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (includePagination) {
|
||||
params.set('page', currentProjectsPage);
|
||||
params.set('pageSize', 10);
|
||||
}
|
||||
|
||||
Object.entries(projectFilters).forEach(([key, value]) => {
|
||||
if (value !== '') params.set(key, value);
|
||||
});
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
function searchProjectResults() {
|
||||
currentProjectsPage = 1;
|
||||
loadProjectResults();
|
||||
@@ -1220,6 +1272,73 @@
|
||||
loadProjectResults();
|
||||
}
|
||||
|
||||
async function exportProjectResults(buttonEl) {
|
||||
const btn = buttonEl || document.getElementById('btnExportProjects');
|
||||
const originalText = btn ? btn.textContent : '导出';
|
||||
|
||||
try {
|
||||
const projectFilters = getProjectFilterValues();
|
||||
validateProjectFilters(projectFilters);
|
||||
|
||||
if (btn) {
|
||||
btn.disabled = true;
|
||||
btn.textContent = '导出中...';
|
||||
}
|
||||
|
||||
const res = await fetch('/api/projects/export?' + buildProjectQueryParams({
|
||||
projectFilters,
|
||||
includePagination: false,
|
||||
}));
|
||||
|
||||
if (!res.ok) {
|
||||
let message = '导出失败';
|
||||
try {
|
||||
const json = await res.json();
|
||||
message = json.error || message;
|
||||
} catch (_error) {
|
||||
// Ignore JSON parse failure and use the fallback message.
|
||||
}
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
const blob = await res.blob();
|
||||
const downloadUrl = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = downloadUrl;
|
||||
link.download = getDownloadFilename(
|
||||
res.headers.get('content-disposition'),
|
||||
`projects-${new Date().toISOString().slice(0, 19).replace(/[-:]/g, '').replace('T', '-')}.csv`
|
||||
);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
URL.revokeObjectURL(downloadUrl);
|
||||
} catch (e) {
|
||||
alert('导出失败: ' + e.message);
|
||||
} finally {
|
||||
if (btn) {
|
||||
btn.disabled = false;
|
||||
btn.textContent = originalText;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getDownloadFilename(contentDisposition, fallbackName) {
|
||||
if (!contentDisposition) return fallbackName;
|
||||
|
||||
const utf8Match = contentDisposition.match(/filename\*=UTF-8''([^;]+)/i);
|
||||
if (utf8Match && utf8Match[1]) {
|
||||
return decodeURIComponent(utf8Match[1]);
|
||||
}
|
||||
|
||||
const asciiMatch = contentDisposition.match(/filename="([^"]+)"/i) || contentDisposition.match(/filename=([^;]+)/i);
|
||||
if (asciiMatch && asciiMatch[1]) {
|
||||
return asciiMatch[1].trim();
|
||||
}
|
||||
|
||||
return fallbackName;
|
||||
}
|
||||
|
||||
function renderResults({ data, total, page, pageSize, listId, paginationId }) {
|
||||
const container = document.getElementById(listId);
|
||||
if (!data || data.length === 0) {
|
||||
@@ -1278,7 +1397,7 @@
|
||||
tbody.innerHTML = data.map(project => `
|
||||
<tr>
|
||||
<td>${escHtml(project.city || '-')}</td>
|
||||
<td>${escHtml(project.section || project.type || '-')}</td>
|
||||
<td>${escHtml( project.type ||project.section || '-')}</td>
|
||||
<td>${escHtml(project.projectName || '-')}</td>
|
||||
<td>${formatAmount(project.amountYuan)}</td>
|
||||
<td>${escHtml(project.date || '-')}</td>
|
||||
@@ -1351,6 +1470,123 @@
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function padSchedulerNumber(value) {
|
||||
return String(value).padStart(2, '0');
|
||||
}
|
||||
|
||||
function normalizeSchedulerTime(value) {
|
||||
const match = /^(\d{1,2}):(\d{1,2})$/.exec(String(value || '').trim());
|
||||
if (!match) return '09:00';
|
||||
|
||||
const hour = Math.min(23, Math.max(0, parseInt(match[1], 10)));
|
||||
const minute = Math.min(59, Math.max(0, parseInt(match[2], 10)));
|
||||
return `${padSchedulerNumber(hour)}:${padSchedulerNumber(minute)}`;
|
||||
}
|
||||
|
||||
function splitSchedulerTime(value) {
|
||||
const [hour, minute] = normalizeSchedulerTime(value).split(':');
|
||||
return { hour, minute };
|
||||
}
|
||||
|
||||
function describeScheduler(mode, time, weekday, monthDay, cronTime) {
|
||||
const normalizedTime = normalizeSchedulerTime(time);
|
||||
const weekdayMap = {
|
||||
'0': '周日',
|
||||
'1': '周一',
|
||||
'2': '周二',
|
||||
'3': '周三',
|
||||
'4': '周四',
|
||||
'5': '周五',
|
||||
'6': '周六',
|
||||
};
|
||||
|
||||
if (mode === 'daily') return `每天 ${normalizedTime}`;
|
||||
if (mode === 'weekly') return `每周${weekdayMap[String(weekday)] || '周一'} ${normalizedTime}`;
|
||||
if (mode === 'monthly') return `每月 ${monthDay} 号 ${normalizedTime}`;
|
||||
return cronTime || '0 9 * * *';
|
||||
}
|
||||
|
||||
function parseSchedulerCron(cronTime) {
|
||||
const normalizedCron = String(cronTime || '0 9 * * *').trim().replace(/\s+/g, ' ');
|
||||
const parts = normalizedCron.split(' ');
|
||||
|
||||
const fallback = {
|
||||
mode: 'custom',
|
||||
time: '09:00',
|
||||
weekday: '1',
|
||||
monthDay: '1',
|
||||
cronTime: normalizedCron || '0 9 * * *',
|
||||
};
|
||||
|
||||
if (parts.length !== 5) return fallback;
|
||||
|
||||
const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
|
||||
if (!/^\d+$/.test(minute) || !/^\d+$/.test(hour)) return fallback;
|
||||
|
||||
const time = `${padSchedulerNumber(parseInt(hour, 10))}:${padSchedulerNumber(parseInt(minute, 10))}`;
|
||||
|
||||
if (dayOfMonth === '*' && month === '*' && dayOfWeek === '*') {
|
||||
return { mode: 'daily', time, weekday: '1', monthDay: '1', cronTime: normalizedCron };
|
||||
}
|
||||
|
||||
if (dayOfMonth === '*' && month === '*' && /^(0|1|2|3|4|5|6|7)$/.test(dayOfWeek)) {
|
||||
return {
|
||||
mode: 'weekly',
|
||||
time,
|
||||
weekday: dayOfWeek === '7' ? '0' : dayOfWeek,
|
||||
monthDay: '1',
|
||||
cronTime: normalizedCron,
|
||||
};
|
||||
}
|
||||
|
||||
if (/^\d+$/.test(dayOfMonth) && month === '*' && dayOfWeek === '*') {
|
||||
const normalizedMonthDay = Math.min(31, Math.max(1, parseInt(dayOfMonth, 10)));
|
||||
return {
|
||||
mode: 'monthly',
|
||||
time,
|
||||
weekday: '1',
|
||||
monthDay: String(normalizedMonthDay),
|
||||
cronTime: normalizedCron,
|
||||
};
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function buildSchedulerCronFromInputs() {
|
||||
const mode = document.getElementById('cfgScheduleMode').value;
|
||||
const time = normalizeSchedulerTime(document.getElementById('cfgScheduleTime').value);
|
||||
const { hour, minute } = splitSchedulerTime(time);
|
||||
const weekday = document.getElementById('cfgWeekday').value || '1';
|
||||
const monthDayValue = parseInt(document.getElementById('cfgMonthDay').value, 10);
|
||||
const monthDay = Number.isFinite(monthDayValue) ? Math.min(31, Math.max(1, monthDayValue)) : 1;
|
||||
|
||||
if (mode === 'daily') return `${minute} ${hour} * * *`;
|
||||
if (mode === 'weekly') return `${minute} ${hour} * * ${weekday}`;
|
||||
if (mode === 'monthly') return `${minute} ${hour} ${monthDay} * *`;
|
||||
return document.getElementById('cfgCronTime').value.trim() || '0 9 * * *';
|
||||
}
|
||||
|
||||
function updateSchedulerCronPreview() {
|
||||
const cronTime = buildSchedulerCronFromInputs();
|
||||
document.getElementById('cfgCronTime').value = cronTime;
|
||||
document.getElementById('cfgCronPreview').value = describeScheduler(
|
||||
document.getElementById('cfgScheduleMode').value,
|
||||
document.getElementById('cfgScheduleTime').value,
|
||||
document.getElementById('cfgWeekday').value,
|
||||
document.getElementById('cfgMonthDay').value,
|
||||
cronTime
|
||||
);
|
||||
}
|
||||
|
||||
function handleSchedulerModeChange() {
|
||||
const mode = document.getElementById('cfgScheduleMode').value;
|
||||
document.getElementById('cfgWeekdayGroup').style.display = mode === 'weekly' ? '' : 'none';
|
||||
document.getElementById('cfgMonthDayGroup').style.display = mode === 'monthly' ? '' : 'none';
|
||||
document.getElementById('cfgCustomCronGroup').style.display = mode === 'custom' ? '' : 'none';
|
||||
updateSchedulerCronPreview();
|
||||
}
|
||||
|
||||
// ===== 设置 =====
|
||||
async function loadSettings() {
|
||||
try {
|
||||
@@ -1366,8 +1602,14 @@
|
||||
|
||||
// Scheduler
|
||||
document.getElementById('cfgSchedulerEnabled').value = String(cfg.scheduler?.enabled ?? false);
|
||||
document.getElementById('cfgCronTime').value = cfg.scheduler?.cronTime || '0 9 * * *';
|
||||
const schedulerUi = parseSchedulerCron(cfg.scheduler?.cronTime || '0 9 * * *');
|
||||
document.getElementById('cfgScheduleMode').value = schedulerUi.mode;
|
||||
document.getElementById('cfgScheduleTime').value = schedulerUi.time;
|
||||
document.getElementById('cfgWeekday').value = schedulerUi.weekday;
|
||||
document.getElementById('cfgMonthDay').value = schedulerUi.monthDay;
|
||||
document.getElementById('cfgCronTime').value = schedulerUi.cronTime;
|
||||
document.getElementById('cfgDescription').value = cfg.scheduler?.description || '';
|
||||
handleSchedulerModeChange();
|
||||
|
||||
// Email
|
||||
document.getElementById('cfgSmtpHost').value = cfg.email?.smtpHost || '';
|
||||
@@ -1391,6 +1633,14 @@
|
||||
|
||||
async function saveSettings() {
|
||||
try {
|
||||
const cronTime = buildSchedulerCronFromInputs();
|
||||
if (document.getElementById('cfgScheduleMode').value === 'custom') {
|
||||
const cronParts = cronTime.trim().split(/\s+/);
|
||||
if (cronParts.length !== 5) {
|
||||
throw new Error('Cron 表达式格式错误,请填写 5 段');
|
||||
}
|
||||
}
|
||||
|
||||
const cfg = {
|
||||
agent: {
|
||||
baseUrl: document.getElementById('cfgBaseUrl').value.trim(),
|
||||
@@ -1399,7 +1649,7 @@
|
||||
},
|
||||
scheduler: {
|
||||
enabled: document.getElementById('cfgSchedulerEnabled').value === 'true',
|
||||
cronTime: document.getElementById('cfgCronTime').value.trim() || '0 9 * * *',
|
||||
cronTime,
|
||||
description: document.getElementById('cfgDescription').value.trim(),
|
||||
},
|
||||
email: {
|
||||
@@ -1463,6 +1713,7 @@
|
||||
// ===== 初始化 =====
|
||||
initProjectSearchInputs();
|
||||
loadTasks();
|
||||
handleSchedulerModeChange();
|
||||
loadSettings();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
100
restart.ps1
Normal file
100
restart.ps1
Normal file
@@ -0,0 +1,100 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$LogDir = Join-Path $ScriptDir 'logs'
|
||||
$PidFile = Join-Path $LogDir 'server.pid'
|
||||
$LogFile = Join-Path $LogDir 'server.log'
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $LogDir | Out-Null
|
||||
|
||||
function Get-PortFromEnvFile {
|
||||
$envFile = Join-Path $ScriptDir '.env'
|
||||
if (-not (Test-Path $envFile)) {
|
||||
return $null
|
||||
}
|
||||
|
||||
$line = Get-Content $envFile | Where-Object { $_ -match '^PORT=' } | Select-Object -Last 1
|
||||
if (-not $line) {
|
||||
return $null
|
||||
}
|
||||
|
||||
return ($line -replace '^PORT=', '').Trim()
|
||||
}
|
||||
|
||||
function Get-ProjectServerProcesses {
|
||||
Get-CimInstance Win32_Process | Where-Object {
|
||||
$_.Name -eq 'node.exe' -and
|
||||
$_.CommandLine -like '*src/server.js*' -and
|
||||
$_.CommandLine -like "*$ScriptDir*"
|
||||
}
|
||||
}
|
||||
|
||||
function Stop-ExistingServer {
|
||||
$processes = @(Get-ProjectServerProcesses)
|
||||
|
||||
if ($processes.Count -eq 0) {
|
||||
Write-Host "No existing server process found for $ScriptDir"
|
||||
return
|
||||
}
|
||||
|
||||
$ids = $processes | ForEach-Object { $_.ProcessId }
|
||||
Write-Host ("Stopping existing server process(es): " + ($ids -join ', '))
|
||||
|
||||
foreach ($process in $processes) {
|
||||
Stop-Process -Id $process.ProcessId -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds 2
|
||||
}
|
||||
|
||||
function Start-Server {
|
||||
$port = if ($env:PORT) { $env:PORT } else { Get-PortFromEnvFile }
|
||||
if (-not $port) { $port = '5000' }
|
||||
|
||||
Write-Host "Starting server from $ScriptDir on port $port"
|
||||
|
||||
$command = "Set-Location -LiteralPath '$ScriptDir'; node src/server.js *>> '$LogFile'"
|
||||
$process = Start-Process -FilePath 'powershell.exe' `
|
||||
-ArgumentList '-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', $command `
|
||||
-WindowStyle Hidden `
|
||||
-PassThru
|
||||
|
||||
Set-Content -Path $PidFile -Value $process.Id
|
||||
Write-Host "Started PID: $($process.Id)"
|
||||
|
||||
return $port
|
||||
}
|
||||
|
||||
function Show-Status {
|
||||
param(
|
||||
[string]$Port
|
||||
)
|
||||
|
||||
Start-Sleep -Seconds 2
|
||||
|
||||
Write-Host ''
|
||||
Write-Host 'Active project server process(es):'
|
||||
$processes = @(Get-ProjectServerProcesses)
|
||||
if ($processes.Count -eq 0) {
|
||||
Write-Host 'None'
|
||||
} else {
|
||||
$processes | Select-Object ProcessId, Name, CommandLine | Format-Table -AutoSize
|
||||
}
|
||||
|
||||
Write-Host ''
|
||||
Write-Host 'Port check:'
|
||||
Get-NetTCPConnection -LocalPort ([int]$Port) -State Listen -ErrorAction SilentlyContinue |
|
||||
Select-Object LocalAddress, LocalPort, OwningProcess | Format-Table -AutoSize
|
||||
|
||||
Write-Host ''
|
||||
Write-Host 'Recent log output:'
|
||||
if (Test-Path $LogFile) {
|
||||
Get-Content $LogFile -Tail 30
|
||||
} else {
|
||||
Write-Host 'No log file yet.'
|
||||
}
|
||||
}
|
||||
|
||||
Stop-ExistingServer
|
||||
$port = Start-Server
|
||||
Show-Status -Port $port
|
||||
83
restart.sh
Normal file
83
restart.sh
Normal file
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
LOG_DIR="$SCRIPT_DIR/logs"
|
||||
PID_FILE="$LOG_DIR/server.pid"
|
||||
LOG_FILE="$LOG_DIR/server.log"
|
||||
|
||||
mkdir -p "$LOG_DIR"
|
||||
|
||||
PORT="${PORT:-}"
|
||||
if [[ -z "$PORT" && -f "$SCRIPT_DIR/.env" ]]; then
|
||||
PORT="$(grep -E '^PORT=' "$SCRIPT_DIR/.env" | tail -n 1 | cut -d '=' -f 2- | tr -d '\r' || true)"
|
||||
fi
|
||||
PORT="${PORT:-5000}"
|
||||
|
||||
find_project_server_pids() {
|
||||
pgrep -f "node src/server.js" | while read -r pid; do
|
||||
[[ -n "$pid" ]] || continue
|
||||
[[ -d "/proc/$pid" ]] || continue
|
||||
|
||||
local cwd
|
||||
cwd="$(readlink -f "/proc/$pid/cwd" 2>/dev/null || true)"
|
||||
if [[ "$cwd" == "$SCRIPT_DIR" ]]; then
|
||||
echo "$pid"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
stop_existing_server() {
|
||||
local pids
|
||||
pids="$(find_project_server_pids || true)"
|
||||
|
||||
if [[ -z "$pids" ]]; then
|
||||
echo "No existing server process found for $SCRIPT_DIR"
|
||||
return
|
||||
fi
|
||||
|
||||
echo "Stopping existing server process(es): $pids"
|
||||
while read -r pid; do
|
||||
[[ -n "$pid" ]] || continue
|
||||
kill "$pid" 2>/dev/null || true
|
||||
done <<< "$pids"
|
||||
|
||||
sleep 2
|
||||
|
||||
local remaining
|
||||
remaining="$(find_project_server_pids || true)"
|
||||
if [[ -n "$remaining" ]]; then
|
||||
echo "Force killing remaining process(es): $remaining"
|
||||
while read -r pid; do
|
||||
[[ -n "$pid" ]] || continue
|
||||
kill -9 "$pid" 2>/dev/null || true
|
||||
done <<< "$remaining"
|
||||
fi
|
||||
}
|
||||
|
||||
start_server() {
|
||||
cd "$SCRIPT_DIR"
|
||||
echo "Starting server from $SCRIPT_DIR on port $PORT"
|
||||
nohup node src/server.js >> "$LOG_FILE" 2>&1 &
|
||||
local pid=$!
|
||||
echo "$pid" > "$PID_FILE"
|
||||
echo "Started PID: $pid"
|
||||
}
|
||||
|
||||
show_status() {
|
||||
sleep 2
|
||||
echo
|
||||
echo "Active project server process(es):"
|
||||
find_project_server_pids || true
|
||||
echo
|
||||
echo "Port check:"
|
||||
ss -lntp 2>/dev/null | grep ":$PORT" || true
|
||||
echo
|
||||
echo "Recent log output:"
|
||||
tail -n 30 "$LOG_FILE" 2>/dev/null || true
|
||||
}
|
||||
|
||||
stop_existing_server
|
||||
start_server
|
||||
show_status
|
||||
@@ -1,15 +1,23 @@
|
||||
import Database from 'better-sqlite3';
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
||||
import {
|
||||
copyFileSync,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
renameSync,
|
||||
unlinkSync,
|
||||
writeFileSync,
|
||||
} from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const DB_PATH =
|
||||
process.env.APP_DB_PATH ||
|
||||
process.env.RESULTS_DB_PATH ||
|
||||
join(__dirname, '..', 'data', 'results.sqlite');
|
||||
const DEFAULT_DB_DIR = join(__dirname, '..', 'data');
|
||||
const DEFAULT_DB_PATH = join(DEFAULT_DB_DIR, 'results.db');
|
||||
const LEGACY_DB_PATH = join(DEFAULT_DB_DIR, 'results.sqlite');
|
||||
const DB_PATH = resolveDbPath();
|
||||
const CONFIG_PATH = join(__dirname, '..', 'config.json');
|
||||
const MAX_RESULT_RECORDS = 500;
|
||||
const DEFAULT_TASK_MODE = 'qwen3.5-plus';
|
||||
@@ -22,6 +30,55 @@ function clone(value) {
|
||||
return JSON.parse(JSON.stringify(value));
|
||||
}
|
||||
|
||||
function removeFileIfExists(filePath) {
|
||||
if (!existsSync(filePath)) return;
|
||||
unlinkSync(filePath);
|
||||
}
|
||||
|
||||
function migrateLegacyDbIfNeeded(nextPath, legacyPath) {
|
||||
if (existsSync(nextPath) || !existsSync(legacyPath)) return nextPath;
|
||||
|
||||
const legacyDb = new Database(legacyPath);
|
||||
|
||||
try {
|
||||
legacyDb.pragma('wal_checkpoint(TRUNCATE)');
|
||||
} catch (_error) {
|
||||
// Ignore checkpoint failures and still attempt to switch to single-file mode.
|
||||
}
|
||||
|
||||
legacyDb.pragma('journal_mode = DELETE');
|
||||
legacyDb.close();
|
||||
|
||||
try {
|
||||
renameSync(legacyPath, nextPath);
|
||||
} catch (_error) {
|
||||
copyFileSync(legacyPath, nextPath);
|
||||
}
|
||||
|
||||
removeFileIfExists(`${legacyPath}-shm`);
|
||||
removeFileIfExists(`${legacyPath}-wal`);
|
||||
|
||||
return nextPath;
|
||||
}
|
||||
|
||||
function resolveDbPath() {
|
||||
const explicitPath = process.env.APP_DB_PATH || process.env.RESULTS_DB_PATH;
|
||||
if (explicitPath) return explicitPath;
|
||||
|
||||
mkdirSync(DEFAULT_DB_DIR, { recursive: true });
|
||||
|
||||
try {
|
||||
return migrateLegacyDbIfNeeded(DEFAULT_DB_PATH, LEGACY_DB_PATH);
|
||||
} catch (error) {
|
||||
if (existsSync(LEGACY_DB_PATH)) {
|
||||
console.warn(`[resultStore] Legacy database migration skipped: ${error.message}`);
|
||||
return LEGACY_DB_PATH;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function generateResultId() {
|
||||
return `result-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
||||
}
|
||||
@@ -141,7 +198,13 @@ function getDb() {
|
||||
|
||||
mkdirSync(dirname(DB_PATH), { recursive: true });
|
||||
db = new Database(DB_PATH);
|
||||
db.pragma('journal_mode = WAL');
|
||||
|
||||
try {
|
||||
db.pragma('journal_mode = DELETE');
|
||||
} catch (error) {
|
||||
console.warn(`[resultStore] Database journal mode unchanged: ${error.message}`);
|
||||
}
|
||||
|
||||
return db;
|
||||
}
|
||||
|
||||
@@ -690,12 +753,11 @@ export function listProjects({
|
||||
} = {}) {
|
||||
initResultsStore();
|
||||
|
||||
const rows = queryBaseRows({});
|
||||
const projects = buildProjectList(rows, {
|
||||
const projects = getProjects({
|
||||
dedupeByName,
|
||||
city,
|
||||
section,
|
||||
projectNameKeyword: projectName,
|
||||
projectName,
|
||||
minAmount,
|
||||
maxAmount,
|
||||
startDate,
|
||||
@@ -713,10 +775,34 @@ export function listProjects({
|
||||
};
|
||||
}
|
||||
|
||||
export function getProjects({
|
||||
city,
|
||||
section,
|
||||
projectName,
|
||||
minAmount,
|
||||
maxAmount,
|
||||
startDate,
|
||||
endDate,
|
||||
dedupeByName = true,
|
||||
} = {}) {
|
||||
initResultsStore();
|
||||
|
||||
return buildProjectList(queryBaseRows({}), {
|
||||
dedupeByName,
|
||||
city,
|
||||
section,
|
||||
projectNameKeyword: projectName,
|
||||
minAmount,
|
||||
maxAmount,
|
||||
startDate,
|
||||
endDate,
|
||||
});
|
||||
}
|
||||
|
||||
export function getProjectFilters({ dedupeByName = true } = {}) {
|
||||
initResultsStore();
|
||||
|
||||
const projects = buildProjectList(queryBaseRows({}), { dedupeByName });
|
||||
const projects = getProjects({ dedupeByName });
|
||||
const cities = [...new Set(projects.map((project) => project.city).filter(Boolean))];
|
||||
const sections = [...new Set(projects.map((project) => project.section).filter(Boolean))];
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
deleteTaskById,
|
||||
listResults,
|
||||
listProjects,
|
||||
getProjects,
|
||||
deleteResultById,
|
||||
clearResults,
|
||||
getResultFilters,
|
||||
@@ -90,6 +91,58 @@ function mergeConfigWithExistingSecrets(incoming = {}) {
|
||||
return next;
|
||||
}
|
||||
|
||||
function getProjectQueryFilters(query = {}) {
|
||||
return {
|
||||
city: query.city,
|
||||
section: query.section,
|
||||
projectName: query.projectName,
|
||||
minAmount: query.minAmount,
|
||||
maxAmount: query.maxAmount,
|
||||
startDate: query.startDate,
|
||||
endDate: query.endDate,
|
||||
};
|
||||
}
|
||||
|
||||
function formatExportTimestamp(date = new Date()) {
|
||||
const pad = (value) => String(value).padStart(2, '0');
|
||||
return [
|
||||
date.getFullYear(),
|
||||
pad(date.getMonth() + 1),
|
||||
pad(date.getDate()),
|
||||
'-',
|
||||
pad(date.getHours()),
|
||||
pad(date.getMinutes()),
|
||||
pad(date.getSeconds()),
|
||||
].join('');
|
||||
}
|
||||
|
||||
function escapeCsvCell(value) {
|
||||
if (value === null || value === undefined) return '';
|
||||
const stringValue = String(value);
|
||||
if (/[",\r\n]/.test(stringValue)) {
|
||||
return `"${stringValue.replace(/"/g, '""')}"`;
|
||||
}
|
||||
return stringValue;
|
||||
}
|
||||
|
||||
function buildProjectsCsv(projects = []) {
|
||||
const columns = [
|
||||
['城市', (project) => project.city || ''],
|
||||
['板块', (project) => project.section || project.type || ''],
|
||||
['项目名称', (project) => project.projectName || ''],
|
||||
['金额(元)', (project) => Number.isFinite(project.amountYuan) ? project.amountYuan : ''],
|
||||
['发布日期', (project) => project.date || ''],
|
||||
['详情链接', (project) => project.detailLink || ''],
|
||||
];
|
||||
|
||||
const lines = [
|
||||
columns.map(([header]) => escapeCsvCell(header)).join(','),
|
||||
...projects.map((project) => columns.map(([, getter]) => escapeCsvCell(getter(project))).join(',')),
|
||||
];
|
||||
|
||||
return `\uFEFF${lines.join('\r\n')}`;
|
||||
}
|
||||
|
||||
let isRunning = false;
|
||||
let runningStatus = null;
|
||||
|
||||
@@ -227,25 +280,10 @@ app.get('/api/results/filters', (_req, res) => {
|
||||
|
||||
app.get('/api/projects', (req, res) => {
|
||||
try {
|
||||
const {
|
||||
city,
|
||||
section,
|
||||
projectName,
|
||||
minAmount,
|
||||
maxAmount,
|
||||
startDate,
|
||||
endDate,
|
||||
page = 1,
|
||||
pageSize = 20,
|
||||
} = req.query;
|
||||
const { page = 1, pageSize = 20 } = req.query;
|
||||
const filters = getProjectQueryFilters(req.query);
|
||||
const result = listProjects({
|
||||
city,
|
||||
section,
|
||||
projectName,
|
||||
minAmount,
|
||||
maxAmount,
|
||||
startDate,
|
||||
endDate,
|
||||
...filters,
|
||||
page,
|
||||
pageSize,
|
||||
dedupeByName: true,
|
||||
@@ -256,6 +294,23 @@ app.get('/api/projects', (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/projects/export', (req, res) => {
|
||||
try {
|
||||
const projects = getProjects({
|
||||
...getProjectQueryFilters(req.query),
|
||||
dedupeByName: true,
|
||||
});
|
||||
const csv = buildProjectsCsv(projects);
|
||||
const filename = `projects-${formatExportTimestamp()}.csv`;
|
||||
|
||||
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${filename}"; filename*=UTF-8''${encodeURIComponent(filename)}`);
|
||||
res.send(csv);
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/projects/filters', (_req, res) => {
|
||||
try {
|
||||
res.json({ success: true, data: getProjectFilters({ dedupeByName: true }) });
|
||||
|
||||
Reference in New Issue
Block a user