mirror of
https://github.com/LifeArchiveProject/WeChatDataAnalysis.git
synced 2026-06-18 15:54:08 +08:00
fix(realtime): 内置 WCDB sidecar 并切换项目内 DLL
- 桌面端启动时自动拉起 WCDB sidecar,并向后端注入连接参数 - 打包附带 sidecar 脚本与 koffi 运行时 - 改为通过项目内置 WeFlow DLL 处理 realtime 调用,规避宿主校验失败和连接超时
This commit is contained in:
@@ -57,6 +57,14 @@
|
||||
{
|
||||
"from": "resources/backend",
|
||||
"to": "backend"
|
||||
},
|
||||
{
|
||||
"from": "src/wcdb-sidecar.cjs",
|
||||
"to": "wcdb-sidecar.cjs"
|
||||
},
|
||||
{
|
||||
"from": "vendor/koffi",
|
||||
"to": "koffi"
|
||||
}
|
||||
],
|
||||
"win": {
|
||||
|
||||
@@ -17,6 +17,7 @@ try {
|
||||
autoUpdaterLoadError = err;
|
||||
}
|
||||
const { spawn, spawnSync } = require("child_process");
|
||||
const crypto = require("crypto");
|
||||
const fs = require("fs");
|
||||
const http = require("http");
|
||||
const net = require("net");
|
||||
@@ -33,6 +34,10 @@ const DEFAULT_BACKEND_HOST = String(process.env.WECHAT_TOOL_HOST || "127.0.0.1")
|
||||
const DEFAULT_BACKEND_PORT = parsePort(process.env.WECHAT_TOOL_PORT) ?? 10392;
|
||||
|
||||
let backendProc = null;
|
||||
let wcdbSidecarProc = null;
|
||||
let wcdbSidecarPort = null;
|
||||
let wcdbSidecarUrl = "";
|
||||
let wcdbSidecarToken = "";
|
||||
let resolvedDataDir = null;
|
||||
let mainWindow = null;
|
||||
let tray = null;
|
||||
@@ -1452,8 +1457,159 @@ function getPackagedWcdbDllPath() {
|
||||
return path.join(process.resourcesPath, "backend", "native", "wcdb_api.dll");
|
||||
}
|
||||
|
||||
function getDevWcdbDllPath() {
|
||||
return path.join(repoRoot(), "src", "wechat_decrypt_tool", "native", "wcdb_api.dll");
|
||||
}
|
||||
|
||||
function getWcdbDllPath() {
|
||||
return app.isPackaged ? getPackagedWcdbDllPath() : getDevWcdbDllPath();
|
||||
}
|
||||
|
||||
function getWcdbDllDir() {
|
||||
return path.dirname(getWcdbDllPath());
|
||||
}
|
||||
|
||||
function getWcdbSidecarScriptPath() {
|
||||
return app.isPackaged
|
||||
? path.join(process.resourcesPath, "wcdb-sidecar.cjs")
|
||||
: path.join(__dirname, "wcdb-sidecar.cjs");
|
||||
}
|
||||
|
||||
function getKoffiDir() {
|
||||
return app.isPackaged ? path.join(process.resourcesPath, "koffi") : path.join(repoRoot(), "desktop", "vendor", "koffi");
|
||||
}
|
||||
|
||||
function getWcdbSidecarStdioLogPath(dataDir) {
|
||||
return path.join(dataDir, "wcdb-sidecar-stdio.log");
|
||||
}
|
||||
|
||||
function getWcdbSidecarPort() {
|
||||
if (parsePort(wcdbSidecarPort) != null) return wcdbSidecarPort;
|
||||
const envPort = parsePort(process.env.WECHAT_TOOL_WCDB_SIDECAR_PORT);
|
||||
wcdbSidecarPort = envPort ?? Math.min(65535, getBackendPort() + 101);
|
||||
return wcdbSidecarPort;
|
||||
}
|
||||
|
||||
async function prepareWcdbSidecarPort() {
|
||||
if (parsePort(wcdbSidecarPort) != null) return wcdbSidecarPort;
|
||||
const envPort = parsePort(process.env.WECHAT_TOOL_WCDB_SIDECAR_PORT);
|
||||
if (envPort != null) {
|
||||
wcdbSidecarPort = envPort;
|
||||
return wcdbSidecarPort;
|
||||
}
|
||||
const preferred = Math.min(65535, getBackendPort() + 101);
|
||||
wcdbSidecarPort = await chooseAvailablePort(preferred, "127.0.0.1");
|
||||
if (wcdbSidecarPort == null) wcdbSidecarPort = preferred;
|
||||
return wcdbSidecarPort;
|
||||
}
|
||||
|
||||
function getWcdbResourcePaths() {
|
||||
const out = [];
|
||||
const seen = new Set();
|
||||
|
||||
const add = (value) => {
|
||||
const raw = String(value || "").trim();
|
||||
if (!raw) return;
|
||||
const resolved = path.resolve(raw);
|
||||
const key = resolved.toLowerCase();
|
||||
if (seen.has(key)) return;
|
||||
seen.add(key);
|
||||
out.push(resolved);
|
||||
};
|
||||
|
||||
const dllDir = getWcdbDllDir();
|
||||
add(dllDir);
|
||||
add(path.dirname(dllDir));
|
||||
add(repoRoot());
|
||||
add(path.join(repoRoot(), "resources"));
|
||||
const dataDir = resolveDataDir();
|
||||
if (dataDir) {
|
||||
add(dataDir);
|
||||
add(path.join(dataDir, "resources"));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function ensureWcdbSidecarEnv(env) {
|
||||
if (!wcdbSidecarUrl || !wcdbSidecarToken) return env;
|
||||
env.WECHAT_TOOL_WCDB_SIDECAR_URL = wcdbSidecarUrl;
|
||||
env.WECHAT_TOOL_WCDB_SIDECAR_TOKEN = wcdbSidecarToken;
|
||||
return env;
|
||||
}
|
||||
|
||||
function startWcdbSidecar() {
|
||||
if (process.env.WECHAT_TOOL_WCDB_SIDECAR === "0") return null;
|
||||
if (wcdbSidecarProc && wcdbSidecarProc.exitCode == null) return wcdbSidecarProc;
|
||||
if (process.platform !== "win32") return null;
|
||||
|
||||
const dllPath = getWcdbDllPath();
|
||||
const sidecarScript = getWcdbSidecarScriptPath();
|
||||
const koffiDir = getKoffiDir();
|
||||
if (!fs.existsSync(dllPath)) {
|
||||
logMain(`[wcdb-sidecar] skip: missing wcdb_api.dll ${dllPath}`);
|
||||
return null;
|
||||
}
|
||||
if (!fs.existsSync(sidecarScript)) {
|
||||
logMain(`[wcdb-sidecar] skip: missing sidecar script ${sidecarScript}`);
|
||||
return null;
|
||||
}
|
||||
if (!fs.existsSync(koffiDir)) {
|
||||
logMain(`[wcdb-sidecar] skip: missing koffi runtime ${koffiDir}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const port = getWcdbSidecarPort();
|
||||
const host = "127.0.0.1";
|
||||
wcdbSidecarUrl = `http://${host}:${port}`;
|
||||
wcdbSidecarToken = wcdbSidecarToken || crypto.randomBytes(24).toString("hex");
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
ELECTRON_RUN_AS_NODE: "1",
|
||||
WECHAT_TOOL_WCDB_SIDECAR_HOST: host,
|
||||
WECHAT_TOOL_WCDB_SIDECAR_PORT: String(port),
|
||||
WECHAT_TOOL_WCDB_SIDECAR_TOKEN: wcdbSidecarToken,
|
||||
WECHAT_TOOL_WCDB_API_DLL_PATH: dllPath,
|
||||
WECHAT_TOOL_WCDB_DLL_DIR: getWcdbDllDir(),
|
||||
WECHAT_TOOL_WCDB_RESOURCE_PATHS: JSON.stringify(getWcdbResourcePaths()),
|
||||
WECHAT_TOOL_KOFFI_DIR: koffiDir,
|
||||
};
|
||||
|
||||
logMain(`[wcdb-sidecar] starting url=${wcdbSidecarUrl} dll=${dllPath}`);
|
||||
wcdbSidecarProc = spawn(process.execPath, [sidecarScript], {
|
||||
cwd: path.dirname(sidecarScript),
|
||||
env,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
windowsHide: true,
|
||||
});
|
||||
|
||||
const dataDir = resolveDataDir() || getUserDataDir() || repoRoot();
|
||||
attachBackendStdio(wcdbSidecarProc, getWcdbSidecarStdioLogPath(dataDir));
|
||||
|
||||
const proc = wcdbSidecarProc;
|
||||
proc.on("exit", (code, signal) => {
|
||||
if (wcdbSidecarProc === proc) wcdbSidecarProc = null;
|
||||
logMain(`[wcdb-sidecar] exited code=${code} signal=${signal}`);
|
||||
});
|
||||
|
||||
process.env.WECHAT_TOOL_WCDB_SIDECAR_URL = wcdbSidecarUrl;
|
||||
process.env.WECHAT_TOOL_WCDB_SIDECAR_TOKEN = wcdbSidecarToken;
|
||||
return wcdbSidecarProc;
|
||||
}
|
||||
|
||||
function stopWcdbSidecar() {
|
||||
if (!wcdbSidecarProc) return;
|
||||
const pid = wcdbSidecarProc.pid;
|
||||
logMain(`[wcdb-sidecar] stop pid=${pid || "?"}`);
|
||||
try {
|
||||
wcdbSidecarProc.kill();
|
||||
} catch {}
|
||||
wcdbSidecarProc = null;
|
||||
}
|
||||
|
||||
function startBackend() {
|
||||
if (backendProc) return backendProc;
|
||||
startWcdbSidecar();
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
@@ -1462,6 +1618,7 @@ function startBackend() {
|
||||
// Make sure Python prints UTF-8 to stdout/stderr.
|
||||
PYTHONIOENCODING: process.env.PYTHONIOENCODING || "utf-8",
|
||||
};
|
||||
ensureWcdbSidecarEnv(env);
|
||||
|
||||
// In packaged mode we expect to provide the generated Nuxt output dir via env.
|
||||
if (app.isPackaged && !env.WECHAT_TOOL_UI_DIR) {
|
||||
@@ -2135,6 +2292,7 @@ async function main() {
|
||||
await applyPendingOutputDirOnStartup();
|
||||
ensureOutputLink();
|
||||
await ensureBackendPortAvailableOnStartup();
|
||||
await prepareWcdbSidecarPort();
|
||||
|
||||
logMain(`[main] app.isPackaged=${app.isPackaged} argv=${JSON.stringify(process.argv)}`);
|
||||
|
||||
@@ -2190,6 +2348,7 @@ async function main() {
|
||||
|
||||
app.on("window-all-closed", () => {
|
||||
stopBackend();
|
||||
stopWcdbSidecar();
|
||||
if (process.platform !== "darwin") app.quit();
|
||||
});
|
||||
|
||||
@@ -2203,6 +2362,7 @@ app.on("before-quit", () => {
|
||||
isQuitting = true;
|
||||
destroyTray();
|
||||
stopBackend();
|
||||
stopWcdbSidecar();
|
||||
});
|
||||
|
||||
if (gotSingleInstanceLock) {
|
||||
@@ -2211,6 +2371,7 @@ if (gotSingleInstanceLock) {
|
||||
console.error(err);
|
||||
logMain(`[main] fatal: ${err?.stack || String(err)}`);
|
||||
stopBackend();
|
||||
stopWcdbSidecar();
|
||||
try {
|
||||
const dir = getUserDataDir();
|
||||
const outputDir = resolveOutputDir();
|
||||
|
||||
@@ -0,0 +1,545 @@
|
||||
"use strict";
|
||||
|
||||
const fs = require("fs");
|
||||
const http = require("http");
|
||||
const path = require("path");
|
||||
|
||||
const HOST = String(process.env.WECHAT_TOOL_WCDB_SIDECAR_HOST || "127.0.0.1").trim() || "127.0.0.1";
|
||||
const PORT = Number.parseInt(String(process.env.WECHAT_TOOL_WCDB_SIDECAR_PORT || "0").trim(), 10);
|
||||
const TOKEN = String(process.env.WECHAT_TOOL_WCDB_SIDECAR_TOKEN || "").trim();
|
||||
|
||||
let koffi = null;
|
||||
let nativeLib = null;
|
||||
let nativeDllPath = "";
|
||||
let initialized = false;
|
||||
let protectionResults = [];
|
||||
const preloadedLibs = [];
|
||||
const funcs = Object.create(null);
|
||||
|
||||
class ApiError extends Error {
|
||||
constructor(message, rc = 0, details = null) {
|
||||
super(message);
|
||||
this.name = "ApiError";
|
||||
this.rc = rc;
|
||||
this.details = details;
|
||||
}
|
||||
}
|
||||
|
||||
function log(message) {
|
||||
process.stderr.write(`[wcdb-sidecar] ${new Date().toISOString()} ${message}\n`);
|
||||
}
|
||||
|
||||
function jsonResponse(res, statusCode, payload) {
|
||||
const body = Buffer.from(JSON.stringify(payload), "utf8");
|
||||
res.writeHead(statusCode, {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
"Content-Length": String(body.length),
|
||||
"Cache-Control": "no-store",
|
||||
});
|
||||
res.end(body);
|
||||
}
|
||||
|
||||
function readRequestJson(req) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks = [];
|
||||
let total = 0;
|
||||
req.on("data", (chunk) => {
|
||||
total += chunk.length;
|
||||
if (total > 16 * 1024 * 1024) {
|
||||
reject(new ApiError("request body too large"));
|
||||
req.destroy();
|
||||
return;
|
||||
}
|
||||
chunks.push(chunk);
|
||||
});
|
||||
req.on("end", () => {
|
||||
try {
|
||||
const raw = Buffer.concat(chunks).toString("utf8").trim();
|
||||
resolve(raw ? JSON.parse(raw) : {});
|
||||
} catch (err) {
|
||||
reject(new ApiError(`invalid json: ${err?.message || err}`));
|
||||
}
|
||||
});
|
||||
req.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
function parseResourcePaths() {
|
||||
const out = [];
|
||||
const seen = new Set();
|
||||
|
||||
function add(value) {
|
||||
const raw = String(value || "").trim();
|
||||
if (!raw) return;
|
||||
const resolved = path.resolve(raw);
|
||||
const key = resolved.toLowerCase();
|
||||
if (seen.has(key)) return;
|
||||
seen.add(key);
|
||||
out.push(resolved);
|
||||
}
|
||||
|
||||
const raw = String(process.env.WECHAT_TOOL_WCDB_RESOURCE_PATHS || "").trim();
|
||||
if (raw) {
|
||||
try {
|
||||
const decoded = JSON.parse(raw);
|
||||
if (Array.isArray(decoded)) {
|
||||
for (const item of decoded) add(item);
|
||||
}
|
||||
} catch {
|
||||
for (const item of raw.split(path.delimiter)) add(item);
|
||||
}
|
||||
}
|
||||
|
||||
const dllDir = getDllDir();
|
||||
add(dllDir);
|
||||
add(path.dirname(dllDir));
|
||||
add(process.cwd());
|
||||
return out;
|
||||
}
|
||||
|
||||
function getDllDir() {
|
||||
const fromEnv = String(process.env.WECHAT_TOOL_WCDB_DLL_DIR || "").trim();
|
||||
if (fromEnv) return path.resolve(fromEnv);
|
||||
|
||||
const dllPath = String(process.env.WECHAT_TOOL_WCDB_API_DLL_PATH || "").trim();
|
||||
if (dllPath) return path.dirname(path.resolve(dllPath));
|
||||
|
||||
return process.cwd();
|
||||
}
|
||||
|
||||
function getDllPath() {
|
||||
const fromEnv = String(process.env.WECHAT_TOOL_WCDB_API_DLL_PATH || "").trim();
|
||||
if (fromEnv) return path.resolve(fromEnv);
|
||||
return path.join(getDllDir(), "wcdb_api.dll");
|
||||
}
|
||||
|
||||
function loadKoffi() {
|
||||
if (koffi) return koffi;
|
||||
|
||||
const koffiDir = String(process.env.WECHAT_TOOL_KOFFI_DIR || "").trim();
|
||||
if (koffiDir) {
|
||||
koffi = require(path.resolve(koffiDir));
|
||||
} else {
|
||||
koffi = require("koffi");
|
||||
}
|
||||
return koffi;
|
||||
}
|
||||
|
||||
function tryFunc(signature) {
|
||||
try {
|
||||
return nativeLib.func(signature);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function loadNative() {
|
||||
if (nativeLib) return nativeLib;
|
||||
|
||||
const ffi = loadKoffi();
|
||||
const dllDir = getDllDir();
|
||||
nativeDllPath = getDllPath();
|
||||
if (!fs.existsSync(nativeDllPath)) {
|
||||
throw new ApiError(`wcdb_api.dll not found: ${nativeDllPath}`);
|
||||
}
|
||||
|
||||
try {
|
||||
process.chdir(dllDir);
|
||||
} catch {}
|
||||
|
||||
for (const dep of ["WCDB.dll", "SDL2.dll", "VoipEngine.dll"]) {
|
||||
const depPath = path.join(dllDir, dep);
|
||||
if (!fs.existsSync(depPath)) continue;
|
||||
try {
|
||||
preloadedLibs.push(ffi.load(depPath));
|
||||
log(`preloaded dependency ${depPath}`);
|
||||
} catch (err) {
|
||||
log(`preload dependency failed ${depPath}: ${err?.message || err}`);
|
||||
}
|
||||
}
|
||||
|
||||
nativeLib = ffi.load(nativeDllPath);
|
||||
funcs.InitProtection = tryFunc("int32 InitProtection(const char* resourcePath)");
|
||||
funcs.wcdb_init = tryFunc("int32 wcdb_init()");
|
||||
funcs.wcdb_shutdown = tryFunc("int32 wcdb_shutdown()");
|
||||
funcs.wcdb_open_account = tryFunc("int32 wcdb_open_account(const char* path, const char* key, _Out_ int64* handle)");
|
||||
funcs.wcdb_close_account = tryFunc("int32 wcdb_close_account(int64 handle)");
|
||||
funcs.wcdb_set_my_wxid = tryFunc("int32 wcdb_set_my_wxid(int64 handle, const char* wxid)");
|
||||
funcs.wcdb_get_sessions = tryFunc("int32 wcdb_get_sessions(int64 handle, _Out_ void** outJson)");
|
||||
funcs.wcdb_get_messages = tryFunc(
|
||||
"int32 wcdb_get_messages(int64 handle, const char* username, int32 limit, int32 offset, _Out_ void** outJson)"
|
||||
);
|
||||
funcs.wcdb_get_message_count = tryFunc(
|
||||
"int32 wcdb_get_message_count(int64 handle, const char* username, _Out_ int32* count)"
|
||||
);
|
||||
funcs.wcdb_get_display_names = tryFunc(
|
||||
"int32 wcdb_get_display_names(int64 handle, const char* usernamesJson, _Out_ void** outJson)"
|
||||
);
|
||||
funcs.wcdb_get_avatar_urls = tryFunc(
|
||||
"int32 wcdb_get_avatar_urls(int64 handle, const char* usernamesJson, _Out_ void** outJson)"
|
||||
);
|
||||
funcs.wcdb_get_group_member_count = tryFunc(
|
||||
"int32 wcdb_get_group_member_count(int64 handle, const char* chatroomId, _Out_ int32* count)"
|
||||
);
|
||||
funcs.wcdb_get_group_members = tryFunc(
|
||||
"int32 wcdb_get_group_members(int64 handle, const char* chatroomId, _Out_ void** outJson)"
|
||||
);
|
||||
funcs.wcdb_get_group_nicknames = tryFunc(
|
||||
"int32 wcdb_get_group_nicknames(int64 handle, const char* chatroomId, _Out_ void** outJson)"
|
||||
);
|
||||
funcs.wcdb_exec_query = tryFunc(
|
||||
"int32 wcdb_exec_query(int64 handle, const char* kind, const char* dbPath, const char* sql, _Out_ void** outJson)"
|
||||
);
|
||||
funcs.wcdb_update_message = tryFunc(
|
||||
"int32 wcdb_update_message(int64 handle, const char* sessionId, int64 localId, int32 createTime, const char* newContent, _Out_ void** outError)"
|
||||
);
|
||||
funcs.wcdb_delete_message = tryFunc(
|
||||
"int32 wcdb_delete_message(int64 handle, const char* sessionId, int64 localId, int32 createTime, const char* dbPathHint, _Out_ void** outError)"
|
||||
);
|
||||
funcs.wcdb_get_sns_timeline = tryFunc(
|
||||
"int32 wcdb_get_sns_timeline(int64 handle, int32 limit, int32 offset, const char* usernamesJson, const char* keyword, int32 startTime, int32 endTime, _Out_ void** outJson)"
|
||||
);
|
||||
funcs.wcdb_decrypt_sns_image = tryFunc(
|
||||
"int32 wcdb_decrypt_sns_image(void* encryptedData, int32 len, const char* key, _Out_ void** outHex)"
|
||||
);
|
||||
funcs.wcdb_get_logs = tryFunc("int32 wcdb_get_logs(_Out_ void** outJson)");
|
||||
funcs.wcdb_free_string = tryFunc("void wcdb_free_string(void* ptr)");
|
||||
|
||||
if (!funcs.wcdb_init || !funcs.wcdb_open_account || !funcs.wcdb_get_logs || !funcs.wcdb_free_string) {
|
||||
throw new ApiError("wcdb_api.dll is missing required exports");
|
||||
}
|
||||
|
||||
log(`loaded ${nativeDllPath}`);
|
||||
return nativeLib;
|
||||
}
|
||||
|
||||
function requireFunc(name) {
|
||||
loadNative();
|
||||
const fn = funcs[name];
|
||||
if (!fn) throw new ApiError(`${name} not exported`, -404);
|
||||
return fn;
|
||||
}
|
||||
|
||||
function ptrToString(ptr) {
|
||||
if (!ptr) return "";
|
||||
return loadKoffi().decode(ptr, "char", -1);
|
||||
}
|
||||
|
||||
function freeStringPtr(ptr) {
|
||||
if (!ptr || !funcs.wcdb_free_string) return;
|
||||
try {
|
||||
funcs.wcdb_free_string(ptr);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function getLogs() {
|
||||
try {
|
||||
loadNative();
|
||||
const out = [null];
|
||||
const rc = Number(funcs.wcdb_get_logs(out));
|
||||
try {
|
||||
if (rc !== 0 || !out[0]) return [];
|
||||
const payload = ptrToString(out[0]);
|
||||
const decoded = JSON.parse(payload || "[]");
|
||||
return Array.isArray(decoded) ? decoded.map((x) => String(x)) : [];
|
||||
} finally {
|
||||
freeStringPtr(out[0]);
|
||||
}
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function callOutJson(name, args) {
|
||||
const fn = requireFunc(name);
|
||||
const out = [null];
|
||||
const rc = Number(fn(...args, out));
|
||||
try {
|
||||
if (rc !== 0) throw new ApiError(`${name} failed`, rc);
|
||||
return ptrToString(out[0]);
|
||||
} finally {
|
||||
freeStringPtr(out[0]);
|
||||
}
|
||||
}
|
||||
|
||||
function callOutError(name, args) {
|
||||
const fn = requireFunc(name);
|
||||
const out = [null];
|
||||
const rc = Number(fn(...args, out));
|
||||
try {
|
||||
if (rc !== 0) {
|
||||
const message = ptrToString(out[0]) || `${name} failed`;
|
||||
throw new ApiError(message, rc);
|
||||
}
|
||||
return null;
|
||||
} finally {
|
||||
freeStringPtr(out[0]);
|
||||
}
|
||||
}
|
||||
|
||||
function ensureInitialized() {
|
||||
loadNative();
|
||||
if (initialized) {
|
||||
return { initialized: true, dllPath: nativeDllPath, protection: protectionResults };
|
||||
}
|
||||
|
||||
protectionResults = [];
|
||||
if (funcs.InitProtection) {
|
||||
for (const resourcePath of parseResourcePaths()) {
|
||||
try {
|
||||
const rc = Number(funcs.InitProtection(resourcePath));
|
||||
protectionResults.push({ path: resourcePath, rc });
|
||||
log(`InitProtection rc=${rc} path=${resourcePath}`);
|
||||
if (rc === 0) break;
|
||||
} catch (err) {
|
||||
protectionResults.push({ path: resourcePath, error: String(err?.message || err) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rc = Number(funcs.wcdb_init());
|
||||
if (rc !== 0) {
|
||||
throw new ApiError("wcdb_init failed", rc, { protection: protectionResults });
|
||||
}
|
||||
initialized = true;
|
||||
return { initialized: true, dllPath: nativeDllPath, protection: protectionResults };
|
||||
}
|
||||
|
||||
function normalizeHandle(value) {
|
||||
const n = Number(value || 0);
|
||||
if (!Number.isFinite(n) || n <= 0) throw new ApiError("invalid handle");
|
||||
return n;
|
||||
}
|
||||
|
||||
function handleAction(action, payload) {
|
||||
const data = payload && typeof payload === "object" ? payload : {};
|
||||
|
||||
switch (action) {
|
||||
case "init":
|
||||
return ensureInitialized();
|
||||
|
||||
case "get_logs":
|
||||
loadNative();
|
||||
return { logs: getLogs() };
|
||||
|
||||
case "open_account": {
|
||||
ensureInitialized();
|
||||
const dbPath = String(data.path || "").trim();
|
||||
const key = String(data.key || "").trim();
|
||||
if (!dbPath) throw new ApiError("missing account path");
|
||||
if (key.length !== 64) throw new ApiError("invalid db key");
|
||||
const out = [0];
|
||||
const rc = Number(requireFunc("wcdb_open_account")(dbPath, key, out));
|
||||
const handle = Number(out[0] || 0);
|
||||
if (rc !== 0 || handle <= 0) throw new ApiError("wcdb_open_account failed", rc);
|
||||
return { handle };
|
||||
}
|
||||
|
||||
case "close_account": {
|
||||
const handle = normalizeHandle(data.handle);
|
||||
try {
|
||||
requireFunc("wcdb_close_account")(handle);
|
||||
} catch {}
|
||||
return { closed: true };
|
||||
}
|
||||
|
||||
case "set_my_wxid": {
|
||||
const fn = requireFunc("wcdb_set_my_wxid");
|
||||
const rc = Number(fn(normalizeHandle(data.handle), String(data.wxid || "").trim()));
|
||||
return { success: rc === 0, rc };
|
||||
}
|
||||
|
||||
case "get_sessions":
|
||||
return { payload: callOutJson("wcdb_get_sessions", [normalizeHandle(data.handle)]) };
|
||||
|
||||
case "get_messages":
|
||||
return {
|
||||
payload: callOutJson("wcdb_get_messages", [
|
||||
normalizeHandle(data.handle),
|
||||
String(data.username || "").trim(),
|
||||
Number.parseInt(String(data.limit || 0), 10) || 0,
|
||||
Number.parseInt(String(data.offset || 0), 10) || 0,
|
||||
]),
|
||||
};
|
||||
|
||||
case "get_message_count": {
|
||||
const out = [0];
|
||||
const rc = Number(
|
||||
requireFunc("wcdb_get_message_count")(normalizeHandle(data.handle), String(data.username || "").trim(), out)
|
||||
);
|
||||
return { count: rc === 0 ? Number(out[0] || 0) : 0, rc };
|
||||
}
|
||||
|
||||
case "get_display_names":
|
||||
return {
|
||||
payload: callOutJson("wcdb_get_display_names", [
|
||||
normalizeHandle(data.handle),
|
||||
JSON.stringify(Array.isArray(data.usernames) ? data.usernames : []),
|
||||
]),
|
||||
};
|
||||
|
||||
case "get_avatar_urls":
|
||||
return {
|
||||
payload: callOutJson("wcdb_get_avatar_urls", [
|
||||
normalizeHandle(data.handle),
|
||||
JSON.stringify(Array.isArray(data.usernames) ? data.usernames : []),
|
||||
]),
|
||||
};
|
||||
|
||||
case "get_group_members":
|
||||
return {
|
||||
payload: callOutJson("wcdb_get_group_members", [
|
||||
normalizeHandle(data.handle),
|
||||
String(data.chatroom_id || "").trim(),
|
||||
]),
|
||||
};
|
||||
|
||||
case "get_group_nicknames":
|
||||
return {
|
||||
payload: callOutJson("wcdb_get_group_nicknames", [
|
||||
normalizeHandle(data.handle),
|
||||
String(data.chatroom_id || "").trim(),
|
||||
]),
|
||||
};
|
||||
|
||||
case "exec_query":
|
||||
return {
|
||||
payload: callOutJson("wcdb_exec_query", [
|
||||
normalizeHandle(data.handle),
|
||||
String(data.kind || "").trim(),
|
||||
data.path == null ? null : String(data.path || "").trim(),
|
||||
String(data.sql || ""),
|
||||
]),
|
||||
};
|
||||
|
||||
case "update_message":
|
||||
callOutError("wcdb_update_message", [
|
||||
normalizeHandle(data.handle),
|
||||
String(data.session_id || "").trim(),
|
||||
Number(data.local_id || 0),
|
||||
Number.parseInt(String(data.create_time || 0), 10) || 0,
|
||||
String(data.new_content || ""),
|
||||
]);
|
||||
return { success: true };
|
||||
|
||||
case "delete_message":
|
||||
callOutError("wcdb_delete_message", [
|
||||
normalizeHandle(data.handle),
|
||||
String(data.session_id || "").trim(),
|
||||
Number(data.local_id || 0),
|
||||
Number.parseInt(String(data.create_time || 0), 10) || 0,
|
||||
String(data.db_path_hint || ""),
|
||||
]);
|
||||
return { success: true };
|
||||
|
||||
case "get_sns_timeline":
|
||||
return {
|
||||
payload: callOutJson("wcdb_get_sns_timeline", [
|
||||
normalizeHandle(data.handle),
|
||||
Number.parseInt(String(data.limit || 0), 10) || 0,
|
||||
Number.parseInt(String(data.offset || 0), 10) || 0,
|
||||
JSON.stringify(Array.isArray(data.usernames) ? data.usernames : []),
|
||||
String(data.keyword || ""),
|
||||
Number.parseInt(String(data.start_time || 0), 10) || 0,
|
||||
Number.parseInt(String(data.end_time || 0), 10) || 0,
|
||||
]),
|
||||
};
|
||||
|
||||
case "decrypt_sns_image": {
|
||||
ensureInitialized();
|
||||
const raw = Buffer.from(String(data.data_b64 || ""), "base64");
|
||||
if (!raw.length) return { data_b64: "" };
|
||||
const key = String(data.key || "").trim();
|
||||
if (!key) return { data_b64: raw.toString("base64") };
|
||||
|
||||
const out = [null];
|
||||
const rc = Number(requireFunc("wcdb_decrypt_sns_image")(raw, raw.length, key, out));
|
||||
try {
|
||||
if (rc !== 0 || !out[0]) return { data_b64: raw.toString("base64"), rc };
|
||||
const hex = ptrToString(out[0]).replace(/[^0-9a-f]/gi, "");
|
||||
if (!hex) return { data_b64: raw.toString("base64"), rc };
|
||||
return { data_b64: Buffer.from(hex, "hex").toString("base64"), rc };
|
||||
} finally {
|
||||
freeStringPtr(out[0]);
|
||||
}
|
||||
}
|
||||
|
||||
case "shutdown": {
|
||||
if (nativeLib && initialized && funcs.wcdb_shutdown) {
|
||||
try {
|
||||
funcs.wcdb_shutdown();
|
||||
} finally {
|
||||
initialized = false;
|
||||
}
|
||||
}
|
||||
return { shutdown: true };
|
||||
}
|
||||
|
||||
default:
|
||||
throw new ApiError(`unknown action: ${action}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRequest(req, res) {
|
||||
try {
|
||||
if (req.method === "GET" && req.url === "/health") {
|
||||
jsonResponse(res, 200, { ok: true, initialized, dllPath: nativeDllPath || getDllPath() });
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method !== "POST" || req.url !== "/call") {
|
||||
jsonResponse(res, 404, { ok: false, error: "not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (TOKEN && String(req.headers["x-wcdb-sidecar-token"] || "") !== TOKEN) {
|
||||
jsonResponse(res, 401, { ok: false, error: "unauthorized" });
|
||||
return;
|
||||
}
|
||||
|
||||
const body = await readRequestJson(req);
|
||||
const action = String(body?.action || "").trim();
|
||||
const result = handleAction(action, body?.payload || {});
|
||||
jsonResponse(res, 200, { ok: true, result });
|
||||
} catch (err) {
|
||||
const rc = Number.isFinite(err?.rc) ? Number(err.rc) : 0;
|
||||
const error = err?.message || String(err);
|
||||
log(`error rc=${rc} message=${error}`);
|
||||
jsonResponse(res, 200, {
|
||||
ok: false,
|
||||
error,
|
||||
rc,
|
||||
details: err?.details || null,
|
||||
logs: getLogs().slice(-20),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!Number.isInteger(PORT) || PORT <= 0 || PORT > 65535) {
|
||||
log(`invalid sidecar port: ${process.env.WECHAT_TOOL_WCDB_SIDECAR_PORT || ""}`);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
void handleRequest(req, res);
|
||||
});
|
||||
|
||||
server.listen(PORT, HOST, () => {
|
||||
log(`listening http://${HOST}:${PORT} dll=${getDllPath()} koffi=${process.env.WECHAT_TOOL_KOFFI_DIR || "node_modules"}`);
|
||||
});
|
||||
|
||||
process.on("SIGTERM", () => {
|
||||
try {
|
||||
server.close(() => process.exit(0));
|
||||
} catch {
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
|
||||
process.on("SIGINT", () => {
|
||||
try {
|
||||
server.close(() => process.exit(0));
|
||||
} catch {
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
Vendored
+22
@@ -0,0 +1,22 @@
|
||||
MIT License
|
||||
|
||||
Copyright (C) 2025 Niels Martignène <niels.martignene@protonmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the “Software”), to deal in
|
||||
the Software without restriction, including without limitation the rights to use,
|
||||
copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
Binary file not shown.
Vendored
+634
@@ -0,0 +1,634 @@
|
||||
"use strict";
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __commonJS = (cb, mod3) => function __require() {
|
||||
return mod3 || (0, cb[__getOwnPropNames(cb)[0]])((mod3 = { exports: {} }).exports, mod3), mod3.exports;
|
||||
};
|
||||
|
||||
// package/src/cnoke/src/tools.js
|
||||
var require_tools = __commonJS({
|
||||
"package/src/cnoke/src/tools.js"(exports2, module2) {
|
||||
"use strict";
|
||||
var crypto = require("crypto");
|
||||
var fs2 = require("fs");
|
||||
var http = require("https");
|
||||
var path2 = require("path");
|
||||
var zlib = require("zlib");
|
||||
async function download_http(url, dest) {
|
||||
console.log(">> Downloading " + url);
|
||||
let [tmp_name, file] = open_temporary_stream(dest);
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
let request = http.get(url, (response) => {
|
||||
if (response.statusCode != 200) {
|
||||
let err = new Error(`Download failed: ${response.statusMessage} [${response.statusCode}]`);
|
||||
err.code = response.statusCode;
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
response.pipe(file);
|
||||
file.on("finish", () => file.close(() => {
|
||||
try {
|
||||
fs2.renameSync(file.path, dest);
|
||||
} catch (err) {
|
||||
if (!fs2.existsSync(dest))
|
||||
reject(err);
|
||||
}
|
||||
resolve();
|
||||
}));
|
||||
});
|
||||
request.on("error", reject);
|
||||
file.on("error", reject);
|
||||
});
|
||||
} catch (err) {
|
||||
file.close();
|
||||
try {
|
||||
fs2.unlinkSync(tmp_name);
|
||||
} catch (err2) {
|
||||
if (err2.code != "ENOENT")
|
||||
throw err2;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
function open_temporary_stream(prefix) {
|
||||
let buf = Buffer.allocUnsafe(4);
|
||||
for (; ; ) {
|
||||
try {
|
||||
crypto.randomFillSync(buf);
|
||||
let suffix = buf.toString("hex").padStart(8, "0");
|
||||
let filename2 = `${prefix}.${suffix}`;
|
||||
let file = fs2.createWriteStream(filename2, { flags: "wx", mode: 420 });
|
||||
return [filename2, file];
|
||||
} catch (err) {
|
||||
if (err.code != "EEXIST")
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
function extract_targz(filename2, dest_dir, strip = 0) {
|
||||
let reader = fs2.createReadStream(filename2).pipe(zlib.createGunzip());
|
||||
return new Promise((resolve, reject) => {
|
||||
let header = null;
|
||||
let extended = {};
|
||||
reader.on("readable", () => {
|
||||
try {
|
||||
for (; ; ) {
|
||||
if (header == null) {
|
||||
let buf = reader.read(512);
|
||||
if (buf == null)
|
||||
break;
|
||||
if (!buf[0])
|
||||
continue;
|
||||
header = {
|
||||
filename: buf.toString("utf-8", 0, 100).replace(/\0/g, ""),
|
||||
mode: parseInt(buf.toString("ascii", 100, 109), 8),
|
||||
size: parseInt(buf.toString("ascii", 124, 137), 8),
|
||||
type: String.fromCharCode(buf[156])
|
||||
};
|
||||
Object.assign(header, extended);
|
||||
extended = {};
|
||||
header.filename = header.filename.replace(/\\/g, "/");
|
||||
if (!header.filename.length)
|
||||
throw new Error(`Insecure empty filename inside TAR archive`);
|
||||
if (path_is_absolute(header.filename[0]))
|
||||
throw new Error(`Insecure filename starting with / inside TAR archive`);
|
||||
if (path_has_dotdot(header.filename))
|
||||
throw new Error(`Insecure filename containing '..' inside TAR archive`);
|
||||
for (let i = 0; i < strip; i++)
|
||||
header.filename = header.filename.substr(header.filename.indexOf("/") + 1);
|
||||
}
|
||||
let aligned = Math.floor((header.size + 511) / 512) * 512;
|
||||
let data = header.size ? reader.read(aligned) : null;
|
||||
if (data == null) {
|
||||
if (header.size)
|
||||
break;
|
||||
data = Buffer.alloc(0);
|
||||
}
|
||||
data = data.subarray(0, header.size);
|
||||
if (header.type == "0" || header.type == "7") {
|
||||
let filename3 = dest_dir + "/" + header.filename;
|
||||
let dirname = path2.dirname(filename3);
|
||||
fs2.mkdirSync(dirname, { recursive: true, mode: 493 });
|
||||
fs2.writeFileSync(filename3, data, { mode: header.mode });
|
||||
} else if (header.type == "5") {
|
||||
let filename3 = dest_dir + "/" + header.filename;
|
||||
fs2.mkdirSync(filename3, { recursive: true, mode: header.mode });
|
||||
} else if (header.type == "L") {
|
||||
extended.filename = data.toString("utf-8").replace(/\0/g, "");
|
||||
} else if (header.type == "x") {
|
||||
let str = data.toString("utf-8");
|
||||
try {
|
||||
while (str.length) {
|
||||
let matches = str.match(/^([0-9]+) ([a-zA-Z0-9\._]+)=(.*)\n/);
|
||||
let skip = parseInt(matches[1], 10);
|
||||
let key = matches[2];
|
||||
let value = matches[3];
|
||||
switch (key) {
|
||||
case "path":
|
||||
{
|
||||
extended.filename = value;
|
||||
}
|
||||
break;
|
||||
case "size":
|
||||
{
|
||||
extended.size = parseInt(value, 10);
|
||||
}
|
||||
break;
|
||||
}
|
||||
str = str.substr(skip).trimStart();
|
||||
}
|
||||
} catch (err) {
|
||||
throw new Error("Malformed PAX entry");
|
||||
}
|
||||
}
|
||||
header = null;
|
||||
}
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
reader.on("error", reject);
|
||||
reader.on("end", resolve);
|
||||
});
|
||||
}
|
||||
function path_is_absolute(path3) {
|
||||
if (process.platform == "win32" && path3.match(/^[a-zA-Z]:/))
|
||||
path3 = path3.substr(2);
|
||||
return is_path_separator(path3[0]);
|
||||
}
|
||||
function path_has_dotdot(path3) {
|
||||
let start = 0;
|
||||
for (; ; ) {
|
||||
let offset = path3.indexOf("..", start);
|
||||
if (offset < 0)
|
||||
break;
|
||||
start = offset + 2;
|
||||
if (offset && !is_path_separator(path3[offset - 1]))
|
||||
continue;
|
||||
if (offset + 2 < path3.length && !is_path_separator(path3[offset + 2]))
|
||||
continue;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function is_path_separator(c) {
|
||||
if (c == "/")
|
||||
return true;
|
||||
if (process.platform == "win32" && c == "\\")
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
function sync_files(src_dir, dest_dir) {
|
||||
let keep = /* @__PURE__ */ new Set();
|
||||
{
|
||||
let entries = fs2.readdirSync(src_dir, { withFileTypes: true });
|
||||
for (let entry of entries) {
|
||||
if (!entry.isFile())
|
||||
continue;
|
||||
keep.add(entry.name);
|
||||
fs2.copyFileSync(src_dir + `/${entry.name}`, dest_dir + `/${entry.name}`);
|
||||
}
|
||||
}
|
||||
{
|
||||
let entries = fs2.readdirSync(dest_dir, { withFileTypes: true });
|
||||
for (let entry of entries) {
|
||||
if (!entry.isFile())
|
||||
continue;
|
||||
if (keep.has(entry.name))
|
||||
continue;
|
||||
fs2.unlinkSync(dest_dir + `/${entry.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
function determine_arch2() {
|
||||
let arch = process.arch;
|
||||
if (arch == "riscv32" || arch == "riscv64") {
|
||||
let buf = read_file_header(process.execPath, 512);
|
||||
let header = decode_elf_header(buf);
|
||||
let float_abi = header.e_flags & 6;
|
||||
switch (float_abi) {
|
||||
case 0:
|
||||
{
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
{
|
||||
arch += "f";
|
||||
}
|
||||
break;
|
||||
case 4:
|
||||
{
|
||||
arch += "d";
|
||||
}
|
||||
break;
|
||||
case 6:
|
||||
{
|
||||
arch += "q";
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else if (arch == "arm") {
|
||||
let buf = read_file_header(process.execPath, 512);
|
||||
let header = decode_elf_header(buf);
|
||||
if (header.e_flags & 1024) {
|
||||
arch += "hf";
|
||||
} else if (header.e_flags & 512) {
|
||||
arch += "sf";
|
||||
} else {
|
||||
throw new Error("Unknown ARM floating-point ABI");
|
||||
}
|
||||
}
|
||||
return arch;
|
||||
}
|
||||
function read_file_header(filename2, read) {
|
||||
let fd = null;
|
||||
try {
|
||||
let fd2 = fs2.openSync(filename2);
|
||||
let buf = Buffer.allocUnsafe(read);
|
||||
let len = fs2.readSync(fd2, buf);
|
||||
return buf.subarray(0, len);
|
||||
} finally {
|
||||
if (fd != null)
|
||||
fs2.closeSync(fd);
|
||||
}
|
||||
}
|
||||
function decode_elf_header(buf) {
|
||||
let header = {};
|
||||
if (buf.length < 16)
|
||||
throw new Error("Truncated header");
|
||||
if (buf[0] != 127 || buf[1] != 69 || buf[2] != 76 || buf[3] != 70)
|
||||
throw new Error("Invalid magic number");
|
||||
if (buf[6] != 1)
|
||||
throw new Error("Invalid ELF version");
|
||||
if (buf[5] != 1)
|
||||
throw new Error("Big-endian architectures are not supported");
|
||||
let machine = buf.readUInt16LE(18);
|
||||
switch (machine) {
|
||||
case 3:
|
||||
{
|
||||
header.e_machine = "ia32";
|
||||
}
|
||||
break;
|
||||
case 40:
|
||||
{
|
||||
header.e_machine = "arm";
|
||||
}
|
||||
break;
|
||||
case 62:
|
||||
{
|
||||
header.e_machine = "amd64";
|
||||
}
|
||||
break;
|
||||
case 183:
|
||||
{
|
||||
header.e_machine = "arm64";
|
||||
}
|
||||
break;
|
||||
case 243:
|
||||
{
|
||||
switch (buf[4]) {
|
||||
case 1:
|
||||
{
|
||||
header.e_machine = "riscv32";
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
{
|
||||
header.e_machine = "riscv64";
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 248:
|
||||
{
|
||||
switch (buf[4]) {
|
||||
case 1:
|
||||
{
|
||||
header.e_machine = "loong32";
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
{
|
||||
header.e_machine = "loong64";
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error("Unknown ELF machine type");
|
||||
}
|
||||
switch (buf[4]) {
|
||||
case 1:
|
||||
{
|
||||
buf = buf.subarray(0, 68);
|
||||
if (buf.length < 68)
|
||||
throw new Error("Truncated ELF header");
|
||||
header.ei_class = 32;
|
||||
header.e_flags = buf.readUInt32LE(36);
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
{
|
||||
buf = buf.subarray(0, 120);
|
||||
if (buf.length < 120)
|
||||
throw new Error("Truncated ELF header");
|
||||
header.ei_class = 64;
|
||||
header.e_flags = buf.readUInt32LE(48);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error("Invalid ELF class");
|
||||
}
|
||||
return header;
|
||||
}
|
||||
function unlink_recursive(path3) {
|
||||
try {
|
||||
fs2.rmSync(path3, { recursive: true, maxRetries: process.platform == "win32" ? 3 : 0 });
|
||||
} catch (err) {
|
||||
if (err.code !== "ENOENT")
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
function get_napi_version2(napi, major) {
|
||||
if (napi > 8)
|
||||
return null;
|
||||
const support = {
|
||||
6: ["6.14.2", "6.14.2", "6.14.2"],
|
||||
8: ["8.6.0", "8.10.0", "8.11.2"],
|
||||
9: ["9.0.0", "9.3.0", "9.11.0"],
|
||||
10: ["10.0.0", "10.0.0", "10.0.0", "10.16.0", "10.17.0", "10.20.0", "10.23.0"],
|
||||
11: ["11.0.0", "11.0.0", "11.0.0", "11.8.0"],
|
||||
12: ["12.0.0", "12.0.0", "12.0.0", "12.0.0", "12.11.0", "12.17.0", "12.19.0", "12.22.0"],
|
||||
13: ["13.0.0", "13.0.0", "13.0.0", "13.0.0", "13.0.0"],
|
||||
14: ["14.0.0", "14.0.0", "14.0.0", "14.0.0", "14.0.0", "14.0.0", "14.12.0", "14.17.0"],
|
||||
15: ["15.0.0", "15.0.0", "15.0.0", "15.0.0", "15.0.0", "15.0.0", "15.0.0", "15.12.0"]
|
||||
};
|
||||
const max = Math.max(...Object.keys(support).map((k) => parseInt(k, 10)));
|
||||
if (major > max)
|
||||
return major + ".0.0";
|
||||
if (support[major] == null)
|
||||
return null;
|
||||
let required = support[major][napi - 1] || null;
|
||||
return required;
|
||||
}
|
||||
function cmp_version(ver1, ver2) {
|
||||
ver1 = String(ver1).replace(/-.*$/, "").split(".").reduce((acc, v, idx) => acc + parseInt(v, 10) * Math.pow(10, 2 * (5 - idx)), 0);
|
||||
ver2 = String(ver2).replace(/-.*$/, "").split(".").reduce((acc, v, idx) => acc + parseInt(v, 10) * Math.pow(10, 2 * (5 - idx)), 0);
|
||||
let cmp = Math.min(Math.max(ver1 - ver2, -1), 1);
|
||||
return cmp;
|
||||
}
|
||||
module2.exports = {
|
||||
download_http,
|
||||
extract_targz,
|
||||
path_is_absolute,
|
||||
path_has_dotdot,
|
||||
sync_files,
|
||||
determine_arch: determine_arch2,
|
||||
unlink_recursive,
|
||||
get_napi_version: get_napi_version2,
|
||||
cmp_version
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// package/src/koffi/package.json
|
||||
var require_package = __commonJS({
|
||||
"package/src/koffi/package.json"(exports2, module2) {
|
||||
module2.exports = {
|
||||
name: "koffi",
|
||||
version: "2.15.2",
|
||||
description: "Fast and simple C FFI (foreign function interface) for Node.js",
|
||||
keywords: [
|
||||
"foreign",
|
||||
"function",
|
||||
"interface",
|
||||
"ffi",
|
||||
"binding",
|
||||
"c",
|
||||
"napi"
|
||||
],
|
||||
repository: {
|
||||
type: "git",
|
||||
url: "https://github.com/Koromix/koffi"
|
||||
},
|
||||
homepage: "https://koffi.dev/",
|
||||
author: {
|
||||
name: "Niels Martign\xE8ne",
|
||||
email: "niels.martignene@protonmail.com",
|
||||
url: "https://koromix.dev/"
|
||||
},
|
||||
main: "./index.js",
|
||||
types: "./index.d.ts",
|
||||
scripts: {
|
||||
test: "node tools/koffi.js test",
|
||||
prepack: `echo 'Use "npm run package" instead' && false`,
|
||||
prepublishOnly: `echo 'Use "npm run package" instead' && false`,
|
||||
package: "node tools/koffi.js build"
|
||||
},
|
||||
license: "MIT",
|
||||
cnoke: {
|
||||
api: "../../vendor/node-api-headers",
|
||||
output: "../../bin/Koffi/{{ toolchain }}",
|
||||
node: 16,
|
||||
napi: 8,
|
||||
require: "./index.js"
|
||||
},
|
||||
funding: "https://liberapay.com/Koromix"
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// package/src/koffi/src/init.js
|
||||
var require_init = __commonJS({
|
||||
"package/src/koffi/src/init.js"(exports, module) {
|
||||
var fs = require("fs");
|
||||
var path = require("path");
|
||||
var util = require("util");
|
||||
var { get_napi_version, determine_arch } = require_tools();
|
||||
var pkg = require_package();
|
||||
function detect() {
|
||||
if (process.versions.napi == null || process.versions.napi < pkg.cnoke.napi) {
|
||||
let major = parseInt(process.versions.node, 10);
|
||||
let required = get_napi_version(pkg.cnoke.napi, major);
|
||||
if (required != null) {
|
||||
throw new Error(`This engine is based on Node ${process.versions.node}, but ${pkg.name} requires Node >= ${required} in the Node ${major}.x branch (N-API >= ${pkg.cnoke.napi})`);
|
||||
} else {
|
||||
throw new Error(`This engine is based on Node ${process.versions.node}, but ${pkg.name} does not support the Node ${major}.x branch (N-API < ${pkg.cnoke.napi})`);
|
||||
}
|
||||
}
|
||||
let arch = determine_arch();
|
||||
let triplet3 = `${process.platform}_${arch}`;
|
||||
return triplet3;
|
||||
}
|
||||
function init(triplet, native) {
|
||||
if (native == null) {
|
||||
let roots = [path.join(__dirname, "..")];
|
||||
let triplets = [triplet];
|
||||
if (process.resourcesPath != null)
|
||||
roots.push(process.resourcesPath);
|
||||
if (triplet.startsWith("linux_")) {
|
||||
let musl = triplet.replace(/^linux_/, "musl_");
|
||||
triplets.push(musl);
|
||||
}
|
||||
let filenames = roots.flatMap((root) => triplets.flatMap((triplet3) => [
|
||||
`${root}/koffi/build/koffi/${triplet3}/koffi.node`,
|
||||
`${root}/build/koffi/${triplet3}/koffi.node`,
|
||||
`${root}/koffi/${triplet3}/koffi.node`,
|
||||
`${root}/node_modules/koffi/build/koffi/${triplet3}/koffi.node`,
|
||||
`${root}/../../bin/Koffi/${triplet3}/koffi.node`
|
||||
]));
|
||||
let first_err = null;
|
||||
for (let filename of filenames) {
|
||||
if (!fs.existsSync(filename))
|
||||
continue;
|
||||
try {
|
||||
native = eval("require")(filename);
|
||||
} catch (err) {
|
||||
if (first_err == null)
|
||||
first_err = err;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (first_err != null)
|
||||
throw first_err;
|
||||
}
|
||||
if (native == null)
|
||||
throw new Error("Cannot find the native Koffi module; did you bundle it correctly?");
|
||||
if (native.version != pkg.version)
|
||||
throw new Error("Mismatched native Koffi modules");
|
||||
let mod = wrap(native);
|
||||
return mod;
|
||||
}
|
||||
function wrap(native3) {
|
||||
let obj = {
|
||||
...native3,
|
||||
// Deprecated functions
|
||||
handle: util.deprecate(native3.opaque, "The koffi.handle() function was deprecated in Koffi 2.1, use koffi.opaque() instead", "KOFFI001"),
|
||||
callback: util.deprecate(native3.proto, "The koffi.callback() function was deprecated in Koffi 2.4, use koffi.proto() instead", "KOFFI002")
|
||||
};
|
||||
obj.load = (...args) => {
|
||||
let lib = native3.load(...args);
|
||||
lib.cdecl = util.deprecate((...args2) => lib.func("__cdecl", ...args2), "The koffi.cdecl() function was deprecated in Koffi 2.7, use koffi.func(...) instead", "KOFFI003");
|
||||
lib.stdcall = util.deprecate((...args2) => lib.func("__stdcall", ...args2), 'The koffi.stdcall() function was deprecated in Koffi 2.7, use koffi.func("__stdcall", ...) instead', "KOFFI004");
|
||||
lib.fastcall = util.deprecate((...args2) => lib.func("__fastcall", ...args2), 'The koffi.fastcall() function was deprecated in Koffi 2.7, use koffi.func("__fastcall", ...) instead', "KOFFI005");
|
||||
lib.thiscall = util.deprecate((...args2) => lib.func("__thiscall", ...args2), 'The koffi.thiscall() function was deprecated in Koffi 2.7, use koffi.func("__thiscall", ...) instead', "KOFFI006");
|
||||
return lib;
|
||||
};
|
||||
return obj;
|
||||
}
|
||||
module.exports = {
|
||||
detect,
|
||||
init
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// package/src/koffi/index.js
|
||||
var { detect: detect2, init: init2 } = require_init();
|
||||
var triplet2 = detect2();
|
||||
var native2 = null;
|
||||
try {
|
||||
switch (triplet2) {
|
||||
case "darwin_arm64":
|
||||
{
|
||||
native2 = require("./build/koffi/darwin_arm64/koffi.node");
|
||||
}
|
||||
break;
|
||||
case "darwin_x64":
|
||||
{
|
||||
native2 = require("./build/koffi/darwin_x64/koffi.node");
|
||||
}
|
||||
break;
|
||||
case "freebsd_arm64":
|
||||
{
|
||||
native2 = require("./build/koffi/freebsd_arm64/koffi.node");
|
||||
}
|
||||
break;
|
||||
case "freebsd_ia32":
|
||||
{
|
||||
native2 = require("./build/koffi/freebsd_ia32/koffi.node");
|
||||
}
|
||||
break;
|
||||
case "freebsd_x64":
|
||||
{
|
||||
native2 = require("./build/koffi/freebsd_x64/koffi.node");
|
||||
}
|
||||
break;
|
||||
case "linux_armhf":
|
||||
{
|
||||
native2 = require("./build/koffi/linux_armhf/koffi.node");
|
||||
}
|
||||
break;
|
||||
case "linux_arm64":
|
||||
{
|
||||
native2 = require("./build/koffi/linux_arm64/koffi.node");
|
||||
}
|
||||
break;
|
||||
case "linux_ia32":
|
||||
{
|
||||
native2 = require("./build/koffi/linux_ia32/koffi.node");
|
||||
}
|
||||
break;
|
||||
case "linux_loong64":
|
||||
{
|
||||
native2 = require("./build/koffi/linux_loong64/koffi.node");
|
||||
}
|
||||
break;
|
||||
case "linux_riscv64d":
|
||||
{
|
||||
native2 = require("./build/koffi/linux_riscv64d/koffi.node");
|
||||
}
|
||||
break;
|
||||
case "linux_x64":
|
||||
{
|
||||
native2 = require("./build/koffi/linux_x64/koffi.node");
|
||||
}
|
||||
break;
|
||||
case "openbsd_ia32":
|
||||
{
|
||||
native2 = require("./build/koffi/openbsd_ia32/koffi.node");
|
||||
}
|
||||
break;
|
||||
case "openbsd_x64":
|
||||
{
|
||||
native2 = require("./build/koffi/openbsd_x64/koffi.node");
|
||||
}
|
||||
break;
|
||||
case "win32_arm64":
|
||||
{
|
||||
native2 = require("./build/koffi/win32_arm64/koffi.node");
|
||||
}
|
||||
break;
|
||||
case "win32_ia32":
|
||||
{
|
||||
native2 = require("./build/koffi/win32_ia32/koffi.node");
|
||||
}
|
||||
break;
|
||||
case "win32_x64":
|
||||
{
|
||||
native2 = require("./build/koffi/win32_x64/koffi.node");
|
||||
}
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
try {
|
||||
switch (triplet2) {
|
||||
case "linux_arm64":
|
||||
{
|
||||
native2 = require("./build/koffi/musl_arm64/koffi.node");
|
||||
}
|
||||
break;
|
||||
case "linux_x64":
|
||||
{
|
||||
native2 = require("./build/koffi/musl_x64/koffi.node");
|
||||
}
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
var mod2 = init2(triplet2, native2);
|
||||
module.exports = mod2;
|
||||
Vendored
+26
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "koffi",
|
||||
"version": "2.15.2",
|
||||
"description": "Fast and simple C FFI (foreign function interface) for Node.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Koromix/koffi"
|
||||
},
|
||||
"homepage": "https://koffi.dev/",
|
||||
"author": {
|
||||
"name": "Niels Martignène",
|
||||
"email": "niels.martignene@protonmail.com",
|
||||
"url": "https://koromix.dev/"
|
||||
},
|
||||
"main": "./index.js",
|
||||
"types": "./index.d.ts",
|
||||
"license": "MIT",
|
||||
"cnoke": {
|
||||
"api": "../../vendor/node-api-headers",
|
||||
"output": "build/koffi/{{ toolchain }}",
|
||||
"node": 16,
|
||||
"napi": 8,
|
||||
"require": "./index.js"
|
||||
},
|
||||
"funding": "https://liberapay.com/Koromix"
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,4 +1,5 @@
|
||||
import ctypes
|
||||
import base64
|
||||
import binascii
|
||||
import json
|
||||
import os
|
||||
@@ -6,6 +7,8 @@ import re
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
@@ -121,14 +124,188 @@ def _resolve_wcdb_api_dll_path() -> Path:
|
||||
_lib_lock = threading.Lock()
|
||||
_lib: Optional[ctypes.CDLL] = None
|
||||
_initialized = False
|
||||
_loaded_wcdb_api_dll: Optional[Path] = None
|
||||
_preloaded_native_libs: list[ctypes.CDLL] = []
|
||||
_protection_checked = False
|
||||
_protection_result: Optional[tuple[int, str]] = None
|
||||
|
||||
|
||||
def _is_windows() -> bool:
|
||||
return sys.platform.startswith("win")
|
||||
|
||||
|
||||
def _iter_wcdb_resource_paths(wcdb_api_dll: Path) -> tuple[Path, ...]:
|
||||
candidates: list[Path] = []
|
||||
seen: set[str] = set()
|
||||
|
||||
def add(path: str | Path | None) -> None:
|
||||
if not path:
|
||||
return
|
||||
try:
|
||||
resolved = Path(path).resolve()
|
||||
except Exception:
|
||||
resolved = Path(path)
|
||||
key = str(resolved).replace("/", "\\").rstrip("\\").lower()
|
||||
if key in seen:
|
||||
return
|
||||
seen.add(key)
|
||||
candidates.append(resolved)
|
||||
|
||||
dll_dir = wcdb_api_dll.parent
|
||||
add(dll_dir)
|
||||
add(dll_dir.parent)
|
||||
add(_NATIVE_DIR)
|
||||
add(_NATIVE_DIR.parent)
|
||||
|
||||
cwd = Path.cwd()
|
||||
add(cwd)
|
||||
add(cwd / "resources")
|
||||
|
||||
data_dir = str(os.environ.get("WECHAT_TOOL_DATA_DIR", "") or "").strip()
|
||||
if data_dir:
|
||||
add(data_dir)
|
||||
add(Path(data_dir) / "resources")
|
||||
|
||||
if getattr(sys, "frozen", False):
|
||||
try:
|
||||
exe_dir = Path(sys.executable).resolve().parent
|
||||
except Exception:
|
||||
exe_dir = Path(sys.executable).parent
|
||||
add(exe_dir)
|
||||
add(exe_dir / "resources")
|
||||
|
||||
return tuple(candidates)
|
||||
|
||||
|
||||
def _preload_wcdb_dependencies(wcdb_api_dll: Path) -> None:
|
||||
dll_dir = wcdb_api_dll.parent
|
||||
for name in ("WCDB.dll", "SDL2.dll", "VoipEngine.dll"):
|
||||
dep_path = dll_dir / name
|
||||
if not dep_path.exists():
|
||||
continue
|
||||
try:
|
||||
_preloaded_native_libs.append(ctypes.CDLL(str(dep_path)))
|
||||
logger.info("[wcdb] preloaded dependency: %s", dep_path)
|
||||
except Exception as exc:
|
||||
logger.warning("[wcdb] preload dependency failed: %s err=%s", dep_path, exc)
|
||||
|
||||
|
||||
def _run_init_protection(lib: ctypes.CDLL, wcdb_api_dll: Path) -> None:
|
||||
global _protection_checked, _protection_result
|
||||
if _protection_checked:
|
||||
return
|
||||
_protection_checked = True
|
||||
|
||||
fn = getattr(lib, "InitProtection", None)
|
||||
if not fn:
|
||||
logger.info("[wcdb] InitProtection not exported: %s", wcdb_api_dll)
|
||||
return
|
||||
|
||||
try:
|
||||
fn.argtypes = [ctypes.c_char_p]
|
||||
fn.restype = ctypes.c_int32
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
best: Optional[tuple[int, str]] = None
|
||||
for resource_path in _iter_wcdb_resource_paths(wcdb_api_dll):
|
||||
try:
|
||||
rc = int(fn(str(resource_path).encode("utf-8")))
|
||||
logger.info("[wcdb] InitProtection rc=%s path=%s", rc, resource_path)
|
||||
if rc == 0:
|
||||
_protection_result = (rc, str(resource_path))
|
||||
return
|
||||
if best is None:
|
||||
best = (rc, str(resource_path))
|
||||
except Exception as exc:
|
||||
logger.warning("[wcdb] InitProtection exception path=%s err=%s", resource_path, exc)
|
||||
|
||||
_protection_result = best
|
||||
|
||||
|
||||
def _format_protection_hint() -> str:
|
||||
if not _protection_result:
|
||||
return ""
|
||||
rc, resource_path = _protection_result
|
||||
return f" protection_rc={rc} protection_path={resource_path}"
|
||||
|
||||
|
||||
def _sidecar_url() -> str:
|
||||
return str(os.environ.get("WECHAT_TOOL_WCDB_SIDECAR_URL", "") or "").strip().rstrip("/")
|
||||
|
||||
|
||||
def _sidecar_enabled() -> bool:
|
||||
return bool(_sidecar_url())
|
||||
|
||||
|
||||
def _sidecar_call(action: str, payload: Optional[dict[str, Any]] = None, *, timeout: float = 30.0) -> dict[str, Any]:
|
||||
base_url = _sidecar_url()
|
||||
if not base_url:
|
||||
raise WCDBRealtimeError("WCDB sidecar is not configured.")
|
||||
|
||||
token = str(os.environ.get("WECHAT_TOOL_WCDB_SIDECAR_TOKEN", "") or "").strip()
|
||||
body = json.dumps(
|
||||
{
|
||||
"action": str(action or "").strip(),
|
||||
"payload": payload or {},
|
||||
},
|
||||
ensure_ascii=False,
|
||||
).encode("utf-8")
|
||||
|
||||
deadline = time.monotonic() + max(1.0, float(timeout or 30.0))
|
||||
last_err: Exception | None = None
|
||||
attempts = 20 if action == "init" else 3
|
||||
for attempt in range(attempts):
|
||||
headers = {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
"Accept": "application/json",
|
||||
}
|
||||
if token:
|
||||
headers["X-WCDB-Sidecar-Token"] = token
|
||||
req = urllib.request.Request(
|
||||
f"{base_url}/call",
|
||||
data=body,
|
||||
headers=headers,
|
||||
method="POST",
|
||||
)
|
||||
try:
|
||||
remaining = max(0.5, deadline - time.monotonic())
|
||||
with urllib.request.urlopen(req, timeout=min(remaining, max(0.5, timeout))) as resp:
|
||||
raw = resp.read()
|
||||
decoded = json.loads(raw.decode("utf-8", errors="replace") or "{}")
|
||||
if not isinstance(decoded, dict):
|
||||
raise WCDBRealtimeError("WCDB sidecar returned invalid response.")
|
||||
if decoded.get("ok"):
|
||||
result = decoded.get("result")
|
||||
return result if isinstance(result, dict) else {}
|
||||
rc = decoded.get("rc")
|
||||
err = str(decoded.get("error") or "WCDB sidecar call failed")
|
||||
logs = decoded.get("logs")
|
||||
hint = ""
|
||||
if isinstance(logs, list) and logs:
|
||||
hint = f" logs={[str(x) for x in logs[:6]]}"
|
||||
raise WCDBRealtimeError(f"{err} rc={rc}.{hint}")
|
||||
except WCDBRealtimeError:
|
||||
raise
|
||||
except (urllib.error.URLError, TimeoutError, OSError) as exc:
|
||||
last_err = exc
|
||||
if attempt >= attempts - 1 or time.monotonic() >= deadline:
|
||||
break
|
||||
time.sleep(0.15)
|
||||
except Exception as exc:
|
||||
last_err = exc
|
||||
break
|
||||
|
||||
raise WCDBRealtimeError(f"WCDB sidecar unavailable: {last_err}")
|
||||
|
||||
|
||||
def _sidecar_payload(action: str, payload: Optional[dict[str, Any]] = None, *, timeout: float = 30.0) -> str:
|
||||
result = _sidecar_call(action, payload, timeout=timeout)
|
||||
return str(result.get("payload") or "")
|
||||
|
||||
|
||||
def _load_wcdb_lib() -> ctypes.CDLL:
|
||||
global _lib
|
||||
global _lib, _loaded_wcdb_api_dll
|
||||
with _lib_lock:
|
||||
if _lib is not None:
|
||||
return _lib
|
||||
@@ -146,6 +323,7 @@ def _load_wcdb_lib() -> ctypes.CDLL:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
_preload_wcdb_dependencies(wcdb_api_dll)
|
||||
lib = ctypes.CDLL(str(wcdb_api_dll))
|
||||
logger.info("[wcdb] using wcdb_api.dll: %s", wcdb_api_dll)
|
||||
|
||||
@@ -291,20 +469,50 @@ def _load_wcdb_lib() -> ctypes.CDLL:
|
||||
lib.wcdb_free_string.argtypes = [ctypes.c_char_p]
|
||||
lib.wcdb_free_string.restype = None
|
||||
|
||||
_loaded_wcdb_api_dll = wcdb_api_dll
|
||||
_lib = lib
|
||||
return lib
|
||||
|
||||
|
||||
def _ensure_initialized() -> None:
|
||||
global _initialized
|
||||
global _initialized, _loaded_wcdb_api_dll, _protection_result
|
||||
if _sidecar_enabled():
|
||||
with _lib_lock:
|
||||
if _initialized:
|
||||
return
|
||||
result = _sidecar_call("init", timeout=30.0)
|
||||
dll_path = str(result.get("dllPath") or "").strip()
|
||||
if dll_path:
|
||||
try:
|
||||
_loaded_wcdb_api_dll = Path(dll_path)
|
||||
except Exception:
|
||||
pass
|
||||
protection = result.get("protection")
|
||||
if isinstance(protection, list):
|
||||
for item in protection:
|
||||
if isinstance(item, dict) and "rc" in item:
|
||||
try:
|
||||
_protection_result = (int(item.get("rc")), str(item.get("path") or ""))
|
||||
if int(item.get("rc")) == 0:
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
with _lib_lock:
|
||||
_initialized = True
|
||||
return
|
||||
|
||||
lib = _load_wcdb_lib()
|
||||
with _lib_lock:
|
||||
if _initialized:
|
||||
return
|
||||
wcdb_api_dll = _loaded_wcdb_api_dll or _resolve_wcdb_api_dll_path()
|
||||
_run_init_protection(lib, wcdb_api_dll)
|
||||
rc = int(lib.wcdb_init())
|
||||
if rc != 0:
|
||||
logs = get_native_logs(require_initialized=False)
|
||||
hint = f" logs={logs[:6]}" if logs else ""
|
||||
hint = _format_protection_hint()
|
||||
if logs:
|
||||
hint += f" logs={logs[:6]}"
|
||||
raise WCDBRealtimeError(f"wcdb_init failed: {rc}.{hint}")
|
||||
_initialized = True
|
||||
|
||||
@@ -368,6 +576,21 @@ def _call_out_error(fn, *args) -> None:
|
||||
|
||||
|
||||
def get_native_logs(*, require_initialized: bool = True) -> list[str]:
|
||||
if _sidecar_enabled():
|
||||
if require_initialized:
|
||||
try:
|
||||
_ensure_initialized()
|
||||
except Exception:
|
||||
return []
|
||||
try:
|
||||
result = _sidecar_call("get_logs", timeout=5.0)
|
||||
logs = result.get("logs")
|
||||
if isinstance(logs, list):
|
||||
return [str(x) for x in logs]
|
||||
return []
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
if require_initialized:
|
||||
try:
|
||||
_ensure_initialized()
|
||||
@@ -404,6 +627,20 @@ def open_account(session_db_path: Path, key_hex: str) -> int:
|
||||
if len(key) != 64:
|
||||
raise WCDBRealtimeError("Invalid db key (must be 64 hex chars).")
|
||||
|
||||
if _sidecar_enabled():
|
||||
result = _sidecar_call(
|
||||
"open_account",
|
||||
{
|
||||
"path": str(p),
|
||||
"key": key,
|
||||
},
|
||||
timeout=30.0,
|
||||
)
|
||||
handle = int(result.get("handle") or 0)
|
||||
if handle <= 0:
|
||||
raise WCDBRealtimeError("wcdb_open_account failed: invalid sidecar handle.")
|
||||
return handle
|
||||
|
||||
lib = _load_wcdb_lib()
|
||||
out_handle = ctypes.c_int64(0)
|
||||
rc = int(lib.wcdb_open_account(str(p).encode("utf-8"), key.encode("utf-8"), ctypes.byref(out_handle)))
|
||||
@@ -421,15 +658,22 @@ def set_my_wxid(handle: int, wxid: str) -> bool:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
w = str(wxid or "").strip()
|
||||
if not w:
|
||||
return False
|
||||
|
||||
if _sidecar_enabled():
|
||||
try:
|
||||
result = _sidecar_call("set_my_wxid", {"handle": int(handle), "wxid": w}, timeout=10.0)
|
||||
return bool(result.get("success"))
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
lib = _load_wcdb_lib()
|
||||
fn = getattr(lib, "wcdb_set_my_wxid", None)
|
||||
if not fn:
|
||||
return False
|
||||
|
||||
w = str(wxid or "").strip()
|
||||
if not w:
|
||||
return False
|
||||
|
||||
try:
|
||||
rc = int(fn(ctypes.c_int64(int(handle)), w.encode("utf-8")))
|
||||
except Exception:
|
||||
@@ -449,6 +693,12 @@ def close_account(handle: int) -> None:
|
||||
_ensure_initialized()
|
||||
except Exception:
|
||||
return
|
||||
if _sidecar_enabled():
|
||||
try:
|
||||
_sidecar_call("close_account", {"handle": h}, timeout=5.0)
|
||||
except Exception:
|
||||
pass
|
||||
return
|
||||
lib = _load_wcdb_lib()
|
||||
try:
|
||||
lib.wcdb_close_account(ctypes.c_int64(h))
|
||||
@@ -458,6 +708,13 @@ def close_account(handle: int) -> None:
|
||||
|
||||
def get_sessions(handle: int) -> list[dict[str, Any]]:
|
||||
_ensure_initialized()
|
||||
if _sidecar_enabled():
|
||||
payload = _sidecar_payload("get_sessions", {"handle": int(handle)}, timeout=30.0)
|
||||
decoded = _safe_load_json(payload)
|
||||
if isinstance(decoded, list):
|
||||
return [x for x in decoded if isinstance(x, dict)]
|
||||
return []
|
||||
|
||||
lib = _load_wcdb_lib()
|
||||
payload = _call_out_json(lib.wcdb_get_sessions, ctypes.c_int64(int(handle)))
|
||||
decoded = _safe_load_json(payload)
|
||||
@@ -472,10 +729,26 @@ def get_sessions(handle: int) -> list[dict[str, Any]]:
|
||||
|
||||
def get_messages(handle: int, username: str, *, limit: int = 50, offset: int = 0) -> list[dict[str, Any]]:
|
||||
_ensure_initialized()
|
||||
lib = _load_wcdb_lib()
|
||||
u = str(username or "").strip()
|
||||
if not u:
|
||||
return []
|
||||
if _sidecar_enabled():
|
||||
payload = _sidecar_payload(
|
||||
"get_messages",
|
||||
{
|
||||
"handle": int(handle),
|
||||
"username": u,
|
||||
"limit": int(limit),
|
||||
"offset": int(offset),
|
||||
},
|
||||
timeout=30.0,
|
||||
)
|
||||
decoded = _safe_load_json(payload)
|
||||
if isinstance(decoded, list):
|
||||
return [x for x in decoded if isinstance(x, dict)]
|
||||
return []
|
||||
|
||||
lib = _load_wcdb_lib()
|
||||
payload = _call_out_json(
|
||||
lib.wcdb_get_messages,
|
||||
ctypes.c_int64(int(handle)),
|
||||
@@ -495,10 +768,21 @@ def get_messages(handle: int, username: str, *, limit: int = 50, offset: int = 0
|
||||
|
||||
def get_message_count(handle: int, username: str) -> int:
|
||||
_ensure_initialized()
|
||||
lib = _load_wcdb_lib()
|
||||
u = str(username or "").strip()
|
||||
if not u:
|
||||
return 0
|
||||
if _sidecar_enabled():
|
||||
result = _sidecar_call(
|
||||
"get_message_count",
|
||||
{"handle": int(handle), "username": u},
|
||||
timeout=30.0,
|
||||
)
|
||||
try:
|
||||
return int(result.get("count") or 0)
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
lib = _load_wcdb_lib()
|
||||
out_count = ctypes.c_int32(0)
|
||||
rc = int(lib.wcdb_get_message_count(ctypes.c_int64(int(handle)), u.encode("utf-8"), ctypes.byref(out_count)))
|
||||
if rc != 0:
|
||||
@@ -508,11 +792,22 @@ def get_message_count(handle: int, username: str) -> int:
|
||||
|
||||
def get_display_names(handle: int, usernames: list[str]) -> dict[str, str]:
|
||||
_ensure_initialized()
|
||||
lib = _load_wcdb_lib()
|
||||
uniq = [str(u or "").strip() for u in usernames if str(u or "").strip()]
|
||||
uniq = list(dict.fromkeys(uniq))
|
||||
if not uniq:
|
||||
return {}
|
||||
if _sidecar_enabled():
|
||||
out_json = _sidecar_payload(
|
||||
"get_display_names",
|
||||
{"handle": int(handle), "usernames": uniq},
|
||||
timeout=30.0,
|
||||
)
|
||||
decoded = _safe_load_json(out_json)
|
||||
if isinstance(decoded, dict):
|
||||
return {str(k): str(v) for k, v in decoded.items()}
|
||||
return {}
|
||||
|
||||
lib = _load_wcdb_lib()
|
||||
payload = json.dumps(uniq, ensure_ascii=False).encode("utf-8")
|
||||
out_json = _call_out_json(lib.wcdb_get_display_names, ctypes.c_int64(int(handle)), payload)
|
||||
decoded = _safe_load_json(out_json)
|
||||
@@ -523,11 +818,22 @@ def get_display_names(handle: int, usernames: list[str]) -> dict[str, str]:
|
||||
|
||||
def get_avatar_urls(handle: int, usernames: list[str]) -> dict[str, str]:
|
||||
_ensure_initialized()
|
||||
lib = _load_wcdb_lib()
|
||||
uniq = [str(u or "").strip() for u in usernames if str(u or "").strip()]
|
||||
uniq = list(dict.fromkeys(uniq))
|
||||
if not uniq:
|
||||
return {}
|
||||
if _sidecar_enabled():
|
||||
out_json = _sidecar_payload(
|
||||
"get_avatar_urls",
|
||||
{"handle": int(handle), "usernames": uniq},
|
||||
timeout=30.0,
|
||||
)
|
||||
decoded = _safe_load_json(out_json)
|
||||
if isinstance(decoded, dict):
|
||||
return {str(k): str(v) for k, v in decoded.items()}
|
||||
return {}
|
||||
|
||||
lib = _load_wcdb_lib()
|
||||
payload = json.dumps(uniq, ensure_ascii=False).encode("utf-8")
|
||||
out_json = _call_out_json(lib.wcdb_get_avatar_urls, ctypes.c_int64(int(handle)), payload)
|
||||
decoded = _safe_load_json(out_json)
|
||||
@@ -538,10 +844,21 @@ def get_avatar_urls(handle: int, usernames: list[str]) -> dict[str, str]:
|
||||
|
||||
def get_group_members(handle: int, chatroom_id: str) -> list[dict[str, Any]]:
|
||||
_ensure_initialized()
|
||||
lib = _load_wcdb_lib()
|
||||
cid = str(chatroom_id or "").strip()
|
||||
if not cid:
|
||||
return []
|
||||
if _sidecar_enabled():
|
||||
out_json = _sidecar_payload(
|
||||
"get_group_members",
|
||||
{"handle": int(handle), "chatroom_id": cid},
|
||||
timeout=30.0,
|
||||
)
|
||||
decoded = _safe_load_json(out_json)
|
||||
if isinstance(decoded, list):
|
||||
return [x for x in decoded if isinstance(x, dict)]
|
||||
return []
|
||||
|
||||
lib = _load_wcdb_lib()
|
||||
out_json = _call_out_json(lib.wcdb_get_group_members, ctypes.c_int64(int(handle)), cid.encode("utf-8"))
|
||||
decoded = _safe_load_json(out_json)
|
||||
if isinstance(decoded, list):
|
||||
@@ -555,15 +872,29 @@ def get_group_members(handle: int, chatroom_id: str) -> list[dict[str, Any]]:
|
||||
|
||||
def get_group_nicknames(handle: int, chatroom_id: str) -> dict[str, str]:
|
||||
_ensure_initialized()
|
||||
cid = str(chatroom_id or "").strip()
|
||||
if not cid:
|
||||
return {}
|
||||
|
||||
if _sidecar_enabled():
|
||||
try:
|
||||
out_json = _sidecar_payload(
|
||||
"get_group_nicknames",
|
||||
{"handle": int(handle), "chatroom_id": cid},
|
||||
timeout=30.0,
|
||||
)
|
||||
except Exception:
|
||||
return {}
|
||||
decoded = _safe_load_json(out_json)
|
||||
if isinstance(decoded, dict):
|
||||
return {str(k): str(v) for k, v in decoded.items()}
|
||||
return {}
|
||||
|
||||
lib = _load_wcdb_lib()
|
||||
fn = getattr(lib, "wcdb_get_group_nicknames", None)
|
||||
if not fn:
|
||||
return {}
|
||||
|
||||
cid = str(chatroom_id or "").strip()
|
||||
if not cid:
|
||||
return {}
|
||||
|
||||
out_json = _call_out_json(fn, ctypes.c_int64(int(handle)), cid.encode("utf-8"))
|
||||
decoded = _safe_load_json(out_json)
|
||||
if isinstance(decoded, dict):
|
||||
@@ -577,11 +908,6 @@ def exec_query(handle: int, *, kind: str, path: Optional[str], sql: str) -> list
|
||||
This is primarily used for SNS/other dbs that are not directly exposed by dedicated APIs.
|
||||
"""
|
||||
_ensure_initialized()
|
||||
lib = _load_wcdb_lib()
|
||||
fn = getattr(lib, "wcdb_exec_query", None)
|
||||
if not fn:
|
||||
raise WCDBRealtimeError("Current wcdb_api.dll does not support exec_query.")
|
||||
|
||||
k = str(kind or "").strip()
|
||||
if not k:
|
||||
raise WCDBRealtimeError("Missing kind for exec_query.")
|
||||
@@ -592,6 +918,27 @@ def exec_query(handle: int, *, kind: str, path: Optional[str], sql: str) -> list
|
||||
|
||||
p = None if path is None else str(path or "").strip()
|
||||
|
||||
if _sidecar_enabled():
|
||||
out_json = _sidecar_payload(
|
||||
"exec_query",
|
||||
{
|
||||
"handle": int(handle),
|
||||
"kind": k,
|
||||
"path": p,
|
||||
"sql": s,
|
||||
},
|
||||
timeout=60.0,
|
||||
)
|
||||
decoded = _safe_load_json(out_json)
|
||||
if isinstance(decoded, list):
|
||||
return [x for x in decoded if isinstance(x, dict)]
|
||||
return []
|
||||
|
||||
lib = _load_wcdb_lib()
|
||||
fn = getattr(lib, "wcdb_exec_query", None)
|
||||
if not fn:
|
||||
raise WCDBRealtimeError("Current wcdb_api.dll does not support exec_query.")
|
||||
|
||||
out_json = _call_out_json(
|
||||
fn,
|
||||
ctypes.c_int64(int(handle)),
|
||||
@@ -615,15 +962,29 @@ def update_message(handle: int, *, session_id: str, local_id: int, create_time:
|
||||
Requires wcdb_update_message export in wcdb_api.dll.
|
||||
"""
|
||||
_ensure_initialized()
|
||||
sid = str(session_id or "").strip()
|
||||
if not sid:
|
||||
raise WCDBRealtimeError("Missing session_id for update_message.")
|
||||
|
||||
if _sidecar_enabled():
|
||||
_sidecar_call(
|
||||
"update_message",
|
||||
{
|
||||
"handle": int(handle),
|
||||
"session_id": sid,
|
||||
"local_id": int(local_id or 0),
|
||||
"create_time": int(create_time or 0),
|
||||
"new_content": str(new_content or ""),
|
||||
},
|
||||
timeout=30.0,
|
||||
)
|
||||
return
|
||||
|
||||
lib = _load_wcdb_lib()
|
||||
fn = getattr(lib, "wcdb_update_message", None)
|
||||
if not fn:
|
||||
raise WCDBRealtimeError("Current wcdb_api.dll does not support update_message.")
|
||||
|
||||
sid = str(session_id or "").strip()
|
||||
if not sid:
|
||||
raise WCDBRealtimeError("Missing session_id for update_message.")
|
||||
|
||||
_call_out_error(
|
||||
fn,
|
||||
ctypes.c_int64(int(handle)),
|
||||
@@ -647,16 +1008,30 @@ def delete_message(
|
||||
Requires wcdb_delete_message export in wcdb_api.dll.
|
||||
"""
|
||||
_ensure_initialized()
|
||||
lib = _load_wcdb_lib()
|
||||
fn = getattr(lib, "wcdb_delete_message", None)
|
||||
if not fn:
|
||||
raise WCDBRealtimeError("Current wcdb_api.dll does not support delete_message.")
|
||||
|
||||
sid = str(session_id or "").strip()
|
||||
if not sid:
|
||||
raise WCDBRealtimeError("Missing session_id for delete_message.")
|
||||
|
||||
hint = str(db_path_hint or "").strip()
|
||||
if _sidecar_enabled():
|
||||
_sidecar_call(
|
||||
"delete_message",
|
||||
{
|
||||
"handle": int(handle),
|
||||
"session_id": sid,
|
||||
"local_id": int(local_id or 0),
|
||||
"create_time": int(create_time or 0),
|
||||
"db_path_hint": hint,
|
||||
},
|
||||
timeout=30.0,
|
||||
)
|
||||
return
|
||||
|
||||
lib = _load_wcdb_lib()
|
||||
fn = getattr(lib, "wcdb_delete_message", None)
|
||||
if not fn:
|
||||
raise WCDBRealtimeError("Current wcdb_api.dll does not support delete_message.")
|
||||
|
||||
_call_out_error(
|
||||
fn,
|
||||
ctypes.c_int64(int(handle)),
|
||||
@@ -682,11 +1057,6 @@ def get_sns_timeline(
|
||||
Requires a newer wcdb_api.dll export: wcdb_get_sns_timeline.
|
||||
"""
|
||||
_ensure_initialized()
|
||||
lib = _load_wcdb_lib()
|
||||
fn = getattr(lib, "wcdb_get_sns_timeline", None)
|
||||
if not fn:
|
||||
raise WCDBRealtimeError("Current wcdb_api.dll does not support sns timeline.")
|
||||
|
||||
lim = max(0, int(limit or 0))
|
||||
off = max(0, int(offset or 0))
|
||||
|
||||
@@ -696,6 +1066,30 @@ def get_sns_timeline(
|
||||
|
||||
kw = str(keyword or "").strip()
|
||||
|
||||
if _sidecar_enabled():
|
||||
payload = _sidecar_payload(
|
||||
"get_sns_timeline",
|
||||
{
|
||||
"handle": int(handle),
|
||||
"limit": lim,
|
||||
"offset": off,
|
||||
"usernames": users,
|
||||
"keyword": kw,
|
||||
"start_time": int(start_time or 0),
|
||||
"end_time": int(end_time or 0),
|
||||
},
|
||||
timeout=60.0,
|
||||
)
|
||||
decoded = _safe_load_json(payload)
|
||||
if isinstance(decoded, list):
|
||||
return [x for x in decoded if isinstance(x, dict)]
|
||||
return []
|
||||
|
||||
lib = _load_wcdb_lib()
|
||||
fn = getattr(lib, "wcdb_get_sns_timeline", None)
|
||||
if not fn:
|
||||
raise WCDBRealtimeError("Current wcdb_api.dll does not support sns timeline.")
|
||||
|
||||
payload = _call_out_json(
|
||||
fn,
|
||||
ctypes.c_int64(int(handle)),
|
||||
@@ -724,11 +1118,6 @@ def decrypt_sns_image(encrypted_data: bytes, key: str) -> bytes:
|
||||
- On failure, returns the original encrypted_data (best-effort behavior like WeFlow).
|
||||
"""
|
||||
_ensure_initialized()
|
||||
lib = _load_wcdb_lib()
|
||||
fn = getattr(lib, "wcdb_decrypt_sns_image", None)
|
||||
if not fn:
|
||||
raise WCDBRealtimeError("Current wcdb_api.dll does not support sns image decryption.")
|
||||
|
||||
raw = bytes(encrypted_data or b"")
|
||||
if not raw:
|
||||
return b""
|
||||
@@ -737,6 +1126,28 @@ def decrypt_sns_image(encrypted_data: bytes, key: str) -> bytes:
|
||||
if not k:
|
||||
return raw
|
||||
|
||||
if _sidecar_enabled():
|
||||
result = _sidecar_call(
|
||||
"decrypt_sns_image",
|
||||
{
|
||||
"data_b64": base64.b64encode(raw).decode("ascii"),
|
||||
"key": k,
|
||||
},
|
||||
timeout=60.0,
|
||||
)
|
||||
data_b64 = str(result.get("data_b64") or "")
|
||||
if not data_b64:
|
||||
return raw
|
||||
try:
|
||||
return base64.b64decode(data_b64)
|
||||
except Exception:
|
||||
return raw
|
||||
|
||||
lib = _load_wcdb_lib()
|
||||
fn = getattr(lib, "wcdb_decrypt_sns_image", None)
|
||||
if not fn:
|
||||
raise WCDBRealtimeError("Current wcdb_api.dll does not support sns image decryption.")
|
||||
|
||||
out_ptr = ctypes.c_void_p()
|
||||
buf = ctypes.create_string_buffer(raw, len(raw))
|
||||
rc = 0
|
||||
@@ -775,6 +1186,17 @@ def decrypt_sns_image(encrypted_data: bytes, key: str) -> bytes:
|
||||
|
||||
def shutdown() -> None:
|
||||
global _initialized
|
||||
if _sidecar_enabled():
|
||||
with _lib_lock:
|
||||
if not _initialized:
|
||||
return
|
||||
try:
|
||||
_sidecar_call("shutdown", timeout=5.0)
|
||||
finally:
|
||||
with _lib_lock:
|
||||
_initialized = False
|
||||
return
|
||||
|
||||
lib = _load_wcdb_lib()
|
||||
with _lib_lock:
|
||||
if not _initialized:
|
||||
@@ -893,6 +1315,7 @@ class WCDBRealtimeManager:
|
||||
with self._mu:
|
||||
failed_at = self._failed.get(account)
|
||||
if failed_at is not None and (time.monotonic() - failed_at) < self._FAILED_TTL:
|
||||
logger.warning("[wcdb] recent failure cache hit account=%s ttl=%ss", account, int(self._FAILED_TTL))
|
||||
raise WCDBRealtimeError("WCDB connection recently failed; retry after 60s.")
|
||||
|
||||
deadline = time.monotonic() + timeout
|
||||
@@ -926,9 +1349,11 @@ class WCDBRealtimeManager:
|
||||
if len(key) != 64:
|
||||
with self._mu:
|
||||
self._failed[account] = time.monotonic()
|
||||
logger.warning("[wcdb] missing/invalid db key account=%s key_len=%s", account, len(key))
|
||||
raise WCDBRealtimeError("Missing db key for this account (call /api/keys or decrypt first).")
|
||||
db_storage_dir = _resolve_account_db_storage_dir(account_dir)
|
||||
if db_storage_dir is None:
|
||||
logger.warning("[wcdb] db_storage resolve failed account=%s account_dir=%s", account, account_dir)
|
||||
raise WCDBRealtimeError("Cannot resolve db_storage directory for this account.")
|
||||
|
||||
session_db_path = _resolve_session_db_path(db_storage_dir)
|
||||
@@ -952,14 +1377,27 @@ class WCDBRealtimeManager:
|
||||
if open_thread.is_alive():
|
||||
with self._mu:
|
||||
self._failed[account] = time.monotonic()
|
||||
logger.warning(
|
||||
"[wcdb] open_account timeout account=%s timeout=%ss session_db=%s",
|
||||
account,
|
||||
int(timeout),
|
||||
session_db_path,
|
||||
)
|
||||
raise WCDBRealtimeError(
|
||||
f"open_account timed out after {timeout:.0f}s for {session_db_path}"
|
||||
)
|
||||
if _open_err:
|
||||
with self._mu:
|
||||
self._failed[account] = time.monotonic()
|
||||
logger.warning(
|
||||
"[wcdb] open_account failed account=%s session_db=%s error=%s",
|
||||
account,
|
||||
session_db_path,
|
||||
_open_err[0],
|
||||
)
|
||||
raise _open_err[0]
|
||||
if not _handle_box:
|
||||
logger.warning("[wcdb] open_account returned no handle account=%s session_db=%s", account, session_db_path)
|
||||
raise WCDBRealtimeError("open_account returned no handle.")
|
||||
|
||||
handle = _handle_box[0]
|
||||
@@ -980,7 +1418,8 @@ class WCDBRealtimeManager:
|
||||
|
||||
with self._mu:
|
||||
self._conns[account] = conn
|
||||
logger.info(f"[wcdb] connected account={account} session_db={session_db_path}")
|
||||
self._failed.pop(account, None)
|
||||
logger.info("[wcdb] connected account=%s handle=%s session_db=%s", account, int(handle), session_db_path)
|
||||
return conn
|
||||
finally:
|
||||
with self._mu:
|
||||
|
||||
Reference in New Issue
Block a user