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:
@@ -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);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user