新增了多个脚本文件,用于监控抖音直播间的弹幕、店铺评价、售后数据及商家体验分。这些脚本通过飞书多维表格进行数据存储,并支持定时任务自动更新数据。具体包括: 1. 直播间弹幕监控脚本 2. 店铺评价监控脚本 3. 售后数据监控脚本 4. 商家体验分监控脚本 5. 竞品、行业及跨行业热门千川素材获取脚本 这些脚本通过飞书API进行数据写入,并支持去重和定时任务调度。
409 lines
15 KiB
JavaScript
409 lines
15 KiB
JavaScript
// ==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();
|
|
}
|
|
})(); |