Compare commits

..

12 Commits

Author SHA1 Message Date
6abae17fea Implement dynamic database path configuration in database.js, enhancing compatibility with Docker and local development environments. Update db.js to log the database connection path. Remove response data truncation in validator.js for improved readability. 2025-10-31 10:32:44 +08:00
da140d5415 Remove response data truncation logic in ProxyManager for improved readability of displayed content. 2025-10-31 10:28:50 +08:00
4cefbbbcd6 Refactor ProxyScraper to limit proxy attempts to 3 and improve error handling. Added a flag to switch to direct connection if proxy fails. Enhanced logging for proxy testing outcomes. 2025-10-31 10:03:40 +08:00
a4127509af Enhance ProxyManager and ProxyValidator to display detailed response information, including status codes, response data, and headers. Implement response data truncation for better readability. Update validation logic to capture and return response details in case of errors. 2025-10-31 09:56:31 +08:00
6c3b3928f9 Add "实时日志" link to navigation in history, index, monitoring, and proxies pages 2025-10-31 09:44:44 +08:00
54bc260453 Restore directory creation in Dockerfile for data and logs, removing previous comment-out for improved setup consistency. 2025-10-31 09:40:50 +08:00
d48e0f2d9f Merge branch 'master' of https://git.theluyuan.com/theluyuan/dailiip 2025-10-31 09:39:57 +08:00
9bdc1bd9b3 Comment out directory creation and user switch in Dockerfile for potential troubleshooting purposes. 2025-10-31 09:39:53 +08:00
8cb074660a Update DOCKER_README.md to address log file permission issues, providing troubleshooting steps for Linux/Mac and Windows users. Clarify the importance of ensuring the logs directory has the correct permissions for the container's node user. 2025-10-31 09:36:08 +08:00
804f996c69 Merge branch 'master' of https://git.theluyuan.com/theluyuan/dailiip 2025-10-31 09:35:24 +08:00
735d3e0677 proxy 2025-10-31 09:34:05 +08:00
c9ccb3435d Update Dockerfile to create both data and logs directories; enhance logger to handle directory and file creation errors gracefully. Log warnings for permission issues without blocking application startup. 2025-10-31 09:33:05 +08:00
12 changed files with 251 additions and 44 deletions

View File

@@ -159,7 +159,27 @@ mkdir -p data logs
sudo chown -R 1000:1000 data logs sudo chown -R 1000:1000 data logs
``` ```
### 3. 性能优化 ### 3. 日志文件权限问题
如果遇到 `EACCES: permission denied` 错误:
```bash
# Linux/Mac
mkdir -p logs
chmod 777 logs # 或者 chmod 755 logs
# Windows PowerShell (如果使用Docker Desktop)
New-Item -ItemType Directory -Path logs -Force
# 在Docker Desktop设置中确保目录有正确权限
# 或者删除logs目录让容器自动创建
rm -rf logs
# 然后重新启动容器
docker-compose up -d
```
**注意**:容器内使用 `node` 用户UID 1000运行确保挂载的logs目录对该用户有写入权限。
### 4. 性能优化
```bash ```bash
# 限制内存使用 # 限制内存使用
docker run -d --memory=512m proxy-ip-manager docker run -d --memory=512m proxy-ip-manager

View File

@@ -21,22 +21,23 @@ RUN apk add --no-cache \
# 配置npm使用国内镜像源 # 配置npm使用国内镜像源
# RUN npm config set registry https://registry.npmmirror.com # RUN npm config set registry https://registry.npmmirror.com
RUN npm -g i pnpm
# 复制package.json和package-lock.json如果存在 # 复制package.json和package-lock.json如果存在
COPY package*.json ./ COPY package*.json ./
# 安装项目依赖 # 安装项目依赖
RUN pnpm i #RUN npm config set proyx http://192.168.3.135:1084
RUN npm config set https-proxy http://192.168.3.135:1084
RUN npm install
# 复制项目文件 # 复制项目文件
COPY . . COPY . .
# 创建数据目录并设置权限 # 创建数据目录和日志目录并设置权限
RUN mkdir -p /app/data && \ RUN mkdir -p /app/data /app/logs
chown -R node:node /app # chown -R node:node /app
# 切换到非root用户 # 切换到非root用户
USER node # USER node
# 暴露端口 # 暴露端口
EXPOSE 3000 EXPOSE 3000

View File

@@ -1,9 +1,38 @@
const path = require('path'); const path = require('path');
const fs = require('fs');
// 数据库文件路径配置
// 在Docker环境中使用 /app/data本地开发使用项目根目录的data文件夹
let dbPath;
if (process.env.DB_PATH) {
// 如果设置了环境变量,使用环境变量的路径
dbPath = process.env.DB_PATH;
} else if (fs.existsSync('/app/data')) {
// Docker容器内使用/app/data目录
dbPath = '/app/data/proxies.db';
} else {
// 本地开发环境使用项目根目录的data文件夹
const dataDir = path.join(__dirname, '../data');
dbPath = path.join(dataDir, 'proxies.db');
// 确保data目录存在
if (!fs.existsSync(dataDir)) {
try {
fs.mkdirSync(dataDir, { recursive: true });
} catch (error) {
console.warn('无法创建data目录将使用项目根目录:', error.message);
// 如果创建失败,回退到项目根目录
dbPath = path.join(__dirname, '../proxies.db');
}
}
}
console.log(`数据库文件路径: ${dbPath}`);
const dbConfig = { const dbConfig = {
development: { development: {
dialect: 'sqlite', dialect: 'sqlite',
storage: path.join(__dirname, '../proxies.db') storage: dbPath
} }
}; };

View File

@@ -43,6 +43,11 @@
<i class="bi bi-activity"></i> 系统监控 <i class="bi bi-activity"></i> 系统监控
</a> </a>
</li> </li>
<li class="nav-item">
<a class="nav-link" href="logs.html">
<i class="bi bi-journal-text"></i> 实时日志
</a>
</li>
</ul> </ul>
<div class="navbar-nav"> <div class="navbar-nav">

View File

@@ -43,6 +43,11 @@
<i class="bi bi-activity"></i> 系统监控 <i class="bi bi-activity"></i> 系统监控
</a> </a>
</li> </li>
<li class="nav-item">
<a class="nav-link" href="logs.html">
<i class="bi bi-journal-text"></i> 实时日志
</a>
</li>
</ul> </ul>
<div class="navbar-nav"> <div class="navbar-nav">

View File

@@ -333,6 +333,42 @@ class ProxyManager {
document.getElementById('validationStatus').textContent = '验证完成'; document.getElementById('validationStatus').textContent = '验证完成';
// 显示结果 // 显示结果
let responseResultHtml = '';
if (result.data.responseStatus !== null && result.data.responseStatus !== undefined) {
responseResultHtml = `
<hr>
<h6>响应结果:</h6>
<p><strong>状态码:</strong> <span class="badge ${result.data.responseStatus >= 200 && result.data.responseStatus < 300 ? 'bg-success' : 'bg-warning'}">${result.data.responseStatus}</span></p>
`;
if (result.data.responseData) {
// 限制显示长度,避免内容过长
let displayData = result.data.responseData;
// const maxLength = 300;
// if (displayData.length > maxLength) {
// displayData = displayData.substring(0, maxLength) + '... (内容已截断,完整内容请查看日志)';
// }
responseResultHtml += `
<p><strong>响应内容:</strong></p>
<pre class="bg-light p-2 border rounded" style="max-height: 200px; overflow-y: auto; font-size: 12px; white-space: pre-wrap; word-wrap: break-word;">${this.escapeHtml(displayData)}</pre>
`;
}
if (result.data.testUrl) {
responseResultHtml += `
<p class="text-muted small"><strong>测试URL:</strong> ${result.data.testUrl}</p>
`;
}
} else if (result.data.error) {
responseResultHtml = `
<hr>
<h6>响应结果:</h6>
<p class="text-danger"><strong>错误:</strong> ${result.data.error}</p>
`;
}
document.getElementById('validationResults').innerHTML = ` document.getElementById('validationResults').innerHTML = `
<div class="alert ${result.data.isValid ? 'alert-success' : 'alert-danger'}"> <div class="alert ${result.data.isValid ? 'alert-success' : 'alert-danger'}">
<h6>验证结果: ${result.data.isValid ? '成功' : '失败'}</h6> <h6>验证结果: ${result.data.isValid ? '成功' : '失败'}</h6>
@@ -340,6 +376,7 @@ class ProxyManager {
<p><strong>响应时间:</strong> ${result.data.responseTime}ms</p> <p><strong>响应时间:</strong> ${result.data.responseTime}ms</p>
${result.data.error ? `<p><strong>错误信息:</strong> ${result.data.error}</p>` : ''} ${result.data.error ? `<p><strong>错误信息:</strong> ${result.data.error}</p>` : ''}
</div> </div>
${responseResultHtml}
`; `;
// 延迟刷新列表 // 延迟刷新列表
@@ -581,6 +618,13 @@ class ProxyManager {
return icons[type] || 'info-circle-fill'; return icons[type] || 'info-circle-fill';
} }
escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
convertToCSV(data) { convertToCSV(data) {
if (!data || data.length === 0) return ''; if (!data || data.length === 0) return '';

View File

@@ -43,6 +43,11 @@
<i class="bi bi-activity"></i> 系统监控 <i class="bi bi-activity"></i> 系统监控
</a> </a>
</li> </li>
<li class="nav-item">
<a class="nav-link" href="logs.html">
<i class="bi bi-journal-text"></i> 实时日志
</a>
</li>
</ul> </ul>
<div class="navbar-nav"> <div class="navbar-nav">

View File

@@ -43,6 +43,11 @@
<i class="bi bi-activity"></i> 系统监控 <i class="bi bi-activity"></i> 系统监控
</a> </a>
</li> </li>
<li class="nav-item">
<a class="nav-link" href="logs.html">
<i class="bi bi-journal-text"></i> 实时日志
</a>
</li>
</ul> </ul>
<div class="navbar-nav"> <div class="navbar-nav">

View File

@@ -9,15 +9,19 @@ class Database {
connect() { connect() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const dbPath = config.development.storage;
console.log(`正在连接数据库: ${dbPath}`);
this.db = new sqlite3.Database( this.db = new sqlite3.Database(
config.development.storage, dbPath,
sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE, sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE,
(err) => { (err) => {
if (err) { if (err) {
console.error('数据库连接失败:', err.message); console.error('数据库连接失败:', err.message);
console.error('数据库路径:', dbPath);
reject(err); reject(err);
} else { } else {
console.log('已连接到 SQLite 数据库'); console.log(`已连接到 SQLite 数据库: ${dbPath}`);
resolve(); resolve();
} }
} }

View File

@@ -28,10 +28,16 @@ class Logger {
} }
ensureLogsDirectory() { ensureLogsDirectory() {
if (!fs.existsSync(this.logsDir)) { try {
fs.mkdirSync(this.logsDir, { recursive: true }); if (!fs.existsSync(this.logsDir)) {
// 使用原始console避免循环调用 fs.mkdirSync(this.logsDir, { recursive: true });
this.originalConsole.log(`创建日志目录: ${this.logsDir}`); // 使用原始console避免循环调用
this.originalConsole.log(`创建日志目录: ${this.logsDir}`);
}
} catch (error) {
// 如果创建目录失败(比如权限问题),警告但不阻止应用启动
this.originalConsole.warn(`无法创建日志目录 ${this.logsDir}:`, error.message);
this.originalConsole.warn('日志将仅输出到控制台,不会保存到文件');
} }
} }
@@ -48,9 +54,15 @@ class Logger {
this.currentDate = today; this.currentDate = today;
this.currentLogFile = this.getLogFileName(); this.currentLogFile = this.getLogFileName();
// 确保文件存在 // 确保文件存在 - 使用try-catch处理权限问题
if (!fs.existsSync(this.currentLogFile)) { if (!fs.existsSync(this.currentLogFile)) {
fs.writeFileSync(this.currentLogFile, ''); try {
// 尝试创建空文件,如果失败则会在下次写入时自动创建
fs.writeFileSync(this.currentLogFile, '', { flag: 'a' });
} catch (error) {
// 如果创建文件失败(比如权限问题),不抛出错误,会在后续写入时处理
this.originalConsole.warn(`无法创建日志文件 ${this.currentLogFile}:`, error.message);
}
} }
} }
} }
@@ -71,8 +83,15 @@ class Logger {
const timestamp = this.formatTimestamp(); const timestamp = this.formatTimestamp();
const logLine = `[${timestamp}] [${level.toUpperCase()}] ${message}\n`; const logLine = `[${timestamp}] [${level.toUpperCase()}] ${message}\n`;
// 写入文件 // 写入文件 - 使用flag: 'a'确保文件不存在时自动创建
fs.appendFileSync(this.currentLogFile, logLine, 'utf8'); try {
fs.appendFileSync(this.currentLogFile, logLine, { encoding: 'utf8', flag: 'a' });
} catch (error) {
// 如果写入失败比如权限问题输出到原始console
this.originalConsole.error('写入日志文件失败:', error.message);
this.originalConsole[level](message);
return; // 不继续处理
}
// 添加到缓冲区用于实时推送 // 添加到缓冲区用于实时推送
const logEntry = { const logEntry = {
@@ -102,9 +121,12 @@ class Logger {
} }
} catch (error) { } catch (error) {
// 如果文件写入失败至少输出到原始console // 如果文件写入失败(已经在上面处理了),这里只是防止其他错误
this.originalConsole.error('写入日志文件失败:', error); // 如果上面的appendFileSync已经捕获了错误这里不会执行
this.originalConsole[level](message); // 这里主要是处理其他可能的错误
if (error.code !== 'EACCES' && !error.message.includes('permission')) {
this.originalConsole.error('日志处理失败:', error);
}
} }
} }

View File

@@ -91,19 +91,25 @@ class ProxyScraper {
return null; // 无本地代理,使用直连 return null; // 无本地代理,使用直连
} }
// 尝试几个代理,找到可用的 // 尝试几个代理,找到可用的最多尝试3个避免耗时过长
for (let i = 0; i < Math.min(5, this.localProxies.length); i++) { const maxAttempts = Math.min(3, this.localProxies.length);
for (let i = 0; i < maxAttempts; i++) {
const proxyConfig = this.getNextLocalProxy(); const proxyConfig = this.getNextLocalProxy();
if (await this.testProxyForScraping(proxyConfig)) { try {
console.log(`✓ 代理 ${proxyConfig.host}:${proxyConfig.port} 可用`); if (await this.testProxyForScraping(proxyConfig)) {
return proxyConfig; console.log(`✓ 代理 ${proxyConfig.host}:${proxyConfig.port} 可用`);
} else { return proxyConfig;
console.log(`✗ 代理 ${proxyConfig.host}:${proxyConfig.port} 不可用,尝试下一个`); } else {
console.log(`✗ 代理 ${proxyConfig.host}:${proxyConfig.port} 不可用,尝试下一个`);
}
} catch (error) {
console.log(`✗ 代理 ${proxyConfig.host}:${proxyConfig.port} 测试出错: ${error.message}`);
// 继续尝试下一个代理
} }
} }
console.log('测试的本地代理都不可用,使用直连'); console.log(`测试了 ${maxAttempts}本地代理都不可用,使用直连`);
return null; // 所有测试的代理都不可用,使用直连 return null; // 所有测试的代理都不可用,使用直连
} }
@@ -181,6 +187,8 @@ class ProxyScraper {
console.log(`正在抓取第 ${pageNum} 页: ${url}`); console.log(`正在抓取第 ${pageNum} 页: ${url}`);
let useDirectConnection = false; // 标志:是否应该直接使用直连
for (let attempt = 1; attempt <= retryCount; attempt++) { for (let attempt = 1; attempt <= retryCount; attempt++) {
let proxyConfig = null; let proxyConfig = null;
let proxyUsed = ''; let proxyUsed = '';
@@ -188,8 +196,14 @@ class ProxyScraper {
try { try {
const userAgent = this.getRandomUserAgent(); const userAgent = this.getRandomUserAgent();
// 获取可用代理配置(每次请求都尝试不同的代理 // 如果之前使用代理失败过,或者标记为使用直连,则跳过代理
proxyConfig = await this.getWorkingProxy(); if (!useDirectConnection && this.localProxies.length > 0) {
// 尝试获取可用代理配置
proxyConfig = await this.getWorkingProxy();
} else {
proxyConfig = null;
console.log('跳过代理,直接使用直连');
}
const requestConfig = { const requestConfig = {
headers: { headers: {
@@ -242,6 +256,12 @@ class ProxyScraper {
} catch (error) { } catch (error) {
console.error(`${attempt} 次尝试抓取第 ${pageNum} 页失败 (${proxyUsed}):`, error.message); console.error(`${attempt} 次尝试抓取第 ${pageNum} 页失败 (${proxyUsed}):`, error.message);
// 如果使用代理失败,下次重试时使用直连
if (proxyConfig) {
console.log(`代理 ${proxyConfig.host}:${proxyConfig.port} 抓取失败,下次重试将使用直连`);
useDirectConnection = true; // 标记为使用直连
}
if (attempt === retryCount) { if (attempt === retryCount) {
throw new Error(`抓取第 ${pageNum} 页失败,已重试 ${retryCount} 次: ${error.message}`); throw new Error(`抓取第 ${pageNum} 页失败,已重试 ${retryCount} 次: ${error.message}`);
} }

View File

@@ -76,16 +76,16 @@ class ProxyValidator {
// 检查响应内容 - 根据不同的测试URL使用不同的验证逻辑 // 检查响应内容 - 根据不同的测试URL使用不同的验证逻辑
if (response.status >= 200 && response.status < 300) { if (response.status >= 200 && response.status < 300) {
if (testUrl.includes('baidu.com')) { // if (testUrl.includes('baidu.com')) {
isValid = response.data && response.data.includes('百度'); // isValid = response.data && response.data.includes('baidu');
} else if (testUrl.includes('httpbin.org')) { // } else if (testUrl.includes('httpbin.org')) {
isValid = response.data && (response.data.includes('origin') || response.data.includes('ip')); // isValid = response.data && (response.data.includes('origin') || response.data.includes('ip'));
} else if (testUrl.includes('google.com')) { // } else if (testUrl.includes('google.com')) {
isValid = response.data && response.data.toLowerCase().includes('google'); // isValid = response.data && response.data.toLowerCase().includes('google');
} else { // } else {
// 对于其他URL只要能连接就认为有效 // 对于其他URL只要能连接就认为有效
isValid = true; isValid = true;
} // }
} }
// 只要有一次验证成功,立即返回成功结果 // 只要有一次验证成功,立即返回成功结果
@@ -94,13 +94,33 @@ class ProxyValidator {
console.log(`✓ 代理 ${ip}:${port}${attemptNumber} 次尝试验证成功,响应时间: ${responseTime}ms`); console.log(`✓ 代理 ${ip}:${port}${attemptNumber} 次尝试验证成功,响应时间: ${responseTime}ms`);
} }
// 提取响应信息用于显示
let responseData = '';
let responseStatus = response.status;
try {
// 只取响应数据的前500个字符避免数据过大
const dataStr = typeof response.data === 'string'
? response.data
: JSON.stringify(response.data);
// responseData = dataStr.substring(0, 500);
responseData = dataStr;
// if (dataStr.length > 500) {
// responseData += '... (已截断)';
// }
} catch (e) {
responseData = '无法解析响应内容';
}
const result = { const result = {
ip: ip, ip: ip,
port: parseInt(port), port: parseInt(port),
isValid: true, isValid: true,
responseTime: responseTime, responseTime: responseTime,
error: null, error: null,
testUrl: testUrl testUrl: testUrl,
responseStatus: responseStatus,
responseData: responseData,
responseHeaders: response.headers || {}
}; };
// 更新数据库中的验证结果(如果需要) // 更新数据库中的验证结果(如果需要)
@@ -124,13 +144,34 @@ class ProxyValidator {
} }
lastError = new Error('响应内容验证失败'); lastError = new Error('响应内容验证失败');
// 提取响应信息
let responseData = '';
let responseStatus = response ? response.status : null;
try {
if (response && response.data) {
const dataStr = typeof response.data === 'string'
? response.data
: JSON.stringify(response.data);
responseData = dataStr;
// responseData = dataStr.substring(0, 500);
// if (dataStr.length > 500) {
// responseData += '... (已截断)';
// }
}
} catch (e) {
responseData = '无法解析响应内容';
}
lastResult = { lastResult = {
ip: ip, ip: ip,
port: parseInt(port), port: parseInt(port),
isValid: false, isValid: false,
responseTime: responseTime, responseTime: responseTime,
error: '响应内容验证失败', error: '响应内容验证失败',
testUrl: testUrl testUrl: testUrl,
responseStatus: responseStatus,
responseData: responseData,
responseHeaders: response ? (response.headers || {}) : {}
}; };
// 如果还有重试机会,继续尝试 // 如果还有重试机会,继续尝试
@@ -147,7 +188,10 @@ class ProxyValidator {
isValid: false, isValid: false,
responseTime: responseTime, responseTime: responseTime,
error: error.message, error: error.message,
testUrl: testUrl testUrl: testUrl,
responseStatus: null,
responseData: null,
responseHeaders: {}
}; };
if (logResult) { if (logResult) {
@@ -184,7 +228,10 @@ class ProxyValidator {
isValid: false, isValid: false,
responseTime: Date.now() - startTime, responseTime: Date.now() - startTime,
error: lastError ? lastError.message : '所有验证尝试都失败', error: lastError ? lastError.message : '所有验证尝试都失败',
testUrl: testUrl testUrl: testUrl,
responseStatus: null,
responseData: null,
responseHeaders: {}
}; };
} }