feat: add fnOS packaging, storage workflows and release pipeline

This commit is contained in:
2026-08-11 18:05:49 +08:00
parent c5922f9b08
commit 95932f0199
181 changed files with 24024 additions and 1164 deletions
+210
View File
@@ -0,0 +1,210 @@
using System.Net;
using dy.net.model.dto;
using dy.net.utils;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace dy.net.service
{
public interface IDouyinLiveStatusClient
{
Task<DouyinLiveStatusProbe> ProbeAsync(
string secUid,
string cookie,
CancellationToken cancellationToken = default);
}
public sealed class DouyinLiveStatusRequestException : InvalidOperationException
{
public DouyinLiveStatusRequestException(
string message,
HttpStatusCode? statusCode = null,
bool requiresAccountCooldown = false,
Exception innerException = null)
: base(message, innerException)
{
StatusCode = statusCode;
RequiresAccountCooldown = requiresAccountCooldown;
}
public HttpStatusCode? StatusCode { get; }
public bool RequiresAccountCooldown { get; }
}
public sealed class DouyinLiveStatusClient : IDouyinLiveStatusClient
{
private const string ProfilePath = "/aweme/v1/web/user/profile/other/";
private readonly IHttpClientFactory _clientFactory;
private readonly DouyinABogusSigner _signer = new();
public DouyinLiveStatusClient(IHttpClientFactory clientFactory) => _clientFactory = clientFactory;
public async Task<DouyinLiveStatusProbe> ProbeAsync(
string secUid,
string cookie,
CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(secUid))
throw new InvalidOperationException("博主 SecUid 为空,无法检查直播状态");
if (string.IsNullOrWhiteSpace(cookie))
throw new InvalidOperationException("授权 Cookie 为空,无法检查直播状态");
var parameters = BuildParameters(secUid.Trim(), cookie);
var unsignedQuery = BuildQueryString(parameters);
var requestUri = ProfilePath + "?" + unsignedQuery + "&a_bogus=" + Encode(_signer.Sign(unsignedQuery));
using var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
request.Headers.Referrer = new Uri("https://www.douyin.com/user/" + Encode(secUid.Trim()));
request.Headers.TryAddWithoutValidation("Accept", "application/json, text/plain, */*");
request.Headers.TryAddWithoutValidation("Cookie", cookie);
var client = _clientFactory.CreateClient(DouyinRequestParamManager.DY_HTTP_CLIENT);
try
{
using var response = await client.SendAsync(
request,
HttpCompletionOption.ResponseHeadersRead,
cancellationToken);
if (!response.IsSuccessStatusCode)
{
var shouldCooldown = response.StatusCode is HttpStatusCode.Forbidden
or HttpStatusCode.TooManyRequests
or HttpStatusCode.Unauthorized;
throw new DouyinLiveStatusRequestException(
$"抖音直播状态请求失败(HTTP {(int)response.StatusCode}",
response.StatusCode,
shouldCooldown);
}
var content = await response.Content.ReadAsStringAsync(cancellationToken);
UserProfileResponse payload;
try
{
payload = JsonConvert.DeserializeObject<UserProfileResponse>(content);
}
catch (JsonException ex)
{
throw new DouyinLiveStatusRequestException(
"抖音返回了无法识别的直播状态响应,可能需要验证授权",
requiresAccountCooldown: true,
innerException: ex);
}
if (payload?.User == null)
throw new DouyinLiveStatusRequestException(
"抖音没有返回博主资料,可能需要验证授权",
requiresAccountCooldown: true);
if (payload.StatusCode != 0)
throw new DouyinLiveStatusRequestException(
$"抖音拒绝了直播状态查询(状态码 {payload.StatusCode}",
requiresAccountCooldown: true);
return DouyinLiveStatusParser.Parse(
payload.User.LiveStatus,
payload.User.RoomId,
payload.User.RoomIdStr,
payload.User.RoomData);
}
catch (DouyinLiveStatusRequestException)
{
throw;
}
catch (HttpRequestException ex)
{
throw new DouyinLiveStatusRequestException("抖音直播状态网络请求失败,请稍后重试", innerException: ex);
}
catch (OperationCanceledException ex) when (!cancellationToken.IsCancellationRequested)
{
throw new DouyinLiveStatusRequestException("抖音直播状态查询超时,请稍后重试", innerException: ex);
}
}
private static List<KeyValuePair<string, string>> BuildParameters(string secUid, string cookie)
{
var cookies = ParseCookie(cookie);
return new List<KeyValuePair<string, string>>
{
new("device_platform", "webapp"),
new("aid", "6383"),
new("channel", "channel_pc_web"),
new("update_version_code", "170400"),
new("pc_client_type", "1"),
new("pc_libra_divert", "Windows"),
new("support_h265", "1"),
new("support_dash", "1"),
new("version_code", "170400"),
new("version_name", "17.4.0"),
new("cookie_enabled", "true"),
new("screen_width", "1536"),
new("screen_height", "864"),
new("browser_language", "zh-CN"),
new("browser_platform", "Win32"),
new("browser_name", "Chrome"),
new("browser_version", "119.0.0.0"),
new("browser_online", "true"),
new("engine_name", "Blink"),
new("engine_version", "119.0.0.0"),
new("os_name", "Windows"),
new("os_version", "10"),
new("cpu_core_num", "16"),
new("device_memory", "8"),
new("platform", "PC"),
new("downlink", "10"),
new("effective_type", "4g"),
new("round_trip_time", "200"),
new("uifid", CookieValue(cookies, "UIFID", "UIFID_TEMP")),
new("msToken", CookieValue(cookies, "msToken")),
new("sec_user_id", secUid),
new("publish_video_strategy_type", "2"),
new("personal_center_strategy", "1")
};
}
private static Dictionary<string, string> ParseCookie(string cookie)
{
var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var part in (cookie ?? string.Empty).Split(';', StringSplitOptions.RemoveEmptyEntries))
{
var separator = part.IndexOf('=');
if (separator <= 0) continue;
values[part[..separator].Trim()] = part[(separator + 1)..].Trim();
}
return values;
}
private static string CookieValue(Dictionary<string, string> cookies, params string[] names)
{
foreach (var name in names)
if (cookies.TryGetValue(name, out var value)) return value;
return string.Empty;
}
private static string BuildQueryString(IEnumerable<KeyValuePair<string, string>> parameters) =>
string.Join("&", parameters.Select(x => Encode(x.Key) + "=" + Encode(x.Value ?? string.Empty)));
private static string Encode(string value) => Uri.EscapeDataString(value ?? string.Empty);
private sealed class UserProfileResponse
{
[JsonProperty("status_code")]
public int StatusCode { get; set; }
[JsonProperty("user")]
public UserProfile User { get; set; }
}
private sealed class UserProfile
{
[JsonProperty("live_status")]
public int? LiveStatus { get; set; }
[JsonProperty("room_id")]
public string RoomId { get; set; }
[JsonProperty("room_id_str")]
public string RoomIdStr { get; set; }
[JsonProperty("room_data")]
public JToken RoomData { get; set; }
}
}
}