补充延时发货,完善自动回复逻辑

This commit is contained in:
zhinianboke 2025-07-30 09:26:06 +08:00
parent 10ab84bf19
commit 20e2dcdc96
9 changed files with 904 additions and 74 deletions

40
.gitignore vendored
View File

@ -59,6 +59,42 @@ Thumbs.db
*~
# Local environment files
.env.local
.env.*.local
.env.*.local
# ==================== 项目特定文件 ====================
# 日志文件
logs/
realtime.log
# 数据目录
data/
backups/
# Excel测试文件
keywords_*.xlsx
*.xls
# 图片缓存
*.png.cache
*.jpg.cache
# 配置文件(包含敏感信息)
config.local.yml
global_config.local.yml
# 测试文件
test_*.py
*_test.py
keywords_sample.xlsx
# 备份文件
*.bak
*.backup
*.old
# 压缩文件
*.zip
*.tar.gz
*.rar
*.7z

View File

@ -76,6 +76,10 @@ RUN apt-get update && \
# 设置时区
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
# 验证Node.js安装并设置环境变量
RUN node --version && npm --version
ENV NODE_PATH=/usr/lib/node_modules
# 复制requirements.txt并安装Python依赖
COPY requirements.txt .
RUN pip install --no-cache-dir --upgrade pip && \

View File

@ -21,15 +21,20 @@
### 🤖 智能回复系统
- **关键词匹配** - 支持精确关键词匹配回复
- **商品专用回复** - 支持为特定商品设置专用关键词回复
- **通用关键词** - 支持全局通用关键词,适用于所有商品
- **批量导入导出** - 支持Excel格式的关键词批量导入导出
- **AI智能回复** - 集成OpenAI API支持上下文理解
- **变量替换** - 回复内容支持动态变量(用户名、商品信息等)
- **优先级策略** - 关键词回复优先AI回复兜底
- **优先级策略** - 商品专用关键词 > 通用关键词 > AI回复
### 🚚 自动发货功能
- **智能匹配** - 基于商品信息自动匹配发货规则
- **延时发货** - 支持设置发货延时时间0-3600秒
- **多种触发** - 支持付款消息、小刀消息等多种触发条件
- **防重复发货** - 智能防重复机制,避免重复发货
- **卡密发货** - 支持文本内容和卡密文件发货
- **多种发货方式** - 支持文本内容、卡密文件、API调用等发货方式
- **发货统计** - 完整的发货记录和统计功能
### 🛍️ 商品管理
- **自动收集** - 消息触发时自动收集商品信息
@ -44,12 +49,63 @@
- **前端分页** - 灵活的前端分页显示
- **商品详情** - 支持查看完整商品详情信息
### <EFBFBD>📊 系统监控
### 📊 系统监控
- **实时日志** - 完整的操作日志记录和查看
- **性能监控** - 系统资源使用情况监控
- **健康检查** - 服务状态健康检查
### 📁 数据管理
- **Excel导入导出** - 支持关键词数据的Excel格式导入导出
- **模板生成** - 自动生成包含示例数据的导入模板
- **批量操作** - 支持批量添加、更新关键词数据
- **数据验证** - 导入时自动验证数据格式和重复性
- **数据备份** - 自动数据备份和恢复
## 📁 项目结构
```
xianyu-auto-reply/
├── 📄 核心文件
│ ├── Start.py # 项目启动入口
│ ├── XianyuAutoAsync.py # 闲鱼WebSocket连接和消息处理
│ ├── reply_server.py # FastAPI Web服务器和API接口
│ ├── db_manager.py # SQLite数据库管理
│ ├── cookie_manager.py # Cookie和账号管理
│ ├── ai_reply_engine.py # AI智能回复引擎
│ ├── file_log_collector.py # 日志收集和管理
│ └── config.py # 配置文件管理
├── 🛠️ 工具模块
│ └── utils/
│ ├── xianyu_utils.py # 闲鱼API工具函数
│ ├── message_utils.py # 消息格式化工具
│ ├── ws_utils.py # WebSocket客户端工具
│ └── item_search.py # 商品搜索功能
├── 🌐 前端界面
│ └── static/
│ ├── index.html # 主管理界面
│ ├── login.html # 用户登录页面
│ ├── register.html # 用户注册页面
│ ├── user_management.html # 用户管理页面
│ ├── data_management.html # 数据管理页面
│ ├── log_management.html # 日志管理页面
│ ├── item_search.html # 商品搜索页面
│ └── lib/ # 前端依赖库
├── 🐳 Docker部署
│ ├── Dockerfile # Docker镜像构建文件
│ ├── docker-compose.yml # Docker Compose配置
│ ├── docker-deploy.sh # Docker部署脚本
│ ├── .env # 环境变量配置文件
│ └── nginx/ # Nginx反向代理配置
├── 📋 配置文件
│ ├── global_config.yml # 全局配置文件
│ ├── requirements.txt # Python依赖包
│ └── README.md # 项目说明文档
└── 📊 数据目录
├── data/ # 数据库和数据文件
├── logs/ # 日志文件
└── backups/ # 备份文件
```
## 🚀 快速开始
### 方式一Docker 一键部署(最简单)
@ -351,6 +407,24 @@ exit
docker restart xianyu-auto-reply
```
**2. JavaScript运行时错误**
```bash
# 错误信息Could not find an available JavaScript runtime
# 解决方案1重新构建镜像推荐
docker-compose down
docker-compose build --no-cache
docker-compose up -d
# 解决方案2手动修复
docker exec -it xianyu-auto-reply bash
apt-get update
apt-get install -y nodejs npm
python fix_js_runtime.py
exit
docker restart xianyu-auto-reply
```
**2. 其他问题排查**
1. 查看日志:`docker-compose logs -f`
2. 检查状态:`./docker-deploy.sh status`

View File

@ -752,21 +752,39 @@ class XianyuLive:
logger.error(f"获取默认回复失败: {self._safe_str(e)}")
return None
async def get_keyword_reply(self, send_user_name: str, send_user_id: str, send_message: str) -> str:
"""获取关键词匹配回复"""
async def get_keyword_reply(self, send_user_name: str, send_user_id: str, send_message: str, item_id: str = None) -> str:
"""获取关键词匹配回复支持商品ID优先匹配"""
try:
from db_manager import db_manager
# 获取当前账号的关键词列表
keywords = db_manager.get_keywords(self.cookie_id)
# 获取当前账号的关键词列表包含商品ID
keywords = db_manager.get_keywords_with_item_id(self.cookie_id)
if not keywords:
logger.debug(f"账号 {self.cookie_id} 没有配置关键词")
return None
# 遍历关键词,查找匹配
for keyword, reply in keywords:
if keyword.lower() in send_message.lower():
# 1. 如果有商品ID优先匹配该商品ID对应的关键词
if item_id:
for keyword, reply, keyword_item_id in keywords:
if keyword_item_id == item_id and keyword.lower() in send_message.lower():
# 进行变量替换
try:
formatted_reply = reply.format(
send_user_name=send_user_name,
send_user_id=send_user_id,
send_message=send_message
)
logger.info(f"商品ID关键词匹配成功: 商品{item_id} '{keyword}' -> {formatted_reply}")
return formatted_reply
except Exception as format_error:
logger.error(f"关键词回复变量替换失败: {self._safe_str(format_error)}")
# 如果变量替换失败,返回原始内容
return reply
# 2. 如果商品ID匹配失败或没有商品ID匹配没有商品ID的通用关键词
for keyword, reply, keyword_item_id in keywords:
if not keyword_item_id and keyword.lower() in send_message.lower():
# 进行变量替换
try:
formatted_reply = reply.format(
@ -774,7 +792,7 @@ class XianyuLive:
send_user_id=send_user_id,
send_message=send_message
)
logger.info(f"关键词匹配成功: '{keyword}' -> {formatted_reply}")
logger.info(f"通用关键词匹配成功: '{keyword}' -> {formatted_reply}")
return formatted_reply
except Exception as format_error:
logger.error(f"关键词回复变量替换失败: {self._safe_str(format_error)}")
@ -1164,6 +1182,13 @@ class XianyuLive:
rule = delivery_rules[0]
logger.info(f"找到匹配的发货规则: {rule['keyword']} -> {rule['card_name']} ({rule['card_type']})")
# 检查是否需要延时发货
delay_seconds = rule.get('card_delay_seconds', 0)
if delay_seconds and delay_seconds > 0:
logger.info(f"检测到延时发货设置: {delay_seconds}秒,开始延时...")
await asyncio.sleep(delay_seconds)
logger.info(f"延时发货完成,开始发送内容")
delivery_content = None
# 根据卡券类型处理发货内容
@ -2099,8 +2124,8 @@ class XianyuLive:
# 如果API回复失败或未启用API按新的优先级顺序处理
if not reply:
# 1. 首先尝试关键词匹配
reply = await self.get_keyword_reply(send_user_name, send_user_id, send_message)
# 1. 首先尝试关键词匹配传入商品ID
reply = await self.get_keyword_reply(send_user_name, send_user_id, send_message, item_id)
if reply:
reply_source = '关键词' # 标记为关键词回复
else:

View File

@ -123,6 +123,7 @@ class DBManager:
cookie_id TEXT,
keyword TEXT,
reply TEXT,
item_id TEXT,
PRIMARY KEY (cookie_id, keyword),
FOREIGN KEY (cookie_id) REFERENCES cookies(id) ON DELETE CASCADE
)
@ -195,6 +196,7 @@ class DBManager:
data_content TEXT,
description TEXT,
enabled BOOLEAN DEFAULT TRUE,
delay_seconds INTEGER DEFAULT 0,
user_id INTEGER NOT NULL DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
@ -212,6 +214,24 @@ class DBManager:
self._execute_sql(cursor, "CREATE INDEX IF NOT EXISTS idx_cards_user_id ON cards(user_id)")
logger.info("cards 表 user_id 列添加完成")
# 检查并添加 delay_seconds 列(用于自动发货延时功能)
try:
self._execute_sql(cursor, "SELECT delay_seconds FROM cards LIMIT 1")
except sqlite3.OperationalError:
# delay_seconds 列不存在,需要添加
logger.info("正在为 cards 表添加 delay_seconds 列...")
self._execute_sql(cursor, "ALTER TABLE cards ADD COLUMN delay_seconds INTEGER DEFAULT 0")
logger.info("cards 表 delay_seconds 列添加完成")
# 检查并添加 item_id 列用于自动回复商品ID功能
try:
self._execute_sql(cursor, "SELECT item_id FROM keywords LIMIT 1")
except sqlite3.OperationalError:
# item_id 列不存在,需要添加
logger.info("正在为 keywords 表添加 item_id 列...")
self._execute_sql(cursor, "ALTER TABLE keywords ADD COLUMN item_id TEXT")
logger.info("keywords 表 item_id 列添加完成")
# 创建商品信息表
cursor.execute('''
CREATE TABLE IF NOT EXISTS item_info (
@ -576,7 +596,13 @@ class DBManager:
# -------------------- 关键字操作 --------------------
def save_keywords(self, cookie_id: str, keywords: List[Tuple[str, str]]) -> bool:
"""保存关键字列表,先删除旧数据再插入新数据"""
"""保存关键字列表,先删除旧数据再插入新数据(向后兼容方法)"""
# 转换为新格式不包含item_id
keywords_with_item_id = [(keyword, reply, None) for keyword, reply in keywords]
return self.save_keywords_with_item_id(cookie_id, keywords_with_item_id)
def save_keywords_with_item_id(self, cookie_id: str, keywords: List[Tuple[str, str, str]]) -> bool:
"""保存关键字列表包含商品ID先删除旧数据再插入新数据"""
with self.lock:
try:
cursor = self.conn.cursor()
@ -585,8 +611,10 @@ class DBManager:
self._execute_sql(cursor, "DELETE FROM keywords WHERE cookie_id = ?", (cookie_id,))
# 插入新关键字
for keyword, reply in keywords:
self._execute_sql(cursor, "INSERT INTO keywords (cookie_id, keyword, reply) VALUES (?, ?, ?)", (cookie_id, keyword, reply))
for keyword, reply, item_id in keywords:
self._execute_sql(cursor,
"INSERT INTO keywords (cookie_id, keyword, reply, item_id) VALUES (?, ?, ?, ?)",
(cookie_id, keyword, reply, item_id))
self.conn.commit()
logger.info(f"关键字保存成功: {cookie_id}, {len(keywords)}")
@ -597,7 +625,7 @@ class DBManager:
return False
def get_keywords(self, cookie_id: str) -> List[Tuple[str, str]]:
"""获取指定Cookie的关键字列表"""
"""获取指定Cookie的关键字列表(向后兼容方法)"""
with self.lock:
try:
cursor = self.conn.cursor()
@ -606,6 +634,41 @@ class DBManager:
except Exception as e:
logger.error(f"获取关键字失败: {e}")
return []
def get_keywords_with_item_id(self, cookie_id: str) -> List[Tuple[str, str, str]]:
"""获取指定Cookie的关键字列表包含商品ID"""
with self.lock:
try:
cursor = self.conn.cursor()
self._execute_sql(cursor, "SELECT keyword, reply, item_id FROM keywords WHERE cookie_id = ?", (cookie_id,))
return [(row[0], row[1], row[2]) for row in cursor.fetchall()]
except Exception as e:
logger.error(f"获取关键字失败: {e}")
return []
def check_keyword_duplicate(self, cookie_id: str, keyword: str, item_id: str = None) -> bool:
"""检查关键词是否重复"""
with self.lock:
try:
cursor = self.conn.cursor()
if item_id:
# 如果有商品ID检查相同cookie_id、keyword、item_id的组合
self._execute_sql(cursor,
"SELECT COUNT(*) FROM keywords WHERE cookie_id = ? AND keyword = ? AND item_id = ?",
(cookie_id, keyword, item_id))
else:
# 如果没有商品ID检查相同cookie_id、keyword且item_id为空的组合
self._execute_sql(cursor,
"SELECT COUNT(*) FROM keywords WHERE cookie_id = ? AND keyword = ? AND (item_id IS NULL OR item_id = '')",
(cookie_id, keyword))
count = cursor.fetchone()[0]
return count > 0
except Exception as e:
logger.error(f"检查关键词重复失败: {e}")
return False
def get_all_keywords(self, user_id: int = None) -> Dict[str, List[Tuple[str, str]]]:
"""获取所有Cookie的关键字支持用户隔离"""
@ -1637,7 +1700,7 @@ class DBManager:
def create_card(self, name: str, card_type: str, api_config=None,
text_content: str = None, data_content: str = None,
description: str = None, enabled: bool = True, user_id: int = None):
description: str = None, enabled: bool = True, delay_seconds: int = 0, user_id: int = None):
"""创建新卡券"""
with self.lock:
try:
@ -1653,10 +1716,10 @@ class DBManager:
cursor = self.conn.cursor()
cursor.execute('''
INSERT INTO cards (name, type, api_config, text_content, data_content,
description, enabled, user_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
description, enabled, delay_seconds, user_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (name, card_type, api_config_str, text_content, data_content,
description, enabled, user_id))
description, enabled, delay_seconds, user_id))
self.conn.commit()
card_id = cursor.lastrowid
logger.info(f"创建卡券成功: {name} (ID: {card_id})")
@ -1673,7 +1736,7 @@ class DBManager:
if user_id is not None:
cursor.execute('''
SELECT id, name, type, api_config, text_content, data_content,
description, enabled, created_at, updated_at
description, enabled, delay_seconds, created_at, updated_at
FROM cards
WHERE user_id = ?
ORDER BY created_at DESC
@ -1681,7 +1744,7 @@ class DBManager:
else:
cursor.execute('''
SELECT id, name, type, api_config, text_content, data_content,
description, enabled, created_at, updated_at
description, enabled, delay_seconds, created_at, updated_at
FROM cards
ORDER BY created_at DESC
''')
@ -1707,8 +1770,9 @@ class DBManager:
'data_content': row[5],
'description': row[6],
'enabled': bool(row[7]),
'created_at': row[8],
'updated_at': row[9]
'delay_seconds': row[8] or 0,
'created_at': row[9],
'updated_at': row[10]
})
return cards
@ -1724,13 +1788,13 @@ class DBManager:
if user_id is not None:
cursor.execute('''
SELECT id, name, type, api_config, text_content, data_content,
description, enabled, created_at, updated_at
description, enabled, delay_seconds, created_at, updated_at
FROM cards WHERE id = ? AND user_id = ?
''', (card_id, user_id))
else:
cursor.execute('''
SELECT id, name, type, api_config, text_content, data_content,
description, enabled, created_at, updated_at
description, enabled, delay_seconds, created_at, updated_at
FROM cards WHERE id = ?
''', (card_id,))
@ -1755,8 +1819,9 @@ class DBManager:
'data_content': row[5],
'description': row[6],
'enabled': bool(row[7]),
'created_at': row[8],
'updated_at': row[9]
'delay_seconds': row[8] or 0,
'created_at': row[9],
'updated_at': row[10]
}
return None
except Exception as e:
@ -1765,7 +1830,7 @@ class DBManager:
def update_card(self, card_id: int, name: str = None, card_type: str = None,
api_config=None, text_content: str = None, data_content: str = None,
description: str = None, enabled: bool = None):
description: str = None, enabled: bool = None, delay_seconds: int = None):
"""更新卡券"""
with self.lock:
try:
@ -1805,6 +1870,9 @@ class DBManager:
if enabled is not None:
update_fields.append("enabled = ?")
params.append(enabled)
if delay_seconds is not None:
update_fields.append("delay_seconds = ?")
params.append(delay_seconds)
if not update_fields:
return True # 没有需要更新的字段
@ -1903,7 +1971,8 @@ class DBManager:
SELECT dr.id, dr.keyword, dr.card_id, dr.delivery_count, dr.enabled,
dr.description, dr.delivery_times,
c.name as card_name, c.type as card_type, c.api_config,
c.text_content, c.data_content, c.enabled as card_enabled, c.description as card_description
c.text_content, c.data_content, c.enabled as card_enabled, c.description as card_description,
c.delay_seconds as card_delay_seconds
FROM delivery_rules dr
LEFT JOIN cards c ON dr.card_id = c.id
WHERE dr.enabled = 1 AND c.enabled = 1
@ -1942,7 +2011,8 @@ class DBManager:
'card_text_content': row[10],
'card_data_content': row[11],
'card_enabled': bool(row[12]),
'card_description': row[13] # 卡券备注信息
'card_description': row[13], # 卡券备注信息
'card_delay_seconds': row[14] or 0 # 延时秒数
})
return rules

View File

@ -1,6 +1,6 @@
from fastapi import FastAPI, HTTPException, Depends, status, UploadFile, File
from fastapi.staticfiles import StaticFiles
from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse, StreamingResponse
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from pydantic import BaseModel
from typing import List, Tuple, Optional, Dict, Any
@ -12,6 +12,8 @@ import time
import json
import os
import uvicorn
import pandas as pd
import io
import cookie_manager
from db_manager import db_manager
@ -1318,6 +1320,9 @@ def remove_cookie(cid: str, current_user: Dict[str, Any] = Depends(get_current_u
class KeywordIn(BaseModel):
keywords: Dict[str, str] # key -> reply
class KeywordWithItemIdIn(BaseModel):
keywords: List[Dict[str, Any]] # [{"keyword": str, "reply": str, "item_id": str}]
@app.get("/keywords/{cid}")
def get_keywords(cid: str, current_user: Dict[str, Any] = Depends(get_current_user)):
@ -1335,6 +1340,35 @@ def get_keywords(cid: str, current_user: Dict[str, Any] = Depends(get_current_us
return cookie_manager.manager.get_keywords(cid)
@app.get("/keywords-with-item-id/{cid}")
def get_keywords_with_item_id(cid: str, current_user: Dict[str, Any] = Depends(get_current_user)):
"""获取包含商品ID的关键词列表"""
if cookie_manager.manager is None:
raise HTTPException(status_code=500, detail="CookieManager 未就绪")
# 检查cookie是否属于当前用户
user_id = current_user['user_id']
from db_manager import db_manager
user_cookies = db_manager.get_all_cookies(user_id)
if cid not in user_cookies:
raise HTTPException(status_code=403, detail="无权限访问该Cookie")
# 获取包含商品ID的关键词
keywords = db_manager.get_keywords_with_item_id(cid)
# 转换为前端需要的格式
result = []
for keyword, reply, item_id in keywords:
result.append({
"keyword": keyword,
"reply": reply,
"item_id": item_id or ""
})
return result
@app.post("/keywords/{cid}")
def update_keywords(cid: str, body: KeywordIn, current_user: Dict[str, Any] = Depends(get_current_user)):
if cookie_manager.manager is None:
@ -1357,6 +1391,263 @@ def update_keywords(cid: str, body: KeywordIn, current_user: Dict[str, Any] = De
return {"msg": "updated", "count": len(kw_list)}
@app.post("/keywords-with-item-id/{cid}")
def update_keywords_with_item_id(cid: str, body: KeywordWithItemIdIn, current_user: Dict[str, Any] = Depends(get_current_user)):
"""更新包含商品ID的关键词列表"""
if cookie_manager.manager is None:
raise HTTPException(status_code=500, detail="CookieManager 未就绪")
# 检查cookie是否属于当前用户
user_id = current_user['user_id']
from db_manager import db_manager
user_cookies = db_manager.get_all_cookies(user_id)
if cid not in user_cookies:
log_with_user('warning', f"尝试操作其他用户的Cookie关键字: {cid}", current_user)
raise HTTPException(status_code=403, detail="无权限操作该Cookie")
# 验证数据格式
keywords_to_save = []
keyword_set = set() # 用于检查当前提交的关键词中是否有重复
for kw_data in body.keywords:
keyword = kw_data.get('keyword', '').strip()
reply = kw_data.get('reply', '').strip()
item_id = kw_data.get('item_id', '').strip() or None
if not keyword or not reply:
raise HTTPException(status_code=400, detail="关键词和回复内容不能为空")
# 检查当前提交的关键词中是否有重复
keyword_key = f"{keyword}|{item_id or ''}"
if keyword_key in keyword_set:
item_id_text = f"商品ID: {item_id}" if item_id else "(通用关键词)"
raise HTTPException(status_code=400, detail=f"关键词 '{keyword}' {item_id_text} 在当前提交中重复")
keyword_set.add(keyword_key)
keywords_to_save.append((keyword, reply, item_id))
# 保存关键词
success = db_manager.save_keywords_with_item_id(cid, keywords_to_save)
if not success:
raise HTTPException(status_code=500, detail="保存关键词失败")
log_with_user('info', f"更新Cookie关键字(含商品ID): {cid}, 数量: {len(keywords_to_save)}", current_user)
return {"msg": "updated", "count": len(keywords_to_save)}
@app.get("/items/{cid}")
def get_items_list(cid: str, current_user: Dict[str, Any] = Depends(get_current_user)):
"""获取指定账号的商品列表"""
if cookie_manager.manager is None:
raise HTTPException(status_code=500, detail="CookieManager 未就绪")
# 检查cookie是否属于当前用户
user_id = current_user['user_id']
from db_manager import db_manager
user_cookies = db_manager.get_all_cookies(user_id)
if cid not in user_cookies:
raise HTTPException(status_code=403, detail="无权限访问该Cookie")
try:
# 获取该账号的所有商品
with db_manager.lock:
cursor = db_manager.conn.cursor()
cursor.execute('''
SELECT item_id, item_title, item_price, created_at
FROM item_info
WHERE cookie_id = ?
ORDER BY created_at DESC
''', (cid,))
items = []
for row in cursor.fetchall():
items.append({
'item_id': row[0],
'item_title': row[1] or '未知商品',
'item_price': row[2] or '价格未知',
'created_at': row[3]
})
return {"items": items, "count": len(items)}
except Exception as e:
logger.error(f"获取商品列表失败: {e}")
raise HTTPException(status_code=500, detail="获取商品列表失败")
@app.get("/keywords-export/{cid}")
def export_keywords(cid: str, current_user: Dict[str, Any] = Depends(get_current_user)):
"""导出指定账号的关键词为Excel文件"""
if cookie_manager.manager is None:
raise HTTPException(status_code=500, detail="CookieManager 未就绪")
# 检查cookie是否属于当前用户
user_id = current_user['user_id']
from db_manager import db_manager
user_cookies = db_manager.get_all_cookies(user_id)
if cid not in user_cookies:
raise HTTPException(status_code=403, detail="无权限访问该Cookie")
try:
# 获取关键词数据
keywords = db_manager.get_keywords_with_item_id(cid)
# 创建DataFrame
data = []
for keyword, reply, item_id in keywords:
data.append({
'关键词': keyword,
'商品ID': item_id or '',
'关键词内容': reply
})
# 如果没有数据创建空的DataFrame但保留列名作为模板
if not data:
df = pd.DataFrame(columns=['关键词', '商品ID', '关键词内容'])
else:
df = pd.DataFrame(data)
# 创建Excel文件
output = io.BytesIO()
with pd.ExcelWriter(output, engine='openpyxl') as writer:
df.to_excel(writer, sheet_name='关键词数据', index=False)
# 如果是空模板,添加一些示例说明
if data == []:
worksheet = writer.sheets['关键词数据']
# 添加示例数据作为注释从第2行开始
worksheet['A2'] = '你好'
worksheet['B2'] = ''
worksheet['C2'] = '您好!欢迎咨询,有什么可以帮助您的吗?'
worksheet['A3'] = '价格'
worksheet['B3'] = '123456'
worksheet['C3'] = '这个商品的价格是99元现在有优惠活动哦'
worksheet['A4'] = '发货'
worksheet['B4'] = ''
worksheet['C4'] = '我们会在24小时内发货请耐心等待。'
# 设置示例行的样式(浅灰色背景)
from openpyxl.styles import PatternFill
gray_fill = PatternFill(start_color='F0F0F0', end_color='F0F0F0', fill_type='solid')
for row in range(2, 5):
for col in range(1, 4):
worksheet.cell(row=row, column=col).fill = gray_fill
output.seek(0)
# 生成文件名使用URL编码处理中文
from urllib.parse import quote
if not data:
filename = f"keywords_template_{cid}_{int(time.time())}.xlsx"
else:
filename = f"keywords_{cid}_{int(time.time())}.xlsx"
encoded_filename = quote(filename.encode('utf-8'))
# 返回文件
return StreamingResponse(
io.BytesIO(output.read()),
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={
"Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}"
}
)
except Exception as e:
logger.error(f"导出关键词失败: {e}")
raise HTTPException(status_code=500, detail=f"导出关键词失败: {str(e)}")
@app.post("/keywords-import/{cid}")
async def import_keywords(cid: str, file: UploadFile = File(...), current_user: Dict[str, Any] = Depends(get_current_user)):
"""导入Excel文件中的关键词到指定账号"""
if cookie_manager.manager is None:
raise HTTPException(status_code=500, detail="CookieManager 未就绪")
# 检查cookie是否属于当前用户
user_id = current_user['user_id']
from db_manager import db_manager
user_cookies = db_manager.get_all_cookies(user_id)
if cid not in user_cookies:
raise HTTPException(status_code=403, detail="无权限访问该Cookie")
# 检查文件类型
if not file.filename.endswith(('.xlsx', '.xls')):
raise HTTPException(status_code=400, detail="请上传Excel文件(.xlsx或.xls)")
try:
# 读取Excel文件
contents = await file.read()
df = pd.read_excel(io.BytesIO(contents))
# 检查必要的列
required_columns = ['关键词', '商品ID', '关键词内容']
missing_columns = [col for col in required_columns if col not in df.columns]
if missing_columns:
raise HTTPException(status_code=400, detail=f"Excel文件缺少必要的列: {', '.join(missing_columns)}")
# 获取现有关键词
existing_keywords = db_manager.get_keywords_with_item_id(cid)
existing_dict = {}
for keyword, reply, item_id in existing_keywords:
key = f"{keyword}|{item_id or ''}"
existing_dict[key] = (keyword, reply, item_id)
# 处理导入数据
import_data = []
update_count = 0
add_count = 0
for index, row in df.iterrows():
keyword = str(row['关键词']).strip()
item_id = str(row['商品ID']).strip() if pd.notna(row['商品ID']) and str(row['商品ID']).strip() else None
reply = str(row['关键词内容']).strip()
if not keyword or not reply:
continue # 跳过空行
# 检查是否重复
key = f"{keyword}|{item_id or ''}"
if key in existing_dict:
# 更新现有关键词
update_count += 1
else:
# 新增关键词
add_count += 1
import_data.append((keyword, reply, item_id))
if not import_data:
raise HTTPException(status_code=400, detail="Excel文件中没有有效的关键词数据")
# 保存到数据库
success = db_manager.save_keywords_with_item_id(cid, import_data)
if not success:
raise HTTPException(status_code=500, detail="保存关键词到数据库失败")
log_with_user('info', f"导入关键词成功: {cid}, 新增: {add_count}, 更新: {update_count}", current_user)
return {
"msg": "导入成功",
"total": len(import_data),
"added": add_count,
"updated": update_count
}
except pd.errors.EmptyDataError:
raise HTTPException(status_code=400, detail="Excel文件为空")
except pd.errors.ParserError:
raise HTTPException(status_code=400, detail="Excel文件格式错误")
except Exception as e:
logger.error(f"导入关键词失败: {e}")
raise HTTPException(status_code=500, detail=f"导入关键词失败: {str(e)}")
# 卡券管理API
@app.get("/cards")
def get_cards(current_user: Dict[str, Any] = Depends(get_current_user)):
@ -1388,6 +1679,7 @@ def create_card(card_data: dict, current_user: Dict[str, Any] = Depends(get_curr
data_content=card_data.get('data_content'),
description=card_data.get('description'),
enabled=card_data.get('enabled', True),
delay_seconds=card_data.get('delay_seconds', 0),
user_id=user_id
)
@ -1426,7 +1718,8 @@ def update_card(card_id: int, card_data: dict, _: None = Depends(require_auth)):
text_content=card_data.get('text_content'),
data_content=card_data.get('data_content'),
description=card_data.get('description'),
enabled=card_data.get('enabled', True)
enabled=card_data.get('enabled', True),
delay_seconds=card_data.get('delay_seconds')
)
if success:
return {"message": "卡券更新成功"}

View File

@ -47,4 +47,8 @@ bcrypt>=4.0.1
python-dateutil>=2.8.2
# 正则表达式增强
regex>=2023.10.3
regex>=2023.10.3
# Excel文件处理导入导出功能
pandas>=2.0.0
openpyxl>=3.1.0

View File

@ -378,7 +378,7 @@
.keyword-input-group {
display: grid;
grid-template-columns: 1fr 2fr auto;
grid-template-columns: 1fr 2fr 1fr auto;
gap: 1rem;
align-items: end;
}
@ -399,7 +399,7 @@
z-index: 1;
}
.input-field input {
.input-field input, .input-field select {
width: 100%;
padding: 1rem 0.75rem 0.75rem;
border: 2px solid #e5e7eb;
@ -1483,8 +1483,18 @@
<i class="bi bi-chat-dots me-2"></i>
关键词管理
</h3>
<div class="account-badge" id="currentAccountBadge">
<!-- 动态显示当前账号 -->
<div class="d-flex align-items-center gap-2">
<div class="account-badge" id="currentAccountBadge">
<!-- 动态显示当前账号 -->
</div>
<div class="btn-group" role="group">
<button class="btn btn-outline-success btn-sm" onclick="exportKeywords()" title="导出关键词">
<i class="bi bi-download"></i> 导出
</button>
<button class="btn btn-outline-primary btn-sm" onclick="showImportModal()" title="导入关键词">
<i class="bi bi-upload"></i> 导入
</button>
</div>
</div>
</div>
@ -1493,12 +1503,18 @@
<div class="keyword-input-group">
<div class="input-field">
<label>关键词</label>
<input type="text" id="newKeyword" placeholder="例如:你好、价格、发货...">
<input type="text" id="newKeyword" placeholder="例如:你好">
</div>
<div class="input-field">
<label>自动回复内容</label>
<input type="text" id="newReply" placeholder="例如:您好,欢迎咨询!有什么可以帮助您的吗?">
</div>
<div class="input-field">
<label>商品ID可选</label>
<select id="newItemIdSelect" class="form-select">
<option value="">选择商品或留空表示通用关键词</option>
</select>
</div>
<button class="add-btn" onclick="addKeyword()">
<i class="bi bi-plus-lg"></i>
添加
@ -1551,6 +1567,7 @@
<th>卡券名称</th>
<th>类型</th>
<th>数据量</th>
<th>延时时间</th>
<th>状态</th>
<th>创建时间</th>
<th>操作</th>
@ -1558,7 +1575,7 @@
</thead>
<tbody id="cardsTableBody">
<tr>
<td colspan="6" class="text-center py-4 text-muted">
<td colspan="7" class="text-center py-4 text-muted">
<i class="bi bi-credit-card fs-1 d-block mb-3"></i>
<h5>暂无卡券数据</h5>
<p class="mb-0">点击"添加卡券"开始创建您的第一个卡券</p>
@ -2215,6 +2232,18 @@
</div>
</div>
<div class="mb-3">
<label class="form-label">延时发货时间</label>
<div class="input-group">
<input type="number" class="form-control" id="cardDelaySeconds" value="0" min="0" max="3600" placeholder="0">
<span class="input-group-text"></span>
</div>
<small class="form-text text-muted">
<i class="bi bi-clock me-1"></i>
设置自动发货的延时时间0表示立即发货最大3600秒(1小时)
</small>
</div>
<div class="mb-3">
<label class="form-label">备注信息</label>
<textarea class="form-control" id="cardDescription" rows="3" placeholder="可选的备注信息,支持变量替换:&#10;{DELIVERY_CONTENT} - 发货内容&#10;例如:您的卡券信息:{DELIVERY_CONTENT},请妥善保管。"></textarea>
@ -2331,8 +2360,24 @@
</div>
<div class="mb-3">
<label class="form-label">备注</label>
<textarea class="form-control" id="editCardDescription" rows="2"></textarea>
<label class="form-label">延时发货时间</label>
<div class="input-group">
<input type="number" class="form-control" id="editCardDelaySeconds" value="0" min="0" max="3600" placeholder="0">
<span class="input-group-text"></span>
</div>
<small class="form-text text-muted">
<i class="bi bi-clock me-1"></i>
设置自动发货的延时时间0表示立即发货最大3600秒(1小时)
</small>
</div>
<div class="mb-3">
<label class="form-label">备注信息</label>
<textarea class="form-control" id="editCardDescription" rows="3" placeholder="可选的备注信息,支持变量替换:&#10;{DELIVERY_CONTENT} - 发货内容&#10;例如:您的卡券信息:{DELIVERY_CONTENT},请妥善保管。"></textarea>
<small class="form-text text-muted">
<i class="bi bi-info-circle me-1"></i>
备注内容会与发货内容一起发送。使用 <code>{DELIVERY_CONTENT}</code> 变量可以在备注中插入实际的发货内容。
</small>
</div>
</form>
</div>
@ -2897,7 +2942,7 @@
console.log(`加载关键词时账号 ${accountId} 状态: enabled=${currentAccount?.enabled}, accountStatus=${accountStatus}`); // 调试信息
}
const response = await fetch(`${apiBase}/keywords/${accountId}`, {
const response = await fetch(`${apiBase}/keywords-with-item-id/${accountId}`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
@ -2907,16 +2952,16 @@
const data = await response.json();
console.log('从服务器获取的关键词数据:', data); // 调试信息
// 后端返回的是 [[keyword, reply], ...] 格式,转换为前端需要的格式
const formattedData = data.map(([keyword, reply]) => ({
keyword: keyword,
reply: reply
}));
// 后端返回的是 [{keyword, reply, item_id}, ...] 格式,直接使用
const formattedData = data;
console.log('格式化后的关键词数据:', formattedData); // 调试信息
keywordsData[accountId] = formattedData;
renderKeywordsList(formattedData);
// 加载商品列表
await loadItemsList(accountId);
// 更新账号徽章显示
updateAccountBadge(accountId, accountStatus);
@ -2962,10 +3007,50 @@
}
}
// 加载商品列表
async function loadItemsList(accountId) {
try {
const response = await fetch(`${apiBase}/items/${accountId}`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
const data = await response.json();
const items = data.items || [];
// 更新商品选择下拉框
const selectElement = document.getElementById('newItemIdSelect');
if (selectElement) {
// 清空现有选项(保留第一个默认选项)
selectElement.innerHTML = '<option value="">选择商品或留空表示通用关键词</option>';
// 添加商品选项
items.forEach(item => {
const option = document.createElement('option');
option.value = item.item_id;
option.textContent = `${item.item_id} - ${item.item_title}`;
selectElement.appendChild(option);
});
}
console.log(`加载了 ${items.length} 个商品到选择列表`);
} else {
console.warn('加载商品列表失败:', response.status);
}
} catch (error) {
console.error('加载商品列表时发生错误:', error);
}
}
// 添加或更新关键词
async function addKeyword() {
const keyword = document.getElementById('newKeyword').value.trim();
const reply = document.getElementById('newReply').value.trim();
const itemId = document.getElementById('newItemIdSelect').value.trim();
if (!keyword || !reply) {
showToast('请填写关键词和回复内容', 'warning');
@ -2992,31 +3077,42 @@
currentKeywords.splice(window.editingIndex, 1);
}
// 检查关键词是否已存在(排除编辑的原关键词)
const existingKeyword = currentKeywords.find(item => item.keyword === keyword);
if (existingKeyword && (!isEditMode || keyword !== window.originalKeyword)) {
showToast(`关键词 "${keyword}" 已存在,请使用其他关键词`, 'warning');
// 准备要保存的关键词列表
let keywordsToSave = [...currentKeywords];
// 如果是编辑模式,先移除原关键词
if (isEditMode && typeof window.editingIndex !== 'undefined') {
keywordsToSave.splice(window.editingIndex, 1);
}
// 检查关键词是否已存在考虑商品ID
const existingKeyword = keywordsToSave.find(item =>
item.keyword === keyword &&
(item.item_id || '') === (itemId || '')
);
if (existingKeyword) {
const itemIdText = itemId ? `商品ID: ${itemId}` : '(通用关键词)';
showToast(`关键词 "${keyword}" ${itemIdText} 已存在请使用其他关键词或商品ID`, 'warning');
toggleLoading(false);
return;
}
// 转换为后端期望的格式
const keywordsObj = {};
currentKeywords.forEach(item => {
keywordsObj[item.keyword] = item.reply;
});
// 添加新关键词或更新的关键词
keywordsObj[keyword] = reply;
const newKeyword = {
keyword: keyword,
reply: reply,
item_id: itemId || ''
};
keywordsToSave.push(newKeyword);
const response = await fetch(`${apiBase}/keywords/${currentCookieId}`, {
const response = await fetch(`${apiBase}/keywords-with-item-id/${currentCookieId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({
keywords: keywordsObj
keywords: keywordsToSave
})
});
@ -3026,10 +3122,14 @@
// 清空输入框并重置样式
const keywordInput = document.getElementById('newKeyword');
const replyInput = document.getElementById('newReply');
const selectElement = document.getElementById('newItemIdSelect');
const addBtn = document.querySelector('.add-btn');
keywordInput.value = '';
replyInput.value = '';
if (selectElement) {
selectElement.value = '';
}
keywordInput.style.borderColor = '#e5e7eb';
replyInput.style.borderColor = '#e5e7eb';
addBtn.style.opacity = '0.7';
@ -3105,11 +3205,17 @@
const keywordItem = document.createElement('div');
keywordItem.className = 'keyword-item';
// 商品ID显示
const itemIdDisplay = item.item_id ?
`<small class="text-muted d-block"><i class="bi bi-box"></i> 商品ID: ${item.item_id}</small>` :
'<small class="text-muted d-block"><i class="bi bi-globe"></i> 通用关键词</small>';
keywordItem.innerHTML = `
<div class="keyword-item-header">
<div class="keyword-tag">
<i class="bi bi-tag-fill"></i>
${item.keyword}
${itemIdDisplay}
</div>
<div class="keyword-actions">
<button class="action-btn edit-btn" onclick="editKeyword(${index})" title="编辑">
@ -3149,9 +3255,16 @@
document.getElementById('newKeyword').value = keyword.keyword;
document.getElementById('newReply').value = keyword.reply;
// 设置商品ID选择框
const selectElement = document.getElementById('newItemIdSelect');
if (selectElement) {
selectElement.value = keyword.item_id || '';
}
// 设置编辑模式标识
window.editingIndex = index;
window.originalKeyword = keyword.keyword;
window.originalItemId = keyword.item_id || '';
// 更新按钮文本和样式
const addBtn = document.querySelector('.add-btn');
@ -3195,9 +3308,16 @@
document.getElementById('newKeyword').value = '';
document.getElementById('newReply').value = '';
// 清空商品ID选择框
const selectElement = document.getElementById('newItemIdSelect');
if (selectElement) {
selectElement.value = '';
}
// 重置编辑状态
delete window.editingIndex;
delete window.originalKeyword;
delete window.originalItemId;
// 恢复添加按钮
const addBtn = document.querySelector('.add-btn');
@ -3227,21 +3347,15 @@
// 移除指定索引的关键词
currentKeywords.splice(index, 1);
// 转换为后端期望的格式
const keywordsObj = {};
currentKeywords.forEach(item => {
keywordsObj[item.keyword] = item.reply;
});
// 更新服务器
const response = await fetch(`${apiBase}/keywords/${cookieId}`, {
const response = await fetch(`${apiBase}/keywords-with-item-id/${cookieId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({
keywords: keywordsObj
keywords: currentKeywords
})
});
@ -4825,7 +4939,7 @@
if (cards.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="6" class="text-center py-4 text-muted">
<td colspan="7" class="text-center py-4 text-muted">
<i class="bi bi-credit-card fs-1 d-block mb-3"></i>
<h5>暂无卡券数据</h5>
<p class="mb-0">点击"添加卡券"开始创建您的第一个卡券</p>
@ -4870,6 +4984,11 @@
dataCount = '1';
}
// 延时时间显示
const delayDisplay = card.delay_seconds > 0 ?
`${card.delay_seconds}秒` :
'<span class="text-muted">立即</span>';
tr.innerHTML = `
<td>
<div class="fw-bold">${card.name}</div>
@ -4877,6 +4996,7 @@
</td>
<td>${typeBadge}</td>
<td>${dataCount}</td>
<td>${delayDisplay}</td>
<td>${statusBadge}</td>
<td>
<small class="text-muted">${new Date(card.created_at).toLocaleString('zh-CN')}</small>
@ -4945,6 +5065,7 @@
name: cardName,
type: cardType,
description: document.getElementById('cardDescription').value,
delay_seconds: parseInt(document.getElementById('cardDelaySeconds').value) || 0,
enabled: true
};
@ -5234,6 +5355,7 @@
document.getElementById('editCardName').value = card.name;
document.getElementById('editCardType').value = card.type;
document.getElementById('editCardDescription').value = card.description || '';
document.getElementById('editCardDelaySeconds').value = card.delay_seconds || 0;
document.getElementById('editCardEnabled').checked = card.enabled;
// 根据类型填充特定字段
@ -5289,6 +5411,7 @@
name: cardName,
type: cardType,
description: document.getElementById('editCardDescription').value,
delay_seconds: parseInt(document.getElementById('editCardDelaySeconds').value) || 0,
enabled: document.getElementById('editCardEnabled').checked
};
@ -6941,7 +7064,134 @@
}
}
// ==================== 导入导出功能 ====================
// 导出关键词
async function exportKeywords() {
if (!currentCookieId) {
showToast('请先选择账号', 'warning');
return;
}
try {
const response = await fetch(`${apiBase}/keywords-export/${currentCookieId}`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
// 创建下载链接
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
// 根据当前账号是否有数据来设置文件名和提示
const currentKeywords = keywordsData[currentCookieId] || [];
const hasData = currentKeywords.length > 0;
if (hasData) {
a.download = `keywords_${currentCookieId}_${new Date().getTime()}.xlsx`;
showToast('关键词导出成功!', 'success');
} else {
a.download = `keywords_template_${currentCookieId}_${new Date().getTime()}.xlsx`;
showToast('导入模板导出成功!模板中包含示例数据供参考', 'success');
}
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
} else {
const error = await response.json();
showToast(`导出失败: ${error.detail}`, 'error');
}
} catch (error) {
console.error('导出关键词失败:', error);
showToast('导出关键词失败', 'error');
}
}
// 显示导入模态框
function showImportModal() {
if (!currentCookieId) {
showToast('请先选择账号', 'warning');
return;
}
const modal = new bootstrap.Modal(document.getElementById('importKeywordsModal'));
modal.show();
}
// 导入关键词
async function importKeywords() {
if (!currentCookieId) {
showToast('请先选择账号', 'warning');
return;
}
const fileInput = document.getElementById('importFileInput');
const file = fileInput.files[0];
if (!file) {
showToast('请选择要导入的Excel文件', 'warning');
return;
}
try {
// 显示进度条
const progressDiv = document.getElementById('importProgress');
const progressBar = progressDiv.querySelector('.progress-bar');
progressDiv.style.display = 'block';
progressBar.style.width = '30%';
const formData = new FormData();
formData.append('file', file);
const response = await fetch(`${apiBase}/keywords-import/${currentCookieId}`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`
},
body: formData
});
progressBar.style.width = '70%';
if (response.ok) {
const result = await response.json();
progressBar.style.width = '100%';
setTimeout(() => {
progressDiv.style.display = 'none';
progressBar.style.width = '0%';
// 关闭模态框
const modal = bootstrap.Modal.getInstance(document.getElementById('importKeywordsModal'));
modal.hide();
// 清空文件输入
fileInput.value = '';
// 重新加载关键词列表
loadAccountKeywords(currentCookieId);
showToast(`导入成功!新增: ${result.added}, 更新: ${result.updated}`, 'success');
}, 500);
} else {
const error = await response.json();
progressDiv.style.display = 'none';
progressBar.style.width = '0%';
showToast(`导入失败: ${error.detail}`, 'error');
}
} catch (error) {
console.error('导入关键词失败:', error);
document.getElementById('importProgress').style.display = 'none';
document.querySelector('#importProgress .progress-bar').style.width = '0%';
showToast('导入关键词失败', 'error');
}
}
</script>
@ -7103,5 +7353,50 @@
</div>
</div>
<!-- 导入关键词模态框 -->
<div class="modal fade" id="importKeywordsModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="bi bi-upload me-2"></i>导入关键词
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">选择Excel文件</label>
<input type="file" class="form-control" id="importFileInput" accept=".xlsx,.xls">
<div class="form-text">
<i class="bi bi-info-circle me-1"></i>
请上传包含"关键词"、"商品ID"、"关键词内容"三列的Excel文件
</div>
</div>
<div class="alert alert-warning">
<h6><i class="bi bi-exclamation-triangle me-1"></i>导入说明:</h6>
<ul class="mb-0">
<li>Excel文件必须包含三列关键词、商品ID、关键词内容</li>
<li>商品ID可以为空表示通用关键词</li>
<li>如果关键词+商品ID组合已存在将更新关键词内容</li>
<li>导入将覆盖当前账号的所有关键词数据</li>
</ul>
</div>
<div id="importProgress" style="display: none;">
<div class="progress mb-2">
<div class="progress-bar" role="progressbar" style="width: 0%"></div>
</div>
<small class="text-muted">正在导入...</small>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" onclick="importKeywords()">
<i class="bi bi-upload me-1"></i>开始导入
</button>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -21,9 +21,38 @@ def get_js_path():
return js_path
try:
# 检查JavaScript运行时是否可用
available_runtimes = execjs.runtime_names
logger.info(f"可用的JavaScript运行时: {available_runtimes}")
# 尝试获取默认运行时
current_runtime = execjs.get()
logger.info(f"当前JavaScript运行时: {current_runtime.name}")
xianyu_js = execjs.compile(open(get_js_path(), 'r', encoding='utf-8').read())
logger.info("JavaScript文件加载成功")
except Exception as e:
raise RuntimeError(f"无法加载JavaScript文件: {e}")
error_msg = str(e)
logger.error(f"JavaScript运行时错误: {error_msg}")
if "Could not find an available JavaScript runtime" in error_msg:
logger.error("解决方案:")
logger.error("1. 确保已安装Node.js: apt-get install nodejs")
logger.error("2. 或安装其他JS运行时: apt-get install nodejs npm")
logger.error("3. 检查PATH环境变量是否包含Node.js路径")
# 尝试检测系统中的JavaScript运行时
import subprocess
try:
result = subprocess.run(['node', '--version'], capture_output=True, text=True)
if result.returncode == 0:
logger.info(f"检测到Node.js版本: {result.stdout.strip()}")
else:
logger.error("Node.js未正确安装或不在PATH中")
except FileNotFoundError:
logger.error("未找到Node.js可执行文件")
raise RuntimeError(f"无法加载JavaScript文件: {error_msg}")
def trans_cookies(cookies_str: str) -> dict:
"""将cookies字符串转换为字典"""