3 / index.js
upppppppp1's picture
Update index.js
3e822c0 verified
Raw
History Blame Contribute Delete
17 kB
import http from 'axios'
import express from 'express'
const config = {
biz: [
{ type: 'ad_dsp_photo_upload', size: 32212254719 },
{ type: 'ad_alliance_ssp', size: 1610612735 },
{ type: 'ad_creative_center', size: 999999999 },
{ type: 'ad_im', size: 104857599 },
{ type: 'kuaishou_ad_clue_crm', size: 31457279 },
{ type: 'ad_dsp_app', size: 10737418239 },
],
material_st: [
// https://material.kuaishou.com/gateway/ad/shop/material/b/upload/token/generate
// 2026/8/12 18:45:12
`userId=5420146991`,
`kuaishou.shop.material_st=ChlrdWFpc2hvdS5zaG9wLm1hdGVyaWFsLnN0EsABjGjFkDe9THa5yyvINO3H0ygZPKtfSPZSkkWE_uh3XDl3a8aWbb946PMzPG5K1RZpJOLegrL83ekywUp7xtOuww1RxokgqY1hyJodRzBoTzIxp1c2rKyBboOFrDqNi7hQLhXvleOh9xHdEjxT4qaAPem1QGbLOjhPIBFvX4eycJmoJKT2tzgSRNs6lEdvVXXCFlSqndxVeyKEmxDrVOI0xb6-axxnARQdCBWd46Q4p9gz-GMnPiE6dBlNb-6cd1xLGhLYUCMEbZREO-r-ABXjxmruTLMiIFUyXC2zm-S0G-r-zVhLrBtl9YI-vFvDDAuw-krRiaH5KAUwAQ`
].join(';'),
im_st: [
// https://adim.kuaishou.com/rest/web/upload/getCdnUrl
// 2026/8/12 18:45:12
`userId=5420146991`,
`kuaishou.customer.service.im_st=Ch9rdWFpc2hvdS5jdXN0b21lci5zZXJ2aWNlLmltLnN0EpABkTWUsb6aJkQXuchU7b1tAiyTgYBhK_ZVDQi6iSF8EUUnFbaY3CfYIXnOJkw6Zia1i3CBsgypNTHjPuvTBBTGPq6af8q8ZH0QpQLrkH1SRwkeRDs8H4yKlSuuE_WzFhTzZFCuufo8BNThEK8rT3rKbqUql2bcfWq4mX5J9UFy_nVW9-pYOS1-4xWH-2FeWKxsGhJFdZeh-m2BQmdYIP0tYijSsHIiIB-TV4anCOIPrDCpwuW3HmBxzbqPqxm9iFQW-NaNETLWKAUwAQ`
].join(';')
}
const app = express()
const port = 7860
app.use(express.json())
app.use(express.urlencoded({ extended: true }))
// 带取消令牌的请求函数
const createRequestWithCancel = (cancelToken) => {
return {
get: (url, config = {}) => http.get(url, { ...config, cancelToken }),
post: (url, data, config = {}) => http.post(url, data, { ...config, cancelToken }),
head: (url, config = {}) => http.head(url, { ...config, cancelToken })
}
}
// 完整下载文件(用于不支持范围请求的情况)
async function downloadFullFileWithRetry(request, finalUrl, maxRetries = 30) {
let lastError = null
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await request.get(finalUrl, {
responseType: 'arraybuffer'
})
if (response.status !== 200) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
console.log(`完整文件下载成功 (尝试 ${attempt})`)
return response.data
} catch (error) {
if (http.isCancel(error)) {
throw error
}
lastError = error
console.warn(`完整文件下载失败 (尝试 ${attempt}/${maxRetries}):`, error.message)
if (attempt < maxRetries) {
const waitTime = 300
console.log(`等待 ${waitTime}ms 后重试完整下载`)
await new Promise(resolve => setTimeout(resolve, waitTime))
}
}
}
throw new Error(`完整文件下载失败,已达到最大重试次数: ${lastError.message}`)
}
// 带重试功能的分片下载函数
async function downloadChunkWithRetry(request, finalUrl, start, end, partNumber, maxRetries = 30) {
let lastError = null
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await request.get(finalUrl, {
headers: {
'Range': `bytes=${start}-${end}`
},
responseType: 'arraybuffer'
})
if (response.status !== 206 && response.status !== 200) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const arrayBuffer = response.data
if (arrayBuffer.byteLength !== (end - start + 1)) {
throw new Error(`分片大小不匹配: 期望 ${end - start + 1} 字节, 实际 ${arrayBuffer.byteLength} 字节`)
}
console.log(`分片 ${partNumber} 下载成功 (尝试 ${attempt})`)
return arrayBuffer
} catch (error) {
if (http.isCancel(error)) {
throw error
}
lastError = error
console.warn(`分片 ${partNumber} 下载失败 (尝试 ${attempt}/${maxRetries}):`, error.message)
if (attempt < maxRetries) {
const waitTime = 300
console.log(`等待 ${waitTime}ms 后重试分片 ${partNumber}`)
await new Promise(resolve => setTimeout(resolve, waitTime))
}
}
}
throw new Error(`分片 ${partNumber} 下载失败,已达到最大重试次数: ${lastError.message}`)
}
// 带重试功能的上传函数
async function uploadChunkWithRetry(request, upload_token, arrayBuffer, partNumber, maxRetries = 3) {
let lastError = null
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const upload_result = await request.post(`https://upload.kuaishouzt.com/api/upload/fragment?upload_token=${upload_token}&fragment_id=${partNumber - 1}`, arrayBuffer, {
headers: {
'Content-Type': 'application/octet-stream'
}
})
if (upload_result.data.result === 1) {
console.log(`分片 ${partNumber} 上传成功 (尝试 ${attempt})`)
return {
checksum: upload_result.data.checksum,
part_number: partNumber,
status: 'success'
}
} else {
throw new Error(`分片 ${partNumber} 上传失败`)
}
} catch (error) {
if (http.isCancel(error)) {
throw error
}
lastError = error
console.warn(`分片 ${partNumber} (尝试 ${attempt}/${maxRetries}):`, error.message)
if (attempt < maxRetries) {
const waitTime = 300
console.log(`等待 ${waitTime}ms 后重试上传分片 ${partNumber}`)
await new Promise(resolve => setTimeout(resolve, waitTime))
}
}
}
throw new Error(`分片 ${partNumber} 上传失败,已达到最大重试次数: ${lastError.message}`)
}
// 并发控制函数
async function runWithConcurrency(tasks, maxConcurrency = 50, cancelToken) {
const results = []
const executing = new Set()
for (let i = 0; i < tasks.length; i++) {
if (cancelToken && cancelToken.reason) {
console.log('检测到取消信号,停止创建新任务')
break
}
const task = tasks[i]
if (executing.size >= maxConcurrency) {
await Promise.race(executing)
}
const promise = task().then(result => {
executing.delete(promise)
return result
}).catch(error => {
executing.delete(promise)
throw error
})
executing.add(promise)
results.push(promise)
}
return Promise.allSettled(results)
}
app.get('*', async (req, res) => {
const cancelTokenSource = http.CancelToken.source()
let isRequestCancelled = false
req.on('close', () => {
if (!res.headersSent) {
console.log('用户断开连接,取消所有请求...')
isRequestCancelled = true
cancelTokenSource.cancel('用户取消请求')
}
})
try {
// const videoUrl = req.path.replace(/^\//, '')
const videoUrl = req.url.replace(/^\//, '')
if (/favicon.ico/gim.test(videoUrl)) {
return
}
const request = createRequestWithCancel(cancelTokenSource.token)
if (isRequestCancelled) {
return
}
// 1. 首先获取文件信息
const headResponse = await request.head(videoUrl, {
maxRedirects: 5
})
const finalUrl = headResponse.request.res.responseUrl || videoUrl
const contentLength = headResponse.headers['content-length']
const acceptRanges = headResponse.headers['accept-ranges']
const contenType = headResponse.headers['content-type']
if (/^image|^text|^application/gim.test(contenType)) {
if (!/application\/octet-stream|image\/avis/gim.test(contenType)) {
return res.json({
video_url: videoUrl,
message: `文件类型不支持 ${contenType}`
})
}
}
const biz = contentLength > config.biz.at(1).size ? config.biz.at(0) : config.biz.at(1)
if (contentLength > biz.size) {
return res.json({
video_url: videoUrl,
message: '文件大小超过限制'
})
}
// 检查服务器是否支持范围请求
if (!acceptRanges || acceptRanges === 'none' || !contentLength) {
// 不支持范围请求:完整下载后分片上传
console.log('服务器不支持范围请求,将完整下载文件...')
// 获取上传token
const upload_info = await request.post('https://material.kuaishou.com/gateway/ad/shop/material/b/upload/token/generate', {
subjectType: 'MERCHANT',
bizType: biz.type,
file: {
fileName: 'file.mp4',
fileLength: 1
}
}, {
headers: {
cookie: config.material_st
}
})
console.log(upload_info.data)
const token = upload_info.data.data.token
const upload_list = {}
const fileSize = parseInt(contentLength)
const chunkSize = 20000 * 1024
// 完整下载文件
const fullFileBuffer = await downloadFullFileWithRetry(request, finalUrl)
if (isRequestCancelled) {
return
}
const threadCount = Math.ceil(fileSize / chunkSize)
console.log(`文件大小: ${fileSize} bytes, 分片数: ${threadCount}`)
const tasks = []
for (let i = 0; i < threadCount; i++) {
const start = i * chunkSize
const end = Math.min(start + chunkSize, fileSize)
const partNumber = i + 1
tasks.push(() => (async () => {
try {
if (isRequestCancelled) {
throw new http.Cancel('请求已取消')
}
const chunkBuffer = fullFileBuffer.slice(start, end)
const uploadResult = await uploadChunkWithRetry(
request,
token,
chunkBuffer,
partNumber
)
upload_list[partNumber] = uploadResult.checksum
return uploadResult
} catch (error) {
if (http.isCancel(error)) {
console.log(`分片 ${partNumber} 处理被取消`)
throw error
}
console.error(`分片 ${partNumber} 处理失败:`, error.message)
return {
part_number: partNumber,
status: 'failed',
error: error.message
}
}
})())
}
const chunksResults = await runWithConcurrency(tasks, 100, cancelTokenSource)
if (isRequestCancelled) {
return
}
// 处理任务结果
const chunks = chunksResults.map(result =>
result.status === 'fulfilled' ? result.value : result.reason
)
const validChunks = chunks.filter(chunk => !(chunk instanceof Error && http.isCancel(chunk)))
const failedChunks = validChunks.filter(chunk => chunk && chunk.status === 'failed')
if (failedChunks.length > 0) {
return res.status(500).json({
video_url: videoUrl,
message: '部分分片处理失败',
failed_chunks: failedChunks,
success_count: validChunks.length - failedChunks.length,
failed_count: failedChunks.length
})
}
const successChunks = validChunks.filter(chunk => chunk && chunk.status === 'success')
if (successChunks.length === 0) {
return res.status(500).json({
video_url: videoUrl,
message: '所有分片处理失败'
})
}
const upload_result = await request.post(`https://upload.kuaishouzt.com/api/upload/complete?fragment_count=${Object.entries(upload_list).length}&upload_token=${token}`, null, {
headers: {}
})
console.log(upload_result.data)
console.log(token)
if (upload_result.data.result === 1) {
const upload_result_url = await request.post(`https://adim.kuaishou.com/rest/web/upload/getCdnUrl?bi=103&ac=105369465`, {
bizType: biz.type,
subjectType: 'MERCHANT',
token: token
}, {
headers: {
cookie: config.im_st
}
})
console.log(upload_result_url.data)
if (upload_result_url.data.result === 1) {
return res.json({
url: 'https://v1.adkwai.com/bs2/' + upload_result_url.data.data?.split('?')?.at(0)?.split('/bs2/')?.at(-1),
message: '文件上传成功',
total_chunks: successChunks.length
})
}
}
return res.json({
video_url: videoUrl,
chunks: successChunks,
fileSize,
chunkSize,
threadCount
})
}
// 支持范围请求:继续原有逻辑
// 获取上传信息
const upload_info = await request.post('https://material.kuaishou.com/gateway/ad/shop/material/b/upload/token/generate', {
subjectType: 'MERCHANT',
bizType: biz.type,
file: {
fileName: 'file.mp4',
fileLength: 1
}
}, {
headers: {
cookie: config.material_st
}
})
console.log(upload_info.data)
const token = upload_info.data.data.token
const upload_list = {}
const fileSize = parseInt(contentLength)
const chunkSize = 20000 * 1024
const threadCount = Math.ceil(fileSize / chunkSize)
console.log(`文件大小: ${fileSize} bytes, 分片数: ${threadCount}`)
// 2. 创建所有分片任务
const tasks = []
for (let i = 0; i < threadCount; i++) {
const start = i * chunkSize
const end = Math.min(start + chunkSize - 1, fileSize - 1)
const partNumber = i + 1
tasks.push(() => (async () => {
try {
if (isRequestCancelled) {
throw new http.Cancel('请求已取消')
}
const arrayBuffer = await downloadChunkWithRetry(request, finalUrl, start, end, partNumber)
if (isRequestCancelled) {
throw new http.Cancel('请求已取消')
}
const uploadResult = await uploadChunkWithRetry(
request,
token,
arrayBuffer,
partNumber
)
upload_list[partNumber] = uploadResult.checksum
return uploadResult
} catch (error) {
if (http.isCancel(error)) {
console.log(`分片 ${partNumber} 处理被取消`)
throw error
}
console.error(`分片 ${partNumber} 处理失败:`, error.message)
return {
part_number: partNumber,
status: 'failed',
error: error.message
}
}
})())
}
const chunksResults = await runWithConcurrency(tasks, 100, cancelTokenSource)
if (isRequestCancelled) {
return
}
const chunks = chunksResults.map(result =>
result.status === 'fulfilled' ? result.value : result.reason
)
const validChunks = chunks.filter(chunk => !(chunk instanceof Error && http.isCancel(chunk)))
const failedChunks = validChunks.filter(chunk => chunk && chunk.status === 'failed')
if (failedChunks.length > 0) {
return res.status(500).json({
video_url: videoUrl,
message: '部分分片处理失败',
failed_chunks: failedChunks,
success_count: validChunks.length - failedChunks.length,
failed_count: failedChunks.length
})
}
const successChunks = validChunks.filter(chunk => chunk && chunk.status === 'success')
if (successChunks.length === 0) {
return res.status(500).json({
video_url: videoUrl,
message: '所有分片处理失败'
})
}
const upload_result = await request.post(`https://upload.kuaishouzt.com/api/upload/complete?fragment_count=${Object.entries(upload_list).length}&upload_token=${token}`, null, {
headers: {}
})
console.log(upload_result.data)
console.log(token)
if (upload_result.data.result === 1) {
const upload_result_url = await request.post(`https://adim.kuaishou.com/rest/web/upload/getCdnUrl?bi=103&ac=105369465`, {
bizType: biz.type,
subjectType: 'MERCHANT',
token: token
}, {
headers: {
cookie: config.im_st
}
})
console.log(upload_result_url.data)
if (upload_result_url.data.result === 1) {
return res.json({
url: 'https://v1.adkwai.com/bs2/' + upload_result_url.data.data?.split('?')?.at(0)?.split('/bs2/')?.at(-1),
message: '文件上传成功',
total_chunks: successChunks.length
})
}
}
return res.json({
video_url: videoUrl,
chunks: successChunks,
fileSize,
chunkSize,
threadCount
})
} catch (error) {
if (http.isCancel(error)) {
console.log('请求已被用户取消')
return
}
console.error('处理失败:', error)
if (!res.headersSent) {
res.status(500).json({
error: `下载失败: ${error.message}`,
video_url: req.path.replace(/^\//, '')
})
}
}
})
app.listen(port, () => {
console.log(`http://localhost:${port}`)
})