From 3cd3aa547c4747b2d6b9d72c1ad8a7f4b5ce303e Mon Sep 17 00:00:00 2001
From: gray <694178258@qq.com>
Date: Fri, 29 Aug 2025 11:31:40 +0800
Subject: [PATCH] =?UTF-8?q?=E5=8A=9F=E8=83=BD:=20=E5=A2=9E=E5=8A=A0API?=
=?UTF-8?q?=E8=87=AA=E5=AE=9A=E4=B9=89=E5=B9=B6=E4=BC=98=E5=8C=96=E6=A0=B8?=
=?UTF-8?q?=E5=BF=83=E4=BD=93=E9=AA=8C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 添加了对自定义第三方API的支持
- 首页的默认AI分析现在可以被删除
- 为API请求失败的情况添加了完整的错误日志记录
- 在聊天记录(chatlog)中增加了不发送图片的选项以节省token
---
.gitignore | 7 +-
ERROR_HANDLING.md | 47 ++
model-settings.json | 7 +
public/css/ai-settings.css | 262 ++++++++++
public/css/style.css | 173 ++++++-
public/js/ai-settings.js | 99 +++-
public/js/app.js | 303 ++++++++++--
public/js/model-settings.js | 46 +-
server.js | 454 +++++++++++++++++-
views/index.ejs | 137 +++---
...71\347\233\256\350\257\264\346\230\216.md" | 281 +++--------
11 files changed, 1426 insertions(+), 390 deletions(-)
create mode 100644 ERROR_HANDLING.md
create mode 100644 public/css/ai-settings.css
diff --git a/.gitignore b/.gitignore
index 91d2c32..f001742 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,7 @@
# 忽略所有数据库文件
-*.db
\ No newline at end of file
+*.db
+
+# 忽略日志文件
+logs/
+*.log
+server.log
diff --git a/ERROR_HANDLING.md b/ERROR_HANDLING.md
new file mode 100644
index 0000000..2c2f340
--- /dev/null
+++ b/ERROR_HANDLING.md
@@ -0,0 +1,47 @@
+# 错误处理增强说明
+
+本次更新增强了系统的错误处理和日志记录功能,特别是针对"socket hang up"等网络连接问题。
+
+## 主要改进
+
+### 1. 服务器端增强 (server.js)
+
+- **详细错误日志**:现在会记录完整的错误对象,包括:
+ - 错误消息、代码和堆栈
+ - 请求参数和配置
+ - 响应状态和数据
+ - 时间戳和重试信息
+
+- **日志文件记录**:
+ - 所有API错误现在会写入 `logs/api-errors.log` 文件
+ - 包含完整的错误上下文和诊断信息
+
+- **增强的错误响应**:
+ - API响应中包含更详细的错误信息
+ - 提供针对性的故障排除建议
+
+### 2. 前端增强
+
+- **增强的错误消息显示**:
+ - 添加了"显示详情"按钮,可展开查看完整错误信息
+ - 添加了"复制错误"按钮,方便分享错误信息
+
+- **改进的CSS样式**:
+ - 更美观的错误消息提示
+ - 支持错误、成功、信息和警告四种消息类型
+ - 添加了消息动画效果
+
+### 3. 使用方法
+
+当遇到"socket hang up"或其他API调用错误时:
+
+1. 错误消息会显示在页面右上角
+2. 点击"显示详情"查看完整错误信息
+3. 点击"复制错误"可复制错误详情用于报告问题
+4. 查看 `logs/api-errors.log` 获取更多诊断信息
+
+## 技术细节
+
+- 错误日志会自动过滤敏感信息(如API密钥)
+- 使用指数退避算法进行重试
+- 针对不同类型的错误提供定制化的解决建议
\ No newline at end of file
diff --git a/model-settings.json b/model-settings.json
index 432e4f5..489c927 100644
--- a/model-settings.json
+++ b/model-settings.json
@@ -8,5 +8,12 @@
"model": "gemini-2.5-pro",
"apiKey": "xx"
},
+ "custom": {
+ "name": "自定义AI",
+ "apiUrl": "https://api.example.com/v1",
+ "model": "custom-model",
+ "apiKey": "",
+ "format": "openai"
+ },
"updatedAt": "2025-07-01T11:15:26.302Z"
}
diff --git a/public/css/ai-settings.css b/public/css/ai-settings.css
new file mode 100644
index 0000000..223164c
--- /dev/null
+++ b/public/css/ai-settings.css
@@ -0,0 +1,262 @@
+/* AI设置模态框样式 */
+.ai-settings-modal {
+ display: none;
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(0, 0, 0, 0.5);
+ z-index: 1000;
+ overflow-y: auto;
+}
+
+.ai-settings-modal.show {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.ai-settings-content {
+ background-color: #fff;
+ border-radius: 8px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+ width: 90%;
+ max-width: 600px;
+ max-height: 90vh;
+ overflow-y: auto;
+ animation: slideDown 0.3s ease;
+}
+
+.ai-settings-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 15px 20px;
+ border-bottom: 1px solid #eee;
+}
+
+.ai-settings-header h3 {
+ margin: 0;
+ font-size: 18px;
+ color: #333;
+}
+
+.ai-settings-close {
+ background: none;
+ border: none;
+ font-size: 24px;
+ cursor: pointer;
+ color: #999;
+}
+
+.ai-settings-close:hover {
+ color: #333;
+}
+
+.ai-settings-body {
+ padding: 20px;
+}
+
+.settings-group {
+ margin-bottom: 20px;
+}
+
+.settings-group label {
+ display: block;
+ margin-bottom: 8px;
+ font-weight: 500;
+ color: #333;
+}
+
+.settings-group input[type="text"],
+.settings-group select,
+.settings-group textarea {
+ width: 100%;
+ padding: 10px;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ font-size: 14px;
+}
+
+.settings-group textarea {
+ min-height: 120px;
+ resize: vertical;
+}
+
+.settings-actions {
+ display: flex;
+ justify-content: flex-end;
+ margin-top: 20px;
+ gap: 10px;
+}
+
+.save-settings-btn,
+.reset-settings-btn,
+.delete-item-btn {
+ padding: 8px 16px;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 14px;
+ display: flex;
+ align-items: center;
+ gap: 5px;
+}
+
+.save-settings-btn {
+ background-color: #4CAF50;
+ color: white;
+}
+
+.reset-settings-btn {
+ background-color: #f0f0f0;
+ color: #333;
+}
+
+.delete-item-btn {
+ background-color: #f44336;
+ color: white;
+ margin-right: auto;
+}
+
+.save-settings-btn:hover {
+ background-color: #45a049;
+}
+
+.reset-settings-btn:hover {
+ background-color: #e0e0e0;
+}
+
+.delete-item-btn:hover {
+ background-color: #d32f2f;
+}
+
+.custom-date-range {
+ margin-top: 10px;
+}
+
+.date-inputs {
+ display: flex;
+ gap: 10px;
+}
+
+.date-inputs > div {
+ flex: 1;
+}
+
+/* 搜索下拉框样式 */
+.searchable-select {
+ position: relative;
+}
+
+.group-search-input {
+ width: 100%;
+ padding: 10px;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ margin-bottom: 8px;
+ font-size: 14px;
+}
+
+.search-results-count {
+ position: absolute;
+ right: 10px;
+ top: 10px;
+ font-size: 12px;
+ color: #666;
+ background-color: #f0f0f0;
+ padding: 2px 6px;
+ border-radius: 10px;
+}
+
+/* 全局设置区域样式 */
+.global-settings-section {
+ margin-top: 30px;
+ padding-top: 20px;
+ border-top: 1px solid #eee;
+}
+
+.global-settings-section h4 {
+ margin-top: 0;
+ margin-bottom: 15px;
+ font-size: 16px;
+ color: #333;
+}
+
+/* 复选框组样式 */
+.checkbox-group {
+ display: flex;
+ align-items: center;
+ margin-bottom: 15px;
+}
+
+.checkbox-label {
+ display: flex;
+ align-items: center;
+ cursor: pointer;
+ user-select: none;
+}
+
+.checkbox-label input[type="checkbox"] {
+ margin-right: 8px;
+ cursor: pointer;
+}
+
+.checkbox-text {
+ font-weight: normal;
+ margin-right: 5px;
+}
+
+/* 提示图标样式 */
+.tooltip-icon {
+ display: inline-block;
+ width: 16px;
+ height: 16px;
+ background-color: #f0f0f0;
+ border-radius: 50%;
+ text-align: center;
+ line-height: 16px;
+ font-size: 12px;
+ color: #666;
+ cursor: help;
+ margin-left: 5px;
+}
+
+.tooltip-icon:hover {
+ background-color: #e0e0e0;
+}
+
+/* 提示框样式 */
+.settings-tooltip {
+ position: absolute;
+ background-color: #333;
+ color: #fff;
+ padding: 5px 10px;
+ border-radius: 4px;
+ font-size: 12px;
+ z-index: 1100;
+ max-width: 200px;
+ box-shadow: 0 2px 5px rgba(0,0,0,0.2);
+}
+
+.settings-tooltip::before {
+ content: '';
+ position: absolute;
+ top: -5px;
+ left: 10px;
+ border-left: 5px solid transparent;
+ border-right: 5px solid transparent;
+ border-bottom: 5px solid #333;
+}
+
+@keyframes slideDown {
+ from {
+ transform: translateY(-20px);
+ opacity: 0;
+ }
+ to {
+ transform: translateY(0);
+ opacity: 1;
+ }
+}
\ No newline at end of file
diff --git a/public/css/style.css b/public/css/style.css
index a611186..7f242f3 100644
--- a/public/css/style.css
+++ b/public/css/style.css
@@ -244,15 +244,15 @@ body {
}
.ai-settings-btn {
- background: rgba(142, 142, 147, 0.12);
- color: #8E8E93;
+ width: 36px;
+ height: 36px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
border: none;
- padding: 0.875rem;
- border-radius: 12px;
+ border-radius: 0;
cursor: pointer;
- transition: all 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94);
- backdrop-filter: blur(10px);
- -webkit-backdrop-filter: blur(10px);
+ transition: all 0.2s ease;
}
.ai-settings-btn:hover {
@@ -674,23 +674,107 @@ body {
background: #a8a8a8;
}
-/* 错误消息样式 */
+/* 消息提示样式 - 增强版 */
+.error-message,
+.success-message,
+.info-message,
+.warning-message {
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ padding: 1rem;
+ border-radius: 12px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+ z-index: 2000;
+ max-width: 300px;
+ animation: slideIn 0.3s ease;
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ font-size: 0.9rem;
+}
+
.error-message {
background: #ffebee;
color: #c62828;
- padding: 1rem;
- border-radius: 8px;
border-left: 4px solid #c62828;
- margin: 1rem 0;
}
.success-message {
background: #e8f5e8;
color: #2e7d32;
- padding: 1rem;
- border-radius: 8px;
border-left: 4px solid #2e7d32;
- margin: 1rem 0;
+}
+
+.info-message {
+ background: #e3f2fd;
+ color: #0277bd;
+ border-left: 4px solid #0277bd;
+}
+
+.warning-message {
+ background: #fff8e1;
+ color: #ff8f00;
+ border-left: 4px solid #ff8f00;
+}
+
+/* 错误详情样式 */
+.error-details {
+ background: rgba(0, 0, 0, 0.03);
+ border-radius: 6px;
+ padding: 0.75rem;
+ margin-top: 0.5rem;
+ max-height: 300px;
+ overflow-y: auto;
+ font-family: 'Courier New', monospace;
+ font-size: 0.8rem;
+ white-space: pre-wrap;
+ word-break: break-word;
+}
+
+.error-actions {
+ display: flex;
+ gap: 0.5rem;
+ margin-top: 0.5rem;
+}
+
+.show-details-btn,
+.copy-error-btn {
+ background: rgba(0, 0, 0, 0.1);
+ border: none;
+ border-radius: 4px;
+ padding: 0.25rem 0.5rem;
+ font-size: 0.8rem;
+ cursor: pointer;
+ transition: background 0.2s;
+}
+
+.show-details-btn:hover,
+.copy-error-btn:hover {
+ background: rgba(0, 0, 0, 0.2);
+}
+
+/* 消息动画 */
+@keyframes slideIn {
+ from {
+ transform: translateX(100%);
+ opacity: 0;
+ }
+ to {
+ transform: translateX(0);
+ opacity: 1;
+ }
+}
+
+@keyframes slideOut {
+ from {
+ transform: translateX(0);
+ opacity: 1;
+ }
+ to {
+ transform: translateX(100%);
+ opacity: 0;
+ }
}
/* AI分析功能样式 - 顶部特色区域 */
@@ -747,12 +831,25 @@ body {
/* AI按钮组和设置按钮样式 */
.ai-btn-group {
display: flex;
- align-items: center;
- gap: 0.5rem;
+ align-items: stretch;
+ margin-bottom: 0.75rem;
+ border-radius: 6px;
+ overflow: hidden;
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.08);
+ transition: all 0.2s ease;
}
+/* 修改按钮圆角,使其适应三按钮布局 */
.ai-btn-group .ai-btn {
- flex: 1;
+ border-radius: 6px 0 0 6px;
+}
+
+.ai-btn-group .ai-settings-btn {
+ border-radius: 0;
+}
+
+.ai-btn-group .ai-delete-btn {
+ border-radius: 0 6px 6px 0;
}
.ai-settings-btn {
@@ -780,21 +877,49 @@ body {
font-size: 0.9rem;
}
+/* 删除按钮样式 */
+.ai-delete-btn {
+ width: 36px;
+ height: 36px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: rgba(220, 53, 69, 0.12);
+ color: #dc3545;
+ border: none;
+ border-radius: 0 6px 6px 0;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.ai-delete-btn:hover {
+ background: rgba(220, 53, 69, 0.2);
+ color: #c82333;
+}
+
+.ai-delete-btn:active {
+ background: rgba(220, 53, 69, 0.3);
+}
+
+.ai-delete-btn i {
+ font-size: 0.9rem;
+}
+
.ai-btn {
+ flex: 1;
+ padding: 0.75rem 1rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
- padding: 0.8rem 1rem;
- border-radius: 8px;
- cursor: pointer;
- transition: all 0.3s ease;
- font-size: 0.9rem;
+ border-radius: 6px 0 0 6px;
font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s ease;
display: flex;
align-items: center;
+ justify-content: center;
gap: 0.5rem;
- position: relative;
- overflow: hidden;
+ min-width: 150px;
}
.ai-btn:hover {
diff --git a/public/js/ai-settings.js b/public/js/ai-settings.js
index 1b08800..a330192 100644
--- a/public/js/ai-settings.js
+++ b/public/js/ai-settings.js
@@ -23,6 +23,11 @@ class AISettingsManager {
}
};
+ // 全局设置
+ this.globalSettings = {
+ filterImages: true // 默认开启图片过滤
+ };
+
// 动态分析项管理
this.dynamicAnalysisItems = this.loadDynamicItems();
this.currentEditingType = null;
@@ -150,6 +155,7 @@ class AISettingsManager {
const resetBtn = document.getElementById('resetSettingsBtn');
const deleteBtn = document.getElementById('deleteItemBtn');
const groupSearch = document.getElementById('settingsGroupSearch');
+ const filterImages = document.getElementById('settingsFilterImages');
if (closeBtn) {
closeBtn.addEventListener('click', () => {
@@ -188,6 +194,35 @@ class AISettingsManager {
this.deleteCurrentItem();
});
}
+
+ // 图片过滤选项
+ if (filterImages) {
+ filterImages.checked = this.globalSettings.filterImages;
+ filterImages.addEventListener('change', (e) => {
+ this.globalSettings.filterImages = e.target.checked;
+ });
+
+ // 添加悬浮提示
+ const tooltipIcons = document.querySelectorAll('.tooltip-icon');
+ tooltipIcons.forEach(icon => {
+ icon.addEventListener('mouseenter', function() {
+ const tooltip = document.createElement('div');
+ tooltip.className = 'settings-tooltip';
+ tooltip.textContent = this.getAttribute('title');
+
+ // 计算位置
+ const rect = this.getBoundingClientRect();
+ tooltip.style.top = `${rect.bottom + 5}px`;
+ tooltip.style.left = `${rect.left}px`;
+
+ document.body.appendChild(tooltip);
+
+ this.addEventListener('mouseleave', function() {
+ tooltip.remove();
+ }, { once: true });
+ });
+ });
+ }
// 群聊搜索功能 - 添加防抖优化
if (groupSearch) {
@@ -282,7 +317,15 @@ class AISettingsManager {
loadSettings() {
try {
const saved = localStorage.getItem('aiAnalysisSettings');
- return saved ? JSON.parse(saved) : {};
+ const settings = saved ? JSON.parse(saved) : {};
+
+ // 加载全局设置
+ const globalSaved = localStorage.getItem('aiGlobalSettings');
+ if (globalSaved) {
+ this.globalSettings = {...this.globalSettings, ...JSON.parse(globalSaved)};
+ }
+
+ return settings;
} catch (error) {
console.warn('加载AI设置失败:', error);
return {};
@@ -293,11 +336,42 @@ class AISettingsManager {
saveSettings() {
try {
localStorage.setItem('aiAnalysisSettings', JSON.stringify(this.settings));
+ localStorage.setItem('aiGlobalSettings', JSON.stringify(this.globalSettings));
console.log('AI设置已保存');
+
+ // 同步到服务器
+ this.syncSettingsToServer();
} catch (error) {
console.error('保存AI设置失败:', error);
}
}
+
+ // 同步设置到服务器
+ async syncSettingsToServer() {
+ try {
+ // 构建完整的设置对象
+ const fullSettings = {
+ ...this.settings,
+ filterImages: this.globalSettings.filterImages
+ };
+
+ // 发送到服务器
+ const response = await fetch('/api/save-analysis-config', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({ analysisConfig: fullSettings })
+ });
+
+ const result = await response.json();
+ if (!result.success) {
+ console.error('同步设置到服务器失败:', result.error);
+ }
+ } catch (error) {
+ console.error('同步设置到服务器失败:', error);
+ }
+ }
// 加载动态分析项
loadDynamicItems() {
@@ -422,6 +496,19 @@ class AISettingsManager {
+
+
+
+
全局设置
+
+
+
+
+
`;
@@ -1317,8 +1478,9 @@ class ChatlogApp {
// 绑定动态分析项事件
bindDynamicAnalysisEvents(itemId) {
- const analysisBtn = document.querySelector(`[data-type="${itemId}"]:not(.ai-settings-btn)`);
+ const analysisBtn = document.querySelector(`[data-type="${itemId}"]:not(.ai-settings-btn):not(.ai-delete-btn)`);
const settingsBtn = document.querySelector(`[data-type="${itemId}"].ai-settings-btn`);
+ const deleteBtn = document.querySelector(`[data-type="${itemId}"].ai-delete-btn`);
if (analysisBtn) {
analysisBtn.addEventListener('click', () => {
@@ -1333,6 +1495,12 @@ class ChatlogApp {
}
});
}
+
+ if (deleteBtn) {
+ deleteBtn.addEventListener('click', () => {
+ this.deleteAnalysisItem(itemId);
+ });
+ }
}
// 执行动态分析
@@ -1384,6 +1552,60 @@ class ChatlogApp {
element.remove();
}
}
+
+ // 删除分析项(处理所有类型的分析项)
+ deleteAnalysisItem(itemId) {
+ // 确认是否删除
+ const isDynamic = itemId.startsWith('dynamic_');
+ const itemType = isDynamic ? '自定义分析项' : '默认分析项';
+
+ // 获取分析项名称
+ let itemName = itemId;
+ if (window.aiSettingsManager) {
+ const settings = window.aiSettingsManager.getSettings(itemId);
+ if (settings && settings.displayName) {
+ itemName = settings.displayName;
+ }
+ }
+
+ const confirmMessage = `确定要删除${itemType}「${itemName}」吗?`;
+
+ if (!confirm(confirmMessage)) {
+ return;
+ }
+
+ if (isDynamic) {
+ // 动态分析项:完全删除
+ if (window.aiSettingsManager) {
+ window.aiSettingsManager.removeDynamicAnalysisItem(itemId);
+ }
+ } else {
+ // 默认分析项:也完全删除
+ // 使用更兼容的选择器方式
+ const analysisBtn = document.querySelector(`[data-type="${itemId}"]:not(.ai-settings-btn):not(.ai-delete-btn)`);
+ if (analysisBtn) {
+ const btnGroup = analysisBtn.closest('.ai-btn-group');
+ if (btnGroup) {
+ btnGroup.style.display = 'none'; // 隐藏整个按钮组
+
+ // 标记该分析项为已删除
+ if (window.aiSettingsManager) {
+ // 确保设置对象存在
+ if (!window.aiSettingsManager.settings[itemId]) {
+ window.aiSettingsManager.settings[itemId] = {};
+ }
+
+ // 标记为已删除
+ window.aiSettingsManager.settings[itemId].isDeleted = true;
+ window.aiSettingsManager.saveSettings();
+ }
+ }
+ }
+ }
+
+ // 显示成功消息
+ this.showMessage(`已删除分析项:${itemName}`, 'success');
+ }
// 删除分析历史记录
async deleteAnalysisHistory(recordId, recordTitle) {
@@ -1507,10 +1729,11 @@ class ChatlogApp {
{ id: 'reading', name: '读者群分析', type: 'default' }
];
- // 检查每个默认分析项是否有配置和群聊选择
+ // 检查每个默认分析项是否有配置和群聊选择,且未被删除
defaultItems.forEach(item => {
const settings = window.aiSettingsManager?.getSettings(item.id);
- if (settings && settings.groupName) {
+ // 只添加未被标记为删除的项
+ if (settings && settings.groupName && !settings.isDeleted) {
analysisItems.push({
id: item.id,
name: settings.displayName || item.name,
@@ -1640,6 +1863,14 @@ class ChatlogApp {
// 执行单个分析
async executeSingleAnalysis(analysisItem) {
try {
+ // 获取图片过滤设置
+ let filterImages = true; // 默认开启图片过滤
+ if (window.aiSettingsManager &&
+ window.aiSettingsManager.globalSettings &&
+ window.aiSettingsManager.globalSettings.filterImages !== undefined) {
+ filterImages = window.aiSettingsManager.globalSettings.filterImages;
+ }
+
const response = await fetch('/api/ai-analysis', {
method: 'POST',
headers: {
@@ -1649,7 +1880,8 @@ class ChatlogApp {
groupName: analysisItem.groupName,
analysisType: analysisItem.analysisType,
customPrompt: analysisItem.customPrompt,
- timeRange: analysisItem.timeRange
+ timeRange: analysisItem.timeRange,
+ filterImages: filterImages
})
});
@@ -2138,14 +2370,12 @@ class ChatlogApp {
container.innerHTML = '正在加载分析项...
';
try {
- const response = await fetch('/api/scheduled-analysis-status');
- const data = await response.json();
+ // 使用本地数据而不是从服务器获取,确保显示的是最新状态(包括删除状态)
+ const analysisItems = this.getAllAnalysisItems();
+ this.displayAnalysisItemsForConfig(analysisItems);
- if (data.success) {
- this.displayAnalysisItemsForConfig(data.analysisItems);
- } else {
- container.innerHTML = '❌ 加载失败
';
- }
+ // 同步最新配置到服务器
+ await this.syncAnalysisConfigToServer();
} catch (error) {
console.error('加载分析项失败:', error);
container.innerHTML = '❌ 加载失败
';
@@ -2436,14 +2666,33 @@ class ChatlogApp {
dynamicAnalysisItems: window.aiSettingsManager?.dynamicAnalysisItems || []
};
- // 添加所有配置的分析项设置
+ // 添加所有配置的分析项设置(只添加未被删除的项)
const allItems = this.getAllAnalysisItems();
- allItems.forEach(item => {
- const settings = window.aiSettingsManager?.getSettings(item.id);
- if (settings && settings.groupName) {
- analysisConfig[item.id] = settings;
- }
- });
+
+ // 添加已配置但未被删除的分析项
+ if (window.aiSettingsManager) {
+ // 获取所有默认分析项ID
+ const defaultItemIds = ['programming', 'science', 'reading'];
+
+ // 遍历所有默认分析项,只添加未被删除的
+ defaultItemIds.forEach(itemId => {
+ const settings = window.aiSettingsManager.getSettings(itemId);
+ // 只添加未被标记为删除的项
+ if (settings && settings.groupName && !settings.isDeleted) {
+ analysisConfig[itemId] = settings;
+ }
+ });
+
+ // 添加动态分析项(这些已经通过getAllAnalysisItems过滤)
+ allItems.forEach(item => {
+ if (item.id.startsWith('dynamic_')) {
+ const settings = window.aiSettingsManager.getSettings(item.id);
+ if (settings && settings.groupName) {
+ analysisConfig[item.id] = settings;
+ }
+ }
+ });
+ }
// 发送到服务器
const response = await fetch('/api/save-analysis-config', {
diff --git a/public/js/model-settings.js b/public/js/model-settings.js
index 6a229c8..69b1d75 100644
--- a/public/js/model-settings.js
+++ b/public/js/model-settings.js
@@ -69,6 +69,10 @@ class ModelSettings {
document.getElementById('geminiApiKey').addEventListener('input', (e) => {
this.validateApiKey('gemini', e.target.value);
});
+
+ document.getElementById('customApiKey').addEventListener('input', (e) => {
+ this.validateApiKey('custom', e.target.value);
+ });
}
// 打开弹窗
@@ -87,13 +91,20 @@ class ModelSettings {
switchProvider(provider) {
const deepseekConfig = document.getElementById('deepseekConfig');
const geminiConfig = document.getElementById('geminiConfig');
+ const customConfig = document.getElementById('customConfig');
+
+ // 隐藏所有配置
+ deepseekConfig.style.display = 'none';
+ geminiConfig.style.display = 'none';
+ customConfig.style.display = 'none';
+ // 显示选中的配置
if (provider === 'DeepSeek') {
deepseekConfig.style.display = 'block';
- geminiConfig.style.display = 'none';
} else if (provider === 'Gemini') {
- deepseekConfig.style.display = 'none';
geminiConfig.style.display = 'block';
+ } else if (provider === 'Custom') {
+ customConfig.style.display = 'block';
}
}
@@ -131,6 +142,10 @@ class ModelSettings {
} else if (provider === 'gemini') {
isValid = apiKey.startsWith('AI') && apiKey.length > 20;
message = isValid ? '✓ API Key 格式正确' : '✗ Gemini API Key 应以 AI 开头';
+ } else if (provider === 'custom') {
+ // 自定义API Key格式较为宽松,只要长度足够即可
+ isValid = apiKey.length > 8;
+ message = isValid ? '✓ API Key 长度合适' : '✗ API Key 长度过短';
}
statusElement.textContent = message;
@@ -201,6 +216,13 @@ class ModelSettings {
gemini: {
model: document.getElementById('geminiModel').value,
apiKey: document.getElementById('geminiApiKey').value
+ },
+ custom: {
+ name: document.getElementById('customName').value,
+ apiUrl: document.getElementById('customApiUrl').value,
+ model: document.getElementById('customModel').value,
+ apiKey: document.getElementById('customApiKey').value,
+ format: document.getElementById('customFormat').value
}
};
}
@@ -283,6 +305,13 @@ class ModelSettings {
gemini: {
model: 'gemini-2.5-pro',
apiKey: ''
+ },
+ custom: {
+ name: '自定义AI',
+ apiUrl: 'https://api.example.com/v1',
+ model: 'custom-model',
+ apiKey: '',
+ format: 'openai'
}
};
}
@@ -311,6 +340,19 @@ class ModelSettings {
if (settings.gemini.apiKey) {
this.validateApiKey('gemini', settings.gemini.apiKey);
}
+
+ // 设置自定义AI配置
+ if (settings.custom) {
+ document.getElementById('customName').value = settings.custom.name || '自定义AI';
+ document.getElementById('customApiUrl').value = settings.custom.apiUrl || '';
+ document.getElementById('customModel').value = settings.custom.model || '';
+ document.getElementById('customApiKey').value = settings.custom.apiKey || '';
+ document.getElementById('customFormat').value = settings.custom.format || 'openai';
+
+ if (settings.custom.apiKey) {
+ this.validateApiKey('custom', settings.custom.apiKey);
+ }
+ }
}
// 显示通知
diff --git a/server.js b/server.js
index 87d51eb..63c7b4f 100644
--- a/server.js
+++ b/server.js
@@ -392,12 +392,94 @@ async function callAI(prompt, systemPrompt, retryCount = 0) {
});
return response.data.candidates[0].content.parts[0].text;
+ } else if (provider === 'Custom') {
+ // 自定义AI提供商处理
+ console.log('📊 使用自定义AI提供商:', config.name);
+
+ // 根据format决定请求格式
+ if (config.format === 'openai') {
+ // OpenAI兼容格式
+ response = await axios.post(`${config.apiUrl}/chat/completions`, {
+ model: config.model,
+ messages: [
+ {
+ role: 'system',
+ content: systemPrompt
+ },
+ {
+ role: 'user',
+ content: prompt
+ }
+ ],
+ temperature: 1.0,
+ max_tokens: 64000,
+ stream: false
+ }, {
+ headers: {
+ 'Authorization': `Bearer ${config.apiKey}`,
+ 'Content-Type': 'application/json'
+ },
+ timeout: timeoutDuration,
+ httpAgent: new (require('http').Agent)({
+ keepAlive: true,
+ maxSockets: 1,
+ timeout: timeoutDuration
+ }),
+ httpsAgent: new (require('https').Agent)({
+ keepAlive: true,
+ maxSockets: 1,
+ timeout: timeoutDuration
+ })
+ });
+
+ return response.data.choices[0].message.content;
+ } else {
+ // 自定义格式,使用通用请求
+ throw new Error('目前仅支持OpenAI兼容格式的自定义API');
+ }
}
throw new Error('不支持的AI提供商');
} catch (error) {
+ // 创建详细的错误日志对象
+ const errorLog = {
+ timestamp: new Date().toISOString(),
+ attempt: retryCount + 1,
+ errorMessage: error.message,
+ errorCode: error.code || 'UNKNOWN',
+ errorName: error.name,
+ errorStack: error.stack
+ };
+
+ // 记录请求配置信息(如果存在)
+ if (error.config) {
+ errorLog.request = {
+ url: error.config.url,
+ method: error.config.method,
+ headers: { ...error.config.headers },
+ timeout: error.config.timeout
+ };
+
+ // 移除敏感信息
+ if (errorLog.request.headers && errorLog.request.headers.Authorization) {
+ errorLog.request.headers.Authorization = '[REDACTED]';
+ }
+ }
+
+ // 记录响应信息(如果存在)
+ if (error.response) {
+ errorLog.response = {
+ status: error.response.status,
+ statusText: error.response.statusText,
+ headers: error.response.headers,
+ data: error.response.data
+ };
+ }
+
+ // 输出格式化的错误信息
console.error(`❌ AI API调用失败 (第${retryCount + 1}次):`, error.message);
+ console.error('详细错误信息:', JSON.stringify(errorLog, null, 2));
// 判断是否需要重试
const shouldRetry = retryCount < maxRetries && (
@@ -413,13 +495,31 @@ async function callAI(prompt, systemPrompt, retryCount = 0) {
const delay = baseDelay * Math.pow(2, retryCount); // 指数退避
console.log(`⏳ ${delay/1000}秒后进行第${retryCount + 2}次重试...`);
+ // 记录重试信息
+ console.log(`重试信息: 尝试次数=${retryCount+1}, 最大重试次数=${maxRetries}, 延迟=${delay}ms`);
+
await new Promise(resolve => setTimeout(resolve, delay));
return await callAI(prompt, systemPrompt, retryCount + 1);
}
- // 记录详细错误信息
- if (error.response) {
- console.error('API错误响应:', error.response.status, error.response.data);
+ // 将错误信息写入日志文件
+ try {
+ const fs = require('fs');
+ const logDir = './logs';
+
+ // 确保日志目录存在
+ if (!fs.existsSync(logDir)) {
+ fs.mkdirSync(logDir, { recursive: true });
+ }
+
+ // 写入错误日志
+ fs.appendFileSync(
+ `${logDir}/api-errors.log`,
+ `[${new Date().toISOString()}] API调用失败 (第${retryCount + 1}次): ${error.message}\n` +
+ `详细信息: ${JSON.stringify(errorLog, null, 2)}\n\n`
+ );
+ } catch (logError) {
+ console.error('写入错误日志失败:', logError);
}
throw error;
@@ -438,7 +538,8 @@ async function callDeepSeekAPI(prompt, systemPrompt) {
async function checkAIModelHealth() {
const results = {
deepseek: { available: false, responseTime: null, error: null },
- gemini: { available: false, responseTime: null, error: null }
+ gemini: { available: false, responseTime: null, error: null },
+ custom: { available: false, responseTime: null, error: null }
};
// 测试DeepSeek
@@ -477,6 +578,42 @@ async function checkAIModelHealth() {
results.gemini.error = error.message;
}
+ // 测试自定义AI提供商
+ try {
+ // 读取自定义AI配置
+ const fs = require('fs');
+ const path = require('path');
+ const modelSettingsPath = path.join(__dirname, 'model-settings.json');
+
+ if (fs.existsSync(modelSettingsPath)) {
+ const settings = JSON.parse(fs.readFileSync(modelSettingsPath, 'utf8'));
+
+ if (settings.custom && settings.custom.apiUrl && settings.custom.apiKey) {
+ const startTime = Date.now();
+
+ // 根据format决定请求格式
+ if (settings.custom.format === 'openai') {
+ await axios.post(`${settings.custom.apiUrl}/chat/completions`, {
+ model: settings.custom.model || 'default',
+ messages: [{ role: 'user', content: 'test' }],
+ max_tokens: 1
+ }, {
+ headers: {
+ 'Authorization': `Bearer ${settings.custom.apiKey}`,
+ 'Content-Type': 'application/json'
+ },
+ timeout: 10000
+ });
+ }
+
+ results.custom.available = true;
+ results.custom.responseTime = Date.now() - startTime;
+ }
+ }
+ } catch (error) {
+ results.custom.error = error.message;
+ }
+
return results;
}
@@ -492,20 +629,26 @@ app.get('/api/ai-model-recommendation', async (req, res) => {
};
// 基于响应时间和可用性推荐
- if (health.deepseek.available && health.gemini.available) {
- if (health.deepseek.responseTime < health.gemini.responseTime) {
- recommendation.recommended = 'deepseek';
- recommendation.reason = `DeepSeek响应更快 (${health.deepseek.responseTime}ms vs ${health.gemini.responseTime}ms)`;
+ const availableModels = [];
+ if (health.deepseek.available) availableModels.push({name: 'deepseek', time: health.deepseek.responseTime});
+ if (health.gemini.available) availableModels.push({name: 'gemini', time: health.gemini.responseTime});
+ if (health.custom.available) availableModels.push({name: 'custom', time: health.custom.responseTime});
+
+ if (availableModels.length > 0) {
+ // 按响应时间排序
+ availableModels.sort((a, b) => a.time - b.time);
+ const fastest = availableModels[0];
+
+ recommendation.recommended = fastest.name;
+
+ if (availableModels.length > 1) {
+ const comparisons = availableModels.slice(1).map(model =>
+ `${model.name} (${model.time}ms)`
+ ).join(', ');
+ recommendation.reason = `${fastest.name}响应最快 (${fastest.time}ms vs ${comparisons})`;
} else {
- recommendation.recommended = 'gemini';
- recommendation.reason = `Gemini响应更快 (${health.gemini.responseTime}ms vs ${health.deepseek.responseTime}ms)`;
+ recommendation.reason = `${fastest.name}是唯一可用的模型 (${fastest.time}ms)`;
}
- } else if (health.deepseek.available) {
- recommendation.recommended = 'deepseek';
- recommendation.reason = 'Gemini当前不可用';
- } else if (health.gemini.available) {
- recommendation.recommended = 'gemini';
- recommendation.reason = 'DeepSeek当前不可用';
} else {
recommendation.recommended = null;
recommendation.reason = '所有AI模型当前都不可用';
@@ -560,7 +703,20 @@ async function getModelConfig() {
}
}
-function generatePromptTemplate(analysisType, chatData, customPrompt = '') {
+function generatePromptTemplate(analysisType, chatData, customPrompt = '', shouldFilterImages = true) {
+ // 获取AI设置
+ const aiSettingsPath = path.join(__dirname, 'ai-settings.json');
+ let filterImages = shouldFilterImages;
+
+ try {
+ if (fs.existsSync(aiSettingsPath)) {
+ const aiSettings = JSON.parse(fs.readFileSync(aiSettingsPath, 'utf8'));
+ filterImages = aiSettings.filterImages === true;
+ }
+ } catch (error) {
+ console.warn('读取AI设置失败,使用默认设置', error);
+ }
+
// 不做任何限制,保留完整数据
const validMessages = chatData.filter(msg => msg.content && msg.content.trim().length > 0);
const userStats = {};
@@ -572,6 +728,21 @@ function generatePromptTemplate(analysisType, chatData, customPrompt = '') {
}
});
+ // 处理消息内容,过滤图片消息
+ const processedMessages = validMessages.map(msg => {
+ let content = msg.content;
+
+ // 如果启用了图片过滤,检测并替换图片消息
+ if (filterImages && content) {
+ // 检测...格式的图片消息
+ if (/.*?<\/msg>/s.test(content)) {
+ content = "此处是用户发聊天图,无法解析";
+ }
+ }
+
+ return `${msg.time} [${msg.senderName}]: ${content}`;
+ });
+
const basicInfo = `
聊天数据概况:
- 群聊名称: ${chatData[0]?.talkerName || '未知群聊'}
@@ -581,7 +752,7 @@ function generatePromptTemplate(analysisType, chatData, customPrompt = '') {
- 主要发言用户: ${Object.entries(userStats).sort((a,b) => b[1] - a[1]).slice(0, 5).map(([name, count]) => `${name}(${count}条)`).join(', ')}
完整聊天数据:
-${validMessages.map(msg => `${msg.time} [${msg.senderName}]: ${msg.content}`).join('\n')}
+${processedMessages.join('\n')}
`;
// 如果有自定义提示词,直接使用
@@ -600,9 +771,31 @@ ${customPrompt}`;
// AI分析接口(修改为返回historyId)
app.post('/api/ai-analysis', async (req, res) => {
try {
- const { groupName, analysisType, customPrompt, timeRange } = req.body;
+ const { groupName, analysisType, customPrompt, timeRange, filterImages } = req.body;
- console.log('AI分析请求:', { groupName, analysisType, customPrompt, timeRange });
+ console.log('AI分析请求:', { groupName, analysisType, customPrompt, timeRange, filterImages });
+
+ // 如果提供了filterImages参数,更新ai-settings.json
+ if (filterImages !== undefined) {
+ try {
+ const aiSettingsPath = path.join(__dirname, 'ai-settings.json');
+ let aiSettings = {};
+
+ // 读取现有设置
+ if (fs.existsSync(aiSettingsPath)) {
+ aiSettings = JSON.parse(fs.readFileSync(aiSettingsPath, 'utf8'));
+ }
+
+ // 更新filterImages设置
+ aiSettings.filterImages = filterImages === true;
+
+ // 保存设置
+ fs.writeFileSync(aiSettingsPath, JSON.stringify(aiSettings, null, 2));
+ console.log('已更新图片过滤设置:', filterImages);
+ } catch (error) {
+ console.error('保存AI设置失败:', error);
+ }
+ }
if (!groupName) {
return res.status(400).json({ error: '请指定群聊名称' });
@@ -657,8 +850,70 @@ app.post('/api/ai-analysis', async (req, res) => {
});
} catch (error) {
- console.error('AI分析失败:', error.message);
+ console.error('AI分析失败:', error);
+ // 创建详细的错误日志对象
+ const errorLog = {
+ timestamp: new Date().toISOString(),
+ errorMessage: error.message,
+ errorCode: error.code || 'UNKNOWN',
+ errorName: error.name,
+ errorStack: error.stack,
+ requestParams: {
+ groupName: req.body.groupName,
+ analysisType: req.body.analysisType,
+ timeRange: req.body.timeRange,
+ customPromptLength: req.body.customPrompt ? req.body.customPrompt.length : 0
+ }
+ };
+
+ // 记录请求配置信息(如果存在)
+ if (error.config) {
+ errorLog.request = {
+ url: error.config.url,
+ method: error.config.method,
+ timeout: error.config.timeout
+ };
+
+ // 移除敏感信息
+ if (error.config.headers && error.config.headers.Authorization) {
+ errorLog.request.hasAuthHeader = true;
+ }
+ }
+
+ // 记录响应信息(如果存在)
+ if (error.response) {
+ errorLog.response = {
+ status: error.response.status,
+ statusText: error.response.statusText,
+ data: error.response.data
+ };
+ }
+
+ // 输出格式化的错误信息到控制台
+ console.error('详细错误信息:', JSON.stringify(errorLog, null, 2));
+
+ // 将错误信息写入日志文件
+ try {
+ const fs = require('fs');
+ const logDir = './logs';
+
+ // 确保日志目录存在
+ if (!fs.existsSync(logDir)) {
+ fs.mkdirSync(logDir, { recursive: true });
+ }
+
+ // 写入错误日志
+ fs.appendFileSync(
+ `${logDir}/api-errors.log`,
+ `[${new Date().toISOString()}] AI分析失败: ${error.message}\n` +
+ `详细信息: ${JSON.stringify(errorLog, null, 2)}\n\n`
+ );
+ } catch (logError) {
+ console.error('写入错误日志失败:', logError);
+ }
+
+ // 用户友好的错误消息
let errorMessage = 'AI分析失败: ' + error.message;
let suggestions = [];
@@ -692,12 +947,15 @@ app.post('/api/ai-analysis', async (req, res) => {
];
}
+ // 返回详细的错误信息
res.json({
success: false,
error: errorMessage,
suggestions: suggestions,
errorCode: error.code,
- httpStatus: error.response?.status
+ httpStatus: error.response?.status,
+ errorDetails: errorLog,
+ timestamp: new Date().toISOString()
});
}
});
@@ -1804,7 +2062,7 @@ app.post('/api/test-cron-expression', (req, res) => {
// 保存模型设置
app.post('/api/model-settings', (req, res) => {
try {
- const { modelProvider, deepseek, gemini } = req.body;
+ const { modelProvider, deepseek, gemini, custom } = req.body;
if (!modelProvider) {
return res.status(400).json({
@@ -1814,7 +2072,27 @@ app.post('/api/model-settings', (req, res) => {
}
// 验证配置
- const selectedConfig = modelProvider === 'DeepSeek' ? deepseek : gemini;
+ let selectedConfig;
+ if (modelProvider === 'DeepSeek') {
+ selectedConfig = deepseek;
+ } else if (modelProvider === 'Gemini') {
+ selectedConfig = gemini;
+ } else if (modelProvider === 'Custom') {
+ selectedConfig = custom;
+ // 自定义提供商需要额外验证API URL
+ if (!selectedConfig.apiUrl || selectedConfig.apiUrl.trim() === '') {
+ return res.status(400).json({
+ success: false,
+ error: 'API URL 不能为空'
+ });
+ }
+ } else {
+ return res.status(400).json({
+ success: false,
+ error: '不支持的模型提供商'
+ });
+ }
+
if (!selectedConfig || !selectedConfig.apiKey || !selectedConfig.model) {
return res.status(400).json({
success: false,
@@ -1841,6 +2119,15 @@ app.post('/api/model-settings', (req, res) => {
'GEMINI_MODEL': gemini.model
};
+ // 如果有自定义AI提供商配置,也添加到环境变量
+ if (custom) {
+ envUpdates['CUSTOM_API_URL'] = custom.apiUrl || '';
+ envUpdates['CUSTOM_API_KEY'] = custom.apiKey || '';
+ envUpdates['CUSTOM_MODEL'] = custom.model || '';
+ envUpdates['CUSTOM_NAME'] = custom.name || '自定义AI';
+ envUpdates['CUSTOM_FORMAT'] = custom.format || 'openai';
+ }
+
Object.keys(envUpdates).forEach(key => {
const value = envUpdates[key];
const regex = new RegExp(`^${key}=.*$`, 'm');
@@ -1860,6 +2147,7 @@ app.post('/api/model-settings', (req, res) => {
modelProvider,
deepseek,
gemini,
+ custom,
updatedAt: new Date().toISOString()
};
@@ -1894,15 +2182,23 @@ app.get('/api/model-settings', (req, res) => {
// 出于安全考虑,不返回完整的API Key
const safeSettings = {
...settings,
- deepseek: {
+ deepseek: {
...settings.deepseek,
apiKey: settings.deepseek.apiKey ? settings.deepseek.apiKey.substring(0, 8) + '...' : ''
},
gemini: {
...settings.gemini,
apiKey: settings.gemini.apiKey ? settings.gemini.apiKey.substring(0, 8) + '...' : ''
+ }
+ };
+
+ // 如果有自定义AI提供商配置,也处理其API Key
+ if (settings.custom) {
+ safeSettings.custom = {
+ ...settings.custom,
+ apiKey: settings.custom.apiKey ? settings.custom.apiKey.substring(0, 8) + '...' : ''
+ };
}
- };
res.json({
success: true,
@@ -1921,6 +2217,13 @@ app.get('/api/model-settings', (req, res) => {
gemini: {
model: 'gemini-2.5-pro',
apiKey: ''
+ },
+ custom: {
+ name: '自定义AI',
+ apiUrl: 'https://api.example.com/v1',
+ model: 'custom-model',
+ apiKey: '',
+ format: 'openai'
}
}
});
@@ -1973,6 +2276,8 @@ app.post('/api/model-settings/test', async (req, res) => {
testResult = await testDeepSeekConnection(config.apiKey, config.model);
} else if (provider === 'Gemini') {
testResult = await testGeminiConnection(config.apiKey, config.model);
+ } else if (provider === 'Custom') {
+ testResult = await testCustomConnection(config);
} else {
return res.status(400).json({
success: false,
@@ -2118,6 +2423,103 @@ async function testGeminiConnection(apiKey, model) {
}
}
+// 自定义AI连接测试函数
+async function testCustomConnection(config) {
+ try {
+ // 验证必要参数
+ if (!config.apiUrl) {
+ return {
+ success: false,
+ error: 'API URL 不能为空'
+ };
+ }
+
+ if (!config.apiKey) {
+ return {
+ success: false,
+ error: 'API Key 不能为空'
+ };
+ }
+
+ if (!config.model) {
+ return {
+ success: false,
+ error: '模型名称不能为空'
+ };
+ }
+
+ // 根据format决定请求格式
+ if (config.format === 'openai') {
+ // OpenAI兼容格式
+ const response = await axios.post(`${config.apiUrl}/chat/completions`, {
+ model: config.model,
+ messages: [
+ {
+ role: 'user',
+ content: '你好,请回复"连接测试成功"'
+ }
+ ],
+ max_tokens: 50,
+ temperature: 0.1
+ }, {
+ headers: {
+ 'Authorization': `Bearer ${config.apiKey}`,
+ 'Content-Type': 'application/json'
+ },
+ timeout: 10000
+ });
+
+ if (response.status === 200 && response.data.choices && response.data.choices[0]) {
+ return {
+ success: true,
+ message: '连接测试成功',
+ model: config.model,
+ response: response.data.choices[0].message.content
+ };
+ } else {
+ return {
+ success: false,
+ error: '模型响应格式异常'
+ };
+ }
+ } else {
+ return {
+ success: false,
+ error: '目前仅支持OpenAI兼容格式的自定义API'
+ };
+ }
+ } catch (error) {
+ console.error('自定义AI连接测试失败:', error.message);
+
+ if (error.response) {
+ const statusCode = error.response.status;
+ const errorData = error.response.data;
+
+ if (statusCode === 401) {
+ return {
+ success: false,
+ error: 'API Key 无效,请检查您的密钥'
+ };
+ } else if (statusCode === 429) {
+ return {
+ success: false,
+ error: 'API 调用频率超限,请稍后重试'
+ };
+ } else {
+ return {
+ success: false,
+ error: `API 错误 (${statusCode}): ${errorData?.error?.message || '未知错误'}`
+ };
+ }
+ } else {
+ return {
+ success: false,
+ error: error.code === 'ECONNABORTED' ? '连接超时,请检查网络' : error.message
+ };
+ }
+ }
+}
+
// 启动服务器
app.listen(PORT, () => {
console.log(`\n🚀 聊天记录查询网站已启动`);
diff --git a/views/index.ejs b/views/index.ejs
index 2f35ed5..94b8d9c 100644
--- a/views/index.ejs
+++ b/views/index.ejs
@@ -5,6 +5,7 @@
聊天记录查询
+