Skip to content

Commit 5ceba05

Browse files
committed
feat(wcdb): 扩展实时 WCDB 原生查询接口
- 增加 native message cursor 的 open/fetch/close 封装,用于跨 message_*.db 分片读取消息 - 增加 wcdb_get_contact 封装,用于按 username 读取 contact 表资料 - 同步 Electron sidecar 的 FFI 声明和 action 分发
1 parent b7ecda8 commit 5ceba05

2 files changed

Lines changed: 273 additions & 0 deletions

File tree

desktop/src/wcdb-sidecar.cjs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,18 @@ function loadNative() {
169169
funcs.wcdb_get_messages = tryFunc(
170170
"int32 wcdb_get_messages(int64 handle, const char* username, int32 limit, int32 offset, _Out_ void** outJson)"
171171
);
172+
funcs.wcdb_open_message_cursor = tryFunc(
173+
"int32 wcdb_open_message_cursor(int64 handle, const char* sessionId, int32 batchSize, int32 ascending, int32 beginTimestamp, int32 endTimestamp, _Out_ int64* cursor)"
174+
);
175+
funcs.wcdb_open_message_cursor_lite = tryFunc(
176+
"int32 wcdb_open_message_cursor_lite(int64 handle, const char* sessionId, int32 batchSize, int32 ascending, int32 beginTimestamp, int32 endTimestamp, _Out_ int64* cursor)"
177+
);
178+
funcs.wcdb_fetch_message_batch = tryFunc(
179+
"int32 wcdb_fetch_message_batch(int64 handle, int64 cursor, _Out_ void** outJson, _Out_ int32* hasMore)"
180+
);
181+
funcs.wcdb_close_message_cursor = tryFunc(
182+
"int32 wcdb_close_message_cursor(int64 handle, int64 cursor)"
183+
);
172184
funcs.wcdb_get_message_count = tryFunc(
173185
"int32 wcdb_get_message_count(int64 handle, const char* username, _Out_ int32* count)"
174186
);
@@ -178,6 +190,9 @@ function loadNative() {
178190
funcs.wcdb_get_avatar_urls = tryFunc(
179191
"int32 wcdb_get_avatar_urls(int64 handle, const char* usernamesJson, _Out_ void** outJson)"
180192
);
193+
funcs.wcdb_get_contact = tryFunc(
194+
"int32 wcdb_get_contact(int64 handle, const char* username, _Out_ void** outJson)"
195+
);
181196
funcs.wcdb_get_group_member_count = tryFunc(
182197
"int32 wcdb_get_group_member_count(int64 handle, const char* chatroomId, _Out_ int32* count)"
183198
);
@@ -362,6 +377,48 @@ function handleAction(action, payload) {
362377
]),
363378
};
364379

380+
case "open_message_cursor":
381+
case "open_message_cursor_lite": {
382+
const fnName = action === "open_message_cursor_lite" ? "wcdb_open_message_cursor_lite" : "wcdb_open_message_cursor";
383+
const out = [0];
384+
const rc = Number(
385+
requireFunc(fnName)(
386+
normalizeHandle(data.handle),
387+
String(data.session_id || "").trim(),
388+
Number.parseInt(String(data.batch_size || 0), 10) || 1,
389+
data.ascending ? 1 : 0,
390+
Number.parseInt(String(data.begin_timestamp || 0), 10) || 0,
391+
Number.parseInt(String(data.end_timestamp || 0), 10) || 0,
392+
out
393+
)
394+
);
395+
const cursor = Number(out[0] || 0);
396+
if (rc !== 0 || cursor <= 0) throw new ApiError(`${fnName} failed`, rc);
397+
return { cursor, rc };
398+
}
399+
400+
case "fetch_message_batch": {
401+
const fn = requireFunc("wcdb_fetch_message_batch");
402+
const out = [null];
403+
const outHasMore = [0];
404+
const rc = Number(fn(normalizeHandle(data.handle), normalizeHandle(data.cursor), out, outHasMore));
405+
try {
406+
if (rc !== 0 || !out[0]) throw new ApiError("wcdb_fetch_message_batch failed", rc);
407+
return {
408+
payload: ptrToString(out[0]),
409+
hasMore: Number(outHasMore[0] || 0) === 1,
410+
rc,
411+
};
412+
} finally {
413+
freeStringPtr(out[0]);
414+
}
415+
}
416+
417+
case "close_message_cursor": {
418+
const rc = Number(requireFunc("wcdb_close_message_cursor")(normalizeHandle(data.handle), normalizeHandle(data.cursor)));
419+
return { closed: rc === 0, rc };
420+
}
421+
365422
case "get_message_count": {
366423
const out = [0];
367424
const rc = Number(
@@ -386,6 +443,14 @@ function handleAction(action, payload) {
386443
]),
387444
};
388445

446+
case "get_contact":
447+
return {
448+
payload: callOutJson("wcdb_get_contact", [
449+
normalizeHandle(data.handle),
450+
String(data.username || "").trim(),
451+
]),
452+
};
453+
389454
case "get_group_members":
390455
return {
391456
payload: callOutJson("wcdb_get_group_members", [

src/wechat_decrypt_tool/wcdb_realtime.py

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,13 @@ def _load_wcdb_lib() -> ctypes.CDLL:
609609
lib.wcdb_get_avatar_urls.argtypes = [ctypes.c_int64, ctypes.c_char_p, ctypes.POINTER(ctypes.c_char_p)]
610610
lib.wcdb_get_avatar_urls.restype = ctypes.c_int
611611

612+
# Optional (newer DLLs): wcdb_get_contact(handle, username, out_json)
613+
try:
614+
lib.wcdb_get_contact.argtypes = [ctypes.c_int64, ctypes.c_char_p, ctypes.POINTER(ctypes.c_char_p)]
615+
lib.wcdb_get_contact.restype = ctypes.c_int
616+
except Exception:
617+
pass
618+
612619
lib.wcdb_get_group_member_count.argtypes = [ctypes.c_int64, ctypes.c_char_p, ctypes.POINTER(ctypes.c_int32)]
613620
lib.wcdb_get_group_member_count.restype = ctypes.c_int
614621

@@ -640,6 +647,50 @@ def _load_wcdb_lib() -> ctypes.CDLL:
640647
except Exception:
641648
pass
642649

650+
# Optional (newer DLLs): native message cursor used by WeFlow for stable
651+
# latest/history paging across message_*.db shards.
652+
try:
653+
lib.wcdb_open_message_cursor.argtypes = [
654+
ctypes.c_int64,
655+
ctypes.c_char_p,
656+
ctypes.c_int32,
657+
ctypes.c_int32,
658+
ctypes.c_int32,
659+
ctypes.c_int32,
660+
ctypes.POINTER(ctypes.c_int64),
661+
]
662+
lib.wcdb_open_message_cursor.restype = ctypes.c_int
663+
except Exception:
664+
pass
665+
try:
666+
lib.wcdb_open_message_cursor_lite.argtypes = [
667+
ctypes.c_int64,
668+
ctypes.c_char_p,
669+
ctypes.c_int32,
670+
ctypes.c_int32,
671+
ctypes.c_int32,
672+
ctypes.c_int32,
673+
ctypes.POINTER(ctypes.c_int64),
674+
]
675+
lib.wcdb_open_message_cursor_lite.restype = ctypes.c_int
676+
except Exception:
677+
pass
678+
try:
679+
lib.wcdb_fetch_message_batch.argtypes = [
680+
ctypes.c_int64,
681+
ctypes.c_int64,
682+
ctypes.POINTER(ctypes.c_char_p),
683+
ctypes.POINTER(ctypes.c_int32),
684+
]
685+
lib.wcdb_fetch_message_batch.restype = ctypes.c_int
686+
except Exception:
687+
pass
688+
try:
689+
lib.wcdb_close_message_cursor.argtypes = [ctypes.c_int64, ctypes.c_int64]
690+
lib.wcdb_close_message_cursor.restype = ctypes.c_int
691+
except Exception:
692+
pass
693+
643694
# Optional (newer DLLs): update a single message content in message db.
644695
# Signature: wcdb_update_message(handle, sessionId, localId, createTime, newContent, outError)
645696
try:
@@ -1086,6 +1137,31 @@ def get_avatar_urls(handle: int, usernames: list[str]) -> dict[str, str]:
10861137
return {}
10871138

10881139

1140+
def get_contact(handle: int, username: str) -> dict[str, Any]:
1141+
_ensure_initialized()
1142+
u = str(username or "").strip()
1143+
if not u:
1144+
return {}
1145+
1146+
if _sidecar_enabled():
1147+
out_json = _sidecar_payload(
1148+
"get_contact",
1149+
{"handle": int(handle), "username": u},
1150+
timeout=30.0,
1151+
)
1152+
decoded = _safe_load_json(out_json)
1153+
return decoded if isinstance(decoded, dict) else {}
1154+
1155+
lib = _load_wcdb_lib()
1156+
fn = getattr(lib, "wcdb_get_contact", None)
1157+
if not fn:
1158+
raise WCDBRealtimeError("Current wcdb_api.dll does not support get_contact.")
1159+
1160+
out_json = _call_out_json(fn, ctypes.c_int64(int(handle)), u.encode("utf-8"))
1161+
decoded = _safe_load_json(out_json)
1162+
return decoded if isinstance(decoded, dict) else {}
1163+
1164+
10891165
def get_group_members(handle: int, chatroom_id: str) -> list[dict[str, Any]]:
10901166
_ensure_initialized()
10911167
cid = str(chatroom_id or "").strip()
@@ -1200,6 +1276,138 @@ def exec_query(handle: int, *, kind: str, path: Optional[str], sql: str) -> list
12001276
return []
12011277

12021278

1279+
def open_message_cursor(
1280+
handle: int,
1281+
session_id: str,
1282+
*,
1283+
batch_size: int,
1284+
ascending: bool = False,
1285+
begin_timestamp: int = 0,
1286+
end_timestamp: int = 0,
1287+
lite: bool = False,
1288+
) -> int:
1289+
"""Open the native WeFlow-style message cursor.
1290+
1291+
The cursor API is important for live chat paging because the native layer
1292+
owns cross-`message_*.db` discovery/merge and can see the same live view
1293+
WeFlow uses instead of a single explicit SQL table.
1294+
"""
1295+
1296+
_ensure_initialized()
1297+
sid = str(session_id or "").strip()
1298+
if not sid:
1299+
return 0
1300+
size = max(1, int(batch_size or 1))
1301+
asc = 1 if ascending else 0
1302+
begin = max(0, int(begin_timestamp or 0))
1303+
end = max(0, int(end_timestamp or 0))
1304+
1305+
action = "open_message_cursor_lite" if lite else "open_message_cursor"
1306+
if _sidecar_enabled():
1307+
result = _sidecar_call(
1308+
action,
1309+
{
1310+
"handle": int(handle),
1311+
"session_id": sid,
1312+
"batch_size": int(size),
1313+
"ascending": bool(ascending),
1314+
"begin_timestamp": int(begin),
1315+
"end_timestamp": int(end),
1316+
},
1317+
timeout=30.0,
1318+
)
1319+
try:
1320+
return int(result.get("cursor") or 0)
1321+
except Exception:
1322+
return 0
1323+
1324+
lib = _load_wcdb_lib()
1325+
fn = getattr(lib, "wcdb_open_message_cursor_lite" if lite else "wcdb_open_message_cursor", None)
1326+
if not fn:
1327+
if lite:
1328+
fn = getattr(lib, "wcdb_open_message_cursor", None)
1329+
if not fn:
1330+
return 0
1331+
1332+
out_cursor = ctypes.c_int64(0)
1333+
rc = int(
1334+
fn(
1335+
ctypes.c_int64(int(handle)),
1336+
sid.encode("utf-8"),
1337+
ctypes.c_int32(int(size)),
1338+
ctypes.c_int32(int(asc)),
1339+
ctypes.c_int32(int(begin)),
1340+
ctypes.c_int32(int(end)),
1341+
ctypes.byref(out_cursor),
1342+
)
1343+
)
1344+
if rc != 0 or int(out_cursor.value) <= 0:
1345+
return 0
1346+
return int(out_cursor.value)
1347+
1348+
1349+
def fetch_message_batch(handle: int, cursor: int) -> tuple[list[dict[str, Any]], bool]:
1350+
_ensure_initialized()
1351+
cur = int(cursor or 0)
1352+
if cur <= 0:
1353+
return [], False
1354+
1355+
if _sidecar_enabled():
1356+
result = _sidecar_call(
1357+
"fetch_message_batch",
1358+
{"handle": int(handle), "cursor": int(cur)},
1359+
timeout=30.0,
1360+
)
1361+
decoded = _safe_load_json(str(result.get("payload") or ""))
1362+
rows = [x for x in decoded if isinstance(x, dict)] if isinstance(decoded, list) else []
1363+
return rows, bool(result.get("hasMore"))
1364+
1365+
lib = _load_wcdb_lib()
1366+
fn = getattr(lib, "wcdb_fetch_message_batch", None)
1367+
if not fn:
1368+
return [], False
1369+
1370+
out_json = ctypes.c_char_p()
1371+
out_has_more = ctypes.c_int32(0)
1372+
rc = int(fn(ctypes.c_int64(int(handle)), ctypes.c_int64(cur), ctypes.byref(out_json), ctypes.byref(out_has_more)))
1373+
try:
1374+
if rc != 0 or not out_json.value:
1375+
return [], False
1376+
payload = out_json.value.decode("utf-8", errors="replace")
1377+
finally:
1378+
try:
1379+
if out_json.value:
1380+
lib.wcdb_free_string(out_json)
1381+
except Exception:
1382+
pass
1383+
decoded = _safe_load_json(payload)
1384+
rows = [x for x in decoded if isinstance(x, dict)] if isinstance(decoded, list) else []
1385+
return rows, bool(int(out_has_more.value or 0) == 1)
1386+
1387+
1388+
def close_message_cursor(handle: int, cursor: int) -> None:
1389+
_ensure_initialized()
1390+
cur = int(cursor or 0)
1391+
if cur <= 0:
1392+
return
1393+
1394+
if _sidecar_enabled():
1395+
try:
1396+
_sidecar_call("close_message_cursor", {"handle": int(handle), "cursor": int(cur)}, timeout=5.0)
1397+
except Exception:
1398+
pass
1399+
return
1400+
1401+
lib = _load_wcdb_lib()
1402+
fn = getattr(lib, "wcdb_close_message_cursor", None)
1403+
if not fn:
1404+
return
1405+
try:
1406+
fn(ctypes.c_int64(int(handle)), ctypes.c_int64(cur))
1407+
except Exception:
1408+
return
1409+
1410+
12031411
def update_message(handle: int, *, session_id: str, local_id: int, create_time: int, new_content: str) -> None:
12041412
"""Update a single message content in the live encrypted db_storage via WCDB.
12051413

0 commit comments

Comments
 (0)