// ==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小于20,CPNativeSearchUv小于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(); })();