feat: 新增多个脚本用于监控抖音直播、店铺评价、售后及体验分数据

新增了多个脚本文件,用于监控抖音直播间的弹幕、店铺评价、售后数据及商家体验分。这些脚本通过飞书多维表格进行数据存储,并支持定时任务自动更新数据。具体包括:
1. 直播间弹幕监控脚本
2. 店铺评价监控脚本
3. 售后数据监控脚本
4. 商家体验分监控脚本
5. 竞品、行业及跨行业热门千川素材获取脚本

这些脚本通过飞书API进行数据写入,并支持去重和定时任务调度。
This commit is contained in:
intelligrow
2025-04-25 15:15:29 +08:00
commit 3c3ecf8947
10 changed files with 3689 additions and 0 deletions
+409
View File
@@ -0,0 +1,409 @@
// ==UserScript==
// @name 自动获取竞品、行业、跨行业热门千川素材
// @namespace https://bbs.tampermonkey.net.cn/
// @version 0.2.0
// @description try to take over the world!
// @author wanxi
// @crontab * 8 once * *
// @match https://yuntu.oceanengine.com/yuntu_ng/content/creative/content_lab*
// @match https://yuntu.oceanengine.com/yuntu_brand/content/creative/content_lab*
// @icon https://www.google.com/s2/favicons?domain=oceanengine.com
// @grant GM_xmlhttpRequest
// ==/UserScript==
const CONFIG = {
id: "cli_a6f25876ea28100d",
secret: "raLC56ZLIara07nKigpysfoDxHTAeyJf",
appId: 'EGKVbQPMCarVmWsJEPgcOQ38nKh',
tableId1: 'tblSW2DBXwYBWHaT', // 竞品表
tableId2: 'tblgwfXH6HD0nMN7', // 同行业
tableId3: 'tbl0hQsktecuZP2W', // 跨行业
logTableId: 'tblutCXYQl4WYmwY', // 运行记录表
urls: {
tenantAccessToken: "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
bitableUpdateRecords: (appId, tableId) => `https://open.feishu.cn/open-apis/bitable/v1/apps/${appId}/tables/${tableId}/records`,
bitableSearchRecords: (appId, tableId) => `https://open.feishu.cn/open-apis/bitable/v1/apps/${appId}/tables/${tableId}/records/search`,
fetchCompetitorUrl: (brandIdList) => {
const industryId = 12;
const dateType = 200;
const endDate = getTMinus2Date();
const startDate = getTMinus2Date();
const brandId = 10209634;
const competitorType = 1;
const itemType = 3;
const level1TriggerPointId = 150000;
const level2TriggerPointId = 150200;
const level3TriggerPointId = 150202;
const firstOrderIndex = 10001;
const secondOrderIndex = 10001;
const secondOrderBy = 'asc';
const queryCnt = 20;
const baseUrl = 'https://yuntu.oceanengine.com/yuntu_ng/api/v1/CompetitorHotItems';
return `${baseUrl}?aadvid=1710507483282439&industry_id=${industryId}&date_type=${dateType}&end_date=${endDate}&start_date=${startDate}&brand_id=${brandId}&brand_id_list=${brandIdList}&competitor_type=${competitorType}&item_type=${itemType}&level_1_trigger_point_id=${level1TriggerPointId}&level_2_trigger_point_id=${level2TriggerPointId}&level_3_trigger_point_id=${level3TriggerPointId}&first_order_index=${firstOrderIndex}&second_order_index=${secondOrderIndex}&second_order_by=${secondOrderBy}&query_cnt=${queryCnt}`;
},
fetchIndustryUrl: (noCheckIndustry) => {
const dateType = 200;
const endDate = getTMinus2Date();
const startDate = getTMinus2Date();
const itemType = 3;
const level1TriggerPointId = 150000;
const level2TriggerPointId = 150200;
const level3TriggerPointId = 150202;
const firstOrderIndex = 10001;
const secondOrderIndex = 10001;
const secondOrderBy = 'asc';
const queryCnt = 20;
const baseUrl = 'https://yuntu.oceanengine.com/yuntu_ng/api/v1/IndustryHotItems';
return `${baseUrl}?aadvid=1710507483282439&no_check_industry=${noCheckIndustry}&date_type=${dateType}&end_date=${endDate}&start_date=${startDate}&item_type=${itemType}&level_1_trigger_point_id=${level1TriggerPointId}&level_2_trigger_point_id=${level2TriggerPointId}&level_3_trigger_point_id=${level3TriggerPointId}&first_order_index=${firstOrderIndex}&second_order_index=${secondOrderIndex}&second_order_by=${secondOrderBy}&query_cnt=${queryCnt}`;
}
}
};
let logs = [];
let result = "Success";
function log(message) {
logs.push(message);
console.log(message);
}
// 重试函数
const retry = async (fn, retries = 3, delay = 1000) => {
for (let i = 0; i < retries; i++) {
try {
return await fn();
} catch (error) {
if (i < retries - 1) {
console.warn(`Retrying... (${i + 1}/${retries})`);
await new Promise(resolve => setTimeout(resolve, delay));
} else {
throw error;
}
}
}
};
async function sendHttpRequest(method, url, body = null, headers = {
'Content-Type': 'application/json'
}) {
return retry(() => new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: method,
url: url,
data: body,
headers: headers,
onload: function(response) {
if (response.status >= 200 && response.status < 300) {
try {
const jsonResponse = JSON.parse(response.responseText);
if (jsonResponse.msg) {
log(jsonResponse.msg);
}
resolve(jsonResponse);
} catch (e) {
reject(`Failed to parse JSON: ${e}`);
}
} else {
reject(`Error: ${response.status} ${response.statusText}`);
}
},
onerror: function(error) {
reject(`Network Error: ${error}`);
}
});
}));
}
async function fetchTenantAccessToken(id, secret) {
try {
const response = await sendHttpRequest("POST", CONFIG.urls.tenantAccessToken, JSON.stringify({
"app_id": id,
"app_secret": secret
}));
return response.tenant_access_token;
} catch (error) {
throw new Error(`Error fetching Tenant Access Token: ${error}`);
}
}
async function updateBitableRecords(accessToken, appId, tableId, items) {
try {
const response = await sendHttpRequest("POST", CONFIG.urls.bitableUpdateRecords(appId, tableId), JSON.stringify(items), {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
});
log('Updated record: ' + JSON.stringify(response));
return response;
} catch (error) {
throw new Error(`Failed to update record in Bitable: ${error}`);
}
}
async function checkIfRecordExists(accessToken, appId, tableId, videoId) {
const url = CONFIG.urls.bitableSearchRecords(appId, tableId);
const headers = {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
};
const body = JSON.stringify({
"field_names": ["视频后台id"],
"filter": {
"conjunction": "and",
"conditions": [{
"field_name": "视频后台id",
"operator": "is",
"value": [videoId]
}]
},
"automatic_fields": false
});
try {
const response = await sendHttpRequest("POST", url, body, headers);
return response.data.total > 0;
} catch (error) {
console.error('Error querying Bitable:', error);
throw new Error(`Failed to query Bitable: ${error.message}`);
}
}
const brandDict = [{
brandId: 533686,
brandName: "PROYA/珀莱雅"
}, {
brandId: 885679,
brandName: "KANS/韩束"
}];
const industryDict = [{
industry_id: 1202,
industry_name: "护肤"
}, {
industry_id: 13,
industry_name: "个护清洁(日化)"
}, {
industry_id: 27,
industry_name: "医疗保健"
}];
const getTMinus2Date = () => {
const date = new Date();
date.setDate(date.getDate() - 2);
return date.toISOString().split('T')[0];
};
const fetchData = async (url, getName, isCompetitor) => {
console.log(`Fetching data from URL: ${url}`);
const fetchDataWithRetry = async () => {
const response = await sendHttpRequest('GET', url, null, {
'accept': 'application/json, text/plain, */*'
});
if (response.status === 0 && response.data.hot_items) {
return response.data.hot_items
.filter(item => item.video_id && item.title && item.id) // 过滤掉任意一个字段为空的条目
.map(item => ({
视频后台id: item.video_id,
[isCompetitor ? '品牌' : '行业']: getName(),
日期: getTMinus2Date(),
视频标题: item.title,
播放量: item.show_cnt,
前台id: item.id,
点击率: item.click_rate,
互动率: item.interact_rate,
完播率: item.play_over_rate
}));
} else {
throw new Error('Invalid response');
}
};
try {
return await retry(fetchDataWithRetry);
} catch (error) {
console.error('Error:', error);
return [];
}
};
const fetchVideoScript = async (video_id, id) => {
const fetchVideoScriptWithRetry = async () => {
const response = await sendHttpRequest('POST', "https://yuntu.oceanengine.com/yuntu_ng/api/v1/GetContentFormulaAndScript?aadvid=1710507483282439", JSON.stringify({
vid: video_id,
item_id: id
}), {
"accept": "application/json, text/plain, */*",
"content-type": "application/json"
});
return response.data.ori_script.text;
};
try {
return await retry(fetchVideoScriptWithRetry);
} catch (error) {
console.error('Error:', error);
return '文案获取失败';
}
};
const executeTasks = async () => {
console.log('Executing tasks immediately.');
const fetchCompetitorData = brandDict.map(brand =>
fetchData(CONFIG.urls.fetchCompetitorUrl(brand.brandId), () => brand.brandName, true)
);
const fetchIndustryData = industryDict
.filter(industry => industry.industry_id == 1202) // 不包括护肤行业
.map(industry =>
fetchData(CONFIG.urls.fetchIndustryUrl(industry.industry_id), () => industry.industry_name, false)
);
const fetchCrossIndustryData = industryDict
.filter(industry => industry.industry_id !== 1202) // 不包括护肤行业
.map(industry =>
fetchData(CONFIG.urls.fetchIndustryUrl(industry.industry_id), () => industry.industry_name, false)
);
const [competitorDataArray, industryDataArray, crossIndustryDataArray] = await Promise.all([
Promise.all(fetchCompetitorData),
Promise.all(fetchIndustryData),
Promise.all(fetchCrossIndustryData)
]);
const competitorData = competitorDataArray.flat();
const industryData = industryDataArray.flat();
const crossIndustryData = crossIndustryDataArray.flat();
const addScripts = async (data) => {
for (const item of data) {
item.文案 = await fetchVideoScript(item.视频后台id, item.前台id);
}
};
await addScripts(competitorData);
await addScripts(industryData);
await addScripts(crossIndustryData);
console.log('Competitor Data:', JSON.stringify(competitorData, null, 2));
console.log('Industry Data:', JSON.stringify(industryData, null, 2));
console.log('Cross-Industry Data:', JSON.stringify(crossIndustryData, null, 2));
const accessToken = await fetchTenantAccessToken(CONFIG.id, CONFIG.secret);
for (const item of competitorData) {
const itemsToWrite = {};
const exists = await checkIfRecordExists(accessToken, CONFIG.appId, CONFIG.tableId1, item.视频后台id);
if (exists === 0) {
itemsToWrite.fields = {
"视频后台id": item.视频后台id,
"品牌/行业": item.品牌 || item.行业,
"日期": Math.floor(new Date(item.日期).getTime()),
"视频标题": item.视频标题,
"播放量": item.播放量,
"视频前台id": item.前台id,
"点击率": item.点击率,
"互动率": item.互动率,
"完播率": item.完播率,
"文案": item.文案
};
}
if (Object.keys(itemsToWrite).length > 0) {
console.log(itemsToWrite);
await updateBitableRecords(accessToken, CONFIG.appId, CONFIG.tableId1, itemsToWrite);
}
}
for (const item of industryData) {
const itemsToWrite = {};
const exists = await checkIfRecordExists(accessToken, CONFIG.appId, CONFIG.tableId2, item.视频后台id);
if (exists === 0) {
itemsToWrite.fields = {
"视频后台id": item.视频后台id,
"品牌/行业": item.品牌 || item.行业,
"日期": Math.floor(new Date(item.日期).getTime()),
"视频标题": item.视频标题,
"播放量": item.播放量,
"视频前台id": item.前台id,
"点击率": item.点击率,
"互动率": item.互动率,
"完播率": item.完播率,
"文案": item.文案
};
}
if (Object.keys(itemsToWrite).length > 0) {
console.log(itemsToWrite);
await updateBitableRecords(accessToken, CONFIG.appId, CONFIG.tableId2, itemsToWrite);
}
}
for (const item of crossIndustryData) {
const itemsToWrite = {};
const exists = await checkIfRecordExists(accessToken, CONFIG.appId, CONFIG.tableId3, item.视频后台id);
if (exists === 0) {
itemsToWrite.fields = {
"视频后台id": item.视频后台id,
"品牌/行业": item.品牌 || item.行业,
"日期": Math.floor(new Date(item.日期).getTime()),
"视频标题": item.视频标题,
"播放量": item.播放量,
"视频前台id": item.前台id,
"点击率": item.点击率,
"互动率": item.互动率,
"完播率": item.完播率,
"文案": item.文案
};
}
if (Object.keys(itemsToWrite).length > 0) {
console.log(itemsToWrite);
await updateBitableRecords(accessToken, CONFIG.appId, CONFIG.tableId3, itemsToWrite);
}
}
};
async function logRunResult() {
try {
const accessToken = await fetchTenantAccessToken(CONFIG.id, CONFIG.secret);
const runTime = new Date().getTime();
const logEntries = [{
"表名": "竞品表",
"表格id": CONFIG.tableId1,
"最近运行时间": runTime,
"运行结果": result,
"日志": logs.join('\n')
}, {
"表名": "同行业表",
"表格id": CONFIG.tableId2,
"最近运行时间": runTime,
"运行结果": result,
"日志": logs.join('\n')
}, {
"表名": "跨行业表",
"表格id": CONFIG.tableId3,
"最近运行时间": runTime,
"运行结果": result,
"日志": logs.join('\n')
}];
for (const logData of logEntries) {
await updateBitableRecords(accessToken, CONFIG.appId, CONFIG.logTableId, {
fields: logData
});
log(`Run result for ${logData.表名} logged successfully.`);
}
} catch (error) {
log('Failed to log run result: ' + error.message);
}
}
(async function() {
try {
await executeTasks();
} catch (error) {
log('Script execution failed: ' + error.message);
result = "Failed";
} finally {
await logRunResult();
}
})();
+616
View File
@@ -0,0 +1,616 @@
// ==UserScript==
// @name 低流量成本达人监控
// @namespace https://bbs.tampermonkey.net.cn/
// @version 2.3
// @description 每天获取T-2的低CPM CPsearch内容,并给到达人列表,写入多维表格
// @author 汪喜
// @crontab 53 8 once * *
// @grant GM_xmlhttpRequest
// @connect open.feishu.cn
// @connect yuntu.oceanengine.com
// ==/UserScript==
const CONFIG = {
feishu: {
id: "cli_a6f25876ea28100d",
secret: "raLC56ZLIara07nKigpysfoDxHTAeyJf",
appId: "Z6Icb88DsaKyLBsOwYhcjHRinTc",
videoTableId: "tblfpGtDGUPhteRH",
starTableId: "tblrqiSDmdtmBhXu",
urls: {
tenantAccessToken:
"https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
bitableBatchCreateRecords: (appId, tableId) =>
`https://open.feishu.cn/open-apis/bitable/v1/apps/${appId}/tables/${tableId}/records/batch_create`,
bitableSearchRecords: (appId, tableId) =>
`https://open.feishu.cn/open-apis/bitable/v1/apps/${appId}/tables/${tableId}/records/search`,
},
},
yuntu: {
hotItemApiUrl:
"https://yuntu.oceanengine.com/yuntu_ng/api/v1/DoubleHotContentListByTag",
aadvid: "1710507483282439",
starDataUrl:
"https://yuntu.oceanengine.com/yuntu_ng/api/v2/get_talent_filter_v3",
industryObj: [
{
industry_id: 12,
industry_name: "美妆",
},
{
industry_id: 13,
industry_name: "个护清洁(日化)",
},
{
industry_id: 14,
industry_name: "食品饮料",
},
{
industry_id: 17,
industry_name: "3C数码",
},
{
industry_id: 18,
industry_name: "宠物",
},
{
industry_id: 20,
industry_name: "母婴",
},
{
industry_id: 27,
industry_name: "医疗保健",
},
{
industry_id: 28,
industry_name: "家用电器",
},
],
},
retry: {
retries: 3,
delay: 10000,
},
batchSize: 500,
requestDelay: Math.floor(Math.random() * 8001) + 2000, // 2-10秒随机延迟
webhook: {
url: "https://bloomagebiotech.feishu.cn/base/automation/webhook/event/ZFVgaDgM9wwBUuhkma5cTYSOnIU", // Webhook URL
},
};
// 重试函数
const retry = async (fn, retries = 3, delay = 10000) => {
for (let i = 0; i < retries; i++) {
try {
return await fn();
} catch (error) {
if (i < retries - 1) {
console.warn(`Retrying... (${i + 1}/${retries})`);
await new Promise((resolve) => setTimeout(resolve, delay));
} else {
throw error;
}
}
}
};
// 延迟函数
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
// HTTP 请求函数
async function sendHttpRequest(
method,
url,
body = null,
headers = {
"Content-Type": "application/json",
},
errorHandler = null
) {
return retry(
() =>
new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: method,
url: url,
data: body,
headers: headers,
onload: function (response) {
if (response.status >= 200 && response.status < 300) {
try {
const jsonResponse = JSON.parse(response.responseText);
if (jsonResponse.msg) {
console.log(jsonResponse.msg);
}
// 使用自定义的错误处理函数
if (errorHandler && !errorHandler(jsonResponse)) {
reject(`API Error: ${JSON.stringify(jsonResponse)}`);
} else {
resolve(jsonResponse);
}
} catch (e) {
reject(`Failed to parse JSON: ${e}`);
}
} else {
reject(`Error: ${response.status} ${response.statusText}`);
}
},
onerror: function (error) {
reject(`Network Error: ${error}`);
},
});
})
);
}
// 计算日期
function formatDate(date) {
const d = new Date(date);
let month = "" + (d.getMonth() + 1);
let day = "" + d.getDate();
const year = d.getFullYear();
if (month.length < 2) month = "0" + month;
if (day.length < 2) day = "0" + day;
return [year, month, day].join("-");
}
// 获取 T-N 的数据
function getDynamicDates(n) {
const today = new Date();
const T_minus_2 = new Date(today);
T_minus_2.setDate(today.getDate() - 2);
const T_minus_n = new Date(T_minus_2);
T_minus_n.setDate(T_minus_2.getDate() - n);
return {
start_date: formatDate(T_minus_n),
end_date: formatDate(T_minus_2),
};
}
// 处理云图 API 返回的数据
function processYuntuData(item, industryName) {
return {
视频id: item.item_id,
视频链接文本: item.item_link,
达人昵称: item.nickname,
达人id: item.aweme_id,
达人星图id: item.star_uid_id,
视频标题: item.title,
视频后台id: item.video_id,
视频时长: Number(item.video_duration),
自然播放次数: item.index_map["80101"] || 0,
自然互动次数: item.index_map["80105"] || 0,
回搜次数: item.index_map["110001"] || 0,
回搜人数: item.index_map["110002"] || 0,
看后搜次数: item.index_map["10010"] || 0,
看后搜人数: Number(item.key_word_after_search_info.search_uv) || 0,
回搜率: item.index_map["110003"] || 0,
看后搜率: item.key_word_after_search_info.search_rate || 0,
//"search_cost": item.search_cost || 0,
发布时间: new Date(
item.create_date.slice(0, 4),
item.create_date.slice(4, 6) - 1,
item.create_date.slice(6, 8)
).getTime(),
品牌ID: Number(item.brand_id),
行业名称: industryName,
视频类型: item.content_type, //数组类型 多选
搜索关键词:
item.key_word_after_search_info.keywords === undefined
? ""
: item.key_word_after_search_info.keywords.join(", "),
完播数: item.index_map["30019"] || 0,
总播放次数: item.index_map["80001"] || 0,
新增A3率: item.index_map["100002"] || 0,
};
}
// 云图 API 类
class YuntuAPI {
constructor(config) {
this.config = config;
}
handleYuntuResponse(response) {
// 检查 status 字段
return response.status === 0;
}
async fetchDoubleHotContentListByTag(body, industryName) {
try {
const url = `${this.config.hotItemApiUrl}?aadvid=${this.config.aadvid}`;
const response = await sendHttpRequest(
"POST",
url,
JSON.stringify(body),
{
"Content-Type": "application/json",
},
this.handleYuntuResponse
);
await delay(CONFIG.requestDelay); // 添加延迟
return response.data.item_list.map((item) =>
processYuntuData(item, industryName)
);
} catch (error) {
console.error("Failed to fetch data from Yuntu API", error);
throw error;
}
}
createRequestBody(industryId, industryName) {
const dates = getDynamicDates(7);
return {
general_cond: {
industry_id: industryId,
brand_id: "10209634",
time_range: {
date_type: 0,
start_date: dates.start_date,
end_date: dates.end_date,
},
time_type: 2,
task_type: 1,
},
hot_type: [1, 0, 2, 3],
item_type: 1, // 0 本品牌 1行业 2竞品
industry_list: [
{
industry_id: industryId,
industry_name: industryName,
},
],
};
}
async fetchAllIndustries() {
const results = [];
for (const industry of this.config.industryObj) {
const { industry_id, industry_name } = industry;
const requestBody = this.createRequestBody(industry_id, industry_name);
const result = await this.fetchDoubleHotContentListByTag(
requestBody,
industry_name
);
results.push(...result);
}
return results;
}
// 获取达人星图数据
async fetchStarData(starIds) {
const results = [];
await batchProcess(starIds, CONFIG.batchSize, async (batch) => {
const body = {
mkt_scene: 1,
task_type: 1,
brand_id: "10209634",
industry_id: "12",
exact_match_filter: {
talent_filter_cond: 1,
exact_match_list: batch,
},
gender: -1,
limit: CONFIG.batchSize,
};
const url = `${this.config.starDataUrl}?aadvid=${this.config.aadvid}`;
const response = await sendHttpRequest(
"POST",
url,
JSON.stringify(body),
{
"Content-Type": "application/json",
},
this.handleYuntuResponse
);
await delay(CONFIG.requestDelay); // 添加延迟
results.push(
...response.data.info_item_list.map((item) => ({
达人星图id: item.star_uid,
达人昵称: item.nick_name,
"20s以下视频报价": Number(item.price),
"20-60s视频报价": Number(item.price_20_60),
"60s以上视频报价": Number(item.price_60),
达人uid: item.aweme_id,
粉丝量: Number(item.fans_cnt),
}))
);
});
return results;
}
}
// 飞书 API 类
class FeishuAPI {
constructor(config) {
this.config = config;
this.accessToken = null;
}
//获取token
async fetchTenantAccessToken() {
try {
const response = await sendHttpRequest(
"POST",
this.config.urls.tenantAccessToken,
JSON.stringify({
app_id: this.config.id,
app_secret: this.config.secret,
})
);
this.accessToken = response.tenant_access_token;
await delay(CONFIG.requestDelay); // 添加延迟
return this.accessToken;
} catch (error) {
throw new Error(`Error fetching Tenant Access Token: ${error}`);
}
}
//批量更新记录
async batchCreateBitableRecords(tableId, items) {
if (!this.accessToken) {
await this.fetchTenantAccessToken();
}
const results = [];
await batchProcess(items, CONFIG.batchSize, async (batch) => {
try {
const response = await sendHttpRequest(
"POST",
this.config.urls.bitableBatchCreateRecords(
this.config.appId,
tableId
),
JSON.stringify({
records: batch,
}),
{
Authorization: `Bearer ${this.accessToken}`,
"Content-Type": "application/json",
}
);
await delay(CONFIG.requestDelay); // 添加延迟
console.log("Batch created records: " + JSON.stringify(response));
results.push(response);
} catch (error) {
throw new Error(`Failed to batch create records in Bitable: ${error}`);
}
});
return results;
}
//返回视频id、recordid和视频标签
async fetchRecordByVideoId(videoId) {
if (!this.accessToken) {
await this.fetchTenantAccessToken();
}
const body = {
field_names: ["视频id", "标签"],
filter: {
conjunction: "and",
conditions: [
{
field_name: "视频id",
operator: "is",
value: [videoId],
},
],
},
sort: [
{
field_name: "记录时间",
desc: true, //倒序
},
],
};
const url = CONFIG.feishu.urls.bitableSearchRecords(
CONFIG.feishu.appId,
CONFIG.feishu.videoTableId
);
const response = await sendHttpRequest("POST", url, JSON.stringify(body), {
Authorization: `Bearer ${this.accessToken}`,
"Content-Type": "application/json",
});
return response.data.items;
}
}
// 批量处理函数
async function batchProcess(items, batchSize, processFunction) {
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
await processFunction(batch);
}
}
// 发送统计结果到 Webhook 服务器
async function sendStatisticsToWebhook(lowCostCount, explosiveCount) {
const payload = {
lowCostCount: lowCostCount,
explosiveCount: explosiveCount,
};
try {
await sendHttpRequest("POST", CONFIG.webhook.url, JSON.stringify(payload), {
"Content-Type": "application/json",
});
console.log("Statistics sent to webhook successfully.");
} catch (error) {
console.error("Failed to send statistics to webhook:", error);
}
}
// 主函数
async function processAndUploadData() {
try {
console.log("Start processing and uploading data...");
// 1. 请求所有的视频数据
const yuntuAPI = new YuntuAPI(CONFIG.yuntu);
const allData = await yuntuAPI.fetchAllIndustries();
console.log("Fetched all video data.");
// 2. 根据视频里面的达人星图id字段,请求所有达人的星图报价数据
const starIds = allData.map((data) => data["达人星图id"]);
const starData = await yuntuAPI.fetchStarData(starIds);
console.log("Fetched all star data.");
// 3. 获取已存在的多维表格记录的视频id和标签信息
const feishuAPI = new FeishuAPI(CONFIG.feishu);
// 4. 根据视频时长,选择合适的报价,如果计算出来的CPM<40,则把视频数据和达人数据push到两个json数组中
const filteredVideoData = [];
const filteredStarData = [];
// 记录标签推送次数
let lowCostCount = 0;
let explosiveCount = 0;
// 遍历所有视频数据
for (const videoData of allData) {
// 找到当前视频对应的达人信息
const currentStarInfo = starData.find(
(star) => star["达人星图id"] === videoData["达人星图id"]
);
// 如果找到了对应的达人信息
if (currentStarInfo) {
// 根据视频时长选择合适的报价
let price;
if (videoData["视频时长"] < 20) {
price = currentStarInfo["20s以下视频报价"];
} else if (videoData["视频时长"] <= 60) {
price = currentStarInfo["20-60s视频报价"];
} else {
price = currentStarInfo["60s以上视频报价"];
}
// 数据过滤部分
// 计算自然流量的每千次曝光成本(CPM)
const cpm = (price / videoData["自然播放次数"]) * 1000;
// 计算预估的自然看后搜人数
const nativeSearchUv =
(videoData["自然播放次数"] / videoData["总播放次数"]) *
videoData["看后搜人数"];
// 计算预估的自然看后搜成本
const CPNativeSearchUv = price / nativeSearchUv;
// 初始化标识字段为数组
videoData["标签"] = [];
// 筛选条件:CPM小于20CPNativeSearchUv小于10,且报价大于0,或者自然播放次数大于1000万
if (
(cpm < 20 && CPNativeSearchUv < 10 && price > 0) ||
(videoData["自然播放次数"] > 10000000 && nativeSearchUv > 3000)
) {
// 为视频数据添加新的字段标识“低成本”或“爆量”
if (cpm < 20 && CPNativeSearchUv < 10 && price > 0) {
if (!videoData["标签"].includes("低成本")) {
videoData["标签"].push("低成本");
//lowCostCount++;
}
}
if (
videoData["自然播放次数"] > 10000000 &&
nativeSearchUv > 3000 &&
price > 0
) {
if (!videoData["标签"].includes("爆量")) {
videoData["标签"].push("爆量");
// explosiveCount++;
}
}
// 添加当前时间字段
videoData["记录时间"] = Date.now();
// 查找多维表格中的记录
const existingRecords = await feishuAPI.fetchRecordByVideoId(
videoData["视频id"]
);
const existingRecord =
existingRecords.length > 0 ? existingRecords[0] : null;
// 如果视频ID已经存在,则判断标签是否不同
if (existingRecord) {
const existingTags = existingRecord.fields["标签"] || [];
const newTags = videoData["标签"];
// 使用集合比较标签
const tagsAreDifferent =
new Set([...existingTags, ...newTags]).size !==
existingTags.length;
// 如果标签不同,则更新记录
if (tagsAreDifferent) {
filteredVideoData.push(videoData);
//标签不同不需要再push达人数据,表格中已经有了
//filteredStarData.push(currentStarInfo);
// 增加计数器
if (videoData["标签"].includes("低成本")) {
lowCostCount++;
}
if (videoData["标签"].includes("爆量")) {
explosiveCount++;
}
}
} else {
// 如果视频ID不存在,则新增记录
filteredVideoData.push(videoData);
filteredStarData.push(currentStarInfo);
// 增加计数器
if (videoData["标签"].includes("低成本")) {
lowCostCount++;
}
if (videoData["标签"].includes("爆量")) {
explosiveCount++;
}
}
}
}
}
// 错误处理:如果没有满足条件的视频,记录日志
if (filteredVideoData.length === 0) {
console.warn("No videos met the criteria.");
return;
}
const videoRecordsToUpload = filteredVideoData.map((data) => ({
fields: data,
}));
const starRecordsToUpload = filteredStarData.map((data) => ({
fields: data,
}));
// 4. 将两个json数组的结果,分别写入达人表和视频表
await feishuAPI.batchCreateBitableRecords(
CONFIG.feishu.videoTableId,
videoRecordsToUpload
);
await feishuAPI.batchCreateBitableRecords(
CONFIG.feishu.starTableId,
starRecordsToUpload
);
// 在数据成功写入多维表格后调用
await sendStatisticsToWebhook(lowCostCount, explosiveCount);
console.log("Data successfully uploaded to Bitable.");
} catch (error) {
console.error("Failed to process and upload data: ", error);
}
}
// 自动启动脚本
(async () => {
await processAndUploadData();
})();