ZHIWEI666 commited on
Commit
52590d6
·
verified ·
1 Parent(s): 56fcd6e

账号、私信与消息提醒配套更新

Browse files
chat_store.py CHANGED
@@ -2,6 +2,7 @@
2
  import copy
3
  import logging
4
  import math
 
5
  import time
6
  import uuid
7
 
@@ -76,7 +77,10 @@ def _project(message):
76
  return result
77
 
78
 
79
- def send_message(sender, receiver, content):
 
 
 
80
  if not isinstance(receiver, str) or not receiver or not isinstance(content, str) or not content.strip() or len(content) > MAX_MESSAGE_LENGTH:
81
  raise HTTPException(status_code=400, detail="私信内容或接收者不正确")
82
  users = _read("users.json")
@@ -89,7 +93,19 @@ def send_message(sender, receiver, content):
89
  message = {"id": f"chat_{int(time.time())}_{uuid.uuid4().hex}", "sender": sender,
90
  "receiver": receiver, "content": content, "created_at": int(time.time()), "is_read": False}
91
  key = f"{min(sender, receiver)}_{max(sender, receiver)}"
92
- _mutate(lambda data: data.setdefault(key, []).append(message))
 
 
 
 
 
 
 
 
 
 
 
 
93
 
94
 
95
  def chat_list(account):
 
2
  import copy
3
  import logging
4
  import math
5
+ import re
6
  import time
7
  import uuid
8
 
 
77
  return result
78
 
79
 
80
+ def send_message(sender, receiver, content, client_request_id=None):
81
+ if client_request_id is not None and (not isinstance(client_request_id, str)
82
+ or re.fullmatch(r"[0-9a-f]{32}", client_request_id) is None):
83
+ raise HTTPException(status_code=400, detail="发送请求标识不正确")
84
  if not isinstance(receiver, str) or not receiver or not isinstance(content, str) or not content.strip() or len(content) > MAX_MESSAGE_LENGTH:
85
  raise HTTPException(status_code=400, detail="私信内容或接收者不正确")
86
  users = _read("users.json")
 
93
  message = {"id": f"chat_{int(time.time())}_{uuid.uuid4().hex}", "sender": sender,
94
  "receiver": receiver, "content": content, "created_at": int(time.time()), "is_read": False}
95
  key = f"{min(sender, receiver)}_{max(sender, receiver)}"
96
+ def append(data):
97
+ if client_request_id is not None:
98
+ for messages in data.values():
99
+ for existing in messages:
100
+ if existing["sender"] == sender and existing.get("client_request_id") == client_request_id:
101
+ if existing["receiver"] != receiver or existing["content"] != content:
102
+ raise HTTPException(status_code=409, detail="发送请求标识已用于其它消息")
103
+ return {"id": existing["id"], "client_request_id": client_request_id}
104
+ message["client_request_id"] = client_request_id
105
+ data.setdefault(key, []).append(message)
106
+ if client_request_id is not None:
107
+ return {"id": message["id"], "client_request_id": client_request_id}
108
+ return _mutate(append)
109
 
110
 
111
  def chat_list(account):
models.py CHANGED
@@ -131,6 +131,10 @@ class PrivateMessage(BaseModel):
131
  receiver: str
132
  content: str
133
 
 
 
 
 
134
  class PrivacySettings(BaseModel):
135
  follows: bool
136
  likes: bool
 
131
  receiver: str
132
  content: str
133
 
134
+ class IdempotentPrivateMessage(PrivateMessage):
135
+ client_request_id: str = Field(..., strict=True, min_length=32, max_length=32, pattern=r"^[0-9a-f]{32}$")
136
+
137
+
138
  class PrivacySettings(BaseModel):
139
  follows: bool
140
  likes: bool
router_messages.py CHANGED
@@ -12,7 +12,7 @@ import os
12
  import 数据库连接 as db
13
  import chat_store
14
  import notification_store
15
- from models import PrivateMessage
16
  from 安全认证 import require_auth, is_admin
17
 
18
  # 🔒 频次限制(与 router_users_auth.py / router_wallet.py / router_prompts.py 同一写法)
@@ -142,6 +142,13 @@ async def send_private_message(request: Request, msg: PrivateMessage, current_us
142
  await asyncio.to_thread(chat_store.send_message, current_user, msg.receiver, msg.content)
143
  return {"status": "success"}
144
 
 
 
 
 
 
 
 
145
  @router.get("/api/chats/{account}")
146
  async def get_chat_list(account: str, current_user: str = Depends(require_auth)):
147
  if account != current_user:
 
12
  import 数据库连接 as db
13
  import chat_store
14
  import notification_store
15
+ from models import PrivateMessage, IdempotentPrivateMessage
16
  from 安全认证 import require_auth, is_admin
17
 
18
  # 🔒 频次限制(与 router_users_auth.py / router_wallet.py / router_prompts.py 同一写法)
 
142
  await asyncio.to_thread(chat_store.send_message, current_user, msg.receiver, msg.content)
143
  return {"status": "success"}
144
 
145
+ @router.post("/api/messages/private/idempotent")
146
+ @shared_limit(30)
147
+ async def send_idempotent_private_message(request: Request, msg: IdempotentPrivateMessage, current_user: str = Depends(require_auth)):
148
+ receipt = await asyncio.to_thread(chat_store.send_message, current_user, msg.receiver, msg.content, msg.client_request_id)
149
+ return {"status": "success", "data": receipt}
150
+
151
+
152
  @router.get("/api/chats/{account}")
153
  async def get_chat_list(account: str, current_user: str = Depends(require_auth)):
154
  if account != current_user:
前端页面/components/用户体验_国际化.js CHANGED
@@ -629,6 +629,10 @@ const translations = {
629
  'chat.date_day': '日',
630
  'chat.load_more': '加载更早的消息',
631
  'chat.msg_too_long': '消息不能超1000字',
 
 
 
 
632
  'chat.send_failed': '发送失败: {msg}',
633
  'chat.load_list_failed': '加载对话列表失败',
634
  'chat.load_history_failed': '加载消息记录失败',
@@ -2340,6 +2344,10 @@ const translations = {
2340
  'chat.date_day': '',
2341
  'chat.load_more': 'Load earlier messages',
2342
  'chat.msg_too_long': 'Message cannot exceed 1000 characters',
 
 
 
 
2343
  'chat.send_failed': 'Send failed: {msg}',
2344
  'chat.load_list_failed': 'Failed to load conversations',
2345
  'chat.load_history_failed': 'Failed to load messages',
 
629
  'chat.date_day': '日',
630
  'chat.load_more': '加载更早的消息',
631
  'chat.msg_too_long': '消息不能超1000字',
632
+ 'chat.send_unconfirmed': '发送结果暂时无法确认,请先查看会话。保持原文重试可复用本次发送标识。',
633
+ 'chat.send_service_unavailable': '私信发送服务暂不可用,请确认接收账号及云端版本。',
634
+ 'chat.send_request_conflict': '发送请求与原消息不一致,请先核对会话中的消息。',
635
+ 'chat.send_rejected': '发送请求未获接受,请检查输入或稍后重试。',
636
  'chat.send_failed': '发送失败: {msg}',
637
  'chat.load_list_failed': '加载对话列表失败',
638
  'chat.load_history_failed': '加载消息记录失败',
 
2344
  'chat.date_day': '',
2345
  'chat.load_more': 'Load earlier messages',
2346
  'chat.msg_too_long': 'Message cannot exceed 1000 characters',
2347
+ 'chat.send_unconfirmed': 'Sending could not be confirmed. Check the conversation first. Retrying unchanged text reuses this send identifier.',
2348
+ 'chat.send_service_unavailable': 'Messaging is unavailable. Check the recipient and cloud version.',
2349
+ 'chat.send_request_conflict': 'This request differs from the original message. Check the conversation first.',
2350
+ 'chat.send_rejected': 'The send request was not accepted. Check your input or try again later.',
2351
  'chat.send_failed': 'Send failed: {msg}',
2352
  'chat.load_list_failed': 'Failed to load conversations',
2353
  'chat.load_history_failed': 'Failed to load messages',
前端页面/core/网络请求_业务API.js CHANGED
@@ -105,7 +105,12 @@ const api = {
105
  // 🔥 新增:轻量级获取未读消息数量(不标记已读,用于轮询红点)
106
  async getUnreadCount(account) { return request(`/api/messages/${account}?count_only=true`); },
107
  async markMessagesRead(account) { return request(`/api/messages/${account}/read`, { method: "POST" }); },
108
- async sendPrivateMessage(sender, receiver, content) { return request("/api/messages/private", { method: "POST", body: { sender, receiver, content } }); },
 
 
 
 
 
109
  async getChatList(account, options = {}) { return request(`/api/chats/${account}`, options); },
110
  async getChatHistory(account, targetAccount, options = {}) { return request(`/api/chats/${account}/${targetAccount}`, options); },
111
  async getWallet(account, options = {}) { return request(`/api/wallet/${account}`, options); },
 
105
  // 🔥 新增:轻量级获取未读消息数量(不标记已读,用于轮询红点)
106
  async getUnreadCount(account) { return request(`/api/messages/${account}?count_only=true`); },
107
  async markMessagesRead(account) { return request(`/api/messages/${account}/read`, { method: "POST" }); },
108
+ async sendPrivateMessage(sender, receiver, content, clientRequestId) {
109
+ const path = clientRequestId === undefined ? "/api/messages/private" : "/api/messages/private/idempotent";
110
+ const body = { sender, receiver, content };
111
+ if (clientRequestId !== undefined) body.client_request_id = clientRequestId;
112
+ return request(path, { method: "POST", body, retries: 0 });
113
+ },
114
  async getChatList(account, options = {}) { return request(`/api/chats/${account}`, options); },
115
  async getChatHistory(account, targetAccount, options = {}) { return request(`/api/chats/${account}/${targetAccount}`, options); },
116
  async getWallet(account, options = {}) { return request(`/api/wallet/${account}`, options); },
前端页面/social/私信聊天组件.js CHANGED
@@ -140,7 +140,7 @@ export function openChatModal(currentUser, targetAccount = null) {
140
 
141
  let constructing = true, observer, listGeneration = 0, historyGeneration = 0, conversationGeneration = 0;
142
  let sending = false;
143
- const drafts = new Map(), clearTimes = new Map();
144
  const componentId = `chat-${++chatSequence}`;
145
  const sessionMatches = captureRequestSession();
146
  const scope = createTaskViewScope({
@@ -153,7 +153,7 @@ export function openChatModal(currentUser, targetAccount = null) {
153
  });
154
  scope.addCleanup(() => {
155
  clearTimeout(pollTimer); pollTimer = null; observer?.disconnect();
156
- drafts.clear(); clearTimes.clear(); allMessages = []; currentTargetInfo = null; currentChatTarget = null;
157
  inputField.value = ''; messagesArea.replaceChildren(); chatListContainer.replaceChildren(); chatHeader.replaceChildren();
158
  window.dispatchEvent(new CustomEvent('comfy-chat-unread-changed'));
159
  });
@@ -416,12 +416,28 @@ export function openChatModal(currentUser, targetAccount = null) {
416
  const current = () => scope.isCurrent() && currentChatTarget === target && generation === conversationGeneration;
417
  sending = true; sendBtn.disabled = true; sendBtn.setAttribute('aria-busy', 'true');
418
  try {
419
- const result = await api.sendPrivateMessage(currentUser.account, target, txt);
 
 
 
 
 
 
 
420
  if (!current()) return;
421
- if (result?.status !== 'success') throw new Error('Unconfirmed message');
 
 
422
  if (inputField.value === originalInput) { inputField.value = ''; drafts.delete(target); adjustHeight(); }
423
  await Promise.allSettled([loadChatHistory(target, currentTargetInfo?.name, currentTargetInfo?.avatar), loadChatList()]);
424
- } catch (_) { if (current()) showToast(t('common.error'), 'error'); }
 
 
 
 
 
 
 
425
  finally {
426
  sending = false;
427
  if (scope.isCurrent()) { sendBtn.disabled = false; sendBtn.setAttribute('aria-busy', 'false'); }
 
140
 
141
  let constructing = true, observer, listGeneration = 0, historyGeneration = 0, conversationGeneration = 0;
142
  let sending = false;
143
+ const drafts = new Map(), clearTimes = new Map(), pendingMessages = new Map();
144
  const componentId = `chat-${++chatSequence}`;
145
  const sessionMatches = captureRequestSession();
146
  const scope = createTaskViewScope({
 
153
  });
154
  scope.addCleanup(() => {
155
  clearTimeout(pollTimer); pollTimer = null; observer?.disconnect();
156
+ drafts.clear(); clearTimes.clear(); pendingMessages.clear(); allMessages = []; currentTargetInfo = null; currentChatTarget = null;
157
  inputField.value = ''; messagesArea.replaceChildren(); chatListContainer.replaceChildren(); chatHeader.replaceChildren();
158
  window.dispatchEvent(new CustomEvent('comfy-chat-unread-changed'));
159
  });
 
416
  const current = () => scope.isCurrent() && currentChatTarget === target && generation === conversationGeneration;
417
  sending = true; sendBtn.disabled = true; sendBtn.setAttribute('aria-busy', 'true');
418
  try {
419
+ let pending = pendingMessages.get(target);
420
+ if (!pending || pending.content !== txt) {
421
+ const bytes = new Uint8Array(16);
422
+ crypto.getRandomValues(bytes);
423
+ pending = { id: Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join(''), content: txt };
424
+ pendingMessages.set(target, pending);
425
+ }
426
+ const result = await api.sendPrivateMessage(currentUser.account, target, txt, pending.id);
427
  if (!current()) return;
428
+ if (result?.status !== 'success' || typeof result.data?.id !== 'string' || !result.data.id
429
+ || result.data.client_request_id !== pending.id) throw new Error('Unconfirmed message');
430
+ if (pendingMessages.get(target) === pending) pendingMessages.delete(target);
431
  if (inputField.value === originalInput) { inputField.value = ''; drafts.delete(target); adjustHeight(); }
432
  await Promise.allSettled([loadChatHistory(target, currentTargetInfo?.name, currentTargetInfo?.avatar), loadChatList()]);
433
+ } catch (error) {
434
+ if (current()) {
435
+ const message = error?.status === 404 ? t('chat.send_service_unavailable')
436
+ : error?.status === 409 ? t('chat.send_request_conflict')
437
+ : error?.status >= 400 && error?.status < 500 ? t('chat.send_rejected') : t('chat.send_unconfirmed');
438
+ showToast(message, 'error');
439
+ }
440
+ }
441
  finally {
442
  sending = false;
443
  if (scope.isCurrent()) { sendBtn.disabled = false; sendBtn.setAttribute('aria-busy', 'false'); }