mirror of
https://github.com/zhinianboke/xianyu-auto-reply.git
synced 2025-08-29 09:07:36 +08:00
支持设置默认回复只回复一次
This commit is contained in:
parent
aae9d1ab46
commit
0b9b81bc4e
@ -961,8 +961,8 @@ class XianyuLive:
|
||||
except Exception as e:
|
||||
logger.error(f"调试消息结构时发生错误: {self._safe_str(e)}")
|
||||
|
||||
async def get_default_reply(self, send_user_name: str, send_user_id: str, send_message: str) -> str:
|
||||
"""获取默认回复内容,支持变量替换"""
|
||||
async def get_default_reply(self, send_user_name: str, send_user_id: str, send_message: str, chat_id: str = None) -> str:
|
||||
"""获取默认回复内容,支持变量替换和只回复一次功能"""
|
||||
try:
|
||||
from db_manager import db_manager
|
||||
|
||||
@ -973,6 +973,13 @@ class XianyuLive:
|
||||
logger.debug(f"账号 {self.cookie_id} 未启用默认回复")
|
||||
return None
|
||||
|
||||
# 检查"只回复一次"功能
|
||||
if default_reply_settings.get('reply_once', False) and chat_id:
|
||||
# 检查是否已经回复过这个chat_id
|
||||
if db_manager.has_default_reply_record(self.cookie_id, chat_id):
|
||||
logger.info(f"【{self.cookie_id}】chat_id {chat_id} 已使用过默认回复,跳过(只回复一次)")
|
||||
return None
|
||||
|
||||
reply_content = default_reply_settings.get('reply_content', '')
|
||||
if not reply_content:
|
||||
logger.warning(f"账号 {self.cookie_id} 默认回复内容为空")
|
||||
@ -985,6 +992,12 @@ class XianyuLive:
|
||||
send_user_id=send_user_id,
|
||||
send_message=send_message
|
||||
)
|
||||
|
||||
# 如果开启了"只回复一次"功能,记录这次回复
|
||||
if default_reply_settings.get('reply_once', False) and chat_id:
|
||||
db_manager.add_default_reply_record(self.cookie_id, chat_id)
|
||||
logger.info(f"【{self.cookie_id}】记录默认回复: chat_id={chat_id}")
|
||||
|
||||
logger.info(f"【{self.cookie_id}】使用默认回复: {formatted_reply}")
|
||||
return formatted_reply
|
||||
except Exception as format_error:
|
||||
@ -2896,7 +2909,7 @@ class XianyuLive:
|
||||
reply_source = 'AI' # 标记为AI回复
|
||||
else:
|
||||
# 3. 最后使用默认回复
|
||||
reply = await self.get_default_reply(send_user_name, send_user_id, send_message)
|
||||
reply = await self.get_default_reply(send_user_name, send_user_id, send_message, chat_id)
|
||||
reply_source = '默认' # 标记为默认回复
|
||||
|
||||
# 注意:这里只有商品ID,没有标题和详情,根据新的规则不保存到数据库
|
||||
|
@ -281,12 +281,34 @@ class DBManager:
|
||||
cookie_id TEXT PRIMARY KEY,
|
||||
enabled BOOLEAN DEFAULT FALSE,
|
||||
reply_content TEXT,
|
||||
reply_once BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (cookie_id) REFERENCES cookies(id) ON DELETE CASCADE
|
||||
)
|
||||
''')
|
||||
|
||||
# 添加 reply_once 字段(如果不存在)
|
||||
try:
|
||||
cursor.execute('ALTER TABLE default_replies ADD COLUMN reply_once BOOLEAN DEFAULT FALSE')
|
||||
self.conn.commit()
|
||||
logger.info("已添加 reply_once 字段到 default_replies 表")
|
||||
except sqlite3.OperationalError as e:
|
||||
if "duplicate column name" not in str(e).lower():
|
||||
logger.warning(f"添加 reply_once 字段失败: {e}")
|
||||
|
||||
# 创建默认回复记录表(记录已回复的chat_id)
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS default_reply_records (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
cookie_id TEXT NOT NULL,
|
||||
chat_id TEXT NOT NULL,
|
||||
replied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(cookie_id, chat_id),
|
||||
FOREIGN KEY (cookie_id) REFERENCES cookies(id) ON DELETE CASCADE
|
||||
)
|
||||
''')
|
||||
|
||||
# 创建通知渠道表
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS notification_channels (
|
||||
@ -1588,17 +1610,17 @@ class DBManager:
|
||||
return {}
|
||||
|
||||
# -------------------- 默认回复操作 --------------------
|
||||
def save_default_reply(self, cookie_id: str, enabled: bool, reply_content: str = None):
|
||||
def save_default_reply(self, cookie_id: str, enabled: bool, reply_content: str = None, reply_once: bool = False):
|
||||
"""保存默认回复设置"""
|
||||
with self.lock:
|
||||
try:
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute('''
|
||||
INSERT OR REPLACE INTO default_replies (cookie_id, enabled, reply_content, updated_at)
|
||||
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
|
||||
''', (cookie_id, enabled, reply_content))
|
||||
INSERT OR REPLACE INTO default_replies (cookie_id, enabled, reply_content, reply_once, updated_at)
|
||||
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||
''', (cookie_id, enabled, reply_content, reply_once))
|
||||
self.conn.commit()
|
||||
logger.debug(f"保存默认回复设置: {cookie_id} -> {'启用' if enabled else '禁用'}")
|
||||
logger.debug(f"保存默认回复设置: {cookie_id} -> {'启用' if enabled else '禁用'}, 只回复一次: {'是' if reply_once else '否'}")
|
||||
except Exception as e:
|
||||
logger.error(f"保存默认回复设置失败: {e}")
|
||||
raise
|
||||
@ -1609,14 +1631,15 @@ class DBManager:
|
||||
try:
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute('''
|
||||
SELECT enabled, reply_content FROM default_replies WHERE cookie_id = ?
|
||||
SELECT enabled, reply_content, reply_once FROM default_replies WHERE cookie_id = ?
|
||||
''', (cookie_id,))
|
||||
result = cursor.fetchone()
|
||||
if result:
|
||||
enabled, reply_content = result
|
||||
enabled, reply_content, reply_once = result
|
||||
return {
|
||||
'enabled': bool(enabled),
|
||||
'reply_content': reply_content or ''
|
||||
'reply_content': reply_content or '',
|
||||
'reply_once': bool(reply_once) if reply_once is not None else False
|
||||
}
|
||||
return None
|
||||
except Exception as e:
|
||||
@ -1628,14 +1651,15 @@ class DBManager:
|
||||
with self.lock:
|
||||
try:
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute('SELECT cookie_id, enabled, reply_content FROM default_replies')
|
||||
cursor.execute('SELECT cookie_id, enabled, reply_content, reply_once FROM default_replies')
|
||||
|
||||
result = {}
|
||||
for row in cursor.fetchall():
|
||||
cookie_id, enabled, reply_content = row
|
||||
cookie_id, enabled, reply_content, reply_once = row
|
||||
result[cookie_id] = {
|
||||
'enabled': bool(enabled),
|
||||
'reply_content': reply_content or ''
|
||||
'reply_content': reply_content or '',
|
||||
'reply_once': bool(reply_once) if reply_once is not None else False
|
||||
}
|
||||
|
||||
return result
|
||||
@ -1643,6 +1667,45 @@ class DBManager:
|
||||
logger.error(f"获取所有默认回复设置失败: {e}")
|
||||
return {}
|
||||
|
||||
def add_default_reply_record(self, cookie_id: str, chat_id: str):
|
||||
"""记录已回复的chat_id"""
|
||||
with self.lock:
|
||||
try:
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute('''
|
||||
INSERT OR IGNORE INTO default_reply_records (cookie_id, chat_id)
|
||||
VALUES (?, ?)
|
||||
''', (cookie_id, chat_id))
|
||||
self.conn.commit()
|
||||
logger.debug(f"记录默认回复: {cookie_id} -> {chat_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"记录默认回复失败: {e}")
|
||||
|
||||
def has_default_reply_record(self, cookie_id: str, chat_id: str) -> bool:
|
||||
"""检查是否已经回复过该chat_id"""
|
||||
with self.lock:
|
||||
try:
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute('''
|
||||
SELECT 1 FROM default_reply_records WHERE cookie_id = ? AND chat_id = ?
|
||||
''', (cookie_id, chat_id))
|
||||
result = cursor.fetchone()
|
||||
return result is not None
|
||||
except Exception as e:
|
||||
logger.error(f"检查默认回复记录失败: {e}")
|
||||
return False
|
||||
|
||||
def clear_default_reply_records(self, cookie_id: str):
|
||||
"""清空指定账号的默认回复记录"""
|
||||
with self.lock:
|
||||
try:
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute('DELETE FROM default_reply_records WHERE cookie_id = ?', (cookie_id,))
|
||||
self.conn.commit()
|
||||
logger.debug(f"清空默认回复记录: {cookie_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"清空默认回复记录失败: {e}")
|
||||
|
||||
def delete_default_reply(self, cookie_id: str) -> bool:
|
||||
"""删除指定账号的默认回复设置"""
|
||||
with self.lock:
|
||||
|
@ -858,13 +858,22 @@ async def register(request: RegisterRequest):
|
||||
@app.post("/xianyu/reply", response_model=ResponseModel)
|
||||
async def xianyu_reply(req: RequestModel):
|
||||
msg_template = match_reply(req.cookie_id, req.send_message)
|
||||
is_default_reply = False
|
||||
|
||||
if not msg_template:
|
||||
# 从数据库获取默认回复
|
||||
from db_manager import db_manager
|
||||
default_reply_settings = db_manager.get_default_reply(req.cookie_id)
|
||||
|
||||
if default_reply_settings and default_reply_settings.get('enabled', False):
|
||||
# 检查是否开启了"只回复一次"功能
|
||||
if default_reply_settings.get('reply_once', False):
|
||||
# 检查是否已经回复过这个chat_id
|
||||
if db_manager.has_default_reply_record(req.cookie_id, req.chat_id):
|
||||
raise HTTPException(status_code=404, detail="该对话已使用默认回复,不再重复回复")
|
||||
|
||||
msg_template = default_reply_settings.get('reply_content', '')
|
||||
is_default_reply = True
|
||||
|
||||
# 如果数据库中没有设置或为空,返回错误
|
||||
if not msg_template:
|
||||
@ -881,6 +890,13 @@ async def xianyu_reply(req: RequestModel):
|
||||
# 如果格式化失败,返回原始内容
|
||||
send_msg = msg_template
|
||||
|
||||
# 如果是默认回复且开启了"只回复一次",记录回复记录
|
||||
if is_default_reply:
|
||||
from db_manager import db_manager
|
||||
default_reply_settings = db_manager.get_default_reply(req.cookie_id)
|
||||
if default_reply_settings and default_reply_settings.get('reply_once', False):
|
||||
db_manager.add_default_reply_record(req.cookie_id, req.chat_id)
|
||||
|
||||
return {"code": 200, "data": {"send_msg": send_msg}}
|
||||
|
||||
# ------------------------- 账号 / 关键字管理接口 -------------------------
|
||||
@ -898,6 +914,7 @@ class CookieStatusIn(BaseModel):
|
||||
class DefaultReplyIn(BaseModel):
|
||||
enabled: bool
|
||||
reply_content: Optional[str] = None
|
||||
reply_once: bool = False
|
||||
|
||||
|
||||
class NotificationChannelIn(BaseModel):
|
||||
@ -1179,7 +1196,7 @@ def get_default_reply(cid: str, current_user: Dict[str, Any] = Depends(get_curre
|
||||
result = db_manager.get_default_reply(cid)
|
||||
if result is None:
|
||||
# 如果没有设置,返回默认值
|
||||
return {'enabled': False, 'reply_content': ''}
|
||||
return {'enabled': False, 'reply_content': '', 'reply_once': False}
|
||||
return result
|
||||
except HTTPException:
|
||||
raise
|
||||
@ -1199,8 +1216,8 @@ def update_default_reply(cid: str, reply_data: DefaultReplyIn, current_user: Dic
|
||||
if cid not in user_cookies:
|
||||
raise HTTPException(status_code=403, detail="无权限操作该Cookie")
|
||||
|
||||
db_manager.save_default_reply(cid, reply_data.enabled, reply_data.reply_content)
|
||||
return {'msg': 'default reply updated', 'enabled': reply_data.enabled}
|
||||
db_manager.save_default_reply(cid, reply_data.enabled, reply_data.reply_content, reply_data.reply_once)
|
||||
return {'msg': 'default reply updated', 'enabled': reply_data.enabled, 'reply_once': reply_data.reply_once}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
@ -1247,6 +1264,26 @@ def delete_default_reply(cid: str, current_user: Dict[str, Any] = Depends(get_cu
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.post('/default-replies/{cid}/clear-records')
|
||||
def clear_default_reply_records(cid: str, current_user: Dict[str, Any] = Depends(get_current_user)):
|
||||
"""清空指定账号的默认回复记录"""
|
||||
from db_manager import db_manager
|
||||
try:
|
||||
# 检查cookie是否属于当前用户
|
||||
user_id = current_user['user_id']
|
||||
user_cookies = db_manager.get_all_cookies(user_id)
|
||||
|
||||
if cid not in user_cookies:
|
||||
raise HTTPException(status_code=403, detail="无权限操作该Cookie")
|
||||
|
||||
db_manager.clear_default_reply_records(cid)
|
||||
return {'msg': 'default reply records cleared'}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
# ------------------------- 通知渠道管理接口 -------------------------
|
||||
|
||||
@app.get('/notification-channels')
|
||||
|
@ -1888,6 +1888,8 @@
|
||||
<code>{send_user_name}</code> 用户昵称、
|
||||
<code>{send_user_id}</code> 用户ID、
|
||||
<code>{send_message}</code> 用户消息
|
||||
<br><br>
|
||||
<strong>只回复一次:</strong>开启后,每个对话只会触发一次默认回复,避免重复回复同一用户。
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
@ -1895,8 +1897,9 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 15%">账号ID</th>
|
||||
<th style="width: 15%">状态</th>
|
||||
<th style="width: 50%">默认回复内容</th>
|
||||
<th style="width: 12%">状态</th>
|
||||
<th style="width: 12%">只回复一次</th>
|
||||
<th style="width: 41%">默认回复内容</th>
|
||||
<th style="width: 20%">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -1946,6 +1949,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="editReplyOnce" />
|
||||
<label class="form-check-label" for="editReplyOnce">
|
||||
只回复一次
|
||||
</label>
|
||||
<div class="form-text">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
开启后,每个对话只会触发一次默认回复,避免重复回复
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3" id="editReplyContentGroup">
|
||||
<label for="editReplyContent" class="form-label">默认回复内容</label>
|
||||
<textarea class="form-control" id="editReplyContent" rows="4"
|
||||
|
@ -1864,7 +1864,7 @@ function renderDefaultRepliesList(accounts, defaultReplies) {
|
||||
if (accounts.length === 0) {
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="4" class="text-center py-4 text-muted">
|
||||
<td colspan="5" class="text-center py-4 text-muted">
|
||||
<i class="bi bi-chat-text fs-1 d-block mb-3"></i>
|
||||
<h5>暂无账号数据</h5>
|
||||
<p class="mb-0">请先添加账号</p>
|
||||
@ -1875,7 +1875,7 @@ function renderDefaultRepliesList(accounts, defaultReplies) {
|
||||
}
|
||||
|
||||
accounts.forEach(accountId => {
|
||||
const replySettings = defaultReplies[accountId] || { enabled: false, reply_content: '' };
|
||||
const replySettings = defaultReplies[accountId] || { enabled: false, reply_content: '', reply_once: false };
|
||||
const tr = document.createElement('tr');
|
||||
|
||||
// 状态标签
|
||||
@ -1883,6 +1883,11 @@ function renderDefaultRepliesList(accounts, defaultReplies) {
|
||||
'<span class="badge bg-success">启用</span>' :
|
||||
'<span class="badge bg-secondary">禁用</span>';
|
||||
|
||||
// 只回复一次标签
|
||||
const replyOnceBadge = replySettings.reply_once ?
|
||||
'<span class="badge bg-warning">是</span>' :
|
||||
'<span class="badge bg-light text-dark">否</span>';
|
||||
|
||||
// 回复内容预览
|
||||
let contentPreview = replySettings.reply_content || '未设置';
|
||||
if (contentPreview.length > 50) {
|
||||
@ -1894,6 +1899,7 @@ function renderDefaultRepliesList(accounts, defaultReplies) {
|
||||
<strong class="text-primary">${accountId}</strong>
|
||||
</td>
|
||||
<td>${statusBadge}</td>
|
||||
<td>${replyOnceBadge}</td>
|
||||
<td>
|
||||
<div class="text-truncate" style="max-width: 300px;" title="${replySettings.reply_content || ''}">
|
||||
${contentPreview}
|
||||
@ -1907,6 +1913,11 @@ function renderDefaultRepliesList(accounts, defaultReplies) {
|
||||
<button class="btn btn-sm btn-outline-info" onclick="testDefaultReply('${accountId}')" title="测试">
|
||||
<i class="bi bi-play"></i>
|
||||
</button>
|
||||
${replySettings.reply_once ? `
|
||||
<button class="btn btn-sm btn-outline-warning" onclick="clearDefaultReplyRecords('${accountId}')" title="清空记录">
|
||||
<i class="bi bi-arrow-clockwise"></i>
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
</td>
|
||||
`;
|
||||
@ -1925,7 +1936,7 @@ async function editDefaultReply(accountId) {
|
||||
}
|
||||
});
|
||||
|
||||
let settings = { enabled: false, reply_content: '' };
|
||||
let settings = { enabled: false, reply_content: '', reply_once: false };
|
||||
if (response.ok) {
|
||||
settings = await response.json();
|
||||
}
|
||||
@ -1935,6 +1946,7 @@ async function editDefaultReply(accountId) {
|
||||
document.getElementById('editAccountIdDisplay').value = accountId;
|
||||
document.getElementById('editDefaultReplyEnabled').checked = settings.enabled;
|
||||
document.getElementById('editReplyContent').value = settings.reply_content || '';
|
||||
document.getElementById('editReplyOnce').checked = settings.reply_once || false;
|
||||
|
||||
// 根据启用状态显示/隐藏内容输入框
|
||||
toggleReplyContentVisibility();
|
||||
@ -1961,6 +1973,7 @@ async function saveDefaultReply() {
|
||||
const accountId = document.getElementById('editAccountId').value;
|
||||
const enabled = document.getElementById('editDefaultReplyEnabled').checked;
|
||||
const replyContent = document.getElementById('editReplyContent').value;
|
||||
const replyOnce = document.getElementById('editReplyOnce').checked;
|
||||
|
||||
if (enabled && !replyContent.trim()) {
|
||||
showToast('启用默认回复时必须设置回复内容', 'warning');
|
||||
@ -1969,7 +1982,8 @@ async function saveDefaultReply() {
|
||||
|
||||
const data = {
|
||||
enabled: enabled,
|
||||
reply_content: enabled ? replyContent : null
|
||||
reply_content: enabled ? replyContent : null,
|
||||
reply_once: replyOnce
|
||||
};
|
||||
|
||||
const response = await fetch(`${apiBase}/default-replies/${accountId}`, {
|
||||
@ -2001,6 +2015,34 @@ function testDefaultReply(accountId) {
|
||||
showToast('测试功能开发中...', 'info');
|
||||
}
|
||||
|
||||
// 清空默认回复记录
|
||||
async function clearDefaultReplyRecords(accountId) {
|
||||
if (!confirm(`确定要清空账号 "${accountId}" 的默认回复记录吗?\n\n清空后,该账号将可以重新对之前回复过的对话进行默认回复。`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${apiBase}/default-replies/${accountId}/clear-records`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${authToken}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showToast(`账号 "${accountId}" 的默认回复记录已清空`, 'success');
|
||||
loadDefaultReplies(); // 刷新列表
|
||||
} else {
|
||||
const error = await response.text();
|
||||
showToast(`清空失败: ${error}`, 'danger');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('清空默认回复记录失败:', error);
|
||||
showToast('清空默认回复记录失败', 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== AI回复配置相关函数 ====================
|
||||
|
||||
// 配置AI回复
|
||||
|
Loading…
x
Reference in New Issue
Block a user