@ikenxuan/amagi 6.0.0-beta.3 → 6.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -17
- package/dist/default/index.cjs +2222 -145
- package/dist/default/index.d.ts +3871 -3190
- package/dist/default/index.mjs +2219 -146
- package/package.json +3 -2
package/dist/default/index.cjs
CHANGED
|
@@ -1262,6 +1262,29 @@ const KuaishouCommentParamsSchema = zod.default.object({
|
|
|
1262
1262
|
photoId: zod.default.string({ error: "photoId must be a string" }).min(1, { error: "photoId cannot be empty" })
|
|
1263
1263
|
});
|
|
1264
1264
|
/**
|
|
1265
|
+
* 快手用户主页参数验证模式
|
|
1266
|
+
*/
|
|
1267
|
+
const KuaishouUserProfileParamsSchema = zod.default.object({
|
|
1268
|
+
methodType: zod.default.literal("userProfile", { error: "methodType must be \"userProfile\"" }),
|
|
1269
|
+
principalId: zod.default.string({ error: "principalId must be a string" }).min(1, { error: "principalId cannot be empty" })
|
|
1270
|
+
});
|
|
1271
|
+
/**
|
|
1272
|
+
* 快手用户作品列表参数验证模式
|
|
1273
|
+
*/
|
|
1274
|
+
const KuaishouUserWorkListParamsSchema = zod.default.object({
|
|
1275
|
+
methodType: zod.default.literal("userWorkList", { error: "methodType must be \"userWorkList\"" }),
|
|
1276
|
+
principalId: zod.default.string({ error: "principalId must be a string" }).min(1, { error: "principalId cannot be empty" }),
|
|
1277
|
+
pcursor: zod.default.string({ error: "pcursor must be a string" }).optional(),
|
|
1278
|
+
count: zod.default.number({ error: "count must be a number" }).int({ error: "count must be an integer" }).positive({ error: "count must be positive" }).max(100, { error: "count must be less than or equal to 100" }).optional()
|
|
1279
|
+
});
|
|
1280
|
+
/**
|
|
1281
|
+
* 快手直播间信息参数验证模式
|
|
1282
|
+
*/
|
|
1283
|
+
const KuaishouLiveRoomInfoParamsSchema = zod.default.object({
|
|
1284
|
+
methodType: zod.default.literal("liveRoomInfo", { error: "methodType must be \"liveRoomInfo\"" }),
|
|
1285
|
+
principalId: zod.default.string({ error: "principalId must be a string" }).min(1, { error: "principalId cannot be empty" })
|
|
1286
|
+
});
|
|
1287
|
+
/**
|
|
1265
1288
|
* 快手表情参数验证模式
|
|
1266
1289
|
*/
|
|
1267
1290
|
const KuaishouEmojiParamsSchema = zod.default.object({ methodType: zod.default.literal("emojiList", { error: "methodType must be \"emojiList\"" }) });
|
|
@@ -1271,6 +1294,9 @@ const KuaishouEmojiParamsSchema = zod.default.object({ methodType: zod.default.l
|
|
|
1271
1294
|
const KuaishouValidationSchemas = {
|
|
1272
1295
|
videoWork: KuaishouVideoParamsSchema,
|
|
1273
1296
|
comments: KuaishouCommentParamsSchema,
|
|
1297
|
+
userProfile: KuaishouUserProfileParamsSchema,
|
|
1298
|
+
userWorkList: KuaishouUserWorkListParamsSchema,
|
|
1299
|
+
liveRoomInfo: KuaishouLiveRoomInfoParamsSchema,
|
|
1274
1300
|
emojiList: KuaishouEmojiParamsSchema
|
|
1275
1301
|
};
|
|
1276
1302
|
/**
|
|
@@ -1279,6 +1305,9 @@ const KuaishouValidationSchemas = {
|
|
|
1279
1305
|
const KuaishouMethodRoutes = {
|
|
1280
1306
|
videoWork: "/fetch_one_work",
|
|
1281
1307
|
comments: "/fetch_work_comments",
|
|
1308
|
+
userProfile: "/fetch_user_profile",
|
|
1309
|
+
userWorkList: "/fetch_user_work_list",
|
|
1310
|
+
liveRoomInfo: "/fetch_live_room_info",
|
|
1282
1311
|
emojiList: "/fetch_emoji_list"
|
|
1283
1312
|
};
|
|
1284
1313
|
|
|
@@ -2065,7 +2094,7 @@ async function fetchDynamicCard(options, cookie, requestConfig) {
|
|
|
2065
2094
|
* console.log(result.data.title) // 直播间标题
|
|
2066
2095
|
* ```
|
|
2067
2096
|
*/
|
|
2068
|
-
async function fetchLiveRoomInfo$
|
|
2097
|
+
async function fetchLiveRoomInfo$2(options, cookie, requestConfig) {
|
|
2069
2098
|
return fetchBilibiliInternal("liveRoomInfo", options, {
|
|
2070
2099
|
cookie,
|
|
2071
2100
|
requestConfig
|
|
@@ -2321,7 +2350,7 @@ function createBoundBilibiliFetcher(cookie, requestConfig) {
|
|
|
2321
2350
|
fetchDynamicCard: (options) => fetchDynamicCard(options, cookie, requestConfig),
|
|
2322
2351
|
fetchBangumiInfo: (options) => fetchBangumiInfo(options, cookie, requestConfig),
|
|
2323
2352
|
fetchBangumiStreamUrl: (options) => fetchBangumiStreamUrl(options, cookie, requestConfig),
|
|
2324
|
-
fetchLiveRoomInfo: (options) => fetchLiveRoomInfo$
|
|
2353
|
+
fetchLiveRoomInfo: (options) => fetchLiveRoomInfo$2(options, cookie, requestConfig),
|
|
2325
2354
|
fetchLiveRoomInitInfo: (options) => fetchLiveRoomInitInfo(options, cookie, requestConfig),
|
|
2326
2355
|
fetchArticleContent: (options) => fetchArticleContent(options, cookie, requestConfig),
|
|
2327
2356
|
fetchArticleCards: (options) => fetchArticleCards(options, cookie, requestConfig),
|
|
@@ -2368,7 +2397,7 @@ const bilibiliFetcher = {
|
|
|
2368
2397
|
fetchDynamicCard,
|
|
2369
2398
|
fetchBangumiInfo,
|
|
2370
2399
|
fetchBangumiStreamUrl,
|
|
2371
|
-
fetchLiveRoomInfo: fetchLiveRoomInfo$
|
|
2400
|
+
fetchLiveRoomInfo: fetchLiveRoomInfo$2,
|
|
2372
2401
|
fetchLiveRoomInitInfo,
|
|
2373
2402
|
fetchArticleContent,
|
|
2374
2403
|
fetchArticleCards,
|
|
@@ -4587,7 +4616,7 @@ async function fetchMusicInfo(options, cookie, requestConfig) {
|
|
|
4587
4616
|
* console.log(result.data.title) // 直播间标题
|
|
4588
4617
|
* ```
|
|
4589
4618
|
*/
|
|
4590
|
-
async function fetchLiveRoomInfo(options, cookie, requestConfig) {
|
|
4619
|
+
async function fetchLiveRoomInfo$1(options, cookie, requestConfig) {
|
|
4591
4620
|
return fetchDouyinInternal("liveRoomInfo", options, {
|
|
4592
4621
|
cookie,
|
|
4593
4622
|
requestConfig
|
|
@@ -4714,7 +4743,7 @@ async function fetchSuggestWords(options, cookie, requestConfig) {
|
|
|
4714
4743
|
* console.log(result.data.user.nickname) // 用户昵称
|
|
4715
4744
|
* ```
|
|
4716
4745
|
*/
|
|
4717
|
-
async function fetchUserProfile$
|
|
4746
|
+
async function fetchUserProfile$2(options, cookie, requestConfig) {
|
|
4718
4747
|
return fetchDouyinInternal("userProfile", options, {
|
|
4719
4748
|
cookie,
|
|
4720
4749
|
requestConfig
|
|
@@ -4926,14 +4955,14 @@ function createBoundDouyinFetcher(cookie, requestConfig) {
|
|
|
4926
4955
|
fetchDanmakuList: (options, reqConfig) => fetchDanmakuList(options, cookie, reqConfig ?? requestConfig),
|
|
4927
4956
|
fetchWorkComments: (options, reqConfig) => fetchWorkComments$1(options, cookie, reqConfig ?? requestConfig),
|
|
4928
4957
|
fetchCommentReplies: (options, reqConfig) => fetchCommentReplies(options, cookie, reqConfig ?? requestConfig),
|
|
4929
|
-
fetchUserProfile: (options, reqConfig) => fetchUserProfile$
|
|
4958
|
+
fetchUserProfile: (options, reqConfig) => fetchUserProfile$2(options, cookie, reqConfig ?? requestConfig),
|
|
4930
4959
|
fetchUserVideoList: (options, reqConfig) => fetchUserVideoList(options, cookie, reqConfig ?? requestConfig),
|
|
4931
4960
|
fetchUserFavoriteList: (options, reqConfig) => fetchUserFavoriteList(options, cookie, reqConfig ?? requestConfig),
|
|
4932
4961
|
fetchUserRecommendList: (options, reqConfig) => fetchUserRecommendList(options, cookie, reqConfig ?? requestConfig),
|
|
4933
4962
|
searchContent: (options, reqConfig) => searchContent(options, cookie, reqConfig ?? requestConfig),
|
|
4934
4963
|
fetchSuggestWords: (options, reqConfig) => fetchSuggestWords(options, cookie, reqConfig ?? requestConfig),
|
|
4935
4964
|
fetchMusicInfo: (options, reqConfig) => fetchMusicInfo(options, cookie, reqConfig ?? requestConfig),
|
|
4936
|
-
fetchLiveRoomInfo: (options, reqConfig) => fetchLiveRoomInfo(options, cookie, reqConfig ?? requestConfig),
|
|
4965
|
+
fetchLiveRoomInfo: (options, reqConfig) => fetchLiveRoomInfo$1(options, cookie, reqConfig ?? requestConfig),
|
|
4937
4966
|
requestLoginQrcode: (options, reqConfig) => requestLoginQrcode(options, cookie, reqConfig ?? requestConfig),
|
|
4938
4967
|
fetchEmojiList: (options, reqConfig) => fetchEmojiList$2(options, cookie, reqConfig ?? requestConfig),
|
|
4939
4968
|
fetchDynamicEmojiList: (options, reqConfig) => fetchDynamicEmojiList(options, cookie, reqConfig ?? requestConfig)
|
|
@@ -4965,14 +4994,14 @@ const douyinFetcher = {
|
|
|
4965
4994
|
fetchDanmakuList,
|
|
4966
4995
|
fetchWorkComments: fetchWorkComments$1,
|
|
4967
4996
|
fetchCommentReplies,
|
|
4968
|
-
fetchUserProfile: fetchUserProfile$
|
|
4997
|
+
fetchUserProfile: fetchUserProfile$2,
|
|
4969
4998
|
fetchUserVideoList,
|
|
4970
4999
|
fetchUserFavoriteList,
|
|
4971
5000
|
fetchUserRecommendList,
|
|
4972
5001
|
searchContent,
|
|
4973
5002
|
fetchSuggestWords,
|
|
4974
5003
|
fetchMusicInfo,
|
|
4975
|
-
fetchLiveRoomInfo,
|
|
5004
|
+
fetchLiveRoomInfo: fetchLiveRoomInfo$1,
|
|
4976
5005
|
requestLoginQrcode,
|
|
4977
5006
|
fetchEmojiList: fetchEmojiList$2,
|
|
4978
5007
|
fetchDynamicEmojiList
|
|
@@ -4981,8 +5010,33 @@ const douyinFetcher = {
|
|
|
4981
5010
|
//#endregion
|
|
4982
5011
|
//#region src/platform/kuaishou/API.ts
|
|
4983
5012
|
/**
|
|
5013
|
+
* 构造快手 `live_api` 请求描述对象。
|
|
5014
|
+
*
|
|
5015
|
+
* 之所以将 `signPath` 放在 API 描述层,而不是放进签名器内部硬编码,
|
|
5016
|
+
* 是为了让“接口公开路径”和“算法规范路径”的关系显式可见。
|
|
5017
|
+
*
|
|
5018
|
+
* @param type - 内部请求类型标识
|
|
5019
|
+
* @param pathname - 实际请求路径
|
|
5020
|
+
* @param query - 要拼接到 URL 上的查询参数
|
|
5021
|
+
* @param signPath - 可选的签名规范路径;当页面路径与算法路径不一致时必须传入
|
|
5022
|
+
* @returns 可供请求层和签名层复用的请求描述对象
|
|
5023
|
+
*/
|
|
5024
|
+
const createKuaishouLiveApiRequest = (type, pathname, query, options) => {
|
|
5025
|
+
const url = new URL(`https://live.kuaishou.com${pathname}`);
|
|
5026
|
+
for (const [key, value] of Object.entries(query)) url.searchParams.set(key, String(value));
|
|
5027
|
+
return {
|
|
5028
|
+
type,
|
|
5029
|
+
url: url.toString(),
|
|
5030
|
+
method: options?.method ?? "GET",
|
|
5031
|
+
...typeof options?.requiresSign === "boolean" ? { requiresSign: options.requiresSign } : {},
|
|
5032
|
+
...options?.signPath ? { signPath: options.signPath } : {},
|
|
5033
|
+
...options?.body ? { body: options.body } : {}
|
|
5034
|
+
};
|
|
5035
|
+
};
|
|
5036
|
+
/**
|
|
4984
5037
|
* 快手 API 地址构建类
|
|
4985
|
-
*
|
|
5038
|
+
* 该类下的方法只负责返回请求描述对象,需要手动请求对应地址以获取数据。
|
|
5039
|
+
* 其中 `live_api` 方法会在必要时额外携带 `signPath`,用于声明快手签名算法实际参与计算的规范路径。
|
|
4986
5040
|
*/
|
|
4987
5041
|
var API = class {
|
|
4988
5042
|
/**
|
|
@@ -5024,6 +5078,241 @@ var API = class {
|
|
|
5024
5078
|
};
|
|
5025
5079
|
}
|
|
5026
5080
|
/**
|
|
5081
|
+
* 获取用户主页基础资料接口地址。
|
|
5082
|
+
*
|
|
5083
|
+
* 该接口需要 `__NS_hxfalcon`,且签名使用的规范路径并不是公开 `live_api`
|
|
5084
|
+
* 路径,而是内部的 `/rest/k/user/info`。
|
|
5085
|
+
*
|
|
5086
|
+
* @param data - 用户主页参数
|
|
5087
|
+
* @returns 请求配置
|
|
5088
|
+
*/
|
|
5089
|
+
userInfoById(data$1) {
|
|
5090
|
+
return createKuaishouLiveApiRequest("userInfoById", "/live_api/baseuser/userinfo/byid", {
|
|
5091
|
+
caver: 2,
|
|
5092
|
+
principalId: data$1.principalId
|
|
5093
|
+
}, { signPath: "/rest/k/user/info" });
|
|
5094
|
+
}
|
|
5095
|
+
/**
|
|
5096
|
+
* 获取用户敏感资料接口地址
|
|
5097
|
+
* @param data - 用户主页参数
|
|
5098
|
+
* @returns 请求配置
|
|
5099
|
+
*/
|
|
5100
|
+
userSensitiveInfo(data$1) {
|
|
5101
|
+
return createKuaishouLiveApiRequest("userSensitiveInfo", "/live_api/baseuser/userinfo/sensitive", {
|
|
5102
|
+
caver: 2,
|
|
5103
|
+
principalId: data$1.principalId
|
|
5104
|
+
}, { signPath: "/rest/k/user/info/sensitive" });
|
|
5105
|
+
}
|
|
5106
|
+
/**
|
|
5107
|
+
* 获取用户主页公开数据接口地址
|
|
5108
|
+
* @param data - 用户主页参数
|
|
5109
|
+
* @returns 请求配置
|
|
5110
|
+
*/
|
|
5111
|
+
profilePublic(data$1) {
|
|
5112
|
+
return createKuaishouLiveApiRequest("profilePublic", "/live_api/profile/public", {
|
|
5113
|
+
caver: 2,
|
|
5114
|
+
count: "count" in data$1 ? data$1.count ?? 12 : 12,
|
|
5115
|
+
hasMore: true,
|
|
5116
|
+
pcursor: "pcursor" in data$1 ? data$1.pcursor ?? "" : "",
|
|
5117
|
+
principalId: data$1.principalId,
|
|
5118
|
+
privacy: "public"
|
|
5119
|
+
}, { signPath: "/rest/k/feed/profile" });
|
|
5120
|
+
}
|
|
5121
|
+
/**
|
|
5122
|
+
* 获取用户主页公开视频列表接口地址。
|
|
5123
|
+
*
|
|
5124
|
+
* 该方法对 `live_api/profile/public` 做领域化封装,
|
|
5125
|
+
* 用于独立承接用户作品列表与分页能力。
|
|
5126
|
+
*
|
|
5127
|
+
* @param data - 用户作品列表参数
|
|
5128
|
+
* @returns 请求配置
|
|
5129
|
+
*/
|
|
5130
|
+
userWorkList(data$1) {
|
|
5131
|
+
return {
|
|
5132
|
+
...this.profilePublic(data$1),
|
|
5133
|
+
type: "userWorkList"
|
|
5134
|
+
};
|
|
5135
|
+
}
|
|
5136
|
+
/**
|
|
5137
|
+
* 获取用户主页私密列表接口地址。
|
|
5138
|
+
*
|
|
5139
|
+
* 真实页面抓包表明该接口可以直接走纯协议请求,不必额外追加 `__NS_hxfalcon`。
|
|
5140
|
+
*
|
|
5141
|
+
* @param data - 用户主页参数
|
|
5142
|
+
* @returns 请求配置
|
|
5143
|
+
*/
|
|
5144
|
+
profilePrivate(data$1) {
|
|
5145
|
+
return createKuaishouLiveApiRequest("profilePrivate", "/live_api/profile/private", {
|
|
5146
|
+
caver: 2,
|
|
5147
|
+
count: 12,
|
|
5148
|
+
hasMore: true,
|
|
5149
|
+
pcursor: "",
|
|
5150
|
+
principalId: data$1.principalId,
|
|
5151
|
+
privacy: "private"
|
|
5152
|
+
}, { requiresSign: false });
|
|
5153
|
+
}
|
|
5154
|
+
/**
|
|
5155
|
+
* 获取用户主页点赞列表接口地址。
|
|
5156
|
+
*
|
|
5157
|
+
* @param data - 用户主页参数
|
|
5158
|
+
* @returns 请求配置
|
|
5159
|
+
*/
|
|
5160
|
+
profileLiked(data$1) {
|
|
5161
|
+
return createKuaishouLiveApiRequest("profileLiked", "/live_api/profile/liked", {
|
|
5162
|
+
caver: 2,
|
|
5163
|
+
count: 12,
|
|
5164
|
+
hasMore: true,
|
|
5165
|
+
pcursor: "",
|
|
5166
|
+
principalId: data$1.principalId,
|
|
5167
|
+
privacy: "liked"
|
|
5168
|
+
}, { requiresSign: false });
|
|
5169
|
+
}
|
|
5170
|
+
/**
|
|
5171
|
+
* 获取用户主页兴趣推荐列表接口地址
|
|
5172
|
+
* @param data - 用户主页参数
|
|
5173
|
+
* @returns 请求配置
|
|
5174
|
+
*/
|
|
5175
|
+
profileInterestList(data$1) {
|
|
5176
|
+
return createKuaishouLiveApiRequest("profileInterestList", "/live_api/profile/interestlist", {
|
|
5177
|
+
caver: 2,
|
|
5178
|
+
limit: 4,
|
|
5179
|
+
principalId: data$1.principalId
|
|
5180
|
+
});
|
|
5181
|
+
}
|
|
5182
|
+
/**
|
|
5183
|
+
* 获取主播回放列表接口地址。
|
|
5184
|
+
*
|
|
5185
|
+
* 快手页面使用 `cursor` 作为翻页参数,而返回体里仍沿用 `pcursor`。
|
|
5186
|
+
*
|
|
5187
|
+
* @param data - 用户主页参数
|
|
5188
|
+
* @returns 请求配置
|
|
5189
|
+
*/
|
|
5190
|
+
playbackList(data$1) {
|
|
5191
|
+
return createKuaishouLiveApiRequest("playbackList", "/live_api/playback/list", {
|
|
5192
|
+
principalId: data$1.principalId,
|
|
5193
|
+
count: 12,
|
|
5194
|
+
cursor: "",
|
|
5195
|
+
hasMore: true
|
|
5196
|
+
}, { requiresSign: false });
|
|
5197
|
+
}
|
|
5198
|
+
/**
|
|
5199
|
+
* 获取用户主页兴趣分组接口地址。
|
|
5200
|
+
*
|
|
5201
|
+
* `interestMask/list` 是当前协议链中更贴近用户兴趣分组语义的接口,
|
|
5202
|
+
* 相比旧的 `profileInterestMask/list` 更适合作为领域数据源。
|
|
5203
|
+
*
|
|
5204
|
+
* @returns 请求配置
|
|
5205
|
+
*/
|
|
5206
|
+
interestMaskList() {
|
|
5207
|
+
return createKuaishouLiveApiRequest("interestMaskList", "/live_api/interestMask/list", {}, { requiresSign: false });
|
|
5208
|
+
}
|
|
5209
|
+
/**
|
|
5210
|
+
* 获取分类配置接口地址。
|
|
5211
|
+
*
|
|
5212
|
+
* 该接口可用于构造用户主页 `categoryMask.config`。
|
|
5213
|
+
*
|
|
5214
|
+
* @returns 请求配置
|
|
5215
|
+
*/
|
|
5216
|
+
categoryConfig() {
|
|
5217
|
+
return createKuaishouLiveApiRequest("categoryConfig", "/live_api/category/config", {}, { requiresSign: false });
|
|
5218
|
+
}
|
|
5219
|
+
/**
|
|
5220
|
+
* 获取热门分类列表接口地址。
|
|
5221
|
+
*
|
|
5222
|
+
* 该接口提供完整的分类卡片字段,可直接映射到
|
|
5223
|
+
* 用户主页 `categoryMask.hotList`。
|
|
5224
|
+
*
|
|
5225
|
+
* @returns 请求配置
|
|
5226
|
+
*/
|
|
5227
|
+
categoryData() {
|
|
5228
|
+
return createKuaishouLiveApiRequest("categoryData", "/live_api/category/data", {}, { requiresSign: false });
|
|
5229
|
+
}
|
|
5230
|
+
/**
|
|
5231
|
+
* 获取分类分组列表接口地址。
|
|
5232
|
+
*
|
|
5233
|
+
* @returns 请求配置
|
|
5234
|
+
*/
|
|
5235
|
+
categoryClassify() {
|
|
5236
|
+
return createKuaishouLiveApiRequest("categoryClassify", "/live_api/category/classify", {
|
|
5237
|
+
type: 4,
|
|
5238
|
+
source: 2,
|
|
5239
|
+
page: 1,
|
|
5240
|
+
pageSize: 20
|
|
5241
|
+
}, { requiresSign: false });
|
|
5242
|
+
}
|
|
5243
|
+
/**
|
|
5244
|
+
* 获取直播间详情主接口地址。
|
|
5245
|
+
*
|
|
5246
|
+
* 真实页面 store 会用该接口按 `principalId` 获取当前房间主体,
|
|
5247
|
+
* 返回结构中直接包含 `liveStream`、`author`、`gameInfo`、`noticeList`
|
|
5248
|
+
* 与基础 `config`,比 `userFollowCount` 更符合“直播间详情”语义。
|
|
5249
|
+
*
|
|
5250
|
+
* @param data - 直播间参数
|
|
5251
|
+
* @param authToken - 私密房间等场景下可能需要的 authToken
|
|
5252
|
+
* @returns 请求配置
|
|
5253
|
+
*/
|
|
5254
|
+
liveDetail(data$1, authToken) {
|
|
5255
|
+
const query = { principalId: data$1.principalId };
|
|
5256
|
+
if (authToken?.trim()) query.authToken = authToken.trim();
|
|
5257
|
+
return createKuaishouLiveApiRequest("liveDetail", "/live_api/liveroom/livedetail", query, { requiresSign: false });
|
|
5258
|
+
}
|
|
5259
|
+
/**
|
|
5260
|
+
* 获取当前登录用户关注中的开播列表接口地址。
|
|
5261
|
+
*
|
|
5262
|
+
* 该接口更接近“关注流”而不是“当前直播间详情”,
|
|
5263
|
+
* 因此不应再作为直播间主对象的首选来源。
|
|
5264
|
+
*
|
|
5265
|
+
* @returns 请求配置
|
|
5266
|
+
*/
|
|
5267
|
+
userFollowCount() {
|
|
5268
|
+
return createKuaishouLiveApiRequest("userFollowCount", "/live_api/baseuser/userFollowCount", {}, { requiresSign: false });
|
|
5269
|
+
}
|
|
5270
|
+
/**
|
|
5271
|
+
* 获取直播间推荐流接口地址。
|
|
5272
|
+
*
|
|
5273
|
+
* 该接口使用 POST JSON,请求体中的 `gameFavour`
|
|
5274
|
+
* 会影响推荐房间列表的主题偏好。
|
|
5275
|
+
*
|
|
5276
|
+
* @param gameId - 当前直播房间所属游戏 ID
|
|
5277
|
+
* @returns 请求配置
|
|
5278
|
+
*/
|
|
5279
|
+
liveReco(gameId) {
|
|
5280
|
+
return createKuaishouLiveApiRequest("liveReco", "/live_api/liveroom/reco", {}, {
|
|
5281
|
+
method: "POST",
|
|
5282
|
+
requiresSign: false,
|
|
5283
|
+
body: {
|
|
5284
|
+
followingParam: {
|
|
5285
|
+
queryFollowing: true,
|
|
5286
|
+
followingWeight: 50
|
|
5287
|
+
},
|
|
5288
|
+
gameFavour: [{
|
|
5289
|
+
gameId: Number(gameId) > 0 ? Number(gameId) : 1001,
|
|
5290
|
+
totalStayLength: 100
|
|
5291
|
+
}]
|
|
5292
|
+
}
|
|
5293
|
+
});
|
|
5294
|
+
}
|
|
5295
|
+
/**
|
|
5296
|
+
* 获取直播间 WebSocket 元信息接口地址。
|
|
5297
|
+
*
|
|
5298
|
+
* @param liveStreamId - 直播流 ID
|
|
5299
|
+
* @returns 请求配置
|
|
5300
|
+
*/
|
|
5301
|
+
liveWebsocketInfo(liveStreamId) {
|
|
5302
|
+
return createKuaishouLiveApiRequest("liveWebsocketInfo", "/live_api/liveroom/websocketinfo", {
|
|
5303
|
+
caver: 2,
|
|
5304
|
+
liveStreamId
|
|
5305
|
+
}, { signPath: "/rest/k/live/websocket/info" });
|
|
5306
|
+
}
|
|
5307
|
+
/**
|
|
5308
|
+
* 获取直播间礼物列表接口地址
|
|
5309
|
+
* @param liveStreamId - 直播流 ID
|
|
5310
|
+
* @returns 请求配置
|
|
5311
|
+
*/
|
|
5312
|
+
liveGiftList(liveStreamId) {
|
|
5313
|
+
return createKuaishouLiveApiRequest("liveGiftList", "/live_api/emoji/gift-list", { liveStreamId }, { requiresSign: false });
|
|
5314
|
+
}
|
|
5315
|
+
/**
|
|
5027
5316
|
* 获取表情列表
|
|
5028
5317
|
* @returns 请求配置
|
|
5029
5318
|
*/
|
|
@@ -5039,147 +5328,1871 @@ var API = class {
|
|
|
5039
5328
|
};
|
|
5040
5329
|
}
|
|
5041
5330
|
};
|
|
5042
|
-
/**
|
|
5331
|
+
/**
|
|
5332
|
+
* 快手 API 请求描述集合。
|
|
5333
|
+
*
|
|
5334
|
+
* 该对象只负责返回请求描述,不直接发起网络请求。
|
|
5335
|
+
*/
|
|
5043
5336
|
const kuaishouApiUrls = new API();
|
|
5044
5337
|
|
|
5045
5338
|
//#endregion
|
|
5046
|
-
//#region src/platform/kuaishou/
|
|
5339
|
+
//#region src/platform/kuaishou/sign/hudr.ts
|
|
5340
|
+
const KUAISHOU_HUDR_PREFIX = "HUDR_";
|
|
5341
|
+
const KUAISHOU_HUDR_MASK_BYTE = 35;
|
|
5342
|
+
const KUAISHOU_HUDR_BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
5343
|
+
const KUAISHOU_HUDR_CHACHA_KEY = [
|
|
5344
|
+
4183807412,
|
|
5345
|
+
394484062,
|
|
5346
|
+
1106561997,
|
|
5347
|
+
2378328696,
|
|
5348
|
+
630790222,
|
|
5349
|
+
2546784104,
|
|
5350
|
+
2891127470,
|
|
5351
|
+
1922531795
|
|
5352
|
+
];
|
|
5353
|
+
const KUAISHOU_HUDR_CHACHA_NONCE = [
|
|
5354
|
+
2215853858,
|
|
5355
|
+
1643070585,
|
|
5356
|
+
1849059804
|
|
5357
|
+
];
|
|
5358
|
+
/** 将字符串转换为 Unicode code point 数组。 */
|
|
5359
|
+
const toCodePointArray = (value) => {
|
|
5360
|
+
return Array.from(value).map((char) => char.codePointAt(0) ?? 0);
|
|
5361
|
+
};
|
|
5362
|
+
/** 以小端序输出固定长度字节数组。 */
|
|
5363
|
+
const toLittleEndianBytes = (value, size = 4) => {
|
|
5364
|
+
if (size >= 4 && value >= Math.pow(2, 32)) return [
|
|
5365
|
+
255,
|
|
5366
|
+
255,
|
|
5367
|
+
255,
|
|
5368
|
+
255
|
|
5369
|
+
];
|
|
5370
|
+
const result = [];
|
|
5371
|
+
for (let index = 0; index <= size - 1; index++) result[index] = value >>> 8 * index & 255;
|
|
5372
|
+
return result;
|
|
5373
|
+
};
|
|
5374
|
+
/** 对字节切片执行标准 Base64 编码。 */
|
|
5375
|
+
const encodeBase64Slice = (bytes, start, end) => {
|
|
5376
|
+
const result = [];
|
|
5377
|
+
for (let index = start; index < end; index += 3) {
|
|
5378
|
+
const value = (bytes[index] << 16 & 16711680) + (bytes[index + 1] << 8 & 65280) + (bytes[index + 2] & 255);
|
|
5379
|
+
result.push(KUAISHOU_HUDR_BASE64_ALPHABET[value >> 18 & 63], KUAISHOU_HUDR_BASE64_ALPHABET[value >> 12 & 63], KUAISHOU_HUDR_BASE64_ALPHABET[value >> 6 & 63], KUAISHOU_HUDR_BASE64_ALPHABET[value & 63]);
|
|
5380
|
+
}
|
|
5381
|
+
return result.join("");
|
|
5382
|
+
};
|
|
5383
|
+
/** 快手 `HUDR_` 使用的 Base64URL 变体编码。 */
|
|
5384
|
+
const encodeBase64Url = (bytes) => {
|
|
5385
|
+
const result = [];
|
|
5386
|
+
const remainder = bytes.length % 3;
|
|
5387
|
+
const tripletLength = bytes.length - remainder;
|
|
5388
|
+
for (let index = 0; index < tripletLength; index += 16383) {
|
|
5389
|
+
const end = tripletLength < index + 16383 ? tripletLength : index + 16383;
|
|
5390
|
+
result.push(encodeBase64Slice(bytes, index, end));
|
|
5391
|
+
}
|
|
5392
|
+
if (remainder === 1) {
|
|
5393
|
+
const value = bytes[bytes.length - 1];
|
|
5394
|
+
result.push(KUAISHOU_HUDR_BASE64_ALPHABET[value >> 2], KUAISHOU_HUDR_BASE64_ALPHABET[value << 4 & 63], "==");
|
|
5395
|
+
} else if (remainder === 2) {
|
|
5396
|
+
const value = (bytes[bytes.length - 2] << 8) + bytes[bytes.length - 1];
|
|
5397
|
+
result.push(KUAISHOU_HUDR_BASE64_ALPHABET[value >> 10], KUAISHOU_HUDR_BASE64_ALPHABET[value >> 4 & 63], KUAISHOU_HUDR_BASE64_ALPHABET[value << 2 & 63], "=");
|
|
5398
|
+
}
|
|
5399
|
+
return result.join("").replace(/[+/=]/g, (char) => {
|
|
5400
|
+
if (char === "+") return "-";
|
|
5401
|
+
if (char === "/") return "_";
|
|
5402
|
+
return ".";
|
|
5403
|
+
});
|
|
5404
|
+
};
|
|
5047
5405
|
/**
|
|
5048
|
-
*
|
|
5406
|
+
* 快手 `HUDR_` 所用的 ChaCha20 变体实现。
|
|
5049
5407
|
*
|
|
5050
|
-
*
|
|
5051
|
-
*
|
|
5052
|
-
*/
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5408
|
+
* 该实现并非通用密码库封装,而是为对齐页面 `$encode`
|
|
5409
|
+
* 数据流而最小化保留的局部算法。
|
|
5410
|
+
*/
|
|
5411
|
+
var KuaishouChaChaCipher = class {
|
|
5412
|
+
wordIndex = 0;
|
|
5413
|
+
state = new Array(16).fill(0);
|
|
5414
|
+
constructor(key, nonce) {
|
|
5415
|
+
this.key = key;
|
|
5416
|
+
this.nonce = nonce;
|
|
5417
|
+
}
|
|
5418
|
+
rotateLeft(value, shift) {
|
|
5419
|
+
return (value << shift | value >>> 32 - shift) >>> 0;
|
|
5420
|
+
}
|
|
5421
|
+
quarterRound(target, a, b, c, d) {
|
|
5422
|
+
target[a] = target[a] + target[b] >>> 0;
|
|
5423
|
+
target[d] ^= target[a];
|
|
5424
|
+
target[d] = this.rotateLeft(target[d], 16);
|
|
5425
|
+
target[c] = target[c] + target[d] >>> 0;
|
|
5426
|
+
target[b] ^= target[c];
|
|
5427
|
+
target[b] = this.rotateLeft(target[b], 12);
|
|
5428
|
+
target[a] = target[a] + target[b] >>> 0;
|
|
5429
|
+
target[d] ^= target[a];
|
|
5430
|
+
target[d] = this.rotateLeft(target[d], 8);
|
|
5431
|
+
target[c] = target[c] + target[d] >>> 0;
|
|
5432
|
+
target[b] ^= target[c];
|
|
5433
|
+
target[b] = this.rotateLeft(target[b], 7);
|
|
5434
|
+
}
|
|
5435
|
+
refillBlock() {
|
|
5436
|
+
const working = this.state.slice();
|
|
5437
|
+
for (let round = 0; round < 20; round += 2) {
|
|
5438
|
+
this.quarterRound(working, 0, 4, 8, 12);
|
|
5439
|
+
this.quarterRound(working, 1, 5, 9, 13);
|
|
5440
|
+
this.quarterRound(working, 2, 6, 10, 14);
|
|
5441
|
+
this.quarterRound(working, 3, 7, 11, 15);
|
|
5442
|
+
this.quarterRound(working, 0, 5, 10, 15);
|
|
5443
|
+
this.quarterRound(working, 1, 6, 11, 12);
|
|
5444
|
+
this.quarterRound(working, 2, 7, 8, 13);
|
|
5445
|
+
this.quarterRound(working, 3, 4, 9, 14);
|
|
5087
5446
|
}
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5447
|
+
const mixedState = new Array(16).fill(0);
|
|
5448
|
+
for (let index = 0; index < 16; index++) mixedState[index] = working[index] + this.state[index] >>> 0;
|
|
5449
|
+
return mixedState;
|
|
5450
|
+
}
|
|
5451
|
+
encrypt(input) {
|
|
5452
|
+
this.wordIndex = 0;
|
|
5453
|
+
this.state[0] = 394484062;
|
|
5454
|
+
this.state[1] = 2378328696;
|
|
5455
|
+
this.state[2] = 630790222;
|
|
5456
|
+
this.state[3] = 1922531795;
|
|
5457
|
+
for (let index = 0; index < 8; index++) this.state[index + 4] = this.key[index] >>> 0;
|
|
5458
|
+
this.state[12] = 1;
|
|
5459
|
+
this.state[13] = this.nonce[0] >>> 0;
|
|
5460
|
+
this.state[14] = this.nonce[1] >>> 0;
|
|
5461
|
+
this.state[15] = this.nonce[2] >>> 0;
|
|
5462
|
+
let mixedState = this.refillBlock();
|
|
5463
|
+
const output = new Uint8Array(input.length);
|
|
5464
|
+
for (let index = 0; index < input.length; index++) {
|
|
5465
|
+
if (this.wordIndex === 64) {
|
|
5466
|
+
this.state[12] = this.state[12] + 1 >>> 0;
|
|
5467
|
+
mixedState = this.refillBlock();
|
|
5468
|
+
this.wordIndex = 0;
|
|
5469
|
+
}
|
|
5470
|
+
const keystreamByte = mixedState[this.wordIndex >> 2] >>> ((this.wordIndex & 3) << 3) & 255;
|
|
5471
|
+
this.wordIndex += 1;
|
|
5472
|
+
output[index] = input[index] ^ keystreamByte;
|
|
5095
5473
|
}
|
|
5096
|
-
|
|
5097
|
-
logger.warn(`Unknown Kuaishou API method: "${logger.red(data$1.methodType)}"`);
|
|
5098
|
-
return null;
|
|
5474
|
+
return output;
|
|
5099
5475
|
}
|
|
5100
5476
|
};
|
|
5101
5477
|
/**
|
|
5102
|
-
*
|
|
5103
|
-
*
|
|
5478
|
+
* 构造 `HUDR_` 头部中的 `infoCache` 字段。
|
|
5479
|
+
*
|
|
5480
|
+
* @param scriptCount - 页面脚本数量
|
|
5481
|
+
* @returns `HUDR_` 载荷中的脚本信息缓存字节
|
|
5104
5482
|
*/
|
|
5105
|
-
const
|
|
5106
|
-
|
|
5107
|
-
try {
|
|
5108
|
-
const result = await fetchData(options);
|
|
5109
|
-
if (isNetworkErrorResult(result)) {
|
|
5110
|
-
const networkError = new Error(result.error.amagiError.errorDescription);
|
|
5111
|
-
Object.assign(networkError, {
|
|
5112
|
-
code: result.error.code,
|
|
5113
|
-
data: null,
|
|
5114
|
-
amagiError: {
|
|
5115
|
-
...result.error.amagiError,
|
|
5116
|
-
requestType: type
|
|
5117
|
-
}
|
|
5118
|
-
});
|
|
5119
|
-
throw networkError;
|
|
5120
|
-
}
|
|
5121
|
-
if (result === "" || !result || result.result === 2) {
|
|
5122
|
-
const Err = {
|
|
5123
|
-
errorDescription: "获取响应数据失败!接口返回内容为空!",
|
|
5124
|
-
requestType: type ?? "未知请求类型",
|
|
5125
|
-
requestUrl: options.url,
|
|
5126
|
-
requestBody: JSON.stringify(options.data)
|
|
5127
|
-
};
|
|
5128
|
-
warningMessage = `
|
|
5129
|
-
获取响应数据失败!原因:${logger.yellow("接口返回内容为空,你的快手ck可能已经失效!")}
|
|
5130
|
-
请求类型:「${type}」
|
|
5131
|
-
请求URL:${options.url}
|
|
5132
|
-
请求参数:${JSON.stringify(options.data, null, 2)}
|
|
5133
|
-
`;
|
|
5134
|
-
logger.warn(warningMessage);
|
|
5135
|
-
const cookieError = new Error(Err.errorDescription);
|
|
5136
|
-
Object.assign(cookieError, {
|
|
5137
|
-
code: kuaishouAPIErrorCode.COOKIE,
|
|
5138
|
-
data: result,
|
|
5139
|
-
amagiError: Err
|
|
5140
|
-
});
|
|
5141
|
-
throw cookieError;
|
|
5142
|
-
}
|
|
5143
|
-
return result;
|
|
5144
|
-
} catch (error) {
|
|
5145
|
-
if (error && typeof error === "object") return {
|
|
5146
|
-
...error,
|
|
5147
|
-
amagiMessage: warningMessage
|
|
5148
|
-
};
|
|
5149
|
-
return {
|
|
5150
|
-
code: amagiAPIErrorCode.UNKNOWN,
|
|
5151
|
-
data: null,
|
|
5152
|
-
amagiError: {
|
|
5153
|
-
errorDescription: "未知错误",
|
|
5154
|
-
requestType: type,
|
|
5155
|
-
requestUrl: options.url
|
|
5156
|
-
},
|
|
5157
|
-
amagiMessage: warningMessage
|
|
5158
|
-
};
|
|
5159
|
-
}
|
|
5483
|
+
const buildKuaishouHudrInfoCache = (scriptCount = 0) => {
|
|
5484
|
+
return [68, 0].concat(toLittleEndianBytes(scriptCount, 4));
|
|
5160
5485
|
};
|
|
5161
|
-
|
|
5162
|
-
//#endregion
|
|
5163
|
-
//#region src/model/fetchers/kuaishou/internal.ts
|
|
5164
5486
|
/**
|
|
5165
|
-
*
|
|
5166
|
-
*
|
|
5487
|
+
* 构造快手 `HUDR_` 原始载荷。
|
|
5488
|
+
*
|
|
5489
|
+
* @param context - 生成 `HUDR_` 所需的上下文参数
|
|
5490
|
+
* @returns 尚未异或和加密的原始字节载荷
|
|
5167
5491
|
*/
|
|
5492
|
+
const buildKuaishouHudrPayload = (context) => {
|
|
5493
|
+
const stackTail = context.secs?.s ?? "";
|
|
5494
|
+
const secsCount = context.secs?.c ?? 0;
|
|
5495
|
+
const count = context.count || 0;
|
|
5496
|
+
return [
|
|
5497
|
+
45,
|
|
5498
|
+
61,
|
|
5499
|
+
0,
|
|
5500
|
+
2
|
|
5501
|
+
].concat(buildKuaishouHudrInfoCache(context.scriptCount)).concat([112, 0], toLittleEndianBytes(count, 4)).concat([114, 1], toLittleEndianBytes(stackTail.length, 2), toCodePointArray(stackTail)).concat([115, 0], toLittleEndianBytes(secsCount, 4));
|
|
5502
|
+
};
|
|
5168
5503
|
/**
|
|
5169
|
-
*
|
|
5170
|
-
*
|
|
5171
|
-
* @param
|
|
5172
|
-
* @
|
|
5173
|
-
* @param config - 配置项 (cookie, requestConfig)
|
|
5174
|
-
* @returns Promise<Result<T>>
|
|
5504
|
+
* 对 `HUDR_` 原始载荷执行页面同款掩码异或。
|
|
5505
|
+
*
|
|
5506
|
+
* @param payload - 原始 `HUDR_` 字节载荷
|
|
5507
|
+
* @returns 掩码处理后的字节数组
|
|
5175
5508
|
*/
|
|
5176
|
-
|
|
5177
|
-
const
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5509
|
+
const maskKuaishouHudrPayload = (payload) => {
|
|
5510
|
+
const masked = new Uint8Array(payload.length);
|
|
5511
|
+
for (let index = 0; index < payload.length; index++) masked[index] = KUAISHOU_HUDR_MASK_BYTE ^ payload[index];
|
|
5512
|
+
return masked;
|
|
5513
|
+
};
|
|
5514
|
+
/**
|
|
5515
|
+
* 推导快手签名中的 `HUDR_` 段。
|
|
5516
|
+
*
|
|
5517
|
+
* @param context - 生成 `HUDR_` 所需的上下文参数
|
|
5518
|
+
* @returns `HUDR_` 的完整结果及若干中间态,便于对拍与调试
|
|
5519
|
+
*/
|
|
5520
|
+
const deriveKuaishouHudrBody = (context) => {
|
|
5521
|
+
const maskedPayload = maskKuaishouHudrPayload(buildKuaishouHudrPayload(context));
|
|
5522
|
+
const body = encodeBase64Url(new KuaishouChaChaCipher(KUAISHOU_HUDR_CHACHA_KEY, KUAISHOU_HUDR_CHACHA_NONCE).encrypt(maskedPayload));
|
|
5523
|
+
return {
|
|
5524
|
+
body,
|
|
5525
|
+
full: `${KUAISHOU_HUDR_PREFIX}${body}`,
|
|
5526
|
+
infoCache: buildKuaishouHudrInfoCache(context.scriptCount),
|
|
5527
|
+
maskedPayload,
|
|
5528
|
+
nextCount: context.count + 1
|
|
5529
|
+
};
|
|
5530
|
+
};
|
|
5531
|
+
|
|
5532
|
+
//#endregion
|
|
5533
|
+
//#region src/platform/kuaishou/sign/primitives.ts
|
|
5534
|
+
const KUAISHOU_BLAKE2S_IV = [
|
|
5535
|
+
2837534710,
|
|
5536
|
+
2845986804,
|
|
5537
|
+
2436420605,
|
|
5538
|
+
706843635,
|
|
5539
|
+
719254516,
|
|
5540
|
+
2557931286,
|
|
5541
|
+
2596197199,
|
|
5542
|
+
2432949778
|
|
5543
|
+
];
|
|
5544
|
+
const KUAISHOU_BLAKE2S_SIGMA = [
|
|
5545
|
+
[
|
|
5546
|
+
0,
|
|
5547
|
+
1,
|
|
5548
|
+
2,
|
|
5549
|
+
3,
|
|
5550
|
+
4,
|
|
5551
|
+
5,
|
|
5552
|
+
6,
|
|
5553
|
+
7,
|
|
5554
|
+
8,
|
|
5555
|
+
9,
|
|
5556
|
+
10,
|
|
5557
|
+
11,
|
|
5558
|
+
12,
|
|
5559
|
+
13,
|
|
5560
|
+
14,
|
|
5561
|
+
15
|
|
5562
|
+
],
|
|
5563
|
+
[
|
|
5564
|
+
14,
|
|
5565
|
+
10,
|
|
5566
|
+
4,
|
|
5567
|
+
8,
|
|
5568
|
+
9,
|
|
5569
|
+
15,
|
|
5570
|
+
13,
|
|
5571
|
+
6,
|
|
5572
|
+
1,
|
|
5573
|
+
12,
|
|
5574
|
+
0,
|
|
5575
|
+
2,
|
|
5576
|
+
11,
|
|
5577
|
+
7,
|
|
5578
|
+
5,
|
|
5579
|
+
3
|
|
5580
|
+
],
|
|
5581
|
+
[
|
|
5582
|
+
11,
|
|
5583
|
+
8,
|
|
5584
|
+
12,
|
|
5585
|
+
0,
|
|
5586
|
+
5,
|
|
5587
|
+
2,
|
|
5588
|
+
15,
|
|
5589
|
+
13,
|
|
5590
|
+
10,
|
|
5591
|
+
14,
|
|
5592
|
+
3,
|
|
5593
|
+
6,
|
|
5594
|
+
7,
|
|
5595
|
+
1,
|
|
5596
|
+
9,
|
|
5597
|
+
4
|
|
5598
|
+
],
|
|
5599
|
+
[
|
|
5600
|
+
7,
|
|
5601
|
+
9,
|
|
5602
|
+
3,
|
|
5603
|
+
1,
|
|
5604
|
+
13,
|
|
5605
|
+
12,
|
|
5606
|
+
11,
|
|
5607
|
+
14,
|
|
5608
|
+
2,
|
|
5609
|
+
6,
|
|
5610
|
+
5,
|
|
5611
|
+
10,
|
|
5612
|
+
4,
|
|
5613
|
+
0,
|
|
5614
|
+
15,
|
|
5615
|
+
8
|
|
5616
|
+
],
|
|
5617
|
+
[
|
|
5618
|
+
9,
|
|
5619
|
+
0,
|
|
5620
|
+
5,
|
|
5621
|
+
7,
|
|
5622
|
+
2,
|
|
5623
|
+
4,
|
|
5624
|
+
10,
|
|
5625
|
+
15,
|
|
5626
|
+
14,
|
|
5627
|
+
1,
|
|
5628
|
+
11,
|
|
5629
|
+
12,
|
|
5630
|
+
6,
|
|
5631
|
+
8,
|
|
5632
|
+
3,
|
|
5633
|
+
13
|
|
5634
|
+
],
|
|
5635
|
+
[
|
|
5636
|
+
2,
|
|
5637
|
+
12,
|
|
5638
|
+
6,
|
|
5639
|
+
10,
|
|
5640
|
+
0,
|
|
5641
|
+
11,
|
|
5642
|
+
8,
|
|
5643
|
+
3,
|
|
5644
|
+
4,
|
|
5645
|
+
13,
|
|
5646
|
+
7,
|
|
5647
|
+
5,
|
|
5648
|
+
15,
|
|
5649
|
+
14,
|
|
5650
|
+
1,
|
|
5651
|
+
9
|
|
5652
|
+
],
|
|
5653
|
+
[
|
|
5654
|
+
12,
|
|
5655
|
+
5,
|
|
5656
|
+
1,
|
|
5657
|
+
15,
|
|
5658
|
+
14,
|
|
5659
|
+
13,
|
|
5660
|
+
4,
|
|
5661
|
+
10,
|
|
5662
|
+
0,
|
|
5663
|
+
7,
|
|
5664
|
+
6,
|
|
5665
|
+
3,
|
|
5666
|
+
9,
|
|
5667
|
+
2,
|
|
5668
|
+
8,
|
|
5669
|
+
11
|
|
5670
|
+
],
|
|
5671
|
+
[
|
|
5672
|
+
13,
|
|
5673
|
+
11,
|
|
5674
|
+
7,
|
|
5675
|
+
14,
|
|
5676
|
+
12,
|
|
5677
|
+
1,
|
|
5678
|
+
3,
|
|
5679
|
+
9,
|
|
5680
|
+
5,
|
|
5681
|
+
0,
|
|
5682
|
+
15,
|
|
5683
|
+
4,
|
|
5684
|
+
8,
|
|
5685
|
+
6,
|
|
5686
|
+
2,
|
|
5687
|
+
10
|
|
5688
|
+
],
|
|
5689
|
+
[
|
|
5690
|
+
6,
|
|
5691
|
+
15,
|
|
5692
|
+
14,
|
|
5693
|
+
9,
|
|
5694
|
+
11,
|
|
5695
|
+
3,
|
|
5696
|
+
0,
|
|
5697
|
+
8,
|
|
5698
|
+
12,
|
|
5699
|
+
2,
|
|
5700
|
+
13,
|
|
5701
|
+
7,
|
|
5702
|
+
1,
|
|
5703
|
+
4,
|
|
5704
|
+
10,
|
|
5705
|
+
5
|
|
5706
|
+
],
|
|
5707
|
+
[
|
|
5708
|
+
10,
|
|
5709
|
+
2,
|
|
5710
|
+
8,
|
|
5711
|
+
4,
|
|
5712
|
+
7,
|
|
5713
|
+
6,
|
|
5714
|
+
1,
|
|
5715
|
+
5,
|
|
5716
|
+
15,
|
|
5717
|
+
11,
|
|
5718
|
+
9,
|
|
5719
|
+
14,
|
|
5720
|
+
3,
|
|
5721
|
+
12,
|
|
5722
|
+
13,
|
|
5723
|
+
0
|
|
5724
|
+
]
|
|
5725
|
+
];
|
|
5726
|
+
const KUAISHOU_CTS_STATE_VECTOR = new Int8Array([
|
|
5727
|
+
98,
|
|
5728
|
+
0,
|
|
5729
|
+
0,
|
|
5730
|
+
128,
|
|
5731
|
+
49,
|
|
5732
|
+
117,
|
|
5733
|
+
185,
|
|
5734
|
+
253,
|
|
5735
|
+
224,
|
|
5736
|
+
172,
|
|
5737
|
+
104,
|
|
5738
|
+
36,
|
|
5739
|
+
223,
|
|
5740
|
+
155,
|
|
5741
|
+
87,
|
|
5742
|
+
19,
|
|
5743
|
+
32,
|
|
5744
|
+
0,
|
|
5745
|
+
0,
|
|
5746
|
+
64,
|
|
5747
|
+
2,
|
|
5748
|
+
0,
|
|
5749
|
+
0,
|
|
5750
|
+
16,
|
|
5751
|
+
255,
|
|
5752
|
+
255,
|
|
5753
|
+
255,
|
|
5754
|
+
127,
|
|
5755
|
+
255,
|
|
5756
|
+
255,
|
|
5757
|
+
255,
|
|
5758
|
+
63,
|
|
5759
|
+
0,
|
|
5760
|
+
0,
|
|
5761
|
+
0,
|
|
5762
|
+
240,
|
|
5763
|
+
0,
|
|
5764
|
+
0,
|
|
5765
|
+
0,
|
|
5766
|
+
192,
|
|
5767
|
+
0,
|
|
5768
|
+
0,
|
|
5769
|
+
0,
|
|
5770
|
+
128,
|
|
5771
|
+
255,
|
|
5772
|
+
255,
|
|
5773
|
+
255,
|
|
5774
|
+
15
|
|
5775
|
+
]);
|
|
5776
|
+
const rotateRight = (value, shift) => {
|
|
5777
|
+
return (value >>> shift | value << 32 - shift) >>> 0;
|
|
5778
|
+
};
|
|
5779
|
+
const toUtf8Int8Array = (value) => {
|
|
5780
|
+
return new Int8Array(unescape(encodeURIComponent(value)).split("").map((char) => char.charCodeAt(0) || 0));
|
|
5781
|
+
};
|
|
5782
|
+
const toHex32 = (value) => {
|
|
5783
|
+
return (value >>> 0).toString(16).padStart(8, "0");
|
|
5784
|
+
};
|
|
5785
|
+
const blake2sQuarterRound = (state, a, b, c, d, x, y) => {
|
|
5786
|
+
state[a] = state[a] + state[b] + x >>> 0;
|
|
5787
|
+
state[d] = rotateRight(state[d] ^ state[a], 16);
|
|
5788
|
+
state[c] = state[c] + state[d] >>> 0;
|
|
5789
|
+
state[b] = rotateRight(state[b] ^ state[c], 12);
|
|
5790
|
+
state[a] = state[a] + state[b] + y >>> 0;
|
|
5791
|
+
state[d] = rotateRight(state[d] ^ state[a], 8);
|
|
5792
|
+
state[c] = state[c] + state[d] >>> 0;
|
|
5793
|
+
state[b] = rotateRight(state[b] ^ state[c], 7);
|
|
5794
|
+
};
|
|
5795
|
+
const blake2sCompress = (hash, words, offset, counter, length, isLastBlock) => {
|
|
5796
|
+
const work = new Array(16).fill(0);
|
|
5797
|
+
const block = new Array(16).fill(0);
|
|
5798
|
+
for (let index = 0; index < 8; index++) {
|
|
5799
|
+
work[index] = hash[index] >>> 0;
|
|
5800
|
+
work[index + 8] = KUAISHOU_BLAKE2S_IV[index] >>> 0;
|
|
5801
|
+
}
|
|
5802
|
+
work[12] ^= counter >>> 0;
|
|
5803
|
+
if (isLastBlock) work[14] ^= 4294967295;
|
|
5804
|
+
for (let index = 0; index < length; index++) block[index % 16] ^= words[offset + index] >>> 0;
|
|
5805
|
+
for (const sigma of KUAISHOU_BLAKE2S_SIGMA) {
|
|
5806
|
+
blake2sQuarterRound(work, 0, 4, 8, 12, block[sigma[0]], block[sigma[1]]);
|
|
5807
|
+
blake2sQuarterRound(work, 1, 5, 9, 13, block[sigma[2]], block[sigma[3]]);
|
|
5808
|
+
blake2sQuarterRound(work, 2, 6, 10, 14, block[sigma[4]], block[sigma[5]]);
|
|
5809
|
+
blake2sQuarterRound(work, 3, 7, 11, 15, block[sigma[6]], block[sigma[7]]);
|
|
5810
|
+
blake2sQuarterRound(work, 0, 5, 10, 15, block[sigma[8]], block[sigma[9]]);
|
|
5811
|
+
blake2sQuarterRound(work, 1, 6, 11, 12, block[sigma[10]], block[sigma[11]]);
|
|
5812
|
+
blake2sQuarterRound(work, 2, 7, 8, 13, block[sigma[12]], block[sigma[13]]);
|
|
5813
|
+
blake2sQuarterRound(work, 3, 4, 9, 14, block[sigma[14]], block[sigma[15]]);
|
|
5814
|
+
}
|
|
5815
|
+
for (let index = 0; index < 8; index++) hash[index] = (hash[index] ^ work[index] ^ work[index + 8]) >>> 0;
|
|
5816
|
+
return hash;
|
|
5817
|
+
};
|
|
5818
|
+
const deriveB2hasWords = (value) => {
|
|
5819
|
+
const utf8 = toUtf8Int8Array(value);
|
|
5820
|
+
const padding = utf8.length % 4 === 0 ? 0 : 4 - utf8.length % 4;
|
|
5821
|
+
const padded = new Int8Array(utf8.length + padding);
|
|
5822
|
+
for (let index = 0; index < utf8.length; index++) padded[index] = utf8[index];
|
|
5823
|
+
const words = new Array(padded.length / 4);
|
|
5824
|
+
for (let index = 0; index < padded.length; index += 4) words[index / 4] = new Int32Array(padded.slice(index, index + 4).buffer)[0];
|
|
5825
|
+
return words;
|
|
5826
|
+
};
|
|
5827
|
+
const deriveB2hasHash = (words) => {
|
|
5828
|
+
const hash = KUAISHOU_BLAKE2S_IV.slice();
|
|
5829
|
+
hash[0] ^= 16842784;
|
|
5830
|
+
let offset = 0;
|
|
5831
|
+
let length = words.length;
|
|
5832
|
+
let counter = 0;
|
|
5833
|
+
while (length > 64) {
|
|
5834
|
+
length -= 64;
|
|
5835
|
+
counter += 64;
|
|
5836
|
+
blake2sCompress(hash, words, offset, counter, 64, false);
|
|
5837
|
+
offset += 64;
|
|
5838
|
+
}
|
|
5839
|
+
return blake2sCompress(hash, words, offset, counter + length, length, true);
|
|
5840
|
+
};
|
|
5841
|
+
/**
|
|
5842
|
+
* 计算快手页面中的 `b2has` 字符串。
|
|
5843
|
+
*
|
|
5844
|
+
* 这是后续 `b2sa` / `cts` 链路的起点。
|
|
5845
|
+
*
|
|
5846
|
+
* @param value - 原始输入字符串
|
|
5847
|
+
* @returns `b2has` 结果字符串
|
|
5848
|
+
*/
|
|
5849
|
+
const deriveKuaishouB2has = (value) => {
|
|
5850
|
+
return deriveB2hasHash(deriveB2hasWords(value)).map(toHex32).join("");
|
|
5851
|
+
};
|
|
5852
|
+
/**
|
|
5853
|
+
* 将输入字符串转换为快手 `b2sa` 所需的字节数组。
|
|
5854
|
+
*
|
|
5855
|
+
* @param value - 原始输入字符串
|
|
5856
|
+
* @returns `b2has` 结果转 UTF-8 后的 Int8Array
|
|
5857
|
+
*/
|
|
5858
|
+
const deriveKuaishouB2sa = (value) => {
|
|
5859
|
+
return toUtf8Int8Array(deriveKuaishouB2has(value));
|
|
5860
|
+
};
|
|
5861
|
+
const createKuaishouCtsState = () => {
|
|
5862
|
+
return {
|
|
5863
|
+
s: new Int32Array(KUAISHOU_CTS_STATE_VECTOR.slice(12, 16).buffer)[0],
|
|
5864
|
+
u: new Int32Array(KUAISHOU_CTS_STATE_VECTOR.slice(8, 12).buffer)[0],
|
|
5865
|
+
c: new Int32Array(KUAISHOU_CTS_STATE_VECTOR.slice(4, 8).buffer)[0],
|
|
5866
|
+
l: new Int32Array(KUAISHOU_CTS_STATE_VECTOR.slice(0, 4).buffer)[0],
|
|
5867
|
+
p: new Int32Array(KUAISHOU_CTS_STATE_VECTOR.slice(16, 20).buffer)[0],
|
|
5868
|
+
f: new Int32Array(KUAISHOU_CTS_STATE_VECTOR.slice(20, 24).buffer)[0],
|
|
5869
|
+
d: new Int32Array(KUAISHOU_CTS_STATE_VECTOR.slice(24, 28).buffer)[0],
|
|
5870
|
+
y: new Int32Array(KUAISHOU_CTS_STATE_VECTOR.slice(28, 32).buffer)[0],
|
|
5871
|
+
h: new Int32Array(KUAISHOU_CTS_STATE_VECTOR.slice(44, 48).buffer)[0],
|
|
5872
|
+
E: new Int32Array(KUAISHOU_CTS_STATE_VECTOR.slice(40, 44).buffer)[0],
|
|
5873
|
+
m: new Int32Array(KUAISHOU_CTS_STATE_VECTOR.slice(36, 40).buffer)[0],
|
|
5874
|
+
b: new Int32Array(KUAISHOU_CTS_STATE_VECTOR.slice(32, 36).buffer)[0]
|
|
5875
|
+
};
|
|
5876
|
+
};
|
|
5877
|
+
const seedKuaishouCtsState = (state, seed) => {
|
|
5878
|
+
const chars = new Int8Array(seed.split("").map((char) => char.codePointAt(0) ?? 0)).slice(0, seed.length);
|
|
5879
|
+
for (let index = 0; index < 4; index++) {
|
|
5880
|
+
state.s = (state.s <<= 8) | chars[index + 4];
|
|
5881
|
+
state.u = (state.u <<= 8) | chars[index + 4];
|
|
5882
|
+
state.c = (state.c <<= 8) | chars[index + 4];
|
|
5883
|
+
}
|
|
5884
|
+
if (state.s === 0) state.s = 324508639;
|
|
5885
|
+
if (state.u === 0) state.u = 610839776;
|
|
5886
|
+
if (state.c === 0) state.c = 4256789809;
|
|
5887
|
+
};
|
|
5888
|
+
const deriveKuaishouCtsByte = (state, value) => {
|
|
5889
|
+
let result = 0;
|
|
5890
|
+
let rightBit = 1 & state.u;
|
|
5891
|
+
let leftBit = 1 & state.c;
|
|
5892
|
+
for (let index = 0; index < 8; index++) {
|
|
5893
|
+
if (1 & state.s) {
|
|
5894
|
+
state.s = state.s ^ state.l >> 1 & 4294967295 | state.E;
|
|
5895
|
+
rightBit = 1 & state.u ? (state.u = state.u ^ state.p >> 1 & 4294967295 | state.m, 1) : (state.u = state.u >> 1 & 4294967295 & state.y, 0);
|
|
5896
|
+
} else {
|
|
5897
|
+
state.s = state.s >> 1 & 4294967295 & state.d;
|
|
5898
|
+
leftBit = 1 & state.c ? (state.c = state.c ^ state.f >> 1 & 4294967295 | state.b, 1) : (state.c = state.c >> 1 & 4294967295 & state.h, 0);
|
|
5899
|
+
}
|
|
5900
|
+
const mixed = result << 1 & 4294967295 | rightBit ^ leftBit;
|
|
5901
|
+
result = mixed > 127 ? mixed - 256 : mixed < -128 ? mixed + 256 : mixed;
|
|
5902
|
+
}
|
|
5903
|
+
return value ^ result + 3;
|
|
5904
|
+
};
|
|
5905
|
+
/**
|
|
5906
|
+
* 执行快手页面中的 `cts` 字节流变换。
|
|
5907
|
+
*
|
|
5908
|
+
* 该变换会作用在 `b2sa` 的输出上,随后被 `$HE_` 使用。
|
|
5909
|
+
*
|
|
5910
|
+
* @param input - 输入字节数组
|
|
5911
|
+
* @returns 变换后的字节数组
|
|
5912
|
+
*/
|
|
5913
|
+
const deriveKuaishouCts = (input) => {
|
|
5914
|
+
const state = createKuaishouCtsState();
|
|
5915
|
+
seedKuaishouCtsState(state, "Vuz4fCHxn1CO");
|
|
5916
|
+
const result = new Int8Array(input.length);
|
|
5917
|
+
for (let index = 0; index < input.length; index++) result[index] = deriveKuaishouCtsByte(state, input[index]);
|
|
5918
|
+
return result;
|
|
5919
|
+
};
|
|
5920
|
+
/**
|
|
5921
|
+
* 将字节数组编码为小写 hex 字符串。
|
|
5922
|
+
*
|
|
5923
|
+
* @param bytes - 任意可按字节访问的数组
|
|
5924
|
+
* @returns 小写 hex 字符串
|
|
5925
|
+
*/
|
|
5926
|
+
const bytesToLowerHex = (bytes) => {
|
|
5927
|
+
let result = "";
|
|
5928
|
+
for (const byte of Array.from(bytes)) {
|
|
5929
|
+
const value = byte & 255;
|
|
5930
|
+
result += value === 0 ? "00" : `${value < 16 ? "0" : ""}${value.toString(16)}`;
|
|
5931
|
+
}
|
|
5932
|
+
return result;
|
|
5933
|
+
};
|
|
5934
|
+
/**
|
|
5935
|
+
* 将 hex 字符串还原为带符号字节数组。
|
|
5936
|
+
*
|
|
5937
|
+
* @param value - 偶数字节长度的 hex 字符串
|
|
5938
|
+
* @returns 带符号 byte 数组
|
|
5939
|
+
*/
|
|
5940
|
+
const hexToSignedBytes = (value) => {
|
|
5941
|
+
const result = [];
|
|
5942
|
+
for (let index = 0; index < value.length; index += 2) {
|
|
5943
|
+
const parsed = parseInt(value.slice(index, index + 2), 16);
|
|
5944
|
+
const range = Math.pow(2, value.slice(index, index + 2).length / 2 * 8);
|
|
5945
|
+
result.push(parsed > range / 2 - 1 ? parsed - range : parsed);
|
|
5946
|
+
}
|
|
5947
|
+
return result;
|
|
5948
|
+
};
|
|
5949
|
+
/**
|
|
5950
|
+
* 对两个字节数组执行循环异或。
|
|
5951
|
+
*
|
|
5952
|
+
* @param left - 被处理的字节数组
|
|
5953
|
+
* @param right - 异或掩码字节数组
|
|
5954
|
+
* @returns 异或后的 Int8Array
|
|
5955
|
+
*/
|
|
5956
|
+
const xorByteArrays = (left, right) => {
|
|
5957
|
+
let index = 0;
|
|
5958
|
+
const result = new Int8Array(left.length);
|
|
5959
|
+
while (index < left.length) for (let rightIndex = 0; rightIndex < right.length && index < left.length; rightIndex++) {
|
|
5960
|
+
result[index] = left[index] ^ 255 & right[rightIndex];
|
|
5961
|
+
index += 1;
|
|
5962
|
+
}
|
|
5963
|
+
return result;
|
|
5964
|
+
};
|
|
5965
|
+
/**
|
|
5966
|
+
* 将数值编码为固定长度的小端 hex。
|
|
5967
|
+
*
|
|
5968
|
+
* @param value - 原始数值
|
|
5969
|
+
* @param size - 输出字节长度
|
|
5970
|
+
* @returns 小端序 hex 字符串
|
|
5971
|
+
*/
|
|
5972
|
+
const toLittleEndianHex = (value, size) => {
|
|
5973
|
+
const normalized = BigInt.asUintN(size * 8, typeof value === "bigint" ? value : BigInt(Math.trunc(value)));
|
|
5974
|
+
let result = "";
|
|
5975
|
+
for (let index = 0; index < size; index++) {
|
|
5976
|
+
const byte = Number(normalized >> BigInt(8 * index) & 255n);
|
|
5977
|
+
result += byte.toString(16).padStart(2, "0");
|
|
5978
|
+
}
|
|
5979
|
+
return result;
|
|
5980
|
+
};
|
|
5981
|
+
/**
|
|
5982
|
+
* 计算快手 `$HE_` 所使用的 8-bit LRC 校验值。
|
|
5983
|
+
*
|
|
5984
|
+
* @param sourceHex - 原始 hex 字符串
|
|
5985
|
+
* @returns 单字节 LRC 的 hex 表示
|
|
5986
|
+
*/
|
|
5987
|
+
const computeKuaishouLrcHex = (sourceHex) => {
|
|
5988
|
+
return (-hexToSignedBytes(sourceHex).reduce((total, value) => total + (value & 255), 0) & 255).toString(16).padStart(2, "0");
|
|
5989
|
+
};
|
|
5990
|
+
/**
|
|
5991
|
+
* 对 `$HE_` 的 preHex + checksum 执行最终变换。
|
|
5992
|
+
*
|
|
5993
|
+
* 页面实现会以最后一个字节为 key,对前面的字节逐个异或,
|
|
5994
|
+
* 最后再整体转回小写 hex。
|
|
5995
|
+
*
|
|
5996
|
+
* @param prefixHex - 不含最终 LRC 的前缀 hex
|
|
5997
|
+
* @param checksumHex - 最终 LRC hex
|
|
5998
|
+
* @returns `$HE_` 最终输出的 hex 字符串
|
|
5999
|
+
*/
|
|
6000
|
+
const transformKuaishouHeHex = (prefixHex, checksumHex) => {
|
|
6001
|
+
const input = hexToSignedBytes(`${prefixHex}${checksumHex}`);
|
|
6002
|
+
const output = new Int8Array(input.length);
|
|
6003
|
+
const xorKey = input[input.length - 1];
|
|
6004
|
+
for (let index = 0; index < input.length - 1; index++) output[index] = input[index] ^ xorKey;
|
|
6005
|
+
output[input.length - 1] = xorKey;
|
|
6006
|
+
return bytesToLowerHex(output);
|
|
6007
|
+
};
|
|
6008
|
+
|
|
6009
|
+
//#endregion
|
|
6010
|
+
//#region src/platform/kuaishou/sign/he.ts
|
|
6011
|
+
const KUAISHOU_HE_HEADER_HEX = "4B54";
|
|
6012
|
+
const KUAISHOU_HE_VERSION_HEX = "cda9";
|
|
6013
|
+
const KUAISHOU_HE_STARTUP_MARKER_HEX = "ab";
|
|
6014
|
+
const KUAISHOU_HE_FIXED_BODY_HEX = "0100000001";
|
|
6015
|
+
const KUAISHOU_HE_INPUT_XOR_MASK = [
|
|
6016
|
+
45,
|
|
6017
|
+
211,
|
|
6018
|
+
69,
|
|
6019
|
+
192
|
|
6020
|
+
];
|
|
6021
|
+
const KUAISHOU_HE_COUNTER_XOR_MASK = 3131873467;
|
|
6022
|
+
const KUAISHOU_HE_TIME_XOR_MASK = 3360347992n;
|
|
6023
|
+
const KUAISHOU_HE_TAIL_HEX = "9b563eda7b563e";
|
|
6024
|
+
const KUAISHOU_HE_RANDOM_MAX = 0xffffffffffff;
|
|
6025
|
+
/**
|
|
6026
|
+
* 计算 `$HE_` 中的 hash field。
|
|
6027
|
+
*
|
|
6028
|
+
* 它由 `signInput + HUDRBody` 经 `b2sa -> cts -> 截断 -> 异或掩码`
|
|
6029
|
+
* 这条链路导出,是 `$HE_` 中与接口输入最直接相关的一段。
|
|
6030
|
+
*
|
|
6031
|
+
* @param signInput - 快手 `__NS_hxfalcon` 的 sign input
|
|
6032
|
+
* @param hudrBody - `HUDR_` 去前缀后的主体
|
|
6033
|
+
* @returns `$HE_` 载荷中的 4 字节 hash field hex
|
|
6034
|
+
*/
|
|
6035
|
+
const deriveKuaishouHeHashFieldHex = (signInput, hudrBody) => {
|
|
6036
|
+
return bytesToLowerHex(xorByteArrays(hexToSignedBytes(bytesToLowerHex(deriveKuaishouCts(deriveKuaishouB2sa(`${signInput}HUDR_${hudrBody}`))).slice(0, 8)), KUAISHOU_HE_INPUT_XOR_MASK));
|
|
6037
|
+
};
|
|
6038
|
+
/**
|
|
6039
|
+
* 推导快手签名中的 `$HE_` 段。
|
|
6040
|
+
*
|
|
6041
|
+
* @param context - 生成 `$HE_` 所需的上下文参数
|
|
6042
|
+
* @returns `$HE_` 的最终 hex、中间 hash field 和 preHex
|
|
6043
|
+
*/
|
|
6044
|
+
const deriveKuaishouHeHex = (context) => {
|
|
6045
|
+
const random48 = Math.floor(context.randomValue * KUAISHOU_HE_RANDOM_MAX);
|
|
6046
|
+
const hashFieldHex = deriveKuaishouHeHashFieldHex(context.signInput, context.hudrBody);
|
|
6047
|
+
const timeXor = BigInt(context.timestamp) ^ KUAISHOU_HE_TIME_XOR_MASK;
|
|
6048
|
+
const preHex = [
|
|
6049
|
+
KUAISHOU_HE_HEADER_HEX,
|
|
6050
|
+
KUAISHOU_HE_VERSION_HEX,
|
|
6051
|
+
KUAISHOU_HE_STARTUP_MARKER_HEX,
|
|
6052
|
+
toLittleEndianHex(context.startupRandom, 6),
|
|
6053
|
+
toLittleEndianHex(random48, 6),
|
|
6054
|
+
KUAISHOU_HE_FIXED_BODY_HEX,
|
|
6055
|
+
toLittleEndianHex(context.count ^ KUAISHOU_HE_COUNTER_XOR_MASK, 4),
|
|
6056
|
+
hashFieldHex,
|
|
6057
|
+
toLittleEndianHex(timeXor, 6),
|
|
6058
|
+
KUAISHOU_HE_TAIL_HEX,
|
|
6059
|
+
computeKuaishouLrcHex(KUAISHOU_HE_TAIL_HEX)
|
|
6060
|
+
].join("");
|
|
6061
|
+
return {
|
|
6062
|
+
finalHex: transformKuaishouHeHex(preHex, computeKuaishouLrcHex(preHex)),
|
|
6063
|
+
hashFieldHex,
|
|
6064
|
+
preHex
|
|
6065
|
+
};
|
|
6066
|
+
};
|
|
6067
|
+
/**
|
|
6068
|
+
* 一次性推导快手完整纯算法签名。
|
|
6069
|
+
*
|
|
6070
|
+
* 该方法会先生成 `HUDR_`,再拼出 `$HE_`,最终返回完整
|
|
6071
|
+
* `HUDR_...$HE_...` 形式的 `__NS_hxfalcon`。
|
|
6072
|
+
*
|
|
6073
|
+
* @param context - 快手纯算法签名上下文
|
|
6074
|
+
* @returns 完整签名结果及关键中间态
|
|
6075
|
+
*/
|
|
6076
|
+
const deriveKuaishouPureSignature = (context) => {
|
|
6077
|
+
const hudr = deriveKuaishouHudrBody(context);
|
|
6078
|
+
const he = deriveKuaishouHeHex({
|
|
6079
|
+
count: context.count,
|
|
6080
|
+
hudrBody: hudr.body,
|
|
6081
|
+
randomValue: context.randomValue,
|
|
6082
|
+
signInput: context.signInput,
|
|
6083
|
+
startupRandom: context.startupRandom,
|
|
6084
|
+
timestamp: context.timestamp
|
|
6085
|
+
});
|
|
6086
|
+
return {
|
|
6087
|
+
...hudr,
|
|
6088
|
+
he,
|
|
6089
|
+
signResult: `${hudr.full}$HE_${he.finalHex}`
|
|
6090
|
+
};
|
|
6091
|
+
};
|
|
6092
|
+
|
|
6093
|
+
//#endregion
|
|
6094
|
+
//#region src/platform/kuaishou/sign/helpers.ts
|
|
6095
|
+
const SIGN_INPUT_SKIP_KEYWORD = "__NS";
|
|
6096
|
+
const KUAISHOU_ANONYMOUS_KWW_KEY = "K8wm5PvY9nX7qJc2";
|
|
6097
|
+
const KUAISHOU_ANONYMOUS_KWW_SUFFIX = "###ssrd";
|
|
6098
|
+
const KUAISHOU_ANONYMOUS_KWW_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
6099
|
+
let kuaishouAnonymousKwwCache = "";
|
|
6100
|
+
const compareLexicographically = (left, right) => {
|
|
6101
|
+
if (left === right) return 0;
|
|
6102
|
+
return left < right ? -1 : 1;
|
|
6103
|
+
};
|
|
6104
|
+
const toComparableEntry = (key, value) => {
|
|
6105
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) return `${key}=[object Object]`;
|
|
6106
|
+
return `${key}=${String(value)}`;
|
|
6107
|
+
};
|
|
6108
|
+
const normalizePathname = (urlOrPath) => {
|
|
6109
|
+
try {
|
|
6110
|
+
return new URL(urlOrPath).pathname;
|
|
6111
|
+
} catch {
|
|
6112
|
+
return urlOrPath.split("?")[0] ?? urlOrPath;
|
|
6113
|
+
}
|
|
6114
|
+
};
|
|
6115
|
+
const sortSearchParams = (searchParams) => {
|
|
6116
|
+
return [...searchParams.entries()].sort(([leftKey], [rightKey]) => compareLexicographically(leftKey, rightKey)).reduce((result, [key, value]) => {
|
|
6117
|
+
result[key] = value;
|
|
6118
|
+
return result;
|
|
6119
|
+
}, {});
|
|
6120
|
+
};
|
|
6121
|
+
/**
|
|
6122
|
+
* 解析快手签名所使用的规范路径。
|
|
6123
|
+
*
|
|
6124
|
+
* 快手部分 `live_api` 接口的公开 URL 与算法内部参与签名的路径并不一致,
|
|
6125
|
+
* 因此这里优先使用调用方显式传入的 `signPath`,否则回退到 URL 自身的 pathname。
|
|
6126
|
+
*
|
|
6127
|
+
* @param urlOrPath - 原始 URL 或路径
|
|
6128
|
+
* @param signPath - 调用方声明的规范签名路径
|
|
6129
|
+
* @returns 参与签名的规范 pathname
|
|
6130
|
+
*/
|
|
6131
|
+
const resolveKuaishouHxfalconSignPath = (urlOrPath, signPath) => {
|
|
6132
|
+
const pathname = normalizePathname(signPath ?? urlOrPath);
|
|
6133
|
+
if (!pathname.startsWith("/")) throw new Error(`Invalid Kuaishou signing path: ${pathname}`);
|
|
6134
|
+
return pathname;
|
|
6135
|
+
};
|
|
6136
|
+
/**
|
|
6137
|
+
* 从请求 URL 构造快手签名载荷。
|
|
6138
|
+
*
|
|
6139
|
+
* @param url - 实际请求 URL
|
|
6140
|
+
* @param signPath - 可选的规范签名路径
|
|
6141
|
+
* @returns 供纯算法签名链路使用的结构化载荷
|
|
6142
|
+
*/
|
|
6143
|
+
const buildKuaishouHxfalconPayload = (url, signPath) => {
|
|
6144
|
+
const parsedUrl = new URL(url);
|
|
6145
|
+
const realPath = resolveKuaishouHxfalconSignPath(parsedUrl.pathname, signPath);
|
|
6146
|
+
const query = sortSearchParams(parsedUrl.searchParams);
|
|
6147
|
+
if (!query.caver) throw new Error(`Missing caver query parameter for Kuaishou signing: ${url}`);
|
|
6148
|
+
return {
|
|
6149
|
+
url: realPath,
|
|
6150
|
+
query,
|
|
6151
|
+
form: {},
|
|
6152
|
+
requestBody: {}
|
|
6153
|
+
};
|
|
6154
|
+
};
|
|
6155
|
+
/**
|
|
6156
|
+
* 构造快手 `__NS_hxfalcon` 的 sign input。
|
|
6157
|
+
*
|
|
6158
|
+
* 该字符串就是后续 `HUDR_` / `$HE_` 两段算法共同依赖的输入材料。
|
|
6159
|
+
*
|
|
6160
|
+
* @param payload - 已标准化的签名载荷
|
|
6161
|
+
* @returns 按快手页面规则拼接后的 sign input
|
|
6162
|
+
*/
|
|
6163
|
+
const buildKuaishouHxfalconSignInput = (payload) => {
|
|
6164
|
+
const combinedParams = {
|
|
6165
|
+
...payload.query,
|
|
6166
|
+
...payload.form
|
|
6167
|
+
};
|
|
6168
|
+
const serializedParams = Object.keys(combinedParams).filter((key) => !key.includes(SIGN_INPUT_SKIP_KEYWORD)).map((key) => toComparableEntry(key, combinedParams[key])).sort(compareLexicographically).join("");
|
|
6169
|
+
const requestBody = Object.keys(payload.requestBody).length > 0 ? JSON.stringify(payload.requestBody) : "";
|
|
6170
|
+
return `${normalizePathname(payload.url)}${serializedParams}${requestBody}`;
|
|
6171
|
+
};
|
|
6172
|
+
/**
|
|
6173
|
+
* 从 Cookie 字符串中提取指定键值。
|
|
6174
|
+
*
|
|
6175
|
+
* @param cookie - 原始 Cookie 字符串
|
|
6176
|
+
* @param key - 目标键名
|
|
6177
|
+
* @returns 命中的 Cookie 值;未命中时返回空字符串
|
|
6178
|
+
*/
|
|
6179
|
+
const extractCookieValue = (cookie, key) => {
|
|
6180
|
+
if (!cookie?.trim()) return "";
|
|
6181
|
+
const pattern = /* @__PURE__ */ new RegExp(`(?:^|;\\s*)${key}=([^;]*)`);
|
|
6182
|
+
return cookie.match(pattern)?.[1] ?? "";
|
|
6183
|
+
};
|
|
6184
|
+
const generateKuaishouAnonymousKwwSeed = () => {
|
|
6185
|
+
let randomPart = "";
|
|
6186
|
+
for (let index = 0; index < 8; index++) {
|
|
6187
|
+
const randomIndex = Math.floor(Math.random() * 62);
|
|
6188
|
+
randomPart += KUAISHOU_ANONYMOUS_KWW_ALPHABET[randomIndex];
|
|
6189
|
+
}
|
|
6190
|
+
return `${Date.now()}|${randomPart}`;
|
|
6191
|
+
};
|
|
6192
|
+
const encryptKuaishouAnonymousKwwSeed = (seed) => {
|
|
6193
|
+
const key = Buffer.from(KUAISHOU_ANONYMOUS_KWW_KEY, "utf8");
|
|
6194
|
+
const cipher = node_crypto.default.createCipheriv("aes-128-cbc", key, key);
|
|
6195
|
+
return Buffer.concat([cipher.update(seed, "utf8"), cipher.final()]).toString("base64");
|
|
6196
|
+
};
|
|
6197
|
+
/**
|
|
6198
|
+
* 生成匿名访问快手 `live_api` 时所需的 `kww`。
|
|
6199
|
+
*
|
|
6200
|
+
* 页面未持有 `kwfv1` 时,会退回到一段本地 AES 生成值。
|
|
6201
|
+
* 这里将其缓存到当前进程,模拟浏览器侧“同一访客会话复用同一份访客标识”的行为。
|
|
6202
|
+
*
|
|
6203
|
+
* @returns 可用于匿名请求的 `kww`
|
|
6204
|
+
*/
|
|
6205
|
+
const deriveKuaishouAnonymousKww = () => {
|
|
6206
|
+
if (!kuaishouAnonymousKwwCache) kuaishouAnonymousKwwCache = `${encryptKuaishouAnonymousKwwSeed(generateKuaishouAnonymousKwwSeed())}${KUAISHOU_ANONYMOUS_KWW_SUFFIX}`;
|
|
6207
|
+
return kuaishouAnonymousKwwCache;
|
|
6208
|
+
};
|
|
6209
|
+
/**
|
|
6210
|
+
* 解析快手 `kww` 请求头。
|
|
6211
|
+
*
|
|
6212
|
+
* 当前页面存在两条路径:
|
|
6213
|
+
* 1. 若已有 `kwfv1`,则 `kww` 直接复用该值
|
|
6214
|
+
* 2. 若还没有访客 Cookie,则退回到页面本地生成的匿名 `kww`
|
|
6215
|
+
*
|
|
6216
|
+
* @param cookie - 原始 Cookie 字符串
|
|
6217
|
+
* @returns 可用于请求头的 `kww` 值
|
|
6218
|
+
*/
|
|
6219
|
+
const deriveKuaishouKww = (cookie) => {
|
|
6220
|
+
const kwfv1 = extractCookieValue(cookie, "kwfv1");
|
|
6221
|
+
if (kwfv1) return kwfv1;
|
|
6222
|
+
return deriveKuaishouAnonymousKww();
|
|
6223
|
+
};
|
|
6224
|
+
|
|
6225
|
+
//#endregion
|
|
6226
|
+
//#region src/platform/kuaishou/sign/state.ts
|
|
6227
|
+
const KUAISHOU_DEFAULT_CAT_VERSION = "2";
|
|
6228
|
+
const KUAISHOU_DEFAULT_COUNT = 100;
|
|
6229
|
+
const KUAISHOU_SECS_STACK_LIMIT = 100;
|
|
6230
|
+
let pureRuntimeState = null;
|
|
6231
|
+
const captureKuaishouEncodeStack = () => {
|
|
6232
|
+
try {
|
|
6233
|
+
throw new Error();
|
|
6234
|
+
} catch (error) {
|
|
6235
|
+
return error instanceof Error ? error.stack ?? "" : "";
|
|
6236
|
+
}
|
|
6237
|
+
};
|
|
6238
|
+
/**
|
|
6239
|
+
* 计算快手 `SECS.s` 所需的调用栈尾部。
|
|
6240
|
+
*
|
|
6241
|
+
* 真实页面会取 `Error().stack` 的最后 100 个字符,
|
|
6242
|
+
* 这里保持同样的裁剪规则。
|
|
6243
|
+
*
|
|
6244
|
+
* @param stack - 可选的完整调用栈;不传时会即时捕获
|
|
6245
|
+
* @returns 用于 `SECS.s` 的栈尾字符串
|
|
6246
|
+
*/
|
|
6247
|
+
const deriveKuaishouSecsStackTail = (stack = captureKuaishouEncodeStack()) => {
|
|
6248
|
+
return stack.length > KUAISHOU_SECS_STACK_LIMIT ? stack.slice(-KUAISHOU_SECS_STACK_LIMIT) : stack;
|
|
6249
|
+
};
|
|
6250
|
+
/**
|
|
6251
|
+
* 构造快手 `window.SECS` 的纯算法等价状态。
|
|
6252
|
+
*
|
|
6253
|
+
* @param count - 当前签名计数器
|
|
6254
|
+
* @param stack - 可选的完整调用栈
|
|
6255
|
+
* @returns 包含 `s` 与 `c` 的 `SECS` 状态对象
|
|
6256
|
+
*/
|
|
6257
|
+
const deriveKuaishouSecsState = (count, stack) => {
|
|
6258
|
+
return {
|
|
6259
|
+
s: deriveKuaishouSecsStackTail(stack),
|
|
6260
|
+
c: count
|
|
6261
|
+
};
|
|
6262
|
+
};
|
|
6263
|
+
/**
|
|
6264
|
+
* 获取快手纯算法运行时状态。
|
|
6265
|
+
*
|
|
6266
|
+
* 首次调用时会初始化默认 `caver`、`count` 和 `startupRandom`,
|
|
6267
|
+
* 后续调用复用同一份状态。
|
|
6268
|
+
*
|
|
6269
|
+
* @returns 当前进程级的快手纯算法运行时状态
|
|
6270
|
+
*/
|
|
6271
|
+
const getKuaishouPureRuntimeState = () => {
|
|
6272
|
+
if (pureRuntimeState) return pureRuntimeState;
|
|
6273
|
+
pureRuntimeState = {
|
|
6274
|
+
catVersion: KUAISHOU_DEFAULT_CAT_VERSION,
|
|
6275
|
+
count: KUAISHOU_DEFAULT_COUNT,
|
|
6276
|
+
startupRandom: Date.now()
|
|
6277
|
+
};
|
|
6278
|
+
return pureRuntimeState;
|
|
6279
|
+
};
|
|
6280
|
+
|
|
6281
|
+
//#endregion
|
|
6282
|
+
//#region src/platform/kuaishou/sign/index.ts
|
|
6283
|
+
/**
|
|
6284
|
+
* 快手签名工具集。
|
|
6285
|
+
*
|
|
6286
|
+
* 负责将请求描述对象转换为带 `__NS_hxfalcon` / `kww` 的最终请求材料。
|
|
6287
|
+
*/
|
|
6288
|
+
var kuaishouSign = class {
|
|
6289
|
+
/**
|
|
6290
|
+
* 获取当前纯算法运行时使用的 `caver`。
|
|
6291
|
+
*
|
|
6292
|
+
* @returns 快手 `caver` 值
|
|
6293
|
+
*/
|
|
6294
|
+
static getCatVersion() {
|
|
6295
|
+
return getKuaishouPureRuntimeState().catVersion;
|
|
6296
|
+
}
|
|
6297
|
+
/**
|
|
6298
|
+
* 生成快手请求头中的 `kww` 值。
|
|
6299
|
+
*
|
|
6300
|
+
* @param cookie - 原始 Cookie 字符串
|
|
6301
|
+
* @returns `kww` 请求头值
|
|
6302
|
+
*/
|
|
6303
|
+
static generateKww(cookie) {
|
|
6304
|
+
return deriveKuaishouKww(cookie);
|
|
6305
|
+
}
|
|
6306
|
+
/**
|
|
6307
|
+
* 根据结构化签名载荷生成 `__NS_hxfalcon`。
|
|
6308
|
+
*
|
|
6309
|
+
* @param payload - 已标准化的快手签名载荷
|
|
6310
|
+
* @returns 包含最终签名串、sign input 与 `caver` 的结果
|
|
6311
|
+
*/
|
|
6312
|
+
static generateHxfalconFromPayload(payload) {
|
|
6313
|
+
const signInput = buildKuaishouHxfalconSignInput(payload);
|
|
6314
|
+
const runtimeState = getKuaishouPureRuntimeState();
|
|
6315
|
+
const count = runtimeState.count;
|
|
6316
|
+
const secs = deriveKuaishouSecsState(count);
|
|
6317
|
+
const scriptCount = globalThis.document?.scripts?.length ?? 0;
|
|
6318
|
+
const catVersion = runtimeState.catVersion;
|
|
6319
|
+
const signResult = deriveKuaishouPureSignature({
|
|
6320
|
+
count,
|
|
6321
|
+
randomValue: Math.random(),
|
|
6322
|
+
scriptCount,
|
|
6323
|
+
secs,
|
|
6324
|
+
signInput,
|
|
6325
|
+
startupRandom: runtimeState.startupRandom,
|
|
6326
|
+
timestamp: Date.now()
|
|
6327
|
+
}).signResult;
|
|
6328
|
+
runtimeState.count += 1;
|
|
6329
|
+
return {
|
|
6330
|
+
signResult,
|
|
6331
|
+
signInput,
|
|
6332
|
+
catVersion
|
|
6333
|
+
};
|
|
6334
|
+
}
|
|
6335
|
+
/**
|
|
6336
|
+
* 为快手 `live_api` URL 签名。
|
|
6337
|
+
*
|
|
6338
|
+
* @param url - 实际请求 URL
|
|
6339
|
+
* @param cookie - 原始 Cookie 字符串
|
|
6340
|
+
* @param signPath - 可选的规范签名路径
|
|
6341
|
+
* @returns 带签名 URL、附加请求头和调试信息
|
|
6342
|
+
*/
|
|
6343
|
+
static signLiveApiUrl(url, cookie, signPath) {
|
|
6344
|
+
const payload = buildKuaishouHxfalconPayload(url, signPath);
|
|
6345
|
+
const { signResult, signInput, catVersion } = this.generateHxfalconFromPayload(payload);
|
|
6346
|
+
const signedUrl = new URL(url);
|
|
6347
|
+
const headers = {};
|
|
6348
|
+
const kww = deriveKuaishouKww(cookie);
|
|
6349
|
+
signedUrl.searchParams.set("__NS_hxfalcon", signResult);
|
|
6350
|
+
signedUrl.searchParams.set("caver", catVersion);
|
|
6351
|
+
if (kww) headers.kww = kww;
|
|
6352
|
+
return {
|
|
6353
|
+
url: signedUrl.toString(),
|
|
6354
|
+
headers,
|
|
6355
|
+
signResult,
|
|
6356
|
+
signInput,
|
|
6357
|
+
catVersion
|
|
6358
|
+
};
|
|
6359
|
+
}
|
|
6360
|
+
/**
|
|
6361
|
+
* 为结构化的快手 `live_api` 请求描述对象签名。
|
|
6362
|
+
*
|
|
6363
|
+
* 这是项目层更推荐使用的入口,因为它能显式保留 `signPath` 元数据。
|
|
6364
|
+
*
|
|
6365
|
+
* @param request - 快手 `live_api` 请求描述对象
|
|
6366
|
+
* @param cookie - 原始 Cookie 字符串
|
|
6367
|
+
* @returns 带签名 URL、附加请求头和调试信息
|
|
6368
|
+
*/
|
|
6369
|
+
static signLiveApiRequest(request, cookie) {
|
|
6370
|
+
return this.signLiveApiUrl(request.url, cookie, request.signPath);
|
|
6371
|
+
}
|
|
6372
|
+
};
|
|
6373
|
+
|
|
6374
|
+
//#endregion
|
|
6375
|
+
//#region src/platform/kuaishou/getdata.ts
|
|
6376
|
+
/**
|
|
6377
|
+
* 快手数据获取模块
|
|
6378
|
+
*
|
|
6379
|
+
* 注意:为避免循环依赖,此文件直接从具体模块导入,而不是从平台 index 文件导入
|
|
6380
|
+
* 循环依赖链:DataFetchers → getdata → platform/kuaishou → DataFetchers
|
|
6381
|
+
*/
|
|
6382
|
+
const KUAISHOU_PROFILE_TAB_TYPE_MAP = {
|
|
6383
|
+
public: "public",
|
|
6384
|
+
private: "private",
|
|
6385
|
+
liked: "liked",
|
|
6386
|
+
playback: "playback"
|
|
6387
|
+
};
|
|
6388
|
+
const KUAISHOU_BAN_STATE_MAP = {
|
|
6389
|
+
banned: "BANNED",
|
|
6390
|
+
socialBanned: "SOCIALBANNED",
|
|
6391
|
+
isolate: "ISOLATE",
|
|
6392
|
+
cleanState: "CLEAN"
|
|
6393
|
+
};
|
|
6394
|
+
/**
|
|
6395
|
+
* 构造快手 `live_api` 请求配置。
|
|
6396
|
+
*
|
|
6397
|
+
* 该函数负责把 API 层产出的 `KuaishouLiveApiRequest`
|
|
6398
|
+
* 与签名层拼装起来,生成最终可发送的 JSON 请求配置。
|
|
6399
|
+
*
|
|
6400
|
+
* @param request - 快手 `live_api` 请求描述对象
|
|
6401
|
+
* @param refererPath - 用于构造 Referer 的页面相对路径
|
|
6402
|
+
* @param cookie - 用户 Cookie
|
|
6403
|
+
* @param requestConfig - 外部透传的请求配置
|
|
6404
|
+
* @returns 可直接用于 `fetchData` 的 Axios 请求配置
|
|
6405
|
+
*/
|
|
6406
|
+
const buildLiveApiRequestConfig = (request, refererPath, cookie, requestConfig) => {
|
|
6407
|
+
const signedRequest = request.requiresSign === false ? {
|
|
6408
|
+
url: request.url,
|
|
6409
|
+
headers: {}
|
|
6410
|
+
} : kuaishouSign.signLiveApiRequest(request, cookie);
|
|
6411
|
+
const kww = kuaishouSign.generateKww(cookie);
|
|
6412
|
+
const isPostRequest = request.method === "POST";
|
|
6413
|
+
return {
|
|
6414
|
+
method: request.method,
|
|
6415
|
+
timeout: 1e4,
|
|
6416
|
+
...requestConfig,
|
|
6417
|
+
url: signedRequest.url,
|
|
6418
|
+
...isPostRequest ? { data: request.body ?? {} } : {},
|
|
6419
|
+
headers: {
|
|
6420
|
+
Accept: "application/json, text/plain, */*",
|
|
6421
|
+
"Accept-Encoding": "gzip, deflate, br, zstd",
|
|
6422
|
+
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
|
|
6423
|
+
"Cache-Control": "no-cache",
|
|
6424
|
+
Cookie: cookie?.trim() ?? "",
|
|
6425
|
+
Pragma: "no-cache",
|
|
6426
|
+
Priority: "u=0, i",
|
|
6427
|
+
Referer: `https://live.kuaishou.com/${refererPath}`,
|
|
6428
|
+
"Sec-Ch-Ua": "\"Chromium\";v=\"146\", \"Not-A.Brand\";v=\"24\", \"Google Chrome\";v=\"146\"",
|
|
6429
|
+
"Sec-Ch-Ua-Mobile": "?0",
|
|
6430
|
+
"Sec-Ch-Ua-Platform": "\"Windows\"",
|
|
6431
|
+
"Sec-Fetch-Dest": "empty",
|
|
6432
|
+
"Sec-Fetch-Mode": "cors",
|
|
6433
|
+
"Sec-Fetch-Site": "same-origin",
|
|
6434
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
|
|
6435
|
+
...isPostRequest ? { "Content-Type": "application/json" } : {},
|
|
6436
|
+
...signedRequest.headers,
|
|
6437
|
+
...!signedRequest.headers.kww && kww ? { kww } : {},
|
|
6438
|
+
...requestConfig?.headers ?? {}
|
|
6439
|
+
}
|
|
6440
|
+
};
|
|
6441
|
+
};
|
|
6442
|
+
/**
|
|
6443
|
+
* 判断返回值是否已是项目统一的错误描述对象。
|
|
6444
|
+
*
|
|
6445
|
+
* @param result - 任意待判断对象
|
|
6446
|
+
* @returns 是否符合 `ErrorDetail` 结构
|
|
6447
|
+
*/
|
|
6448
|
+
const isErrorDetailLike = (result) => {
|
|
6449
|
+
return Boolean(result && typeof result === "object" && "amagiError" in result);
|
|
6450
|
+
};
|
|
6451
|
+
/**
|
|
6452
|
+
* 判断值是否为普通对象。
|
|
6453
|
+
*
|
|
6454
|
+
* @param value - 待判断的值
|
|
6455
|
+
* @returns 是否为对象字面量
|
|
6456
|
+
*/
|
|
6457
|
+
const isRecord = (value) => {
|
|
6458
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
6459
|
+
};
|
|
6460
|
+
/**
|
|
6461
|
+
* 判断对象是否至少包含一个键。
|
|
6462
|
+
*
|
|
6463
|
+
* @param value - 待判断对象
|
|
6464
|
+
* @returns 是否为非空对象
|
|
6465
|
+
*/
|
|
6466
|
+
const hasPopulatedRecord = (value) => {
|
|
6467
|
+
return isRecord(value) && Object.keys(value).length > 0;
|
|
6468
|
+
};
|
|
6469
|
+
/**
|
|
6470
|
+
* 取首个非空字符串。
|
|
6471
|
+
*
|
|
6472
|
+
* @param values - 候选字符串
|
|
6473
|
+
* @returns 首个非空字符串;若都为空则返回空串
|
|
6474
|
+
*/
|
|
6475
|
+
const pickFirstNonEmptyString = (...values) => {
|
|
6476
|
+
for (const value of values) if (typeof value === "string" && value.trim()) return value;
|
|
6477
|
+
return "";
|
|
6478
|
+
};
|
|
6479
|
+
/**
|
|
6480
|
+
* 创建快手用户主页通用空列表数据。
|
|
6481
|
+
*
|
|
6482
|
+
* @returns 空列表 tab 数据
|
|
6483
|
+
*/
|
|
6484
|
+
const createEmptyUserListTabData = () => ({
|
|
6485
|
+
list: [],
|
|
6486
|
+
pcursor: ""
|
|
6487
|
+
});
|
|
6488
|
+
/**
|
|
6489
|
+
* 创建快手用户主页公开视频 tab 的空数据。
|
|
6490
|
+
*
|
|
6491
|
+
* @returns 含 `live` 字段的空 tab 数据
|
|
6492
|
+
*/
|
|
6493
|
+
const createEmptyUserPublicTabData = () => ({
|
|
6494
|
+
live: null,
|
|
6495
|
+
list: [],
|
|
6496
|
+
pcursor: ""
|
|
6497
|
+
});
|
|
6498
|
+
/**
|
|
6499
|
+
* 创建空的用户作品列表结果。
|
|
6500
|
+
*
|
|
6501
|
+
* @param principalId - 用户主页 principalId
|
|
6502
|
+
* @returns 空的 `userWorkList` 结果
|
|
6503
|
+
*/
|
|
6504
|
+
const createEmptyUserWorkListResult = (principalId) => {
|
|
6505
|
+
return {
|
|
6506
|
+
principalId,
|
|
6507
|
+
list: [],
|
|
6508
|
+
pcursor: "",
|
|
6509
|
+
hasMore: false,
|
|
6510
|
+
result: 0
|
|
6511
|
+
};
|
|
6512
|
+
};
|
|
6513
|
+
/**
|
|
6514
|
+
* 创建匿名态/降级场景下的空用户结果。
|
|
6515
|
+
*
|
|
6516
|
+
* 当补充接口不可用时,使用该结构保证
|
|
6517
|
+
* `userProfile` 仍能返回稳定的最小骨架。
|
|
6518
|
+
*
|
|
6519
|
+
* @param principalId - 用户主页 principalId
|
|
6520
|
+
* @returns 空的 `userProfile` 结果
|
|
6521
|
+
*/
|
|
6522
|
+
const createEmptyUserProfileResult = (principalId) => {
|
|
6523
|
+
return {
|
|
6524
|
+
principalId,
|
|
6525
|
+
author: {
|
|
6526
|
+
principalId,
|
|
6527
|
+
userInfo: {
|
|
6528
|
+
id: "",
|
|
6529
|
+
name: "",
|
|
6530
|
+
description: "",
|
|
6531
|
+
avatar: "",
|
|
6532
|
+
sex: "",
|
|
6533
|
+
living: false,
|
|
6534
|
+
followStatus: "",
|
|
6535
|
+
constellation: "",
|
|
6536
|
+
cityName: "",
|
|
6537
|
+
originUserId: 0,
|
|
6538
|
+
privacy: false,
|
|
6539
|
+
isNew: false,
|
|
6540
|
+
timestamp: 0,
|
|
6541
|
+
verifiedStatus: {
|
|
6542
|
+
verified: false,
|
|
6543
|
+
description: "",
|
|
6544
|
+
type: 0,
|
|
6545
|
+
new: false,
|
|
6546
|
+
iconUrl: ""
|
|
6547
|
+
},
|
|
6548
|
+
bannedStatus: {
|
|
6549
|
+
banned: false,
|
|
6550
|
+
socialBanned: false,
|
|
6551
|
+
isolate: false,
|
|
6552
|
+
defriend: false
|
|
6553
|
+
},
|
|
6554
|
+
counts: {}
|
|
6555
|
+
},
|
|
6556
|
+
sensitiveInfo: null,
|
|
6557
|
+
followInfo: {},
|
|
6558
|
+
banStateMap: KUAISHOU_BAN_STATE_MAP
|
|
6559
|
+
},
|
|
6560
|
+
profile: {
|
|
6561
|
+
currentTab: "public",
|
|
6562
|
+
pageSize: 12,
|
|
6563
|
+
tabTypeMap: KUAISHOU_PROFILE_TAB_TYPE_MAP,
|
|
6564
|
+
showPlayback: false,
|
|
6565
|
+
publicData: createEmptyUserPublicTabData(),
|
|
6566
|
+
privateData: createEmptyUserListTabData(),
|
|
6567
|
+
likedData: createEmptyUserListTabData(),
|
|
6568
|
+
playbackData: createEmptyUserListTabData(),
|
|
6569
|
+
interestList: [],
|
|
6570
|
+
currentProduct: {}
|
|
6571
|
+
},
|
|
6572
|
+
follow: null,
|
|
6573
|
+
followButton: null,
|
|
6574
|
+
interestMask: [],
|
|
6575
|
+
categoryMask: {
|
|
6576
|
+
config: [],
|
|
6577
|
+
list: [],
|
|
6578
|
+
hotList: [],
|
|
6579
|
+
hasMore: false,
|
|
6580
|
+
hasMoreHot: false
|
|
6581
|
+
}
|
|
6582
|
+
};
|
|
6583
|
+
};
|
|
6584
|
+
/**
|
|
6585
|
+
* 创建空的直播间结果。
|
|
6586
|
+
*
|
|
6587
|
+
* @param principalId - 直播间 principalId
|
|
6588
|
+
* @returns 空的 `liveRoomInfo`
|
|
6589
|
+
*/
|
|
6590
|
+
const createEmptyLiveRoomInfoResult = (principalId) => {
|
|
6591
|
+
return {
|
|
6592
|
+
principalId,
|
|
6593
|
+
activeIndex: 0,
|
|
6594
|
+
current: null,
|
|
6595
|
+
playList: [],
|
|
6596
|
+
websocketUrls: [],
|
|
6597
|
+
token: "",
|
|
6598
|
+
noticeList: [],
|
|
6599
|
+
loading: false,
|
|
6600
|
+
emoji: {
|
|
6601
|
+
iconUrls: {},
|
|
6602
|
+
giftList: [],
|
|
6603
|
+
giftPanelList: [],
|
|
6604
|
+
token: "",
|
|
6605
|
+
panelToken: "",
|
|
6606
|
+
longSendGiftType: null
|
|
6607
|
+
}
|
|
6608
|
+
};
|
|
6609
|
+
};
|
|
6610
|
+
/**
|
|
6611
|
+
* 从用户资料中派生关注状态对象。
|
|
6612
|
+
*
|
|
6613
|
+
* 真实页面中的 `follow` 与 `followBtn` 来自页面装配状态;
|
|
6614
|
+
* 在纯协议路径下,这里基于用户资料接口回填稳定最小字段。
|
|
6615
|
+
*
|
|
6616
|
+
* @param userInfo - 用户公开资料
|
|
6617
|
+
* @param sensitiveInfo - 用户敏感资料
|
|
6618
|
+
* @returns 归一化后的 follow 状态
|
|
6619
|
+
*/
|
|
6620
|
+
const createDerivedFollowState = (userInfo, sensitiveInfo) => {
|
|
6621
|
+
const followStatus = userInfo?.followStatus ?? sensitiveInfo?.followStatus;
|
|
6622
|
+
if (!followStatus) return null;
|
|
6623
|
+
return {
|
|
6624
|
+
currentFollowStatus: followStatus,
|
|
6625
|
+
needToFollow: false,
|
|
6626
|
+
authorId: userInfo?.id ?? "",
|
|
6627
|
+
data: 0
|
|
6628
|
+
};
|
|
6629
|
+
};
|
|
6630
|
+
/**
|
|
6631
|
+
* 从用户资料中派生关注按钮状态。
|
|
6632
|
+
*
|
|
6633
|
+
* @param userInfo - 用户公开资料
|
|
6634
|
+
* @param sensitiveInfo - 用户敏感资料
|
|
6635
|
+
* @returns 归一化后的 follow button 状态
|
|
6636
|
+
*/
|
|
6637
|
+
const createDerivedFollowButtonState = (userInfo, sensitiveInfo) => {
|
|
6638
|
+
const followStatus = userInfo?.followStatus ?? sensitiveInfo?.followStatus;
|
|
6639
|
+
if (!followStatus) return null;
|
|
6640
|
+
return { followStatus };
|
|
6641
|
+
};
|
|
6642
|
+
/**
|
|
6643
|
+
* 解析用户主页 tab 数据。
|
|
6644
|
+
*
|
|
6645
|
+
* @param payload - 接口响应
|
|
6646
|
+
* @param fallback - 兜底数据
|
|
6647
|
+
* @returns 归一化后的 tab 数据
|
|
6648
|
+
*/
|
|
6649
|
+
const resolveUserProfileTabData = (payload, fallback) => {
|
|
6650
|
+
if (isErrorDetailLike(payload)) return fallback;
|
|
6651
|
+
const nextData = payload?.data;
|
|
6652
|
+
if (!isRecord(nextData) || nextData.result !== 1 || !Array.isArray(nextData.list)) return fallback;
|
|
6653
|
+
const resolvedData = {
|
|
6654
|
+
...fallback,
|
|
6655
|
+
...nextData,
|
|
6656
|
+
list: nextData.list,
|
|
6657
|
+
pcursor: nextData.pcursor ?? fallback.pcursor ?? ""
|
|
6658
|
+
};
|
|
6659
|
+
if ("live" in fallback || "live" in nextData) resolvedData.live = nextData.live ?? fallback.live ?? null;
|
|
6660
|
+
return resolvedData;
|
|
6661
|
+
};
|
|
6662
|
+
/**
|
|
6663
|
+
* 解析快手用户作品分页结果。
|
|
6664
|
+
*
|
|
6665
|
+
* @param principalId - 用户主页 principalId
|
|
6666
|
+
* @param payload - `profile/public` 响应
|
|
6667
|
+
* @returns 归一化后的作品列表结果
|
|
6668
|
+
*/
|
|
6669
|
+
const resolveKuaishouUserWorkList = (principalId, payload) => {
|
|
6670
|
+
const fallback = createEmptyUserWorkListResult(principalId);
|
|
6671
|
+
if (isErrorDetailLike(payload)) return fallback;
|
|
6672
|
+
const nextData = payload?.data;
|
|
6673
|
+
if (!isRecord(nextData)) return fallback;
|
|
6674
|
+
return {
|
|
6675
|
+
principalId,
|
|
6676
|
+
list: Array.isArray(nextData.list) ? nextData.list : fallback.list,
|
|
6677
|
+
pcursor: typeof nextData.pcursor === "string" ? nextData.pcursor : fallback.pcursor,
|
|
6678
|
+
hasMore: Boolean(nextData.hasMore ?? nextData.pcursor),
|
|
6679
|
+
result: Number(nextData.result ?? fallback.result)
|
|
6680
|
+
};
|
|
6681
|
+
};
|
|
6682
|
+
/**
|
|
6683
|
+
* 解析快手直播间详情主接口数据。
|
|
6684
|
+
*
|
|
6685
|
+
* `live_api/liveroom/livedetail` 是当前房间主体的纯协议主接口;
|
|
6686
|
+
* 当接口返回 `result=1` 时,`data` 中会直接包含 `liveStream`、
|
|
6687
|
+
* `author`、`gameInfo`、`noticeList`、`websocketInfo` 与 `config`。
|
|
6688
|
+
*
|
|
6689
|
+
* @param payload - `liveroom/livedetail` 响应
|
|
6690
|
+
* @returns 成功时返回标准化后的 `data`,否则返回 `null`
|
|
6691
|
+
*/
|
|
6692
|
+
const resolveKuaishouLiveDetailData = (payload) => {
|
|
6693
|
+
if (isErrorDetailLike(payload)) return null;
|
|
6694
|
+
const nextData = payload?.data;
|
|
6695
|
+
if (!isRecord(nextData) || Number(nextData.result ?? 0) !== 1) return null;
|
|
6696
|
+
return nextData;
|
|
6697
|
+
};
|
|
6698
|
+
/**
|
|
6699
|
+
* 从 `livedetail` 中提取 WebSocket 元信息。
|
|
6700
|
+
*
|
|
6701
|
+
* `livedetail` 有时已经自带 `websocketInfo`,这时可以直接复用,
|
|
6702
|
+
* 仅在字段缺失时再补请求 `websocketinfo`。
|
|
6703
|
+
*
|
|
6704
|
+
* @param detailData - `liveroom/livedetail.data`
|
|
6705
|
+
* @returns 归一化后的 WebSocket 元信息
|
|
6706
|
+
*/
|
|
6707
|
+
const resolveKuaishouLiveDetailWebsocketMeta = (detailData) => {
|
|
6708
|
+
const websocketInfo = isRecord(detailData?.websocketInfo) ? detailData.websocketInfo : {};
|
|
6709
|
+
return {
|
|
6710
|
+
websocketUrls: Array.isArray(websocketInfo?.websocketUrls) ? websocketInfo.websocketUrls : [],
|
|
6711
|
+
token: typeof websocketInfo?.token === "string" ? websocketInfo.token : ""
|
|
6712
|
+
};
|
|
6713
|
+
};
|
|
6714
|
+
/**
|
|
6715
|
+
* 从 `livedetail` 中提取推荐房间列表。
|
|
6716
|
+
*
|
|
6717
|
+
* @param detailData - `liveroom/livedetail.data`
|
|
6718
|
+
* @returns 推荐房间列表
|
|
6719
|
+
*/
|
|
6720
|
+
const resolveKuaishouLiveDetailRecommendList = (detailData) => {
|
|
6721
|
+
return Array.isArray(detailData?.recommendList) ? detailData.recommendList : [];
|
|
6722
|
+
};
|
|
6723
|
+
/**
|
|
6724
|
+
* 归一化快手直播作者对象。
|
|
6725
|
+
*
|
|
6726
|
+
* @param author - 原始作者对象
|
|
6727
|
+
* @returns 与项目返回结构一致的作者对象
|
|
6728
|
+
*/
|
|
6729
|
+
const normalizeKuaishouLiveAuthor = (author) => {
|
|
6730
|
+
const verifiedDetail = author?.verifiedDetail;
|
|
6731
|
+
return {
|
|
6732
|
+
id: author?.id ?? author?.principalId ?? author?.kwaiId ?? "",
|
|
6733
|
+
name: author?.name ?? author?.user_name ?? "",
|
|
6734
|
+
description: author?.description ?? author?.user_text ?? "",
|
|
6735
|
+
avatar: author?.avatar ?? author?.headurl ?? "",
|
|
6736
|
+
sex: author?.sex ?? author?.user_sex ?? "",
|
|
6737
|
+
living: Boolean(author?.living ?? author?.live),
|
|
6738
|
+
followStatus: author?.followStatus ?? (author?.following ? "FOLLOWING" : "UN_FOLLOWED"),
|
|
6739
|
+
constellation: author?.constellation ?? "",
|
|
6740
|
+
cityName: author?.cityName ?? "",
|
|
6741
|
+
originUserId: Number(author?.originUserId ?? author?.user_id ?? 0),
|
|
6742
|
+
privacy: Boolean(author?.privacy),
|
|
6743
|
+
isNew: Boolean(author?.isNew),
|
|
6744
|
+
timestamp: Number(author?.timestamp ?? 0),
|
|
6745
|
+
verifiedStatus: author?.verifiedStatus ?? {
|
|
6746
|
+
verified: Boolean(author?.verified),
|
|
6747
|
+
description: verifiedDetail?.description ?? "",
|
|
6748
|
+
type: Number(verifiedDetail?.type ?? 0),
|
|
6749
|
+
new: Boolean(verifiedDetail?.newVerified),
|
|
6750
|
+
iconUrl: verifiedDetail?.iconUrl ?? ""
|
|
6751
|
+
},
|
|
6752
|
+
bannedStatus: author?.bannedStatus ?? {
|
|
6753
|
+
banned: false,
|
|
6754
|
+
socialBanned: false,
|
|
6755
|
+
isolate: false,
|
|
6756
|
+
defriend: false
|
|
6757
|
+
},
|
|
6758
|
+
counts: hasPopulatedRecord(author?.counts) ? author.counts : {}
|
|
6759
|
+
};
|
|
6760
|
+
};
|
|
6761
|
+
/**
|
|
6762
|
+
* 用用户资料接口补强直播作者对象。
|
|
6763
|
+
*
|
|
6764
|
+
* @param fallbackAuthor - 直播接口自带的作者信息
|
|
6765
|
+
* @param userInfo - `userinfo/byid` 返回的公开资料
|
|
6766
|
+
* @param sensitiveInfo - `userinfo/sensitive` 返回的补充资料
|
|
6767
|
+
* @returns 补强后的作者对象
|
|
6768
|
+
*/
|
|
6769
|
+
const mergeKuaishouLiveAuthor = (fallbackAuthor, userInfo, sensitiveInfo) => {
|
|
6770
|
+
const normalizedFallback = normalizeKuaishouLiveAuthor(fallbackAuthor);
|
|
6771
|
+
return {
|
|
6772
|
+
...normalizedFallback,
|
|
6773
|
+
...hasPopulatedRecord(userInfo) ? userInfo : {},
|
|
6774
|
+
followStatus: userInfo?.followStatus ?? sensitiveInfo?.followStatus ?? normalizedFallback.followStatus,
|
|
6775
|
+
constellation: pickFirstNonEmptyString(userInfo?.constellation, sensitiveInfo?.constellation, normalizedFallback.constellation),
|
|
6776
|
+
cityName: pickFirstNonEmptyString(userInfo?.cityName, sensitiveInfo?.cityName, normalizedFallback.cityName),
|
|
6777
|
+
verifiedStatus: userInfo?.verifiedStatus ?? sensitiveInfo?.verifiedStatus ?? normalizedFallback.verifiedStatus,
|
|
6778
|
+
bannedStatus: userInfo?.bannedStatus ?? sensitiveInfo?.bannedStatus ?? normalizedFallback.bannedStatus,
|
|
6779
|
+
counts: hasPopulatedRecord(userInfo?.counts) ? userInfo.counts : sensitiveInfo?.counts ?? normalizedFallback.counts
|
|
6780
|
+
};
|
|
6781
|
+
};
|
|
6782
|
+
/**
|
|
6783
|
+
* 将 `liveroom/livedetail` 映射为用户主页 `publicData.live`。
|
|
6784
|
+
*
|
|
6785
|
+
* @param detailData - `liveroom/livedetail.data`
|
|
6786
|
+
* @param author - 归一化后的作者对象
|
|
6787
|
+
* @returns 用户主页 live 对象
|
|
6788
|
+
*/
|
|
6789
|
+
const mapLiveDetailToUserProfileLiveInfo = (detailData, author) => {
|
|
6790
|
+
const liveStream = isRecord(detailData?.liveStream) ? detailData.liveStream : {};
|
|
6791
|
+
const config = isRecord(detailData?.config) ? detailData.config : {};
|
|
6792
|
+
const gameInfo = isRecord(detailData?.gameInfo) ? detailData.gameInfo : {};
|
|
6793
|
+
const liveStreamId = pickFirstNonEmptyString(liveStream?.id, config?.liveStreamId);
|
|
6794
|
+
return {
|
|
6795
|
+
...detailData,
|
|
6796
|
+
id: liveStreamId,
|
|
6797
|
+
poster: pickFirstNonEmptyString(liveStream?.poster, config?.coverUrl, config?.rtCoverUrl),
|
|
6798
|
+
playUrls: liveStream?.playUrls ?? config?.multiResolutionPlayUrls ?? [],
|
|
6799
|
+
caption: config?.caption ?? detailData?.caption ?? "",
|
|
6800
|
+
statrtTime: Number(detailData?.startTime ?? config?.startTime ?? liveStream?.startTime ?? 0),
|
|
6801
|
+
author,
|
|
6802
|
+
gameInfo,
|
|
6803
|
+
hasRedPack: Boolean(detailData?.hasRedPack ?? config?.hasRedPack),
|
|
6804
|
+
hasBet: Boolean(detailData?.hasBet ?? config?.hasBet),
|
|
6805
|
+
followed: author?.followStatus === "FOLLOWING",
|
|
6806
|
+
expTag: liveStream?.expTag ?? config?.expTag ?? "",
|
|
6807
|
+
hotIcon: config?.hotIcon ?? detailData?.hotIcon ?? "",
|
|
6808
|
+
living: Boolean(detailData?.isLiving ?? liveStreamId),
|
|
6809
|
+
quality: config?.quality ?? "",
|
|
6810
|
+
qualityLabel: config?.qualityLabel ?? "",
|
|
6811
|
+
watchingCount: config?.watchingCount ?? detailData?.watchingCount ?? gameInfo?.watchingCount ?? "",
|
|
6812
|
+
landscape: Boolean(config?.landscape),
|
|
6813
|
+
likeCount: config?.likeCount ?? detailData?.likeCount ?? "",
|
|
6814
|
+
type: liveStream?.type ?? "live"
|
|
6815
|
+
};
|
|
6816
|
+
};
|
|
6817
|
+
/**
|
|
6818
|
+
* 将 `liveroom/livedetail` 映射为 `liveRoomInfo.current`。
|
|
6819
|
+
*
|
|
6820
|
+
* @param detailData - `liveroom/livedetail.data`
|
|
6821
|
+
* @param author - 补强后的主播对象
|
|
6822
|
+
* @returns 当前直播间对象
|
|
6823
|
+
*/
|
|
6824
|
+
const mapLiveDetailToLiveRoomPlayItem = (detailData, author) => {
|
|
6825
|
+
const liveStream = isRecord(detailData?.liveStream) ? detailData.liveStream : {};
|
|
6826
|
+
const config = isRecord(detailData?.config) ? detailData.config : {};
|
|
6827
|
+
const gameInfo = isRecord(detailData?.gameInfo) ? detailData.gameInfo : {};
|
|
6828
|
+
const liveStreamId = pickFirstNonEmptyString(liveStream?.id, config?.liveStreamId);
|
|
6829
|
+
const coverUrl = pickFirstNonEmptyString(config?.coverUrl, config?.rtCoverUrl, liveStream?.poster);
|
|
6830
|
+
return {
|
|
6831
|
+
liveStream: {
|
|
6832
|
+
id: liveStreamId,
|
|
6833
|
+
poster: pickFirstNonEmptyString(liveStream?.poster, coverUrl),
|
|
6834
|
+
playUrls: liveStream?.playUrls ?? config?.multiResolutionPlayUrls ?? {},
|
|
6835
|
+
url: liveStream?.url ?? "",
|
|
6836
|
+
hlsPlayUrl: pickFirstNonEmptyString(config?.hlsPlayUrl, liveStream?.hlsPlayUrl),
|
|
6837
|
+
location: liveStream?.location ?? null,
|
|
6838
|
+
type: liveStream?.type ?? "live",
|
|
6839
|
+
liveGuess: Boolean(liveStream?.liveGuess),
|
|
6840
|
+
expTag: liveStream?.expTag ?? config?.expTag ?? "",
|
|
6841
|
+
privateLive: Boolean(liveStream?.privateLive ?? config?.privateLive)
|
|
6842
|
+
},
|
|
6843
|
+
author,
|
|
6844
|
+
gameInfo: {
|
|
6845
|
+
id: String(gameInfo?.id ?? gameInfo?.gameId ?? ""),
|
|
6846
|
+
name: gameInfo?.name ?? "",
|
|
6847
|
+
poster: pickFirstNonEmptyString(gameInfo?.poster, gameInfo?.coverUrl),
|
|
6848
|
+
description: gameInfo?.description ?? "",
|
|
6849
|
+
categoryAbbr: gameInfo?.categoryAbbr ?? gameInfo?.category ?? "",
|
|
6850
|
+
categoryName: gameInfo?.categoryName ?? "",
|
|
6851
|
+
watchingCount: config?.watchingCount ?? detailData?.watchingCount ?? gameInfo?.watchingCount ?? "",
|
|
6852
|
+
roomCount: gameInfo?.roomCount ?? ""
|
|
6853
|
+
},
|
|
6854
|
+
isLiving: Boolean(detailData?.isLiving ?? liveStreamId),
|
|
6855
|
+
authToken: typeof detailData?.authToken === "string" ? detailData.authToken : null,
|
|
6856
|
+
config: {
|
|
6857
|
+
...config,
|
|
6858
|
+
liveStreamId,
|
|
6859
|
+
hlsPlayUrl: pickFirstNonEmptyString(config?.hlsPlayUrl, liveStream?.hlsPlayUrl),
|
|
6860
|
+
coverUrl,
|
|
6861
|
+
rtCoverUrl: pickFirstNonEmptyString(config?.rtCoverUrl, liveStream?.poster),
|
|
6862
|
+
gameInfo,
|
|
6863
|
+
multiResolutionPlayUrls: config?.multiResolutionPlayUrls ?? liveStream?.playUrls ?? []
|
|
6864
|
+
},
|
|
6865
|
+
websocketInfo: isRecord(detailData?.websocketInfo) ? detailData.websocketInfo : {},
|
|
6866
|
+
status: detailData?.status ?? { forbiddenState: Number(detailData?.result ?? 0) }
|
|
6867
|
+
};
|
|
6868
|
+
};
|
|
6869
|
+
/**
|
|
6870
|
+
* 将 `liveroom/reco` 返回项映射为 `playList` 项。
|
|
6871
|
+
*
|
|
6872
|
+
* @param recoItem - 推荐房间对象
|
|
6873
|
+
* @returns 归一化后的推荐房间对象
|
|
6874
|
+
*/
|
|
6875
|
+
const mapRecoItemToLiveRoomPlayItem = (recoItem) => ({
|
|
6876
|
+
liveStream: {
|
|
6877
|
+
id: recoItem?.liveStream?.id ?? "",
|
|
6878
|
+
poster: recoItem?.liveStream?.poster ?? recoItem?.config?.coverUrl ?? "",
|
|
6879
|
+
playUrls: recoItem?.liveStream?.playUrls ?? {},
|
|
6880
|
+
url: recoItem?.liveStream?.url ?? "",
|
|
6881
|
+
hlsPlayUrl: recoItem?.liveStream?.hlsPlayUrl ?? recoItem?.config?.hlsPlayUrl ?? "",
|
|
6882
|
+
location: recoItem?.liveStream?.location ?? null,
|
|
6883
|
+
type: recoItem?.liveStream?.type ?? "live",
|
|
6884
|
+
liveGuess: Boolean(recoItem?.liveStream?.liveGuess),
|
|
6885
|
+
expTag: recoItem?.liveStream?.expTag ?? "",
|
|
6886
|
+
privateLive: Boolean(recoItem?.liveStream?.privateLive ?? recoItem?.config?.privateLive)
|
|
6887
|
+
},
|
|
6888
|
+
author: normalizeKuaishouLiveAuthor(recoItem?.author),
|
|
6889
|
+
gameInfo: {
|
|
6890
|
+
id: recoItem?.gameInfo?.id ?? "",
|
|
6891
|
+
name: recoItem?.gameInfo?.name ?? "",
|
|
6892
|
+
poster: recoItem?.gameInfo?.poster ?? "",
|
|
6893
|
+
description: recoItem?.gameInfo?.description ?? "",
|
|
6894
|
+
categoryAbbr: recoItem?.gameInfo?.categoryAbbr ?? "",
|
|
6895
|
+
categoryName: recoItem?.gameInfo?.categoryName ?? "",
|
|
6896
|
+
watchingCount: recoItem?.gameInfo?.watchingCount ?? "",
|
|
6897
|
+
roomCount: recoItem?.gameInfo?.roomCount ?? ""
|
|
6898
|
+
},
|
|
6899
|
+
isLiving: Boolean(recoItem?.isLiving),
|
|
6900
|
+
authToken: recoItem?.authToken ?? null,
|
|
6901
|
+
config: {
|
|
6902
|
+
...recoItem?.config ?? {},
|
|
6903
|
+
liveStreamId: recoItem?.config?.liveStreamId ?? recoItem?.liveStream?.id ?? ""
|
|
6904
|
+
},
|
|
6905
|
+
websocketInfo: recoItem?.websocketInfo ?? {},
|
|
6906
|
+
status: recoItem?.status ?? {}
|
|
6907
|
+
});
|
|
6908
|
+
/**
|
|
6909
|
+
* 对直播房间列表按直播流 ID 去重。
|
|
6910
|
+
*
|
|
6911
|
+
* @param items - 待去重房间列表
|
|
6912
|
+
* @returns 去重后的房间列表
|
|
6913
|
+
*/
|
|
6914
|
+
const dedupeLiveRoomPlayList = (items) => {
|
|
6915
|
+
const seenLiveStreamIds = /* @__PURE__ */ new Set();
|
|
6916
|
+
const normalizedItems = [];
|
|
6917
|
+
for (const item of items) {
|
|
6918
|
+
if (!item) continue;
|
|
6919
|
+
const liveStreamId = item.liveStream?.id ?? item.config?.liveStreamId ?? "";
|
|
6920
|
+
if (liveStreamId && seenLiveStreamIds.has(liveStreamId)) continue;
|
|
6921
|
+
if (liveStreamId) seenLiveStreamIds.add(liveStreamId);
|
|
6922
|
+
normalizedItems.push(item);
|
|
6923
|
+
}
|
|
6924
|
+
return normalizedItems;
|
|
6925
|
+
};
|
|
6926
|
+
/**
|
|
6927
|
+
* 快手数据获取函数
|
|
6928
|
+
* @param data - 请求数据参数
|
|
6929
|
+
* @param cookie - 用户Cookie
|
|
6930
|
+
* @param requestConfig - 外部请求配置(优先级最高)
|
|
6931
|
+
* @returns 返回快手数据;当前 user/live 能力已优先走纯协议聚合链路
|
|
6932
|
+
*/
|
|
6933
|
+
const KuaishouData = async (data$1, cookie, requestConfig) => {
|
|
6934
|
+
const defHeaders = getKuaishouDefaultConfig(cookie)["headers"];
|
|
6935
|
+
const baseRequestConfig = {
|
|
6936
|
+
method: "POST",
|
|
6937
|
+
timeout: 1e4,
|
|
6938
|
+
...requestConfig,
|
|
6939
|
+
headers: {
|
|
6940
|
+
...defHeaders,
|
|
6941
|
+
...requestConfig?.headers ?? {}
|
|
6942
|
+
}
|
|
6943
|
+
};
|
|
6944
|
+
/**
|
|
6945
|
+
* 执行一次快手 GraphQL 请求。
|
|
6946
|
+
*
|
|
6947
|
+
* @param type - 当前请求所属的方法类型
|
|
6948
|
+
* @param request - GraphQL 请求描述对象
|
|
6949
|
+
* @param config - 附加容错配置
|
|
6950
|
+
* @returns 原始响应或标准化错误对象
|
|
6951
|
+
*/
|
|
6952
|
+
const fetchKuaishouGraphqlPayload = (type, request, config) => {
|
|
6953
|
+
return GlobalGetData$2(type, {
|
|
6954
|
+
...baseRequestConfig,
|
|
6955
|
+
url: request.url,
|
|
6956
|
+
data: request.body
|
|
6957
|
+
}, config);
|
|
6958
|
+
};
|
|
6959
|
+
/**
|
|
6960
|
+
* 执行一次快手 `live_api` 请求。
|
|
6961
|
+
*
|
|
6962
|
+
* @param type - 当前请求所属的方法类型
|
|
6963
|
+
* @param request - `live_api` 请求描述对象
|
|
6964
|
+
* @param refererPath - 请求对应的页面 Referer 路径
|
|
6965
|
+
* @param config - 附加容错配置
|
|
6966
|
+
* @returns 原始响应或标准化错误对象
|
|
6967
|
+
*/
|
|
6968
|
+
const fetchKuaishouLiveApiPayload = (type, request, refererPath, config) => {
|
|
6969
|
+
return GlobalGetData$2(type, buildLiveApiRequestConfig(request, refererPath, cookie, requestConfig), config);
|
|
6970
|
+
};
|
|
6971
|
+
switch (data$1.methodType) {
|
|
6972
|
+
case "videoWork": return fetchKuaishouGraphqlPayload(data$1.methodType, kuaishouApiUrls.videoWork({ photoId: data$1.photoId }));
|
|
6973
|
+
case "comments": return fetchKuaishouGraphqlPayload(data$1.methodType, kuaishouApiUrls.comments({ photoId: data$1.photoId }));
|
|
6974
|
+
case "userProfile": {
|
|
6975
|
+
const refererPath = `profile/${encodeURIComponent(data$1.principalId)}`;
|
|
6976
|
+
const [userInfoPayload, sensitivePayload, publicPayload, privatePayload, likedPayload, playbackPayload, interestListPayload, interestMaskPayload, categoryConfigPayload, categoryDataPayload, categoryClassifyPayload, liveDetailPayload] = await Promise.all([
|
|
6977
|
+
fetchKuaishouLiveApiPayload(data$1.methodType, kuaishouApiUrls.userInfoById({ principalId: data$1.principalId }), refererPath),
|
|
6978
|
+
fetchKuaishouLiveApiPayload(data$1.methodType, kuaishouApiUrls.userSensitiveInfo({ principalId: data$1.principalId }), refererPath, { allowResult2: true }),
|
|
6979
|
+
fetchKuaishouLiveApiPayload(data$1.methodType, kuaishouApiUrls.profilePublic({ principalId: data$1.principalId }), refererPath, { allowResult2: true }),
|
|
6980
|
+
fetchKuaishouLiveApiPayload(data$1.methodType, kuaishouApiUrls.profilePrivate({ principalId: data$1.principalId }), refererPath, { allowResult2: true }),
|
|
6981
|
+
fetchKuaishouLiveApiPayload(data$1.methodType, kuaishouApiUrls.profileLiked({ principalId: data$1.principalId }), refererPath, { allowResult2: true }),
|
|
6982
|
+
fetchKuaishouLiveApiPayload(data$1.methodType, kuaishouApiUrls.playbackList({ principalId: data$1.principalId }), refererPath, { allowResult2: true }),
|
|
6983
|
+
fetchKuaishouLiveApiPayload(data$1.methodType, kuaishouApiUrls.profileInterestList({ principalId: data$1.principalId }), refererPath, { allowResult2: true }),
|
|
6984
|
+
fetchKuaishouLiveApiPayload(data$1.methodType, kuaishouApiUrls.interestMaskList(), refererPath, { allowResult2: true }),
|
|
6985
|
+
fetchKuaishouLiveApiPayload(data$1.methodType, kuaishouApiUrls.categoryConfig(), refererPath),
|
|
6986
|
+
fetchKuaishouLiveApiPayload(data$1.methodType, kuaishouApiUrls.categoryData(), refererPath),
|
|
6987
|
+
fetchKuaishouLiveApiPayload(data$1.methodType, kuaishouApiUrls.categoryClassify(), refererPath),
|
|
6988
|
+
fetchKuaishouLiveApiPayload(data$1.methodType, kuaishouApiUrls.liveDetail({ principalId: data$1.principalId }), refererPath, { allowResult2: true })
|
|
6989
|
+
]);
|
|
6990
|
+
if (isErrorDetailLike(userInfoPayload)) return userInfoPayload;
|
|
6991
|
+
const userProfile = createEmptyUserProfileResult(data$1.principalId);
|
|
6992
|
+
const userInfo = userInfoPayload?.data?.userInfo;
|
|
6993
|
+
const sensitiveInfo = isErrorDetailLike(sensitivePayload) ? null : sensitivePayload?.data?.sensitiveUserInfo ?? null;
|
|
6994
|
+
const liveDetailData = resolveKuaishouLiveDetailData(liveDetailPayload);
|
|
6995
|
+
const normalizedAuthor = mergeKuaishouLiveAuthor(liveDetailData?.author, userInfo, sensitiveInfo ?? void 0);
|
|
6996
|
+
const nextPublicData = resolveUserProfileTabData(publicPayload, userProfile.profile.publicData);
|
|
6997
|
+
const nextPrivateData = resolveUserProfileTabData(privatePayload, userProfile.profile.privateData);
|
|
6998
|
+
const nextLikedData = resolveUserProfileTabData(likedPayload, userProfile.profile.likedData);
|
|
6999
|
+
const nextPlaybackData = resolveUserProfileTabData(playbackPayload, userProfile.profile.playbackData);
|
|
7000
|
+
if (!nextPublicData.live && liveDetailData) nextPublicData.live = mapLiveDetailToUserProfileLiveInfo(liveDetailData, normalizedAuthor);
|
|
7001
|
+
const nextInterestList = !isErrorDetailLike(interestListPayload) && Array.isArray(interestListPayload?.data) ? interestListPayload.data : userProfile.profile.interestList;
|
|
7002
|
+
const nextInterestMask = !isErrorDetailLike(interestMaskPayload) && Array.isArray(interestMaskPayload?.data) ? interestMaskPayload.data : userProfile.interestMask;
|
|
7003
|
+
return {
|
|
7004
|
+
...userProfile,
|
|
7005
|
+
principalId: data$1.principalId,
|
|
7006
|
+
author: {
|
|
7007
|
+
...userProfile.author,
|
|
7008
|
+
principalId: data$1.principalId,
|
|
7009
|
+
userInfo: normalizedAuthor,
|
|
7010
|
+
sensitiveInfo,
|
|
7011
|
+
followInfo: {},
|
|
7012
|
+
banStateMap: KUAISHOU_BAN_STATE_MAP
|
|
7013
|
+
},
|
|
7014
|
+
profile: {
|
|
7015
|
+
...userProfile.profile,
|
|
7016
|
+
pageSize: 12,
|
|
7017
|
+
tabTypeMap: KUAISHOU_PROFILE_TAB_TYPE_MAP,
|
|
7018
|
+
showPlayback: Boolean(nextPublicData.showPlayback ?? nextPlaybackData.list.length > 0),
|
|
7019
|
+
publicData: nextPublicData,
|
|
7020
|
+
privateData: nextPrivateData,
|
|
7021
|
+
likedData: nextLikedData,
|
|
7022
|
+
playbackData: nextPlaybackData,
|
|
7023
|
+
interestList: nextInterestList,
|
|
7024
|
+
currentProduct: {}
|
|
7025
|
+
},
|
|
7026
|
+
follow: createDerivedFollowState(userInfo, sensitiveInfo ?? void 0),
|
|
7027
|
+
followButton: createDerivedFollowButtonState(userInfo, sensitiveInfo ?? void 0),
|
|
7028
|
+
interestMask: nextInterestMask,
|
|
7029
|
+
categoryMask: {
|
|
7030
|
+
config: !isErrorDetailLike(categoryConfigPayload) && Array.isArray(categoryConfigPayload?.data) ? categoryConfigPayload.data : userProfile.categoryMask.config,
|
|
7031
|
+
list: !isErrorDetailLike(categoryClassifyPayload) && Array.isArray(categoryClassifyPayload?.data?.list) ? categoryClassifyPayload.data.list : userProfile.categoryMask.list,
|
|
7032
|
+
hotList: !isErrorDetailLike(categoryDataPayload) && Array.isArray(categoryDataPayload?.data?.list) ? categoryDataPayload.data.list : userProfile.categoryMask.hotList,
|
|
7033
|
+
hasMore: Boolean(categoryClassifyPayload?.data?.hasMore),
|
|
7034
|
+
hasMoreHot: Boolean(categoryDataPayload?.data?.hasMore)
|
|
7035
|
+
}
|
|
7036
|
+
};
|
|
7037
|
+
}
|
|
7038
|
+
case "userWorkList": {
|
|
7039
|
+
const refererPath = `profile/${encodeURIComponent(data$1.principalId)}`;
|
|
7040
|
+
const publicPayload = await fetchKuaishouLiveApiPayload(data$1.methodType, kuaishouApiUrls.userWorkList({
|
|
7041
|
+
principalId: data$1.principalId,
|
|
7042
|
+
pcursor: data$1.pcursor,
|
|
7043
|
+
count: data$1.count
|
|
7044
|
+
}), refererPath, { allowResult2: true });
|
|
7045
|
+
if (isErrorDetailLike(publicPayload)) return publicPayload;
|
|
7046
|
+
return resolveKuaishouUserWorkList(data$1.principalId, publicPayload);
|
|
7047
|
+
}
|
|
7048
|
+
case "liveRoomInfo": {
|
|
7049
|
+
const refererPath = `u/${encodeURIComponent(data$1.principalId)}`;
|
|
7050
|
+
const [liveDetailPayload, userInfoPayload, sensitivePayload, emojiPayload] = await Promise.all([
|
|
7051
|
+
fetchKuaishouLiveApiPayload(data$1.methodType, kuaishouApiUrls.liveDetail({ principalId: data$1.principalId }), refererPath, { allowResult2: true }),
|
|
7052
|
+
fetchKuaishouLiveApiPayload(data$1.methodType, kuaishouApiUrls.userInfoById({ principalId: data$1.principalId }), refererPath, { allowResult2: true }),
|
|
7053
|
+
fetchKuaishouLiveApiPayload(data$1.methodType, kuaishouApiUrls.userSensitiveInfo({ principalId: data$1.principalId }), refererPath, { allowResult2: true }),
|
|
7054
|
+
fetchKuaishouGraphqlPayload(data$1.methodType, kuaishouApiUrls.emojiList(), { allowResult2: true })
|
|
7055
|
+
]);
|
|
7056
|
+
if (isErrorDetailLike(liveDetailPayload)) return liveDetailPayload;
|
|
7057
|
+
const liveRoomInfo = createEmptyLiveRoomInfoResult(data$1.principalId);
|
|
7058
|
+
const liveDetailData = resolveKuaishouLiveDetailData(liveDetailPayload);
|
|
7059
|
+
if (!liveDetailData) return liveRoomInfo;
|
|
7060
|
+
const userInfo = isErrorDetailLike(userInfoPayload) ? void 0 : userInfoPayload?.data?.userInfo;
|
|
7061
|
+
const sensitiveInfo = isErrorDetailLike(sensitivePayload) ? void 0 : sensitivePayload?.data?.sensitiveUserInfo;
|
|
7062
|
+
const currentLiveRoomItem = mapLiveDetailToLiveRoomPlayItem(liveDetailData, mergeKuaishouLiveAuthor(liveDetailData?.author, userInfo, sensitiveInfo));
|
|
7063
|
+
const liveStreamId = currentLiveRoomItem.liveStream?.id ?? currentLiveRoomItem.config?.liveStreamId;
|
|
7064
|
+
const currentGameId = liveDetailData?.gameInfo?.id ?? liveDetailData?.gameInfo?.gameId;
|
|
7065
|
+
const liveDetailWebsocketMeta = resolveKuaishouLiveDetailWebsocketMeta(liveDetailData);
|
|
7066
|
+
const liveDetailRecommendList = resolveKuaishouLiveDetailRecommendList(liveDetailData);
|
|
7067
|
+
if (!liveStreamId) return {
|
|
7068
|
+
...liveRoomInfo,
|
|
7069
|
+
current: currentLiveRoomItem,
|
|
7070
|
+
playList: [currentLiveRoomItem]
|
|
7071
|
+
};
|
|
7072
|
+
const shouldFetchWebsocketInfo = liveDetailWebsocketMeta.websocketUrls.length === 0 || !liveDetailWebsocketMeta.token;
|
|
7073
|
+
const shouldFetchRecommendList = liveDetailRecommendList.length === 0;
|
|
7074
|
+
const [giftPayload, websocketPayload, recoPayload] = await Promise.all([
|
|
7075
|
+
fetchKuaishouLiveApiPayload(data$1.methodType, kuaishouApiUrls.liveGiftList(liveStreamId), refererPath, { allowResult2: true }),
|
|
7076
|
+
shouldFetchWebsocketInfo ? fetchKuaishouLiveApiPayload(data$1.methodType, kuaishouApiUrls.liveWebsocketInfo(liveStreamId), refererPath, { allowResult2: true }) : Promise.resolve(null),
|
|
7077
|
+
shouldFetchRecommendList ? fetchKuaishouLiveApiPayload(data$1.methodType, kuaishouApiUrls.liveReco(currentGameId), refererPath, { allowResult2: true }) : Promise.resolve(null)
|
|
7078
|
+
]);
|
|
7079
|
+
const resolvedRecommendList = !isErrorDetailLike(recoPayload) && Array.isArray(recoPayload?.data?.list) ? recoPayload.data.list : liveDetailRecommendList;
|
|
7080
|
+
const nextPlayList = dedupeLiveRoomPlayList([currentLiveRoomItem, ...Array.isArray(resolvedRecommendList) ? resolvedRecommendList.map((item) => mapRecoItemToLiveRoomPlayItem(item)) : []]);
|
|
7081
|
+
return {
|
|
7082
|
+
...liveRoomInfo,
|
|
7083
|
+
principalId: data$1.principalId,
|
|
7084
|
+
activeIndex: 0,
|
|
7085
|
+
current: currentLiveRoomItem,
|
|
7086
|
+
playList: nextPlayList,
|
|
7087
|
+
websocketUrls: !isErrorDetailLike(websocketPayload) && Array.isArray(websocketPayload?.data?.websocketUrls) ? websocketPayload.data.websocketUrls : liveDetailWebsocketMeta.websocketUrls,
|
|
7088
|
+
token: !isErrorDetailLike(websocketPayload) ? websocketPayload?.data?.token ?? liveDetailWebsocketMeta.token : liveDetailWebsocketMeta.token,
|
|
7089
|
+
noticeList: Array.isArray(liveDetailData?.noticeList) ? liveDetailData.noticeList : liveRoomInfo.noticeList,
|
|
7090
|
+
loading: false,
|
|
7091
|
+
emoji: {
|
|
7092
|
+
iconUrls: !isErrorDetailLike(emojiPayload) && isRecord(emojiPayload?.data?.visionBaseEmoticons?.iconUrls) ? emojiPayload.data.visionBaseEmoticons.iconUrls : liveRoomInfo.emoji.iconUrls,
|
|
7093
|
+
giftList: !isErrorDetailLike(giftPayload) && Array.isArray(giftPayload?.data?.gifts) ? giftPayload.data.gifts : liveRoomInfo.emoji.giftList,
|
|
7094
|
+
giftPanelList: !isErrorDetailLike(giftPayload) && Array.isArray(giftPayload?.data?.giftPanelList) ? giftPayload.data.giftPanelList : liveRoomInfo.emoji.giftPanelList,
|
|
7095
|
+
token: !isErrorDetailLike(giftPayload) ? giftPayload?.data?.token ?? liveRoomInfo.emoji.token : liveRoomInfo.emoji.token,
|
|
7096
|
+
panelToken: !isErrorDetailLike(giftPayload) ? giftPayload?.data?.panelToken ?? liveRoomInfo.emoji.panelToken : liveRoomInfo.emoji.panelToken,
|
|
7097
|
+
longSendGiftType: !isErrorDetailLike(giftPayload) ? giftPayload?.data?.longSendGiftType ?? liveRoomInfo.emoji.longSendGiftType : liveRoomInfo.emoji.longSendGiftType
|
|
7098
|
+
}
|
|
7099
|
+
};
|
|
7100
|
+
}
|
|
7101
|
+
case "emojiList": return fetchKuaishouGraphqlPayload(data$1.methodType, kuaishouApiUrls.emojiList());
|
|
7102
|
+
default:
|
|
7103
|
+
logger.warn(`Unknown Kuaishou API method: "${logger.red(data$1.methodType)}"`);
|
|
7104
|
+
return null;
|
|
7105
|
+
}
|
|
7106
|
+
};
|
|
7107
|
+
/**
|
|
7108
|
+
* 快手底层数据请求函数。
|
|
7109
|
+
*
|
|
7110
|
+
* 负责执行单次网络请求,并将网络错误、空响应和 `result=2`
|
|
7111
|
+
* 这类平台错误统一归一化为 `ErrorDetail`。
|
|
7112
|
+
*
|
|
7113
|
+
* @param type - 当前请求所属的方法类型
|
|
7114
|
+
* @param options - 网络请求配置选项
|
|
7115
|
+
* @param config - 附加容错配置
|
|
7116
|
+
* @returns 请求成功时返回原始响应,否则返回 `ErrorDetail`
|
|
7117
|
+
*/
|
|
7118
|
+
const GlobalGetData$2 = async (type, options, config) => {
|
|
7119
|
+
let warningMessage = "";
|
|
7120
|
+
try {
|
|
7121
|
+
const result = await fetchData(options);
|
|
7122
|
+
if (isNetworkErrorResult(result)) {
|
|
7123
|
+
const networkError = new Error(result.error.amagiError.errorDescription);
|
|
7124
|
+
Object.assign(networkError, {
|
|
7125
|
+
code: result.error.code,
|
|
7126
|
+
data: null,
|
|
7127
|
+
amagiError: {
|
|
7128
|
+
...result.error.amagiError,
|
|
7129
|
+
requestType: type
|
|
7130
|
+
}
|
|
7131
|
+
});
|
|
7132
|
+
throw networkError;
|
|
7133
|
+
}
|
|
7134
|
+
if (result === "" || !result || !config?.allowResult2 && typeof result === "object" && result !== null && result.result === 2) {
|
|
7135
|
+
const Err = {
|
|
7136
|
+
errorDescription: "获取响应数据失败!接口返回内容为空!",
|
|
7137
|
+
requestType: type ?? "未知请求类型",
|
|
7138
|
+
requestUrl: options.url,
|
|
7139
|
+
requestBody: JSON.stringify(options.data)
|
|
7140
|
+
};
|
|
7141
|
+
warningMessage = `
|
|
7142
|
+
获取响应数据失败!原因:${logger.yellow("接口返回内容为空,你的快手ck可能已经失效!")}
|
|
7143
|
+
请求类型:「${type}」
|
|
7144
|
+
请求URL:${options.url}
|
|
7145
|
+
请求参数:${JSON.stringify(options.data, null, 2)}
|
|
7146
|
+
`;
|
|
7147
|
+
logger.warn(warningMessage);
|
|
7148
|
+
const cookieError = new Error(Err.errorDescription);
|
|
7149
|
+
Object.assign(cookieError, {
|
|
7150
|
+
code: kuaishouAPIErrorCode.COOKIE,
|
|
7151
|
+
data: result,
|
|
7152
|
+
amagiError: Err
|
|
7153
|
+
});
|
|
7154
|
+
throw cookieError;
|
|
7155
|
+
}
|
|
7156
|
+
return result;
|
|
7157
|
+
} catch (error) {
|
|
7158
|
+
if (error && typeof error === "object") return {
|
|
7159
|
+
...error,
|
|
7160
|
+
amagiMessage: warningMessage
|
|
7161
|
+
};
|
|
7162
|
+
return {
|
|
7163
|
+
code: amagiAPIErrorCode.UNKNOWN,
|
|
7164
|
+
data: null,
|
|
7165
|
+
amagiError: {
|
|
7166
|
+
errorDescription: "未知错误",
|
|
7167
|
+
requestType: type,
|
|
7168
|
+
requestUrl: options.url
|
|
7169
|
+
},
|
|
7170
|
+
amagiMessage: warningMessage
|
|
7171
|
+
};
|
|
7172
|
+
}
|
|
7173
|
+
};
|
|
7174
|
+
|
|
7175
|
+
//#endregion
|
|
7176
|
+
//#region src/model/fetchers/kuaishou/internal.ts
|
|
7177
|
+
/**
|
|
7178
|
+
* 快手 Fetcher 内部通用逻辑
|
|
7179
|
+
* @module fetchers/kuaishou/internal
|
|
7180
|
+
*/
|
|
7181
|
+
/**
|
|
7182
|
+
* 快手 API 内部调用函数
|
|
7183
|
+
* @internal
|
|
7184
|
+
* @param methodType - 方法类型
|
|
7185
|
+
* @param options - 请求参数
|
|
7186
|
+
* @param config - 配置项 (cookie, requestConfig)
|
|
7187
|
+
* @returns Promise<Result<T>>
|
|
7188
|
+
*/
|
|
7189
|
+
async function fetchKuaishouInternal(methodType, options, config) {
|
|
7190
|
+
const startTime = Date.now();
|
|
7191
|
+
try {
|
|
7192
|
+
const rawData = await KuaishouData({ ...validateKuaishouParams(methodType, options) }, config.cookie, config.requestConfig);
|
|
7193
|
+
const duration = Date.now() - startTime;
|
|
7194
|
+
if (rawData.code && Object.values(kuaishouAPIErrorCode).includes(rawData.code)) {
|
|
7195
|
+
emitApiError({
|
|
5183
7196
|
platform: "kuaishou",
|
|
5184
7197
|
methodType,
|
|
5185
7198
|
errorCode: rawData.code,
|
|
@@ -5253,6 +7266,50 @@ async function fetchWorkComments(options, cookie, requestConfig) {
|
|
|
5253
7266
|
});
|
|
5254
7267
|
}
|
|
5255
7268
|
/**
|
|
7269
|
+
* 获取快手用户主页数据
|
|
7270
|
+
* @param options - 用户主页参数
|
|
7271
|
+
* @param options.principalId - 用户 principalId
|
|
7272
|
+
* @param cookie - 快手 Cookie (可选)
|
|
7273
|
+
* @param requestConfig - 请求配置 (可选)
|
|
7274
|
+
* @returns 用户主页纯协议聚合结果
|
|
7275
|
+
*/
|
|
7276
|
+
async function fetchUserProfile$1(options, cookie, requestConfig) {
|
|
7277
|
+
return fetchKuaishouInternal("userProfile", options, {
|
|
7278
|
+
cookie,
|
|
7279
|
+
requestConfig
|
|
7280
|
+
});
|
|
7281
|
+
}
|
|
7282
|
+
/**
|
|
7283
|
+
* 获取快手用户作品列表数据
|
|
7284
|
+
* @param options - 用户作品列表参数
|
|
7285
|
+
* @param options.principalId - 用户 principalId
|
|
7286
|
+
* @param options.pcursor - 分页游标 (可选)
|
|
7287
|
+
* @param options.count - 每页数量,默认 12
|
|
7288
|
+
* @param cookie - 快手 Cookie (可选)
|
|
7289
|
+
* @param requestConfig - 请求配置 (可选)
|
|
7290
|
+
* @returns 用户作品列表纯协议分页结果
|
|
7291
|
+
*/
|
|
7292
|
+
async function fetchUserWorkList(options, cookie, requestConfig) {
|
|
7293
|
+
return fetchKuaishouInternal("userWorkList", options, {
|
|
7294
|
+
cookie,
|
|
7295
|
+
requestConfig
|
|
7296
|
+
});
|
|
7297
|
+
}
|
|
7298
|
+
/**
|
|
7299
|
+
* 获取快手直播间信息数据
|
|
7300
|
+
* @param options - 直播间参数
|
|
7301
|
+
* @param options.principalId - 直播间 principalId
|
|
7302
|
+
* @param cookie - 快手 Cookie (可选)
|
|
7303
|
+
* @param requestConfig - 请求配置 (可选)
|
|
7304
|
+
* @returns 当前直播间纯协议聚合结果
|
|
7305
|
+
*/
|
|
7306
|
+
async function fetchLiveRoomInfo(options, cookie, requestConfig) {
|
|
7307
|
+
return fetchKuaishouInternal("liveRoomInfo", options, {
|
|
7308
|
+
cookie,
|
|
7309
|
+
requestConfig
|
|
7310
|
+
});
|
|
7311
|
+
}
|
|
7312
|
+
/**
|
|
5256
7313
|
* 获取快手表情列表
|
|
5257
7314
|
* @param options - 请求选项 (可选)
|
|
5258
7315
|
* @param options.typeMode - 类型模式: 'strict' 返回严格类型, 'loose' 返回 any
|
|
@@ -5287,6 +7344,9 @@ async function fetchEmojiList$1(options, cookie, requestConfig) {
|
|
|
5287
7344
|
const kuaishouFetcher = {
|
|
5288
7345
|
fetchVideoWork,
|
|
5289
7346
|
fetchWorkComments,
|
|
7347
|
+
fetchUserProfile: fetchUserProfile$1,
|
|
7348
|
+
fetchUserWorkList,
|
|
7349
|
+
fetchLiveRoomInfo,
|
|
5290
7350
|
fetchEmojiList: fetchEmojiList$1
|
|
5291
7351
|
};
|
|
5292
7352
|
/**
|
|
@@ -5306,6 +7366,9 @@ function createBoundKuaishouFetcher(cookie, requestConfig) {
|
|
|
5306
7366
|
return {
|
|
5307
7367
|
fetchVideoWork: (options, reqConfig) => fetchVideoWork(options, cookie, reqConfig ?? requestConfig),
|
|
5308
7368
|
fetchWorkComments: (options, reqConfig) => fetchWorkComments(options, cookie, reqConfig ?? requestConfig),
|
|
7369
|
+
fetchUserProfile: (options, reqConfig) => fetchUserProfile$1(options, cookie, reqConfig ?? requestConfig),
|
|
7370
|
+
fetchUserWorkList: (options, reqConfig) => fetchUserWorkList(options, cookie, reqConfig ?? requestConfig),
|
|
7371
|
+
fetchLiveRoomInfo: (options, reqConfig) => fetchLiveRoomInfo(options, cookie, reqConfig ?? requestConfig),
|
|
5309
7372
|
fetchEmojiList: (options, reqConfig) => fetchEmojiList$1(options, cookie, reqConfig ?? requestConfig)
|
|
5310
7373
|
};
|
|
5311
7374
|
}
|
|
@@ -7135,15 +9198,6 @@ const douyinUtils = {
|
|
|
7135
9198
|
//#endregion
|
|
7136
9199
|
//#region src/platform/kuaishou/KuaishouApi.ts
|
|
7137
9200
|
/**
|
|
7138
|
-
* 快手 API 模块 (已废弃)
|
|
7139
|
-
*
|
|
7140
|
-
* 此模块中的 API 已在 v6 版本废弃
|
|
7141
|
-
* 请使用 kuaishouFetcher 或 client.kuaishou.fetcher 替代
|
|
7142
|
-
*
|
|
7143
|
-
* @module platform/kuaishou/KuaishouApi
|
|
7144
|
-
* @deprecated v6 已废弃,请使用 fetcher API 替代
|
|
7145
|
-
*/
|
|
7146
|
-
/**
|
|
7147
9201
|
* 创建废弃的 API 存根函数
|
|
7148
9202
|
*/
|
|
7149
9203
|
const createDeprecatedStub$1 = (methodName) => {
|
|
@@ -7160,6 +9214,9 @@ const createDeprecatedStub$1 = (methodName) => {
|
|
|
7160
9214
|
const kuaishou = {
|
|
7161
9215
|
getWorkInfo: createDeprecatedStub$1("getWorkInfo"),
|
|
7162
9216
|
getComments: createDeprecatedStub$1("getComments"),
|
|
9217
|
+
getUserProfile: createDeprecatedStub$1("getUserProfile"),
|
|
9218
|
+
getUserWorkList: createDeprecatedStub$1("getUserWorkList"),
|
|
9219
|
+
getLiveRoomInfo: createDeprecatedStub$1("getLiveRoomInfo"),
|
|
7163
9220
|
getEmojiList: createDeprecatedStub$1("getEmojiList")
|
|
7164
9221
|
};
|
|
7165
9222
|
/**
|
|
@@ -7223,6 +9280,7 @@ const createKuaishouRoutes = (cookie, requestConfig = getKuaishouDefaultConfig(c
|
|
|
7223
9280
|
//#region src/platform/kuaishou/index.ts
|
|
7224
9281
|
/** 快手相关功能模块 (工具集) */
|
|
7225
9282
|
const kuaishouUtils = {
|
|
9283
|
+
sign: kuaishouSign,
|
|
7226
9284
|
kuaishouApiUrls,
|
|
7227
9285
|
api: kuaishou
|
|
7228
9286
|
};
|
|
@@ -7688,12 +9746,18 @@ const DouyinFetcherMethods = {
|
|
|
7688
9746
|
const KuaishouInternalMethods = {
|
|
7689
9747
|
VIDEO_WORK: "单个视频作品数据",
|
|
7690
9748
|
WORK_COMMENTS: "评论数据",
|
|
9749
|
+
USER_PROFILE: "用户主页数据",
|
|
9750
|
+
USER_WORK_LIST: "用户作品列表数据",
|
|
9751
|
+
LIVE_ROOM_INFO: "直播间信息数据",
|
|
7691
9752
|
EMOJI_LIST: "Emoji数据"
|
|
7692
9753
|
};
|
|
7693
9754
|
/** 快手 Fetcher 方法名 (英文,对外 API) */
|
|
7694
9755
|
const KuaishouFetcherMethods = {
|
|
7695
9756
|
VIDEO_WORK: "fetchVideoWork",
|
|
7696
9757
|
WORK_COMMENTS: "fetchWorkComments",
|
|
9758
|
+
USER_PROFILE: "fetchUserProfile",
|
|
9759
|
+
USER_WORK_LIST: "fetchUserWorkList",
|
|
9760
|
+
LIVE_ROOM_INFO: "fetchLiveRoomInfo",
|
|
7697
9761
|
EMOJI_LIST: "fetchEmojiList"
|
|
7698
9762
|
};
|
|
7699
9763
|
/** 小红书内部方法名 (中文,用于 getdata.ts) */
|
|
@@ -7770,6 +9834,9 @@ const DouyinMethodToFetcher = {
|
|
|
7770
9834
|
const KuaishouMethodToFetcher = {
|
|
7771
9835
|
[KuaishouInternalMethods.VIDEO_WORK]: KuaishouFetcherMethods.VIDEO_WORK,
|
|
7772
9836
|
[KuaishouInternalMethods.WORK_COMMENTS]: KuaishouFetcherMethods.WORK_COMMENTS,
|
|
9837
|
+
[KuaishouInternalMethods.USER_PROFILE]: KuaishouFetcherMethods.USER_PROFILE,
|
|
9838
|
+
[KuaishouInternalMethods.USER_WORK_LIST]: KuaishouFetcherMethods.USER_WORK_LIST,
|
|
9839
|
+
[KuaishouInternalMethods.LIVE_ROOM_INFO]: KuaishouFetcherMethods.LIVE_ROOM_INFO,
|
|
7773
9840
|
[KuaishouInternalMethods.EMOJI_LIST]: KuaishouFetcherMethods.EMOJI_LIST
|
|
7774
9841
|
};
|
|
7775
9842
|
/** 小红书:内部中文方法名 -> Fetcher 英文方法名 */
|
|
@@ -7881,6 +9948,9 @@ const BilibiliMethodReverseMapping = Object.fromEntries(Object.entries(BilibiliM
|
|
|
7881
9948
|
const KuaishouMethodMapping = {
|
|
7882
9949
|
单个视频作品数据: "fetchVideoWork",
|
|
7883
9950
|
评论数据: "fetchWorkComments",
|
|
9951
|
+
用户主页数据: "fetchUserProfile",
|
|
9952
|
+
用户作品列表数据: "fetchUserWorkList",
|
|
9953
|
+
直播间信息数据: "fetchLiveRoomInfo",
|
|
7884
9954
|
Emoji数据: "fetchEmojiList"
|
|
7885
9955
|
};
|
|
7886
9956
|
/** 英文方法名 -> 中文方法名 反向映射 */
|
|
@@ -7964,6 +10034,9 @@ const BilibiliApiRoutes = {
|
|
|
7964
10034
|
const KuaishouApiRoutes = {
|
|
7965
10035
|
videoWork: "/work",
|
|
7966
10036
|
comments: "/comments",
|
|
10037
|
+
userProfile: "/user",
|
|
10038
|
+
userWorkList: "/user/works",
|
|
10039
|
+
liveRoomInfo: "/live",
|
|
7967
10040
|
emojiList: "/emoji"
|
|
7968
10041
|
};
|
|
7969
10042
|
/**
|
|
@@ -8008,7 +10081,7 @@ function getApiRoute(platform, methodType) {
|
|
|
8008
10081
|
* 构建后使用 __VERSION__,开发环境从 package.json 读取
|
|
8009
10082
|
*/
|
|
8010
10083
|
const getVersion = () => {
|
|
8011
|
-
return "6.
|
|
10084
|
+
return "6.1.2";
|
|
8012
10085
|
};
|
|
8013
10086
|
const VERSION = getVersion();
|
|
8014
10087
|
/**
|
|
@@ -8116,9 +10189,12 @@ exports.KuaishouCommentParamsSchema = KuaishouCommentParamsSchema;
|
|
|
8116
10189
|
exports.KuaishouEmojiParamsSchema = KuaishouEmojiParamsSchema;
|
|
8117
10190
|
exports.KuaishouFetcherMethods = KuaishouFetcherMethods;
|
|
8118
10191
|
exports.KuaishouInternalMethods = KuaishouInternalMethods;
|
|
10192
|
+
exports.KuaishouLiveRoomInfoParamsSchema = KuaishouLiveRoomInfoParamsSchema;
|
|
8119
10193
|
exports.KuaishouMethodMapping = KuaishouMethodMapping;
|
|
8120
10194
|
exports.KuaishouMethodRoutes = KuaishouMethodRoutes;
|
|
8121
10195
|
exports.KuaishouMethodToFetcher = KuaishouMethodToFetcher;
|
|
10196
|
+
exports.KuaishouUserProfileParamsSchema = KuaishouUserProfileParamsSchema;
|
|
10197
|
+
exports.KuaishouUserWorkListParamsSchema = KuaishouUserWorkListParamsSchema;
|
|
8122
10198
|
exports.KuaishouValidationSchemas = KuaishouValidationSchemas;
|
|
8123
10199
|
exports.KuaishouVideoParamsSchema = KuaishouVideoParamsSchema;
|
|
8124
10200
|
exports.MajorType = MajorType;
|
|
@@ -8189,6 +10265,7 @@ exports.isNetworkErrorResult = isNetworkErrorResult;
|
|
|
8189
10265
|
exports.kuaishou = kuaishou;
|
|
8190
10266
|
exports.kuaishouApiUrls = kuaishouApiUrls;
|
|
8191
10267
|
exports.kuaishouFetcher = kuaishouFetcher;
|
|
10268
|
+
exports.kuaishouSign = kuaishouSign;
|
|
8192
10269
|
exports.kuaishouUtils = kuaishouUtils;
|
|
8193
10270
|
exports.logMiddleware = logMiddleware;
|
|
8194
10271
|
exports.logger = logger;
|