mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-07-17 11:07:11 +08:00
refactor(web-console): polish message actions on bubbles after #2865
This commit is contained in:
@@ -509,6 +509,65 @@ class ConversationStore:
|
|||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
def get_latest_pair_seqs(self, session_id: str) -> Dict[str, Optional[int]]:
|
||||||
|
"""Return the seq numbers of the latest visible user message and the
|
||||||
|
latest assistant message in a session.
|
||||||
|
|
||||||
|
A "visible" user message is one whose content is real user text
|
||||||
|
(not just a tool_result block), so tool-execution turns do not
|
||||||
|
shadow the actual user query.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with keys ``user_seq`` and ``bot_seq``; either may be None
|
||||||
|
when no matching message exists.
|
||||||
|
"""
|
||||||
|
result: Dict[str, Optional[int]] = {"user_seq": None, "bot_seq": None}
|
||||||
|
with self._lock:
|
||||||
|
conn = self._connect()
|
||||||
|
try:
|
||||||
|
# Latest assistant message (cheap: single row by seq DESC).
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT seq FROM messages "
|
||||||
|
"WHERE session_id = ? AND role = 'assistant' "
|
||||||
|
"ORDER BY seq DESC LIMIT 1",
|
||||||
|
(session_id,),
|
||||||
|
).fetchone()
|
||||||
|
if row:
|
||||||
|
result["bot_seq"] = int(row[0])
|
||||||
|
|
||||||
|
# Latest visible user message: scan recent user rows and
|
||||||
|
# skip pure tool_result entries.
|
||||||
|
rows = conn.execute(
|
||||||
|
"SELECT seq, content FROM messages "
|
||||||
|
"WHERE session_id = ? AND role = 'user' "
|
||||||
|
"ORDER BY seq DESC LIMIT 20",
|
||||||
|
(session_id,),
|
||||||
|
).fetchall()
|
||||||
|
for seq, content_raw in rows:
|
||||||
|
try:
|
||||||
|
content = json.loads(content_raw)
|
||||||
|
except Exception:
|
||||||
|
result["user_seq"] = int(seq)
|
||||||
|
break
|
||||||
|
if isinstance(content, list):
|
||||||
|
has_text = any(
|
||||||
|
isinstance(b, dict) and b.get("type") == "text"
|
||||||
|
for b in content
|
||||||
|
)
|
||||||
|
has_tool_result = any(
|
||||||
|
isinstance(b, dict) and b.get("type") == "tool_result"
|
||||||
|
for b in content
|
||||||
|
)
|
||||||
|
if has_text and not has_tool_result:
|
||||||
|
result["user_seq"] = int(seq)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
result["user_seq"] = int(seq)
|
||||||
|
break
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
return result
|
||||||
|
|
||||||
def clear_session(self, session_id: str) -> None:
|
def clear_session(self, session_id: str) -> None:
|
||||||
"""Delete all messages and the session record for a given session_id."""
|
"""Delete all messages and the session record for a given session_id."""
|
||||||
with self._lock:
|
with self._lock:
|
||||||
|
|||||||
@@ -383,7 +383,49 @@ class AgentBridge:
|
|||||||
"""Initialize agent for a specific session"""
|
"""Initialize agent for a specific session"""
|
||||||
agent = self.initializer.initialize_agent(session_id=session_id)
|
agent = self.initializer.initialize_agent(session_id=session_id)
|
||||||
self.agents[session_id] = agent
|
self.agents[session_id] = agent
|
||||||
|
|
||||||
|
def sync_session_messages_from_store(self, session_id: str) -> int:
|
||||||
|
"""Reload an agent's in-memory ``messages`` list from the persistent
|
||||||
|
conversation store.
|
||||||
|
|
||||||
|
Used after an external mutation (e.g. user edits / deletes a message
|
||||||
|
via the web console) so the agent's next turn sees the same history
|
||||||
|
as the database. The operation is a no-op when the agent has not been
|
||||||
|
instantiated yet for the session.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Number of messages now held in the agent's memory. Returns -1 if
|
||||||
|
the agent does not exist or has no compatible ``messages`` attr.
|
||||||
|
"""
|
||||||
|
if not session_id or session_id not in self.agents:
|
||||||
|
return -1
|
||||||
|
agent = self.agents[session_id]
|
||||||
|
if not (hasattr(agent, "messages") and hasattr(agent, "messages_lock")):
|
||||||
|
return -1
|
||||||
|
try:
|
||||||
|
from agent.memory import get_conversation_store
|
||||||
|
store = get_conversation_store()
|
||||||
|
# No turn cap here: we want a faithful mirror of what the store
|
||||||
|
# has for this session after deletion.
|
||||||
|
remaining = store.load_messages(session_id, max_turns=10**6)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(
|
||||||
|
f"[AgentBridge] Failed to load messages for sync (session={session_id}): {e}"
|
||||||
|
)
|
||||||
|
return -1
|
||||||
|
with agent.messages_lock:
|
||||||
|
agent.messages.clear()
|
||||||
|
for msg in remaining:
|
||||||
|
agent.messages.append({
|
||||||
|
"role": msg["role"],
|
||||||
|
"content": msg["content"],
|
||||||
|
})
|
||||||
|
count = len(agent.messages)
|
||||||
|
logger.info(
|
||||||
|
f"[AgentBridge] Synced agent memory for session={session_id}, messages={count}"
|
||||||
|
)
|
||||||
|
return count
|
||||||
|
|
||||||
def agent_reply(self, query: str, context: Context = None,
|
def agent_reply(self, query: str, context: Context = None,
|
||||||
on_event=None, clear_history: bool = False) -> Reply:
|
on_event=None, clear_history: bool = False) -> Reply:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1476,6 +1476,11 @@
|
|||||||
/* =====================================================================
|
/* =====================================================================
|
||||||
Drag and Drop Overlay
|
Drag and Drop Overlay
|
||||||
===================================================================== */
|
===================================================================== */
|
||||||
|
/* Anchor the absolutely-positioned overlay to the chat view. */
|
||||||
|
#view-chat {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
.drag-overlay {
|
.drag-overlay {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -1554,8 +1559,7 @@
|
|||||||
|
|
||||||
.user-message-group:hover .edit-msg-btn,
|
.user-message-group:hover .edit-msg-btn,
|
||||||
.user-message-group:hover .delete-msg-btn,
|
.user-message-group:hover .delete-msg-btn,
|
||||||
.flex.gap-3:hover .regenerate-msg-btn,
|
.bot-message-group:hover .regenerate-msg-btn {
|
||||||
.flex.gap-3:hover .delete-msg-btn {
|
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1180,68 +1180,40 @@ messagesDiv.addEventListener('click', (e) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete message
|
// Delete message (user bubble only; bot bubbles intentionally lack a
|
||||||
|
// delete button — removing only the bot reply would leave an orphan
|
||||||
|
// user message that breaks LLM context alternation).
|
||||||
const deleteBtn = e.target.closest('.delete-msg-btn');
|
const deleteBtn = e.target.closest('.delete-msg-btn');
|
||||||
if (deleteBtn) {
|
if (deleteBtn) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const userMsgEl = deleteBtn.closest('.user-message-group');
|
const userMsgEl = deleteBtn.closest('.user-message-group');
|
||||||
const botMsgEl = deleteBtn.closest('.flex.gap-3:not(.user-message-group)');
|
if (!userMsgEl) return;
|
||||||
|
|
||||||
if (userMsgEl) {
|
showConfirmModal(t('delete_message_title'), t('delete_message_confirm'), () => {
|
||||||
showConfirmModal(t('delete_message_title'), t('delete_message_confirm'), () => {
|
// Find the next bot reply for this turn (skip non-message nodes).
|
||||||
let nextSibling = userMsgEl.nextElementSibling;
|
let botReplyEl = null;
|
||||||
let botReplyEl = null;
|
let sibling = userMsgEl.nextElementSibling;
|
||||||
while (nextSibling) {
|
while (sibling) {
|
||||||
if (nextSibling.classList && nextSibling.classList.contains('flex') && nextSibling.classList.contains('gap-3') && !nextSibling.classList.contains('user-message-group')) {
|
if (sibling.classList && sibling.classList.contains('bot-message-group')) {
|
||||||
botReplyEl = nextSibling;
|
botReplyEl = sibling;
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
nextSibling = nextSibling.nextElementSibling;
|
|
||||||
}
|
}
|
||||||
userMsgEl.remove();
|
sibling = sibling.nextElementSibling;
|
||||||
if (botReplyEl) botReplyEl.remove();
|
}
|
||||||
|
userMsgEl.remove();
|
||||||
const userSeq = userMsgEl.dataset.seq;
|
if (botReplyEl) botReplyEl.remove();
|
||||||
if (userSeq) {
|
|
||||||
fetch('/api/messages/delete', {
|
const userSeq = userMsgEl.dataset.seq;
|
||||||
method: 'POST',
|
if (userSeq) {
|
||||||
headers: { 'Content-Type': 'application/json' },
|
fetch('/api/messages/delete', {
|
||||||
body: JSON.stringify({ session_id: sessionId, user_seq: parseInt(userSeq) })
|
method: 'POST',
|
||||||
}).then(r => r.json()).then(data => {
|
headers: { 'Content-Type': 'application/json' },
|
||||||
if (data.status === 'success') console.log(`Deleted ${data.deleted} messages`);
|
body: JSON.stringify({ session_id: sessionId, user_seq: parseInt(userSeq) })
|
||||||
}).catch(err => console.error('Failed to delete:', err));
|
}).then(r => r.json()).then(data => {
|
||||||
}
|
if (data.status === 'success') console.log(`Deleted ${data.deleted} messages`);
|
||||||
});
|
}).catch(err => console.error('Failed to delete:', err));
|
||||||
} else if (botMsgEl) {
|
}
|
||||||
showConfirmModal(t('delete_message_title'), t('delete_message_confirm'), () => {
|
});
|
||||||
// Find the preceding user message to get its seq
|
|
||||||
let prevUserEl = botMsgEl.previousElementSibling;
|
|
||||||
while (prevUserEl && !prevUserEl.classList.contains('user-message-group')) {
|
|
||||||
prevUserEl = prevUserEl.previousElementSibling;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete from database (keep user message, only delete bot reply)
|
|
||||||
if (prevUserEl) {
|
|
||||||
const userSeq = prevUserEl.dataset.seq;
|
|
||||||
if (userSeq) {
|
|
||||||
fetch('/api/messages/delete', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
session_id: sessionId,
|
|
||||||
user_seq: parseInt(userSeq),
|
|
||||||
delete_user: false
|
|
||||||
})
|
|
||||||
}).then(r => r.json()).then(data => {
|
|
||||||
if (data.status === 'success') console.log(`Deleted ${data.deleted} bot reply messages`);
|
|
||||||
}).catch(err => console.error('Failed to delete bot reply:', err));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove from DOM
|
|
||||||
botMsgEl.remove();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2028,17 +2000,25 @@ async function editUserMessage(msgEl) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find all subsequent messages (this message and everything after it)
|
// Remove this message bubble and every later bubble that belongs to
|
||||||
|
// this or a subsequent turn. We mirror the backend cascade contract:
|
||||||
|
// anything with a data-seq >= current seq, plus any live SSE bubble
|
||||||
|
// that is still being streamed (no seq yet) after this point.
|
||||||
|
const currentSeqNum = userSeq ? parseInt(userSeq) : null;
|
||||||
const messagesToRemove = [];
|
const messagesToRemove = [];
|
||||||
let current = msgEl;
|
let current = msgEl;
|
||||||
while (current) {
|
while (current) {
|
||||||
if (current.classList && (current.classList.contains('user-message-group') || current.classList.contains('flex'))) {
|
if (current.classList && (current.classList.contains('user-message-group') || current.classList.contains('bot-message-group'))) {
|
||||||
messagesToRemove.push(current);
|
const seqAttr = current.dataset.seq;
|
||||||
|
if (seqAttr === undefined || seqAttr === '') {
|
||||||
|
// Live message without a persisted seq yet — treat as later.
|
||||||
|
messagesToRemove.push(current);
|
||||||
|
} else if (currentSeqNum === null || parseInt(seqAttr) >= currentSeqNum) {
|
||||||
|
messagesToRemove.push(current);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
current = current.nextElementSibling;
|
current = current.nextElementSibling;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove all messages from this one onwards
|
|
||||||
messagesToRemove.forEach(el => {
|
messagesToRemove.forEach(el => {
|
||||||
if (el && el.parentNode) el.parentNode.removeChild(el);
|
if (el && el.parentNode) el.parentNode.removeChild(el);
|
||||||
});
|
});
|
||||||
@@ -2266,8 +2246,10 @@ function startSSE(requestId, loadingEl, timestamp, titleInfo) {
|
|||||||
if (botEl) return;
|
if (botEl) return;
|
||||||
if (loadingEl) { loadingEl.remove(); loadingEl = null; }
|
if (loadingEl) { loadingEl.remove(); loadingEl = null; }
|
||||||
botEl = document.createElement('div');
|
botEl = document.createElement('div');
|
||||||
botEl.className = 'flex gap-3 px-4 sm:px-6 py-3';
|
botEl.className = 'flex gap-3 px-4 sm:px-6 py-3 bot-message-group';
|
||||||
botEl.dataset.requestId = requestId;
|
botEl.dataset.requestId = requestId;
|
||||||
|
// Regenerate button starts hidden; it's revealed in the "done"
|
||||||
|
// event handler once seq metadata arrives from the backend.
|
||||||
botEl.innerHTML = `
|
botEl.innerHTML = `
|
||||||
<img src="assets/logo.jpg" alt="CowAgent" class="w-8 h-8 rounded-lg flex-shrink-0">
|
<img src="assets/logo.jpg" alt="CowAgent" class="w-8 h-8 rounded-lg flex-shrink-0">
|
||||||
<div class="min-w-0 flex-1 max-w-[85%]">
|
<div class="min-w-0 flex-1 max-w-[85%]">
|
||||||
@@ -2285,6 +2267,9 @@ function startSSE(requestId, loadingEl, timestamp, titleInfo) {
|
|||||||
<button class="speak-msg-btn text-xs text-slate-300 dark:text-slate-600 hover:text-slate-500 dark:hover:text-slate-400 transition-colors cursor-pointer" title="${t('speak_msg')}" style="display:none;">
|
<button class="speak-msg-btn text-xs text-slate-300 dark:text-slate-600 hover:text-slate-500 dark:hover:text-slate-400 transition-colors cursor-pointer" title="${t('speak_msg')}" style="display:none;">
|
||||||
<i class="fas fa-volume-up"></i>
|
<i class="fas fa-volume-up"></i>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="regenerate-msg-btn text-xs text-slate-300 dark:text-slate-600 hover:text-primary-400 dark:hover:text-primary-400 transition-colors cursor-pointer" title="${t('regenerate_response')}" style="display:none;">
|
||||||
|
<i class="fas fa-rotate-right"></i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -2534,6 +2519,29 @@ function startSSE(requestId, loadingEl, timestamp, titleInfo) {
|
|||||||
if (copyBtn && finalText) copyBtn.style.display = '';
|
if (copyBtn && finalText) copyBtn.style.display = '';
|
||||||
applyHighlighting(botEl);
|
applyHighlighting(botEl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Backfill seq metadata so edit/regenerate buttons can call
|
||||||
|
// the delete API without a page refresh. Backend includes
|
||||||
|
// user_seq / bot_seq on the done event after persistence.
|
||||||
|
const targetBotEl = botEl || (requestId ? messagesDiv.querySelector(`[data-request-id="${requestId}"]`) : null);
|
||||||
|
if (targetBotEl) {
|
||||||
|
if (item.bot_seq !== undefined && item.bot_seq !== null) {
|
||||||
|
targetBotEl.dataset.seq = item.bot_seq;
|
||||||
|
}
|
||||||
|
// Reveal regenerate button now that the seq is wired up.
|
||||||
|
const regenBtn = targetBotEl.querySelector('.regenerate-msg-btn');
|
||||||
|
if (regenBtn) regenBtn.style.display = '';
|
||||||
|
if (item.user_seq !== undefined && item.user_seq !== null) {
|
||||||
|
// Locate the preceding user bubble for this turn.
|
||||||
|
let prev = targetBotEl.previousElementSibling;
|
||||||
|
while (prev && !prev.classList.contains('user-message-group')) {
|
||||||
|
prev = prev.previousElementSibling;
|
||||||
|
}
|
||||||
|
if (prev && !prev.dataset.seq) {
|
||||||
|
prev.dataset.seq = item.user_seq;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
renderBotSpeakerButton(botEl, finalText);
|
renderBotSpeakerButton(botEl, finalText);
|
||||||
scrollChatToBottom();
|
scrollChatToBottom();
|
||||||
|
|
||||||
@@ -2857,7 +2865,7 @@ function localizeCancelMarker(text) {
|
|||||||
|
|
||||||
function createBotMessageEl(content, timestamp, requestId, msg) {
|
function createBotMessageEl(content, timestamp, requestId, msg) {
|
||||||
const el = document.createElement('div');
|
const el = document.createElement('div');
|
||||||
el.className = 'flex gap-3 px-4 sm:px-6 py-3';
|
el.className = 'flex gap-3 px-4 sm:px-6 py-3 bot-message-group';
|
||||||
if (requestId) el.dataset.requestId = requestId;
|
if (requestId) el.dataset.requestId = requestId;
|
||||||
|
|
||||||
let stepsHtml = '';
|
let stepsHtml = '';
|
||||||
@@ -2889,15 +2897,12 @@ function createBotMessageEl(content, timestamp, requestId, msg) {
|
|||||||
<button class="copy-msg-btn text-xs text-slate-300 dark:text-slate-600 hover:text-slate-500 dark:hover:text-slate-400 transition-colors cursor-pointer" title="${currentLang === 'zh' ? '复制' : 'Copy'}">
|
<button class="copy-msg-btn text-xs text-slate-300 dark:text-slate-600 hover:text-slate-500 dark:hover:text-slate-400 transition-colors cursor-pointer" title="${currentLang === 'zh' ? '复制' : 'Copy'}">
|
||||||
<i class="fas fa-copy"></i>
|
<i class="fas fa-copy"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="regenerate-msg-btn text-xs text-slate-300 dark:text-slate-600 hover:text-primary-400 dark:hover:text-primary-400 transition-colors cursor-pointer" title="${t('regenerate_response')}">
|
|
||||||
<i class="fas fa-rotate-right"></i>
|
|
||||||
</button>
|
|
||||||
<button class="delete-msg-btn text-xs text-slate-300 dark:text-slate-600 hover:text-red-500 dark:hover:text-red-400 transition-colors cursor-pointer" title="${t('delete_message_title')}">
|
|
||||||
<i class="fas fa-trash"></i>
|
|
||||||
</button>
|
|
||||||
<button class="speak-msg-btn text-xs text-slate-300 dark:text-slate-600 hover:text-slate-500 dark:hover:text-slate-400 transition-colors cursor-pointer" title="${t('speak_msg')}" style="display:none;">
|
<button class="speak-msg-btn text-xs text-slate-300 dark:text-slate-600 hover:text-slate-500 dark:hover:text-slate-400 transition-colors cursor-pointer" title="${t('speak_msg')}" style="display:none;">
|
||||||
<i class="fas fa-volume-up"></i>
|
<i class="fas fa-volume-up"></i>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="regenerate-msg-btn text-xs text-slate-300 dark:text-slate-600 hover:text-primary-400 dark:hover:text-primary-400 transition-colors cursor-pointer" title="${t('regenerate_response')}">
|
||||||
|
<i class="fas fa-rotate-right"></i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -251,6 +251,21 @@ class WebChannel(ChatChannel):
|
|||||||
"""生成唯一的请求ID"""
|
"""生成唯一的请求ID"""
|
||||||
return str(uuid.uuid4())
|
return str(uuid.uuid4())
|
||||||
|
|
||||||
|
def _fetch_latest_pair_seqs(self, session_id: str):
|
||||||
|
"""Query the conversation store for the latest user/bot message seqs.
|
||||||
|
|
||||||
|
Returned as ``{"user_seq": int|None, "bot_seq": int|None}``; used to
|
||||||
|
attach seq metadata onto the SSE ``done`` event so the frontend can
|
||||||
|
wire edit / regenerate buttons for live-streamed bubbles without a
|
||||||
|
page refresh.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from agent.memory import get_conversation_store
|
||||||
|
return get_conversation_store().get_latest_pair_seqs(session_id)
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"[WebChannel] _fetch_latest_pair_seqs failed: {e}")
|
||||||
|
return {"user_seq": None, "bot_seq": None}
|
||||||
|
|
||||||
def send(self, reply: Reply, context: Context):
|
def send(self, reply: Reply, context: Context):
|
||||||
try:
|
try:
|
||||||
if reply.type in self.NOT_SUPPORT_REPLYTYPE:
|
if reply.type in self.NOT_SUPPORT_REPLYTYPE:
|
||||||
@@ -291,11 +306,14 @@ class WebChannel(ChatChannel):
|
|||||||
if reply.type in (ReplyType.IMAGE_URL, ReplyType.FILE) and content.startswith("file://"):
|
if reply.type in (ReplyType.IMAGE_URL, ReplyType.FILE) and content.startswith("file://"):
|
||||||
text_content = getattr(reply, 'text_content', '')
|
text_content = getattr(reply, 'text_content', '')
|
||||||
if text_content:
|
if text_content:
|
||||||
|
seqs = self._fetch_latest_pair_seqs(session_id)
|
||||||
self.sse_queues[request_id].put({
|
self.sse_queues[request_id].put({
|
||||||
"type": "done",
|
"type": "done",
|
||||||
"content": text_content,
|
"content": text_content,
|
||||||
"request_id": request_id,
|
"request_id": request_id,
|
||||||
"timestamp": time.time()
|
"timestamp": time.time(),
|
||||||
|
"user_seq": seqs.get("user_seq"),
|
||||||
|
"bot_seq": seqs.get("bot_seq"),
|
||||||
})
|
})
|
||||||
logger.debug(f"SSE skipped duplicate file for request {request_id}")
|
logger.debug(f"SSE skipped duplicate file for request {request_id}")
|
||||||
return
|
return
|
||||||
@@ -307,11 +325,14 @@ class WebChannel(ChatChannel):
|
|||||||
logger.debug(f"SSE skipped http media reply for request {request_id}")
|
logger.debug(f"SSE skipped http media reply for request {request_id}")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
seqs = self._fetch_latest_pair_seqs(session_id)
|
||||||
self.sse_queues[request_id].put({
|
self.sse_queues[request_id].put({
|
||||||
"type": "done",
|
"type": "done",
|
||||||
"content": content,
|
"content": content,
|
||||||
"request_id": request_id,
|
"request_id": request_id,
|
||||||
"timestamp": time.time()
|
"timestamp": time.time(),
|
||||||
|
"user_seq": seqs.get("user_seq"),
|
||||||
|
"bot_seq": seqs.get("bot_seq"),
|
||||||
})
|
})
|
||||||
logger.debug(f"SSE done sent for request {request_id}")
|
logger.debug(f"SSE done sent for request {request_id}")
|
||||||
# Auto-trigger TTS once the bot finishes its text reply. The
|
# Auto-trigger TTS once the bot finishes its text reply. The
|
||||||
@@ -3940,31 +3961,15 @@ class MessageDeleteHandler:
|
|||||||
from agent.memory import get_conversation_store
|
from agent.memory import get_conversation_store
|
||||||
store = get_conversation_store()
|
store = get_conversation_store()
|
||||||
deleted = store.delete_message_pair(session_id, int(user_seq), delete_user=delete_user, cascade=cascade)
|
deleted = store.delete_message_pair(session_id, int(user_seq), delete_user=delete_user, cascade=cascade)
|
||||||
|
|
||||||
# 2. Sync agent's in-memory context
|
# 2. Sync agent's in-memory context so its next turn sees the
|
||||||
|
# same history as the DB. Handled by the agent_bridge helper.
|
||||||
try:
|
try:
|
||||||
from bridge import Bridge
|
from bridge import Bridge
|
||||||
ab = Bridge().get_agent_bridge()
|
Bridge().get_agent_bridge().sync_session_messages_from_store(session_id)
|
||||||
agent = ab.get_agent(session_id)
|
|
||||||
if agent and hasattr(agent, 'messages') and hasattr(agent, 'messages_lock'):
|
|
||||||
with agent.messages_lock:
|
|
||||||
# Rebuild agent.messages from database
|
|
||||||
# load_messages returns: [{"role": "user", "content": "..."}, ...]
|
|
||||||
remaining_msgs = store.load_messages(session_id, max_turns=1000)
|
|
||||||
|
|
||||||
agent.messages.clear()
|
|
||||||
for msg in remaining_msgs:
|
|
||||||
agent.messages.append({
|
|
||||||
"role": msg["role"],
|
|
||||||
"content": msg["content"]
|
|
||||||
})
|
|
||||||
|
|
||||||
logger.info(f"[WebChannel] Synced agent memory for session {session_id}, "
|
|
||||||
f"remaining messages: {len(agent.messages)}")
|
|
||||||
except Exception as sync_err:
|
except Exception as sync_err:
|
||||||
logger.warning(f"[WebChannel] Failed to sync agent memory: {sync_err}")
|
logger.warning(f"[WebChannel] Failed to sync agent memory: {sync_err}")
|
||||||
# Don't fail the request if memory sync fails
|
|
||||||
|
|
||||||
return json.dumps({"status": "success", "deleted": deleted}, ensure_ascii=False)
|
return json.dumps({"status": "success", "deleted": deleted}, ensure_ascii=False)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[WebChannel] Message delete error: {e}")
|
logger.error(f"[WebChannel] Message delete error: {e}")
|
||||||
|
|||||||
Reference in New Issue
Block a user