6868 _split_group_sender_prefix ,
6969 _to_char_token_text ,
7070)
71- from ..media_helpers import _resolve_account_db_storage_dir , _try_find_decrypted_resource
71+ from ..media_helpers import _clean_weflow_account_dir_name , _resolve_account_db_storage_dir , _try_find_decrypted_resource
7272from .. import chat_edit_store
7373from ..app_paths import get_output_dir
7474from ..database_filters import list_countable_database_names
@@ -1470,6 +1470,28 @@ def _coerce_realtime_blobish_value(value: Any) -> Any:
14701470 return value
14711471
14721472
1473+ def _coerce_realtime_packed_info_value (value : Any ) -> Any :
1474+ """Normalize realtime packed_info_data without losing plain md5 tokens."""
1475+
1476+ raw = value
1477+ if isinstance (raw , memoryview ):
1478+ raw = raw .tobytes ()
1479+ if isinstance (raw , bytearray ):
1480+ raw = bytes (raw )
1481+ if isinstance (raw , bytes ):
1482+ try :
1483+ s = raw .decode ("ascii" ).strip ()
1484+ except Exception :
1485+ s = ""
1486+ if _is_hex_md5 (s ):
1487+ return s .lower ()
1488+ if isinstance (raw , str ):
1489+ s = raw .strip ()
1490+ if _is_hex_md5 (s ):
1491+ return s .lower ()
1492+ return _coerce_realtime_blobish_value (value )
1493+
1494+
14731495def _normalize_realtime_message_item (item : dict [str , Any ]) -> dict [str , Any ]:
14741496 def _pick (* keys : str ) -> Any :
14751497 return _pick_case_insensitive_value (item , * keys )
@@ -1497,7 +1519,7 @@ def _to_int(value: Any) -> int:
14971519 "compress_content" : _coerce_realtime_blobish_value (
14981520 _pick ("compress_content" , "compressContent" , "CompressContent" )
14991521 ),
1500- "packed_info_data" : _coerce_realtime_blobish_value (
1522+ "packed_info_data" : _coerce_realtime_packed_info_value (
15011523 _pick ("packed_info_data" , "packedInfoData" , "PackedInfoData" )
15021524 ),
15031525 "sender_username" : str (
@@ -5234,6 +5256,199 @@ def _fetch_realtime_message_rows(
52345256 return rows , truncated , scanned
52355257
52365258
5259+ def _iter_realtime_message_db_paths (account_dir : Path , username : str ) -> list [Path ]:
5260+ """Return live db_storage/message DB candidates for a conversation.
5261+
5262+ This stays in realtime scope: it only looks under the original db_storage path,
5263+ not the decrypted output snapshot.
5264+ """
5265+
5266+ db_storage_dir = _resolve_account_db_storage_dir (account_dir )
5267+ if db_storage_dir is None :
5268+ return []
5269+ message_dir = Path (db_storage_dir ) / "message"
5270+ try :
5271+ if not message_dir .exists () or not message_dir .is_dir ():
5272+ return []
5273+ except Exception :
5274+ return []
5275+
5276+ normal : list [Path ] = []
5277+ biz : list [Path ] = []
5278+ other : list [Path ] = []
5279+ try :
5280+ for p in message_dir .iterdir ():
5281+ try :
5282+ if not p .is_file ():
5283+ continue
5284+ except Exception :
5285+ continue
5286+ ln = p .name .lower ()
5287+ if re .match (r"^message(_\d+)?\.db$" , ln ):
5288+ normal .append (p )
5289+ elif re .match (r"^biz_message(_\d+)?\.db$" , ln ):
5290+ biz .append (p )
5291+ elif ln .endswith (".db" ) and "message" in ln :
5292+ other .append (p )
5293+ except Exception :
5294+ return []
5295+
5296+ normal .sort (key = lambda x : x .name .lower ())
5297+ biz .sort (key = lambda x : x .name .lower ())
5298+ other .sort (key = lambda x : x .name .lower ())
5299+
5300+ uname = str (username or "" ).strip ()
5301+ if uname .startswith ("gh_" ):
5302+ return biz + normal + other
5303+ return normal + biz + other
5304+
5305+
5306+ def _resolve_realtime_message_db_table_via_exec (
5307+ * ,
5308+ rt_conn : Any ,
5309+ account_dir : Path ,
5310+ username : str ,
5311+ ) -> Optional [tuple [Path , str ]]:
5312+ """Resolve the live message DB/table with explicit WCDB exec_query.
5313+
5314+ The high-level native `wcdb_get_messages` path has its own message-db cache
5315+ and can return an empty list even when db_storage/message/message_*.db has
5316+ the Msg_<md5> table. This resolver bypasses that cache by specifying the
5317+ exact live DB path.
5318+ """
5319+
5320+ uname = str (username or "" ).strip ()
5321+ if not uname :
5322+ return None
5323+ import hashlib
5324+
5325+ expected = f"Msg_{ hashlib .md5 (uname .encode ('utf-8' )).hexdigest ()} "
5326+ sql = (
5327+ "SELECT name FROM sqlite_master "
5328+ "WHERE type='table' AND lower(name)=lower("
5329+ + _sql_literal (expected )
5330+ + ") LIMIT 1"
5331+ )
5332+
5333+ for db_path in _iter_realtime_message_db_paths (account_dir , uname ):
5334+ try :
5335+ rows = _wcdb_exec_query (rt_conn .handle , kind = "message" , path = str (db_path ), sql = sql )
5336+ except Exception :
5337+ continue
5338+ for row in rows or []:
5339+ if not isinstance (row , dict ):
5340+ continue
5341+ actual = str (_pick_case_insensitive_value (row , "name" ) or "" ).strip ()
5342+ if actual :
5343+ return db_path , actual
5344+ return None
5345+
5346+
5347+ def _lookup_realtime_my_rowid_via_exec (
5348+ * ,
5349+ rt_conn : Any ,
5350+ db_path : Path ,
5351+ account_dir : Path ,
5352+ ) -> Optional [int ]:
5353+ candidates : list [str ] = []
5354+ for value in (
5355+ getattr (rt_conn , "native_wxid" , "" ),
5356+ account_dir .name ,
5357+ _clean_weflow_account_dir_name (account_dir .name ),
5358+ ):
5359+ v = str (value or "" ).strip ()
5360+ if v and v not in candidates :
5361+ candidates .append (v )
5362+ if not candidates :
5363+ return None
5364+
5365+ values = ", " .join (_sql_literal (v ) for v in candidates )
5366+ order = " " .join (
5367+ f"WHEN user_name = { _sql_literal (v )} THEN { i } " for i , v in enumerate (candidates )
5368+ )
5369+ sql = (
5370+ "SELECT rowid AS rowid FROM Name2Id "
5371+ f"WHERE user_name IN ({ values } ) "
5372+ f"ORDER BY CASE { order } ELSE { len (candidates )} END "
5373+ "LIMIT 1"
5374+ )
5375+ try :
5376+ rows = _wcdb_exec_query (rt_conn .handle , kind = "message" , path = str (db_path ), sql = sql )
5377+ except Exception :
5378+ return None
5379+ if not rows :
5380+ return None
5381+ try :
5382+ rowid = int (_pick_case_insensitive_value (rows [0 ], "rowid" ) or 0 )
5383+ except Exception :
5384+ rowid = 0
5385+ return rowid if rowid > 0 else None
5386+
5387+
5388+ def _fetch_realtime_message_rows_via_exec (
5389+ * ,
5390+ rt_conn : Any ,
5391+ account_dir : Path ,
5392+ username : str ,
5393+ take : int ,
5394+ ) -> tuple [list [dict [str , Any ]], bool , Optional [Path ], str , Optional [int ]]:
5395+ """Fetch newest-first live rows via explicit db_storage/message SQL.
5396+
5397+ Returns (rows, has_more, db_path, table_name, my_rowid).
5398+ """
5399+
5400+ if int (take ) <= 0 :
5401+ return [], False , None , "" , None
5402+ resolved = _resolve_realtime_message_db_table_via_exec (
5403+ rt_conn = rt_conn ,
5404+ account_dir = account_dir ,
5405+ username = username ,
5406+ )
5407+ if resolved is None :
5408+ return [], False , None , "" , None
5409+
5410+ db_path , table_name = resolved
5411+ my_rowid = _lookup_realtime_my_rowid_via_exec (rt_conn = rt_conn , db_path = db_path , account_dir = account_dir )
5412+
5413+ quoted_table = _quote_ident (table_name )
5414+ limit_probe = int (take ) + 1
5415+ base_cols = (
5416+ "m.local_id, m.server_id, m.local_type, m.sort_seq, m.real_sender_id, m.create_time, "
5417+ "m.message_content, m.compress_content, "
5418+ )
5419+ packed_select = "m.packed_info_data AS packed_info_data, "
5420+ sender_join = "n.user_name AS sender_username "
5421+ sql_with_join = (
5422+ "SELECT "
5423+ + base_cols
5424+ + packed_select
5425+ + sender_join
5426+ + f"FROM { quoted_table } m "
5427+ + "LEFT JOIN Name2Id n ON m.real_sender_id = n.rowid "
5428+ + "ORDER BY m.create_time DESC, m.sort_seq DESC, m.local_id DESC "
5429+ + f"LIMIT { int (limit_probe )} "
5430+ )
5431+ sql_no_packed = (
5432+ "SELECT "
5433+ + base_cols
5434+ + "NULL AS packed_info_data, "
5435+ + sender_join
5436+ + f"FROM { quoted_table } m "
5437+ + "LEFT JOIN Name2Id n ON m.real_sender_id = n.rowid "
5438+ + "ORDER BY m.create_time DESC, m.sort_seq DESC, m.local_id DESC "
5439+ + f"LIMIT { int (limit_probe )} "
5440+ )
5441+ try :
5442+ raw_rows = _wcdb_exec_query (rt_conn .handle , kind = "message" , path = str (db_path ), sql = sql_with_join )
5443+ except Exception :
5444+ raw_rows = _wcdb_exec_query (rt_conn .handle , kind = "message" , path = str (db_path ), sql = sql_no_packed )
5445+
5446+ has_more = len (raw_rows or []) > int (take )
5447+ raw_rows = list (raw_rows or [])[: int (take )]
5448+ rows = [_normalize_realtime_message_item (r ) for r in raw_rows if isinstance (r , dict )]
5449+ return rows , bool (has_more ), db_path , table_name , my_rowid
5450+
5451+
52375452def _sort_realtime_rows_chronological (rows : list [dict [str , Any ]]) -> list [dict [str , Any ]]:
52385453 return sorted (
52395454 rows ,
@@ -5647,6 +5862,8 @@ def list_chat_messages(
56475862 # Realtime mode: fetch from newest (offset handled after render_type filtering).
56485863 table_name = _realtime_message_table_name (username )
56495864 rt_db_path = _realtime_message_db_path (account_dir )
5865+ my_rowid_realtime : Optional [int ] = None
5866+ used_exec_query = False
56505867
56515868 while True :
56525869 probe = int (scan_take ) + 1
@@ -5655,17 +5872,44 @@ def list_chat_messages(
56555872 if probe > 50000 :
56565873 probe = 50000
56575874
5658- with rt_conn .lock :
5659- raw_rows = _wcdb_get_messages (rt_conn .handle , username , limit = probe , offset = 0 )
5660- has_more_any = len (raw_rows ) > int (scan_take )
5661- raw_rows = raw_rows [: int (scan_take )] if int (scan_take ) > 0 else []
5875+ norm_rows : list [dict [str , Any ]] = []
5876+ try :
5877+ with rt_conn .lock :
5878+ (
5879+ norm_rows ,
5880+ has_more_any ,
5881+ exec_db_path ,
5882+ exec_table_name ,
5883+ my_rowid_realtime ,
5884+ ) = _fetch_realtime_message_rows_via_exec (
5885+ rt_conn = rt_conn ,
5886+ account_dir = account_dir ,
5887+ username = username ,
5888+ take = int (scan_take ),
5889+ )
5890+ if exec_db_path is not None :
5891+ used_exec_query = True
5892+ rt_db_path = exec_db_path
5893+ table_name = exec_table_name or table_name
5894+ else :
5895+ used_exec_query = False
5896+ except Exception as e :
5897+ trace ("realtime:exec-query:error" , error = str (e ))
5898+ norm_rows = []
5899+ used_exec_query = False
5900+
5901+ if not used_exec_query :
5902+ with rt_conn .lock :
5903+ raw_rows = _wcdb_get_messages (rt_conn .handle , username , limit = probe , offset = 0 )
5904+ has_more_any = len (raw_rows ) > int (scan_take )
5905+ raw_rows = raw_rows [: int (scan_take )] if int (scan_take ) > 0 else []
5906+ norm_rows = [_normalize_realtime_message_item (r ) for r in raw_rows if isinstance (r , dict )]
56625907
56635908 merged = []
56645909 sender_usernames = []
56655910 quote_usernames = []
56665911 pat_usernames = set ()
56675912
5668- norm_rows = [_normalize_realtime_message_item (r ) for r in raw_rows if isinstance (r , dict )]
56695913 _append_full_messages_from_rows (
56705914 merged = merged ,
56715915 sender_usernames = sender_usernames ,
@@ -5677,7 +5921,7 @@ def list_chat_messages(
56775921 username = username ,
56785922 account_dir = account_dir ,
56795923 is_group = bool (username .endswith ("@chatroom" )),
5680- my_rowid = None ,
5924+ my_rowid = my_rowid_realtime if used_exec_query else None ,
56815925 resource_conn = resource_conn ,
56825926 resource_chat_id = resource_chat_id ,
56835927 )
0 commit comments