微信 iLink Bot API 的 C# / .NET SDK。
这版文档默认按现代 C# 写示例:
using varvar- 顶层语句
- 局部函数 / 方法组优先
完整可运行示例见 examples/OpenILink.ConsoleEchoBot。
dotnet add package OpenILink.SDKnet462netstandard2.0net8.0
同一个 NuGet 包可以同时用于老的 .NET Framework、.NET Core
和现代 .NET。
using OpenILink.SDK;
var tokenPath = "bot_token.txt";
var bufferPath = "get_updates_buf.txt";
using var client = OpenILinkClient.Create(ReadText(tokenPath));
if (string.IsNullOrWhiteSpace(client.Token))
{
var login = await client.LoginWithQrAsync(ShowQrCode, OnScanned);
if (!login.Connected)
{
Console.Error.WriteLine($"登录失败: {login.Message}");
return;
}
File.WriteAllText(tokenPath, login.BotToken ?? string.Empty);
}
await client.MonitorAsync(HandleMessageAsync, new MonitorOptions
{
InitialBuffer = ReadText(bufferPath),
OnBufferUpdated = SaveBuffer,
OnError = ReportError,
OnSessionExpired = ReportSessionExpired
});
Task HandleMessageAsync(WeixinMessage message)
{
var text = message.ExtractText();
if (string.IsNullOrWhiteSpace(text))
{
return Task.CompletedTask;
}
Console.WriteLine($"[{message.FromUserId}] {text}");
return client.ReplyTextAsync(message, $"echo: {text}");
}
void ShowQrCode(string qrCodeImage)
{
Console.WriteLine(qrCodeImage);
}
void OnScanned()
{
Console.WriteLine("已扫码,请在微信端确认。");
}
void SaveBuffer(string buffer)
{
File.WriteAllText(bufferPath, buffer);
}
void ReportError(Exception exception)
{
Console.Error.WriteLine(exception.Message);
}
void ReportSessionExpired()
{
Console.Error.WriteLine("会话过期,请重新登录。");
}
static string ReadText(string path)
{
return File.Exists(path) ? File.ReadAllText(path).Trim() : string.Empty;
}最常用的三种写法:
using var client = OpenILinkClient.Create(token);using var client = new OpenILinkClient(token);using var httpClient = new HttpClient();
using var client = OpenILinkClient.Builder()
.Token(token)
.BaseUri("https://ilinkai.weixin.qq.com/")
.CdnBaseUri("https://novac2c.cdn.weixin.qq.com/c2c/")
.RouteTag("gray-route")
.HttpClient(httpClient)
.ApiTimeout(TimeSpan.FromSeconds(15))
.LongPollingTimeout(TimeSpan.FromSeconds(35))
.Build();如果你是从配置系统里读参数,直接用 OpenILinkClientOptions:
var options = new OpenILinkClientOptions(token)
{
BaseUri = new Uri("https://ilinkai.weixin.qq.com/"),
CdnBaseUri = new Uri("https://novac2c.cdn.weixin.qq.com/c2c/"),
RouteTag = "gray-route",
LoginTimeout = TimeSpan.FromMinutes(8)
};
using var client = new OpenILinkClient(options);首次启动通常没有 bot_token,直接扫码:
var login = await client.LoginWithQrAsync(ShowQrCode, OnScanned, OnExpired);
if (login.Connected)
{
File.WriteAllText("bot_token.txt", login.BotToken ?? string.Empty);
}
void ShowQrCode(string qrCodeImage)
{
Console.WriteLine(qrCodeImage);
}
void OnScanned()
{
Console.WriteLine("已扫码,请确认。");
}
void OnExpired(int attempt, int maxAttempt)
{
Console.WriteLine($"二维码过期,正在刷新 ({attempt}/{maxAttempt})");
}登录成功后 SDK 会自动更新:
client.Tokenclient.BaseUri
下次启动时直接复用 bot_token 即可。
await client.MonitorAsync(HandleMessageAsync, new MonitorOptions
{
InitialBuffer = ReadText("get_updates_buf.txt"),
OnBufferUpdated = buffer => File.WriteAllText("get_updates_buf.txt", buffer),
OnError = exception => Console.Error.WriteLine(exception.Message),
OnSessionExpired = () => Console.Error.WriteLine("会话过期")
});
Task HandleMessageAsync(WeixinMessage message)
{
var text = message.ExtractText();
if (string.IsNullOrWhiteSpace(text))
{
return Task.CompletedTask;
}
return client.ReplyTextAsync(message, $"收到: {text}");
}MonitorAsync 会自动:
- 重试和退避
- 跟进服务端返回的
longpolling_timeout_ms - 缓存每个用户的
contextToken - 推进
get_updates_buf
收到消息后,优先直接回复:
await client.ReplyTextAsync(message, "你好");需要主动推送时:
if (client.CanPushTo(userId))
{
await client.PushTextAsync(userId, "这是一条主动消息");
}也可以显式读取缓存的上下文:
var contextToken = client.GetContextToken(userId);var config = await client.GetConfigAsync(userId, contextToken);
await client.SendTypingAsync(userId, config.TypingTicket ?? string.Empty, TypingStatus.Typing);最省心的写法:
var bytes = File.ReadAllBytes("photo.jpg");
await client.SendMediaFileAsync(toUserId, contextToken, bytes, "photo.jpg", "看看这张图");需要手动控制上传和发送时:
var bytes = File.ReadAllBytes("photo.jpg");
var upload = await client.UploadFileAsync(bytes, toUserId, UploadMediaType.Image);
await client.SendImageAsync(toUserId, contextToken, upload);同理也可以调用:
SendVideoAsyncSendFileAttachmentAsync
下载文件:
var plaintext = await client.DownloadFileAsync(
media.EncryptQueryParam ?? string.Empty,
media.AesKey ?? string.Empty);下载语音前,需要先注入 ISilkDecoder:
public sealed class MySilkDecoder : ISilkDecoder
{
public Task<byte[]> DecodeAsync(byte[] silkData, int sampleRate, CancellationToken cancellationToken)
{
return Task.FromResult(Array.Empty<byte>());
}
}
using var client = OpenILinkClient.Builder()
.Token(token)
.SilkDecoder(new MySilkDecoder())
.Build();
var wav = await client.DownloadVoiceAsync(voiceItem);var text = message.ExtractText();
var isMedia = MessageUtilities.IsMediaItem(item);
var mime = MimeUtilities.MimeFromFilename("photo.jpg");
var extension = MimeUtilities.ExtensionFromMime("video/mp4");
var isImage = MimeUtilities.IsImageMime("image/png");
var isVideo = MimeUtilities.IsVideoMime("video/mp4");try
{
await client.PushTextAsync(userId, "hello");
}
catch (MissingContextTokenException)
{
Console.WriteLine("该用户还没有可用的 contextToken。");
}
catch (OpenILinkApiException exception) when (exception.IsSessionExpired())
{
Console.WriteLine("会话过期,请重新登录。");
}
catch (OpenILinkHttpException exception)
{
Console.WriteLine($"HTTP 状态码: {exception.StatusCode}");
}