微信 iLink Bot API 的 PHP SDK。
composer require openilink/openilink-sdk-php- 扫码登录,支持扫码/过期回调
- 长轮询消息监听,自动重试与退避,动态超时
- 主动推送(自动缓存
context_token) - 发送图片、视频、文件,MIME 自动路由
- CDN 加密上传/下载(AES-128-ECB)
- 语音消息解码(可插拔 SILK 解码器 + WAV 封装)
- 输入状态指示器、Bot 配置
- 可注入自定义 transport,便于测试或接入自有 HTTP 栈
- 结构化错误类型(
APIError、HTTPError、NoContextTokenException) - 轻量依赖,仅依赖 PHP 扩展
- PHP 8.1+
ext-curlext-jsonext-openssl
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use OpenILink\Client;
use OpenILink\MessageHelper;
$client = new Client('');
$result = $client->loginWithQr([
'on_qrcode' => static function (string $img): void {
echo "请扫码:\n{$img}\n";
},
'on_scanned' => static function (): void {
echo "已扫码,请在微信中确认...\n";
},
]);
if (!($result['connected'] ?? false)) {
throw new RuntimeException((string) ($result['message'] ?? '登录失败'));
}
echo '已连接 BotID=' . (string) ($result['bot_id'] ?? '') . PHP_EOL;
$syncBufFile = __DIR__ . '/sync_buf.dat';
$savedBuf = is_file($syncBufFile) ? (string) file_get_contents($syncBufFile) : '';
$client->monitor(
static function (array $message) use ($client): void {
$text = MessageHelper::extractText($message);
if ($text === '') {
return;
}
$client->push((string) $message['from_user_id'], '收到: ' . $text);
},
[
'initial_buf' => $savedBuf,
'on_response' => static function (array $response): void {
echo (string) ($response['sync_buf'] ?? '') . PHP_EOL;
echo (string) ($response['raw_response']['status_code'] ?? 0) . PHP_EOL;
},
'on_buf_update' => static function (string $buf) use ($syncBufFile): void {
file_put_contents($syncBufFile, $buf);
},
],
);use OpenILink\Client;
$client = new Client($token, [
'base_url' => 'https://custom.endpoint.com',
'cdn_base_url' => 'https://custom.cdn.com/c2c',
'bot_type' => '3',
'version' => '2.1.6',
'route_tag' => 'my-route-tag',
'transport' => static function (
string $method,
string $url,
array $headers,
?string $body,
int $timeoutMs,
): array {
return [
'status_code' => 200,
'body' => '{}',
'headers' => ['content-type' => 'application/json'],
];
},
'silk_decoder' => static function (string $silkData, int $sampleRate): string {
return decodeSilkSomehow($silkData, $sampleRate);
},
]);$result = $client->loginWithQr([
'on_qrcode' => static function (string $imgContent): void {},
'on_scanned' => static function (): void {},
'on_expired' => static function (int $attempt, int $max): void {},
]);登录成功后,客户端的 Token 和 BaseURL 会自动更新。
use OpenILink\MessageHelper;
$client->monitor(
static function (array $message): void {
$text = MessageHelper::extractText($message);
// $message['from_user_id'], $message['context_token'], $message['item_list']
},
[
'initial_buf' => $savedBuf,
'on_response' => static function (array $response): void {},
'on_buf_update' => static function (string $buf): void {},
'on_error' => static function (Throwable $error): void {},
'on_session_expired' => static function (): void {},
'should_continue' => static function (): bool {
return true;
},
],
);monitor() 会自动缓存每个用户的 context_token,供 push() 使用。服务端返回的 longpolling_timeout_ms 会被自动采纳;成功响应里的 sync_buf 和原始 HTTP 元数据可通过 on_response / raw_response 读取。
$client->sendText($userId, '你好', $contextToken);
$client->push($userId, '这是一条定时通知');use OpenILink\Constants;
$data = file_get_contents('photo.jpg');
// 高级接口:自动识别 MIME 类型 -> 上传 -> 发送
$client->sendMediaFile($userId, $contextToken, $data, 'photo.jpg', '看看这张图');
// 分步操作:上传 -> 发送
$uploaded = $client->uploadFile($data, $userId, Constants::MEDIA_IMAGE);
$client->sendImage($userId, $contextToken, $uploaded);
$client->sendVideo($userId, $contextToken, $uploaded);
$client->sendFileAttachment($userId, $contextToken, 'report.pdf', $uploaded);use OpenILink\Constants;
foreach (($message['item_list'] ?? []) as $item) {
switch ($item['type'] ?? null) {
case Constants::ITEM_TYPE_IMAGE:
$data = $client->downloadMedia($item['image_item']['media'] ?? null);
break;
case Constants::ITEM_TYPE_VOICE:
$wav = $client->downloadVoice($item['voice_item'] ?? null);
break;
}
}SDK 通过可插拔的 silk_decoder 支持语音消息解码,保持对外部解码器的开放性:
use OpenILink\Client;
use OpenILink\Voice;
$client = new Client($token, [
'silk_decoder' => static function (string $silkData, int $sampleRate): string {
return decodeSilkSomehow($silkData, $sampleRate);
},
]);
$wav = $client->downloadVoice($voiceItem);也可以单独使用 WAV 封装:
$wav = Voice::buildWav($pcmBytes, 24000, 1, 16);use OpenILink\Constants;
use OpenILink\MessageHelper;
use OpenILink\Mime;
$client->sendTyping($userId, $typingTicket, Constants::TYPING);
$client->sendTyping($userId, $typingTicket, Constants::CANCEL_TYPING);
$config = $client->getConfig($userId, $contextToken);
$text = MessageHelper::extractText($message);
$isMedia = MessageHelper::isMediaItem($item);
$mime = Mime::mimeFromFilename('photo.jpg'); // image/jpeg
$ext = Mime::extensionFromMime('image/jpg'); // .jpg
$isImage = Mime::isImageMime('image/png'); // true
$isVideo = Mime::isVideoMime('video/mp4'); // trueuse OpenILink\Exception\APIError;
use OpenILink\Exception\HTTPError;
use OpenILink\Exception\NoContextTokenException;
use OpenILink\Exception\RequestException;
try {
$client->push($userId, 'hello');
} catch (APIError $error) {
if ($error->isSessionExpired()) {
// 需要重新登录
}
} catch (HTTPError $error) {
echo $error->getStatusCode();
} catch (NoContextTokenException $error) {
// 该用户尚未发送过消息,无法主动推送
} catch (RequestException $error) {
if ($error->isTimeout()) {
// 请求超时
}
}use OpenILink\Constants;
Constants::MEDIA_IMAGE; // 1
Constants::MEDIA_VIDEO; // 2
Constants::MEDIA_FILE; // 3
Constants::MEDIA_VOICE; // 4
Constants::MESSAGE_TYPE_USER; // 1
Constants::MESSAGE_TYPE_BOT; // 2
Constants::ITEM_TYPE_TEXT; // 1
Constants::ITEM_TYPE_IMAGE; // 2
Constants::ITEM_TYPE_VOICE; // 3
Constants::ITEM_TYPE_FILE; // 4
Constants::ITEM_TYPE_VIDEO; // 5
Constants::MESSAGE_STATE_NEW; // 0
Constants::MESSAGE_STATE_GENERATING; // 1
Constants::MESSAGE_STATE_FINISH; // 2MIT