mochat/wework-finance-sdk
PHP SDK for the WeCom (WeWork) Finance API, offering Laravel-friendly access to encrypted chat archive retrieval, media downloads, and signature verification. Wraps the official finance endpoints in a configurable package with a service provider for quick setup.
This is not a Laravel package — it is a thin PHP wrapper around Tencent’s proprietary libWeWorkFinanceSdk_C (企业微信会话内容存档). There is no service provider, no facade, and no auto-discovery. Treat it as a native-library binding you must wire yourself.
Preconditions before you write a line of code:
.so for glibc/x86_64. There is no macOS, Windows, or ARM build. On an M1/M2 dev machine you must run inside docker run --platform linux/amd64, or you cannot even instantiate the SDK.extension=ffi, ffi.enable=pre or true) — most wrappers in this space load the .so via FFI. Check composer.json and src/ for whether it uses FFI or a prebuilt PHP extension; the wiring differs..so file itself is not in this repo. You must download the WeWorkFinanceSdk from the WeCom admin console (应用管理 → 会话内容存档) and place libWeWorkFinanceSdk_C.so somewhere writable/readable, then point config at it.secret shown in the 会话内容存档 panel, paired with the RSA public/private key you generated there. Using the app secret will fail with an unhelpful errcode.Install and first call:
composer require mochat/wework-finance-sdk
Confirm the namespace from src/ (likely Mochat\WeWorkFinanceSdk\...). Then the minimal end-to-end smoke test:
$sdk = new FinanceSDK($corpId, $financeSecret, $privateKeyPath, $soPath);
// 1) pull a page of encrypted envelopes
$raw = $sdk->getChatData($seq = 0, $limit = 100);
$page = json_decode($raw->getData(), true); // wrapper may return an object or string
// 2) decrypt one message
$first = $page['chatdata'][0];
$plain = $sdk->decryptData($first['encrypt_random_key'], $first['encrypt_chat_msg']);
$msg = json_decode($plain, true); // { msgid, action, from, tolist, msgtype, text:{content}, ... }
dump($msg['msgtype'], $msg['text']['content'] ?? null);
If that runs on your prod/staging box without a segfault, you have a working baseline. Do this on the target server, not locally.
First real use case: a scheduled job that pulls new chat records incrementally, decrypts them, and persists text messages + media pointers. Everything else is a variation on that.
Bind the SDK as a singleton, configured from config/. You need a provider regardless of what the package ships, because you must inject paths and credentials:
// config/wework-finance.php
return [
'corp_id' => env('WEWORK_CORP_ID'),
'secret' => env('WEWORK_FINANCE_SECRET'),
'private_key' => storage_path('app/wework/private_key.pem'),
'sdk_path' => env('WEWORK_FINANCE_SO', '/usr/local/lib/libWeWorkFinanceSdk_C.so'),
'page_size' => 1000,
];
// AppServiceProvider::register()
$this->app->singleton(FinanceSDK::class, fn ($app) => new FinanceSDK(
config('wework-finance.corp_id'),
config('wework-finance.secret'),
config('wework-finance.private_key'),
config('wework-finance.sdk_path'),
));
Incremental pull with a persisted sequence. getChatData($seq, $limit) is exclusive of $seq — you pass the last seq you stored, and you must advance it to the max seq in the returned page, not to count(). Keep it in a dedicated table so it moves transactionally with the messages:
DB::transaction(function () use ($sdk, $cursor) {
$page = json_decode($sdk->getChatData($cursor->seq, 1000)->getData(), true);
foreach ($page['chatdata'] as $item) {
$plain = json_decode($sdk->decryptData($item['encrypt_random_key'], $item['encrypt_chat_msg']), true);
ChatMessage::updateOrCreate(
['msgid' => $plain['msgid']],
['seq' => $item['seq'], 'from' => $plain['from'], 'type' => $plain['msgtype'],
'payload' => $plain, 'sent_at' => $plain['msgtime'] / 1000]
);
$cursor->seq = max($cursor->seq, $item['seq']);
}
$cursor->save();
});
Queue the pull, don’t inline it. Wrap the loop in a job with $timeout = 120, $tries = 3, and backoff() of [10, 60, 300]. Schedule PullFinanceMessages::dispatch() every minute via Kernel::schedule. The native call is blocking and network-bound; it will pin a worker.
Media pipeline is a second job. Messages of msgtype image/voice/video/file carry a sdkfileid, not the bytes. Decryption gives you the sdkfileid; you then call getMediaData(...) to stream it to disk. Store the sdkfileid + key in your row at decrypt time, dispatch DownloadFinanceMedia separately, and write to storage/app/wework/{yyyy}/{mm}/{msgid}.{ext}. Never do this in the pull transaction — one slow file download shouldn’t roll back a page of messages.
Normalise into a ChatMessage model with a JSON payload column rather than modelling every msgtype. WeCom adds types (meeting, docmsg, markdown, chatrecord with nested items) without notice; a JSON column absorbs that.
Sequence cursor table: finance_cursors(id, corp_id, seq, updated_at) — one row, lockForUpdate() on read. This is the single most important design decision in the whole integration.
updateOrCreate on msgid gives you idempotency, but you’ll still burn rate limit re-decrypting. Guard with Cache::lock('wework-finance:pull', 120).FreeChatData / FreeDecryptChatData calls; not all PHP wrappers do this reliably. Recycle your queue workers (php artisan queue:work --max-jobs=500), and never run this in a long-lived Octane worker without --max-requests..so path or a missing ffi.enable frequently manifests as a segfault or a null returned object rather than an exception. Check php -m | grep ffi and ldd libWeWorkFinanceSdk_C.so on the server before debugging your own code..so is built against a specific glibc; Alpine (musl) will not load it. Use a Debian/Ubuntu base image.getChatData and media downloads. Sleep ~500ms between pages, keep $limit at ≤1000, and expect errcode values like 45009 (freq limit) in the JSON body — the SDK does not throw on these; you must inspect errcode after every call.decryptData.msgtime is milliseconds, text.content is pre-encoded (escaped); decode and be careful with emoji/\u sequences.FinanceClient interface so that when the native .so or PHP version breaks it, you swap one class. Also verify the license terms from the WeCom console before shipping commercially; NOASSERTION here means you’re relying on Tencent’s own terms, not this repo’s.getChatData returns empty with no meaningful error.How can I help you explore Laravel packages today?