支持设置默认回复只回复一次

This commit is contained in:
zhinianboke 2025-08-06 10:26:57 +08:00
parent aae9d1ab46
commit 0b9b81bc4e
5 changed files with 194 additions and 23 deletions

View File

@ -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没有标题和详情根据新的规则不保存到数据库

View File

@ -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:

View File

@ -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')

View File

@ -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"

View File

@ -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回复