commit 3c3ecf89472cc59581580dac9bff32efea157754 Author: intelligrow <15511098+intelligrow@user.noreply.gitee.com> Date: Fri Apr 25 15:15:29 2025 +0800 feat: 新增多个脚本用于监控抖音直播、店铺评价、售后及体验分数据 新增了多个脚本文件,用于监控抖音直播间的弹幕、店铺评价、售后数据及商家体验分。这些脚本通过飞书多维表格进行数据存储,并支持定时任务自动更新数据。具体包括: 1. 直播间弹幕监控脚本 2. 店铺评价监控脚本 3. 售后数据监控脚本 4. 商家体验分监控脚本 5. 竞品、行业及跨行业热门千川素材获取脚本 这些脚本通过飞书API进行数据写入,并支持去重和定时任务调度。 diff --git a/douyinLive/liveRoomCommentCount/liveRoomCommentCount.js b/douyinLive/liveRoomCommentCount/liveRoomCommentCount.js new file mode 100644 index 0000000..11c8312 --- /dev/null +++ b/douyinLive/liveRoomCommentCount/liveRoomCommentCount.js @@ -0,0 +1,169 @@ +// ==UserScript== +// @name 抖音直播间评论次数监控(独立版) +// @namespace https://bbs.tampermonkey.net.cn/ +// @version 1.2 +// @description 每五分钟获取一次直播间的评论次数,并写入飞书多维表格中展示 +// @author 汪喜 +// @match https://compass.jinritemai.com/screen/live/shop?live_room_id=* +// @match https://compass.jinritemai.com/screen/live/* +// @match https://compass.jinritemai.com/shop/live-detail?live_room_id=* +// @grant GM_xmlhttpRequest +// @connect open.feishu.cn +// @connect compass.jinritemai.com +// @crontab */5 * * * * +// ==/UserScript== + +const CONFIG = { + id: "cli_a6f25876ea28100d", + secret: "raLC56ZLIara07nKigpysfoDxHTAeyJf", + appId: 'Wx1Jb6chwagzmfsOtyycaYmLnpf', + tableId: 'tbl0Ig2UVbIRuRFZ', // 修改为评论数据的表格ID + rooms: [ + { name: "夸迪官方旗舰店" }, + { name: "夸迪官方旗舰店直播间" } // 添加更多直播间 + ], + 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`, + fetchLiveRoomId: 'https://compass.jinritemai.com/compass_api/shop/live/live_list/realtime?page_no=1&page_size=100', + fetchCommentsData: (roomId) => `https://compass.jinritemai.com/compass_api/content_live/shop_official/live_screen/five_min_data?room_id=${roomId}` + } +}; + +function log(message) { + console.log(message); +} + +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; + } + } + } +}; + +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); + 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 fetchLiveRoomId(authorNickName) { + try { + const response = await sendHttpRequest("GET", CONFIG.urls.fetchLiveRoomId); + const liveRooms = response.data.card_list; + const targetRoom = liveRooms.find(room => room.author.author_nick_name === authorNickName); + if (targetRoom) { + return targetRoom.live_room_id; + } else { + throw new Error(`未找到符合条件的直播间: ${authorNickName}`); + } + } catch (error) { + log('Error fetching live room list: ' + error.message); + return null; + } +} + +async function fetchCommentsData(roomId) { + try { + const response = await sendHttpRequest('GET', CONFIG.urls.fetchCommentsData(roomId)); + const comments = response.data.card.find(d => d.index_display === '评论次数').value.value; + const updateTimeStr = response.data.update_time.replace("更新", "").trim(); + const updateTime = new Date(updateTimeStr).getTime(); + return { count: comments, timestamp: updateTime }; + } catch (error) { + log('Error fetching interaction data: ' + error.message); + return { count: 0, timestamp: new Date().getTime() }; + } +} + +async function fetchComments() { + try { + for (const room of CONFIG.rooms) { + log(`Fetching comments data for ${room.name}...`); + const liveRoomId = await fetchLiveRoomId(room.name); + if (!liveRoomId) { + log(`未找到符合条件的直播间 ${room.name},跳过执行`); + continue; + } + log(`Live room id for ${room.name}: ${liveRoomId}`); + const commentsData = await fetchCommentsData(liveRoomId); + log(`Fetched comments count for ${room.name}: ${commentsData.count} at ${new Date(commentsData.timestamp).toLocaleString()}`); + + // 上传数据到多维表格 + log(`Uploading comments data to bitable for ${room.name}...`); + const accessToken = await fetchTenantAccessToken(CONFIG.id, CONFIG.secret); + const itemsToWrite = { + fields: { + "评论次数": commentsData.count, + "时间": commentsData.timestamp, // 直接使用毫秒级时间戳 + "直播间名": room.name, + "直播间id": liveRoomId + } + }; + await updateBitableRecords(accessToken, CONFIG.appId, CONFIG.tableId, itemsToWrite); + log(`Comments data for ${room.name} uploaded successfully.`); + } + } catch (error) { + log('Error fetching comments data: ' + error.message); + } +} + +(async function() { + log('Script started'); + await fetchComments(); +})(); \ No newline at end of file diff --git a/douyinLive/liveRoomComments/liveRoomComments.js b/douyinLive/liveRoomComments/liveRoomComments.js new file mode 100644 index 0000000..d093a4e --- /dev/null +++ b/douyinLive/liveRoomComments/liveRoomComments.js @@ -0,0 +1,149 @@ +// ==UserScript== +// @name 罗盘直播大屏弹幕监听 +// @namespace https://bbs.tampermonkey.net.cn/ +// @version 0.3.0 +// @description 监听直播间弹幕 +// @author 汪喜 +// @match https://compass.jinritemai.com/screen/live* +// @grant GM_xmlhttpRequest +// @require https://scriptcat.org/lib/637/1.4.3/ajaxHooker.js#sha256=y1sWy1M/U5JP1tlAY5e80monDp27fF+GMRLsOiIrSUY= +// ==/UserScript== + +(function () { + 'use strict'; + //一些更改 + // 添加一个按钮来控制监听状态 + function addToggleButton() { + // 使用setTimeout来延迟按钮的添加 + setTimeout(() => { + const targetContainerSelector = '.header--XNxuk'; + const targetContainer = document.querySelector(targetContainerSelector); + + if (!targetContainer) { + console.error('The target container for the toggle button was not found.'); + return; + } + + const button = document.createElement('button'); + button.textContent = '开始监听'; + button.style.marginLeft = '10px'; + + let isListening = false; + + button.addEventListener('click', () => { + if (isListening) { + ah.unProxy(); + button.textContent = '开始监听'; + isListening = false; + alert('监听结束'); + } else { + startListening(); + button.textContent = '监听中 点击停止'; + isListening = true; + alert('监听开始'); + } + }); + + targetContainer.appendChild(button); + }, 3000); // 延迟3000毫秒(3秒)后执行 + } + + window.addEventListener('load', addToggleButton); + + const appId = "cli_a6f25876ea28100d"; + const appSecret = "raLC56ZLIara07nKigpysfoDxHTAeyJf"; + const appToken = 'HyV1bstZra3P1xstDiecfCSjnLe'; //多维表格appToken + const tableId = 'tblEqZ2x8eujgl0c'; //多维表格tableId + + class FeishuAPI { + constructor() { + this.tenantAccessToken = null; + } + + async getTenantAccessToken() { + return new Promise((resolve, reject) => { + GM_xmlhttpRequest({ + method: "POST", + url: "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal/", + headers: { + "Content-Type": "application/json", + "accept": "application/json, text/plain, */*" + }, + data: JSON.stringify({ + "app_id": appId, + "app_secret": appSecret + }), + onload: (response) => { + if (response.status === 200 && response.responseText) { + let responseData = JSON.parse(response.responseText); + this.tenantAccessToken = responseData.tenant_access_token; + console.log('Tenant Access Token:', this.tenantAccessToken); + resolve(this.tenantAccessToken); + } else { + console.error('Failed to get tenant access token:', response.statusText); + reject(new Error('Failed to get tenant access token')); + } + } + }); + }); + } + + async writeIntoBase(dataItem) { + if (!this.tenantAccessToken) { + await this.getTenantAccessToken(); + } + + return new Promise((resolve, reject) => { + const headers = { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${this.tenantAccessToken}`, + }; + + const requestBody = { + fields: dataItem + }; + + GM_xmlhttpRequest({ + method: 'POST', + url: `https://open.feishu.cn/open-apis/bitable/v1/apps/${appToken}/tables/${tableId}/records`, + headers: headers, + data: JSON.stringify(requestBody), + onload: function (response) { + if (response.status === 200) { + console.log('Data written to Feishu successfully:', response.responseText); + resolve(response.responseText); + } else { + console.error('Failed to write data:', response.statusText); + reject(new Error(`HTTP error! Status: ${response.status}`)); + } + }, + onerror: function (error) { + console.error('Request failed:', error); + reject(new Error('Request failed')); + } + }); + }); + } + } + + function startListening() { + ajaxHooker.hook(request => { + if (request.url.includes('https://compass.jinritemai.com/business_api/shop/live_bigscreen/comment')) { + request.response = async res => { + const jsonResponse = await res.json(); + const comments = jsonResponse.data.comments; + comments.forEach(async (comment) => { + const dataItem = { + nickname: comment.nick_name, + content: comment.content, + createtime: Math.round(new Date().getTime()) + }; + console.log(dataItem); + const feishuAPI = new FeishuAPI(); + await feishuAPI.writeIntoBase(dataItem); + }); + }; + } + }); + } +})(); diff --git a/douyinLive/liveRoomComments/test.js b/douyinLive/liveRoomComments/test.js new file mode 100644 index 0000000..56f894d --- /dev/null +++ b/douyinLive/liveRoomComments/test.js @@ -0,0 +1,119 @@ +// ==UserScript== +// @name 罗盘直播大屏弹幕监听 +// @namespace https://bbs.tampermonkey.net.cn/ +// @version 0.3.0 +// @description 监听直播间弹幕 +// @author 汪喜 +// @match https://compass.jinritemai.com/screen/* +// @grant GM_xmlhttpRequest +// @require https://scriptcat.org/lib/637/1.4.3/ajaxHooker.js#sha256=y1sWy1M/U5JP1tlAY5e80monDp27fF+GMRLsOiIrSUY +// ==/UserScript== + +(function () { + 'use strict'; + + const appId = "cli_a6f25876ea28100d"; + const appSecret = "raLC56ZLIara07nKigpysfoDxHTAeyJf"; + const appToken = 'LzcmbMqJxakIjFstWplcF26lnOh'; //多维表格appToken + const tableId = 'tblkwX3Sl7hzzYWb'; //多维表格tableId + + class FeishuAPI { + constructor() { + this.tenantAccessToken = null; + } + + async getTenantAccessToken() { + return new Promise((resolve, reject) => { + GM_xmlhttpRequest({ + method: "POST", + url: "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal/", + headers: { + "Content-Type": "application/json", + "accept": "application/json, text/plain, */*" + }, + data: JSON.stringify({ + "app_id": appId, + "app_secret": appSecret + }), + onload: (response) => { + if (response.status === 200 && response.responseText) { + let responseData = JSON.parse(response.responseText); + this.tenantAccessToken = responseData.tenant_access_token; + console.log('Tenant Access Token:', this.tenantAccessToken); + resolve(this.tenantAccessToken); + } else { + console.error('Failed to get tenant access token:', response.statusText); + reject(new Error('Failed to get tenant access token')); + } + } + }); + }); + } + + async writeIntoBase(dataItem) { + if (!this.tenantAccessToken) { + await this.getTenantAccessToken(); + } + + return new Promise((resolve, reject) => { + const headers = { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${this.tenantAccessToken}`, + }; + + const requestBody = { + fields: dataItem + }; + + GM_xmlhttpRequest({ + method: 'POST', + url: `https://open.feishu.cn/open-apis/bitable/v1/apps/${appToken}/tables/${tableId}/records`, + headers: headers, + data: JSON.stringify(requestBody), + onload: function (response) { + if (response.status === 200) { + console.log('Data written to Feishu successfully:', response.responseText); + resolve(response.responseText); + } else { + console.error('Failed to write data:', response.statusText); + reject(new Error(`HTTP error! Status: ${response.status}`)); + } + }, + onerror: function (error) { + console.error('Request failed:', error); + reject(new Error('Request failed')); + } + }); + }); + } + } + + function startListening() { + console.log('Starting to listen for API requests...'); + ajaxHooker.hook(request => { + if (request.url.includes('https://compass.jinritemai.com/business_api/shop/live_bigscreen/comment')) { + console.log('Intercepted API request:', request.url); + request.response = async res => { + const jsonResponse = await res.json(); + const comments = jsonResponse.data.comments; + comments.forEach(async (comment) => { + const dataItem = { + nickname: comment.nick_name, + content: comment.content, + createtime: Math.round(new Date().getTime()) + }; + console.log('Extracted comment:', dataItem); + const feishuAPI = new FeishuAPI(); + await feishuAPI.writeIntoBase(dataItem); + }); + }; + } + }); + } + + // 自动开始监听 + window.addEventListener('load', () => { + console.log('Page loaded, starting listener...'); + startListening(); + }); +})(); diff --git a/douyinLive/liveRoomHourData/liveRoomHourData - 全域.js b/douyinLive/liveRoomHourData/liveRoomHourData - 全域.js new file mode 100644 index 0000000..53ff4fc --- /dev/null +++ b/douyinLive/liveRoomHourData/liveRoomHourData - 全域.js @@ -0,0 +1,598 @@ +// ==UserScript== +// @name 抖音直播间数据监控(全自动)半小时 +// @namespace https://bbs.tampermonkey.net.cn/ +// @version 2.3 +// @description 后台定时获取直播间数据,并在多维表格中显示 +// @author 汪喜 +// @require https://cdn.staticfile.net/sweetalert2/11.10.3/sweetalert2.all.min.js +// @match https://compass.jinritemai.com/screen/live/shop?live_room_id=* +// @match https://compass.jinritemai.com/screen/live/* +// @match https://compass.jinritemai.com/shop/live-detail?live_room_id=* +// @grant GM_xmlhttpRequest +// @grant GM_setValue +// @grant GM_getValue +// @grant GM_deleteValue +// @crontab */30 * * * * +// @connect open.feishu.cn +// @connect compass.jinritemai.com +// @connect compass.jinritemai.com +// @connect qianchuan.jinritemai.com +// ==/UserScript== + +/* +更新日志: +1. 支持多个直播间 +*/ + +const CONFIG = { + rooms: [{ + name: "夸迪官方旗舰店", + aadvid: "1794468035923978", + type: "official" + }, { + name: "夸迪官方旗舰店直播间", + aadvid: "1798388578394176", + type: "shop" + + }], + + id: "cli_a6f25876ea28100d", + secret: "raLC56ZLIara07nKigpysfoDxHTAeyJf", + appId: 'Wx1Jb6chwagzmfsOtyycaYmLnpf', + tableId: 'tblixcMnCd4GfF08', + logTableId: 'tbld90KC73YBSu0B', + rawDataTableId: 'tblbzx4nG0PbZ6US', + + 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`, + fetchLiveRoomId: 'https://compass.jinritemai.com/compass_api/shop/live/live_list/realtime?page_no=1&page_size=100', + /*fetchLiveRoomData: (roomId) => { + const indexSelected = encodeURIComponent("pay_ucnt,live_show_watch_cnt_ratio,avg_watch_duration,online_user_cnt,live_show_cnt,real_refund_amt"); + return `https://compass.jinritemai.com/compass_api/content_live/shop_official/live_screen/core_data?room_id=${roomId}&index_selected=${indexSelected}`; + }, + */ + fetchQianchuanData: (aadvid) => `https://qianchuan.jinritemai.com/ad/api/pmc/v1/standard/get_summary_info?aavid=${aadvid}`, + fetchCommentsData: (roomId) => `https://compass.jinritemai.com/compass_api/content_live/shop_official/live_screen/five_min_data?room_id=${roomId}`, //使用五分钟数据来加总评论次数 + fetchConvertData: (roomId) => `https://compass.jinritemai.com/business_api/shop/live_room/flow/gmv_interaction?live_room_id=${roomId}` + } +}; + +let logs = []; +let liveRoomId = null; +let intervalId = null; +let timeoutId = null; +//let commentCounts = []; // 用于存储每五分钟获取到的评论次数 + + + +function log(message) { + logs.push(message); + console.log(message); +} + +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; + } + } + } +}; + +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}`); + } +} +//获取正在直播的直播间id +async function fetchLiveRoomId(authorNickName) { + const url = CONFIG.urls.fetchLiveRoomId; + try { + const response = await sendHttpRequest("GET", url); + const liveRooms = response.data.card_list; + const targetRoom = liveRooms.find(room => room.author.author_nick_name === authorNickName); + if (targetRoom) { + return targetRoom.live_room_id; + } else { + throw new Error("未找到符合条件的直播间"); + } + } catch (error) { + log('Error fetching live room list: ' + error.message); + return null; + } +} + + +// 获取直播间大屏数据 +async function fetchLiveRoomData(roomId, roomType) { + try { + let url; + if (roomType === "official") { + const indexSelected = encodeURIComponent("pay_ucnt,live_show_watch_cnt_ratio,avg_watch_duration,online_user_cnt,live_show_cnt,real_refund_amt"); + url = `https://compass.jinritemai.com/compass_api/content_live/shop_official/live_screen/core_data?room_id=${roomId}&index_selected=${indexSelected}`; + } else if (roomType === "shop") { + const indexSelected = encodeURIComponent("pay_ucnt,watch_cnt_show_ratio,avg_watch_duration,current_cnt,live_show_cnt,real_refund_amt"); + url = `https://compass.jinritemai.com/compass_api/shop/live/live_screen/core_data?room_id=${roomId}&index_selected=${indexSelected}`; + } else { + throw new Error(`Unknown room type: ${roomType}`); + } + + const headers = { + "accept": "application/json, text/plain, */*" + }; + const response = await sendHttpRequest("GET", url, null, headers); + + const data = response.data; + if (roomType === "official") { + return { + pay_amt: data.pay_amt.value, + pay_ucnt: data.core_data.find(d => d.index_display === "成交人数").value.value, + real_refund_amt: data.core_data.find(d => d.index_display === "退款金额").value.value, + avg_watch_duration: data.core_data.find(d => d.index_display === "人均观看时长").value.value, + current_cnt: data.core_data.find(d => d.index_display === "实时在线人数").value.value, + live_show_watch_cnt_ratio: data.core_data.find(d => d.index_display === "曝光-观看率(次数)").value.value, + live_show_cnt: data.core_data.find(d => d.index_display === "曝光次数").value.value + }; + } else if (roomType === "shop") { + // 根据实际返回的数据结构,提取所需字段 + // 这里假设返回的数据结构与官方直播间类似 + return { + pay_amt: data.pay_amt.value, + pay_ucnt: data.core_data.find(d => d.index_display === "成交人数").value.value, + real_refund_amt: data.core_data.find(d => d.index_display === "退款金额").value.value, + avg_watch_duration: data.core_data.find(d => d.index_display === "人均观看时长").value.value, + current_cnt: data.core_data.find(d => d.index_display === "实时在线人数").value.value, + live_show_watch_cnt_ratio: data.core_data.find(d => d.index_display === "曝光-观看率(次数)").value.value, + live_show_cnt: data.core_data.find(d => d.index_display === "曝光次数").value.value + }; + } + } catch (error) { + log('Error fetching live room data: ' + error.message); + return { + pay_amt: 0, + pay_ucnt: 0, + real_refund_amt: 0, + avg_watch_duration: 0, + current_cnt: 0, + live_show_watch_cnt_ratio: 0, + live_show_cnt: 0 + }; + } +} + + + +/* +//获取评论次数的数据 +async function fetchCommentsData(roomId) { + try { + const url = CONFIG.urls.fetchCommentsData(roomId); + const response = await sendHttpRequest('GET', url); + const comments = response.data.card.find(d => d.index_display === '评论次数').value.value; + const updateTimeStr = response.data.update_time.replace("更新", "").trim(); + const updateTime = new Date(updateTimeStr).getTime(); + return { count: comments, timestamp: updateTime }; + } catch (error) { + log('Error fetching interaction data: ' + error.message); + return { count: 0, timestamp: new Date().getTime() }; + } +} + +// 每五分钟获取一次评论数据 +async function fetchComments() { + try { + log('Fetching comments data...'); + const commentsData = await fetchCommentsData(liveRoomId); + commentCounts.push(commentsData); + log(commentCounts) + log(`Fetched comments count: ${commentsData.count} at ${new Date(commentsData.timestamp).toLocaleString()}`); + } catch (error) { + log('Error fetching comments data: ' + error.message); + } +} + +// 初始化评论次数数组并设置定时器 +async function initializeCommentFetching() { + if (intervalId !== null) { + clearInterval(intervalId); + } + commentCounts = []; + await fetchComments(); // 立即执行一次 + intervalId = setInterval(fetchComments, 5 * 60 * 1000); // 每五分钟重新获取一次评论数据 +} + +// 计算过去一小时的评论次数 +function getCommentsCountLastHour() { + log(`Current commentCounts array: ${JSON.stringify(commentCounts)}`); + const oneHourAgo = new Date().getTime() - (60 * 60 * 1000); + const filteredComments = commentCounts.filter(comment => comment.timestamp >= oneHourAgo); + log(`Filtered comments array: ${JSON.stringify(filteredComments)}`); + const totalComments = filteredComments.reduce((total, comment) => total + comment.count, 0); + log(`Total comments in the last hour: ${totalComments}`); + return totalComments; +} + +*/ + +//获取直播间开始时间数据 +async function fetchLiveRoomStartTime(roomId) { + const url = `https://compass.jinritemai.com/compass_api/shop/live/live_screen/live_base_info?room_id=${roomId}`; + try { + const response = await sendHttpRequest("GET", url); + return response.data.live_start_time; + } catch (error) { + log('Error fetching live room start time: ' + error.message); + return null; // 默认返回 null + } +} + + + +// 获取千川消耗金额数据 +async function fetchQianchuanData(aadvid) { + const now = new Date(); + const formatNumber = (num) => num.toString().padStart(2, '0'); + + // 结束时间为动态的当前时间 + const year = now.getFullYear(); + const month = formatNumber(now.getMonth() + 1); // 月份从0开始 + const day = formatNumber(now.getDate()); + const hours = formatNumber(now.getHours()); + const minutes = formatNumber(now.getMinutes()); + const seconds = formatNumber(now.getSeconds()); + const end_time = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; + + let start_time; + + log('Failed to fetch start time, using default start time.'); + if (now.getHours() >= 6) { + // 时间超过6点,则使用当天6点作为开始时间 + start_time = `${year}-${month}-${day} 06:00:00`; + } else { + // 如果当前时间在0点到6点之间,则开始时间为昨天的6点 + const yesterday = new Date(now); + yesterday.setDate(now.getDate() - 1); + const yesterdayYear = yesterday.getFullYear(); + const yesterdayMonth = formatNumber(yesterday.getMonth() + 1); + const yesterdayDay = formatNumber(yesterday.getDate()); + start_time = `${yesterdayYear}-${yesterdayMonth}-${yesterdayDay} 06:00:00`; + } + + const body = { + "mar_goal": 2, + "page": 1, + "page_size": 10, + "order_by_type": 2, + "order_by_field": "create_time", + "start_time": start_time, + "end_time": end_time, + "smartBidType": 0, + "metrics": "stat_cost", + "adlab_mode": 1, + "dataSetKey": "site_promotion_list", + "smart_bid_type": 0, + "aavid": aadvid + }; + + try { + const response = await sendHttpRequest("POST", CONFIG.urls.fetchQianchuanData(aadvid), JSON.stringify(body), { + 'Content-Type': 'application/json' + }); + return response.data.totalMetrics.metrics.statCost.value; + } catch (error) { + log('Error fetching Qianchuan data: ' + error.message); + return 0; + } +} + + + +// 获取直播间转化漏斗数据 +async function fetchConvertData(roomId) { + try { + const url = CONFIG.urls.fetchConvertData(roomId); + const response = await sendHttpRequest("GET", url); + return { + exposure_ucnt: response.data.gmv_change.find(d => d.index_name === "直播间曝光人数").value, + watch_ucnt: response.data.gmv_change.find(d => d.index_name === "直播间观看人数").value, + product_exposure_ucnt: response.data.gmv_change.find(d => d.index_name === "商品曝光人数").value, + product_click_ucnt: response.data.gmv_change.find(d => d.index_name === "商品点击人数").value + }; + } catch (error) { + log('Error fetching flow data: ' + error.message); + return { + exposure_ucnt: 0, + watch_ucnt: 0, + product_exposure_ucnt: 0, + product_click_ucnt: 0 + }; + } +} + +// 存储数据,每个直播间单独一个key,不覆盖之前的数据 +function saveDataUsingGM(roomId, data) { + let existingData = GM_getValue(roomId, []); + if (!Array.isArray(existingData)) { + existingData = []; + } + existingData.push(data); // 将新的数据推入到现有数据数组中 + GM_setValue(roomId, existingData); +} + +// 读取数据 +function getDataUsingGM(roomId) { + let data = GM_getValue(roomId, []); + return Array.isArray(data) ? data : []; +} + +//延迟函数 +function delay(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + + +//计算直播场次所在的日期 +function calculateSessionDate(timestamp) { + const date = new Date(timestamp); + + // 直播时间在凌晨2点前,则认为是前一天的场次 + if (date.getHours() <= 2) { + date.setDate(date.getDate() - 1); + } + + // 创建一个新的日期对象,表示计算后的日期,并返回其时间戳 + const sessionDate = new Date(date.getFullYear(), date.getMonth(), date.getDate()); + return sessionDate.getTime(); +} + + +async function fetchAndUploadLiveRoomData() { + for (const room of CONFIG.rooms) { + try { + log(`Fetching live room id for ${room.name}`); + const liveRoomId = await fetchLiveRoomId(room.name); + + if (!liveRoomId) { + log(`未找到符合条件的直播间 ${room.name},跳过执行`); + continue; // 未找到符合条件的直播间,跳过执行 + } + + log(`Fetching live room start time for ${room.name}...`); + const start_time = await fetchLiveRoomStartTime(liveRoomId); + if (!start_time) { + Swal.fire({ + icon: 'error', + title: `无法获取直播间 ${room.name} 开始时间,请检查直播间ID`, + text: '使用默认开始时间 06:00:00', + }); + } + + log(`Fetching live room data for ${room.name}...`); + const liveRoomData = await fetchLiveRoomData(liveRoomId, room.type); + log('Live room data fetched successfully.'); + + await delay(2000); + + log(`Fetching Qianchuan data for ${room.name}...`); + const qianchuanData = await fetchQianchuanData(room.aadvid); + log('Qianchuan data fetched successfully.'); + + await delay(2000); + + log(`Fetching flow data for ${room.name}...`); + const convertData = await fetchConvertData(liveRoomId); + log('Flow data fetched successfully.'); + + const previousData = getDataUsingGM(liveRoomId); + + const currentTime = new Date().toLocaleString('zh-CN', { + timeZone: 'Asia/Shanghai' + }); + + let incrementData = {}; + + if (previousData.length > 0) { + const lastData = previousData[previousData.length - 1]; + incrementData = { + "直播间": room.name, // 添加直播间名字字段 + "GMV": liveRoomData.pay_amt / 100 - lastData.pay_amt, + "商品成交人数": liveRoomData.pay_ucnt - lastData.pay_ucnt, + "实时在线人数": liveRoomData.current_cnt, + "退款金额": liveRoomData.real_refund_amt / 100 - lastData.real_refund_amt, + "千川消耗": qianchuanData - lastData.qianchuan_cost, + "直播间曝光人数": convertData.exposure_ucnt - lastData.exposure_ucnt, + "直播观看人数": convertData.watch_ucnt - lastData.watch_ucnt, + "商品曝光人数": convertData.product_exposure_ucnt - lastData.product_exposure_ucnt, + "商品点击人数": convertData.product_click_ucnt - lastData.product_click_ucnt, + "直播间ID": liveRoomId, + "时间": new Date(currentTime).getTime(), + "直播间曝光次数": liveRoomData.live_show_cnt - lastData.live_show_cnt, + "直播观看次数": liveRoomData.live_show_watch_cnt_ratio * liveRoomData.live_show_cnt - lastData.live_show_cnt * lastData.live_show_watch_cnt_ratio, + "人均停留时长": (liveRoomData.avg_watch_duration * convertData.watch_ucnt - lastData.avg_watch_duration * lastData.watch_ucnt) / (convertData.watch_ucnt - lastData.watch_ucnt), + "直播场次日期": calculateSessionDate(new Date(currentTime).getTime()), + "差值时间": lastData.timestamp + }; + } else { + incrementData = { + "直播间": room.name, // 添加直播间名字字段 + "GMV": liveRoomData.pay_amt / 100, + "商品成交人数": liveRoomData.pay_ucnt, + "实时在线人数": liveRoomData.current_cnt, + "退款金额": liveRoomData.real_refund_amt / 100, + "千川消耗": qianchuanData, + "直播间曝光人数": convertData.exposure_ucnt, + "直播观看人数": convertData.watch_ucnt, + "商品曝光人数": convertData.product_exposure_ucnt, + "商品点击人数": convertData.product_click_ucnt, + "直播间ID": liveRoomId, + "时间": new Date(currentTime).getTime(), + "直播间曝光次数": liveRoomData.live_show_cnt, + "直播观看次数": liveRoomData.live_show_watch_cnt_ratio * liveRoomData.live_show_cnt, + "人均停留时长": liveRoomData.avg_watch_duration, + "直播场次日期": calculateSessionDate(new Date(currentTime).getTime()), + "差值时间": new Date(start_time).getTime() + + }; + } + log('Data extracted successfully'); + log(incrementData); + + // 获取tenant access token + log('Fetching tenant access token...'); + const accessToken = await fetchTenantAccessToken(CONFIG.id, CONFIG.secret); + log('Tenant access token fetched successfully.'); + + // 把计算后的数据写入到多维表格 + log(`Uploading live room data to bitable for ${room.name}...`); + const itemsToWrite = { + fields: incrementData + }; + await updateBitableRecords(accessToken, CONFIG.appId, CONFIG.tableId, itemsToWrite); + log('Live room data uploaded successfully.'); + + log(`Uploading live room data to localstorage for ${room.name}...`); + + // 写入localStorage + const dataToSave = { + room_name: room.name, + pay_amt: liveRoomData.pay_amt / 100, + pay_ucnt: liveRoomData.pay_ucnt, + real_refund_amt: liveRoomData.real_refund_amt / 100, + avg_watch_duration: liveRoomData.avg_watch_duration, + current_cnt: liveRoomData.current_cnt, + qianchuan_cost: qianchuanData, + exposure_ucnt: convertData.exposure_ucnt, + watch_ucnt: convertData.watch_ucnt, + product_exposure_ucnt: convertData.product_exposure_ucnt, + product_click_ucnt: convertData.product_click_ucnt, + liveRoomId: liveRoomId, + timestamp: new Date(currentTime).getTime(), + timestring: currentTime, + live_show_watch_cnt_ratio: liveRoomData.live_show_watch_cnt_ratio, + live_show_cnt: liveRoomData.live_show_cnt + }; + log(dataToSave); + saveDataUsingGM(liveRoomId, dataToSave); + log('Writing to localstorage successfully'); + + // 写入rawdata多维表格 + log(`Uploading saved data to another bitable table for ${room.name}...`); + await updateBitableRecords(accessToken, CONFIG.appId, CONFIG.rawDataTableId, { + fields: dataToSave + + }); + log('Saved data uploaded to another bitable table successfully.'); + + + } catch (error) { + log(`Error for ${room.name}: ` + error.message); + Swal.fire({ + icon: 'error', + title: `直播间 ${room.name} 出错了,请联系@汪喜`, + text: error.message, + }); + } + } +} + +(() => { + let runResult = "成功"; // 默认值为成功 + let logs = []; + + const log = message => { + logs.push(message); + console.log(message); + }; + + async function logRunResult() { + try { + const accessToken = await fetchTenantAccessToken(CONFIG.id, CONFIG.secret); + const runTime = new Date().getTime(); + const logData = { + fields: { + "表名": "直播间数据", + "表格id": CONFIG.tableId, + "最近运行时间": runTime, + "运行结果": runResult, + "日志": logs.join('\n') + } + }; + await updateBitableRecords(accessToken, CONFIG.appId, CONFIG.logTableId, logData); + log('Run result logged successfully.'); + } catch (error) { + log('Failed to log run result: ' + error.message); + } + } + + (async function() { + try { + log('Script started'); + await fetchAndUploadLiveRoomData(); + log('Data fetched and uploaded'); + } catch (error) { + runResult = "失败"; + log('Script execution failed: ' + error.message); + } finally { + await logRunResult(); + log('Run result logged'); + } + })(); +})(); \ No newline at end of file diff --git a/douyinLive/liveRoomHourData/liveRoomHourData.js b/douyinLive/liveRoomHourData/liveRoomHourData.js new file mode 100644 index 0000000..e66a2e8 --- /dev/null +++ b/douyinLive/liveRoomHourData/liveRoomHourData.js @@ -0,0 +1,696 @@ +// ==UserScript== +// @name 抖音直播间数据监控(全自动)半小时 +// @namespace https://bbs.tampermonkey.net.cn/ +// @version 2.3 +// @description 后台定时获取直播间数据,并在多维表格中显示 +// @author 汪喜 +// @require https://cdn.staticfile.net/sweetalert2/11.10.3/sweetalert2.all.min.js +// @match https://compass.jinritemai.com/screen/live/shop?live_room_id=* +// @match https://compass.jinritemai.com/screen/live/* +// @match https://compass.jinritemai.com/shop/live-detail?live_room_id=* +// @grant GM_xmlhttpRequest +// @grant GM_setValue +// @grant GM_getValue +// @grant GM_deleteValue +// @crontab */30 * * * * +// @connect open.feishu.cn +// @connect compass.jinritemai.com +// @connect compass.jinritemai.com +// @connect qianchuan.jinritemai.com +// ==/UserScript== + +/* +更新日志: +1. 支持多个直播间 +*/ + +const CONFIG = { + rooms: [ + { + name: "夸迪官方旗舰店", + aadvid: "1794468035923978", + type: "official", + }, + { + name: "夸迪官方旗舰店直播间", + aadvid: "1798388578394176", + type: "shop", + }, + ], + + id: "cli_a6f25876ea28100d", + secret: "raLC56ZLIara07nKigpysfoDxHTAeyJf", + appId: "Wx1Jb6chwagzmfsOtyycaYmLnpf", + tableId: "tblixcMnCd4GfF08", + logTableId: "tbld90KC73YBSu0B", + rawDataTableId: "tblbzx4nG0PbZ6US", + + 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`, + fetchLiveRoomId: + "https://compass.jinritemai.com/compass_api/shop/live/live_list/realtime?page_no=1&page_size=100", + /*fetchLiveRoomData: (roomId) => { + const indexSelected = encodeURIComponent("pay_ucnt,live_show_watch_cnt_ratio,avg_watch_duration,online_user_cnt,live_show_cnt,real_refund_amt"); + return `https://compass.jinritemai.com/compass_api/content_live/shop_official/live_screen/core_data?room_id=${roomId}&index_selected=${indexSelected}`; + }, + */ + fetchQianchuanData: (aadvid) => + `https://qianchuan.jinritemai.com/ad/api/pmc/v1/standard/get_summary_info?aavid=${aadvid}`, + fetchCommentsData: (roomId) => + `https://compass.jinritemai.com/compass_api/content_live/shop_official/live_screen/five_min_data?room_id=${roomId}`, //使用五分钟数据来加总评论次数 + fetchConvertData: (roomId) => + `https://compass.jinritemai.com/business_api/shop/live_room/flow/gmv_interaction?live_room_id=${roomId}`, + }, +}; + +let logs = []; +let liveRoomId = null; +let intervalId = null; +let timeoutId = null; +//let commentCounts = []; // 用于存储每五分钟获取到的评论次数 + +function log(message) { + logs.push(message); + console.log(message); +} + +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; + } + } + } +}; + +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}`); + } +} +//获取正在直播的直播间id +async function fetchLiveRoomId(authorNickName) { + const url = CONFIG.urls.fetchLiveRoomId; + try { + const response = await sendHttpRequest("GET", url); + const liveRooms = response.data.card_list; + const targetRoom = liveRooms.find( + (room) => room.author.author_nick_name === authorNickName + ); + if (targetRoom) { + return targetRoom.live_room_id; + } else { + throw new Error("未找到符合条件的直播间"); + } + } catch (error) { + log("Error fetching live room list: " + error.message); + return null; + } +} + +// 获取直播间大屏数据 +async function fetchLiveRoomData(roomId, roomType) { + try { + let url; + if (roomType === "official") { + const indexSelected = encodeURIComponent( + "pay_ucnt,live_show_watch_cnt_ratio,avg_watch_duration,online_user_cnt,live_show_cnt,real_refund_amt" + ); + url = `https://compass.jinritemai.com/compass_api/content_live/shop_official/live_screen/core_data?room_id=${roomId}&index_selected=${indexSelected}`; + } else if (roomType === "shop") { + const indexSelected = encodeURIComponent( + "pay_ucnt,watch_cnt_show_ratio,avg_watch_duration,current_cnt,live_show_cnt,real_refund_amt" + ); + url = `https://compass.jinritemai.com/compass_api/shop/live/live_screen/core_data?room_id=${roomId}&index_selected=${indexSelected}`; + } else { + throw new Error(`Unknown room type: ${roomType}`); + } + + const headers = { + accept: "application/json, text/plain, */*", + }; + const response = await sendHttpRequest("GET", url, null, headers); + + const data = response.data; + if (roomType === "official") { + return { + pay_amt: data.pay_amt.value, + pay_ucnt: data.core_data.find((d) => d.index_display === "成交人数") + .value.value, + real_refund_amt: data.core_data.find( + (d) => d.index_display === "退款金额" + ).value.value, + avg_watch_duration: data.core_data.find( + (d) => d.index_display === "人均观看时长" + ).value.value, + current_cnt: data.core_data.find( + (d) => d.index_display === "实时在线人数" + ).value.value, + live_show_watch_cnt_ratio: data.core_data.find( + (d) => d.index_display === "曝光-观看率(次数)" + ).value.value, + live_show_cnt: data.core_data.find( + (d) => d.index_display === "曝光次数" + ).value.value, + }; + } else if (roomType === "shop") { + // 根据实际返回的数据结构,提取所需字段 + // 这里假设返回的数据结构与官方直播间类似 + return { + pay_amt: data.pay_amt.value, + pay_ucnt: data.core_data.find((d) => d.index_display === "成交人数") + .value.value, + real_refund_amt: data.core_data.find( + (d) => d.index_display === "退款金额" + ).value.value, + avg_watch_duration: data.core_data.find( + (d) => d.index_display === "人均观看时长" + ).value.value, + current_cnt: data.core_data.find( + (d) => d.index_display === "实时在线人数" + ).value.value, + live_show_watch_cnt_ratio: data.core_data.find( + (d) => d.index_display === "曝光-观看率(次数)" + ).value.value, + live_show_cnt: data.core_data.find( + (d) => d.index_display === "曝光次数" + ).value.value, + }; + } + } catch (error) { + log("Error fetching live room data: " + error.message); + return { + pay_amt: 0, + pay_ucnt: 0, + real_refund_amt: 0, + avg_watch_duration: 0, + current_cnt: 0, + live_show_watch_cnt_ratio: 0, + live_show_cnt: 0, + }; + } +} + +/* +//获取评论次数的数据 +async function fetchCommentsData(roomId) { + try { + const url = CONFIG.urls.fetchCommentsData(roomId); + const response = await sendHttpRequest('GET', url); + const comments = response.data.card.find(d => d.index_display === '评论次数').value.value; + const updateTimeStr = response.data.update_time.replace("更新", "").trim(); + const updateTime = new Date(updateTimeStr).getTime(); + return { count: comments, timestamp: updateTime }; + } catch (error) { + log('Error fetching interaction data: ' + error.message); + return { count: 0, timestamp: new Date().getTime() }; + } +} + +// 每五分钟获取一次评论数据 +async function fetchComments() { + try { + log('Fetching comments data...'); + const commentsData = await fetchCommentsData(liveRoomId); + commentCounts.push(commentsData); + log(commentCounts) + log(`Fetched comments count: ${commentsData.count} at ${new Date(commentsData.timestamp).toLocaleString()}`); + } catch (error) { + log('Error fetching comments data: ' + error.message); + } +} + +// 初始化评论次数数组并设置定时器 +async function initializeCommentFetching() { + if (intervalId !== null) { + clearInterval(intervalId); + } + commentCounts = []; + await fetchComments(); // 立即执行一次 + intervalId = setInterval(fetchComments, 5 * 60 * 1000); // 每五分钟重新获取一次评论数据 +} + +// 计算过去一小时的评论次数 +function getCommentsCountLastHour() { + log(`Current commentCounts array: ${JSON.stringify(commentCounts)}`); + const oneHourAgo = new Date().getTime() - (60 * 60 * 1000); + const filteredComments = commentCounts.filter(comment => comment.timestamp >= oneHourAgo); + log(`Filtered comments array: ${JSON.stringify(filteredComments)}`); + const totalComments = filteredComments.reduce((total, comment) => total + comment.count, 0); + log(`Total comments in the last hour: ${totalComments}`); + return totalComments; +} + +*/ + +//获取直播间开始时间数据 +async function fetchLiveRoomStartTime(roomId) { + const url = `https://compass.jinritemai.com/compass_api/shop/live/live_screen/live_base_info?room_id=${roomId}`; + try { + const response = await sendHttpRequest("GET", url); + return response.data.live_start_time; + } catch (error) { + log("Error fetching live room start time: " + error.message); + return null; // 默认返回 null + } +} + +// 获取千川消耗金额数据 +async function fetchQianchuanData(aadvid) { + const now = new Date(); + const formatNumber = (num) => num.toString().padStart(2, "0"); + + // 结束时间为动态的当前时间 + const year = now.getFullYear(); + const month = formatNumber(now.getMonth() + 1); // 月份从0开始 + const day = formatNumber(now.getDate()); + const hours = formatNumber(now.getHours()); + const minutes = formatNumber(now.getMinutes()); + const seconds = formatNumber(now.getSeconds()); + const end_time = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; + + let start_time; + + log("Failed to fetch start time, using default start time."); + if (now.getHours() >= 6) { + // 时间超过6点,则使用当天6点作为开始时间 + start_time = `${year}-${month}-${day} 06:00:00`; + } else { + // 如果当前时间在0点到6点之间,则开始时间为昨天的6点 + const yesterday = new Date(now); + yesterday.setDate(now.getDate() - 1); + const yesterdayYear = yesterday.getFullYear(); + const yesterdayMonth = formatNumber(yesterday.getMonth() + 1); + const yesterdayDay = formatNumber(yesterday.getDate()); + start_time = `${yesterdayYear}-${yesterdayMonth}-${yesterdayDay} 06:00:00`; + } + + const body = { + DataSetKey: "home_cost_total_prom", + Dimensions: ["adlab_mode", "pricing_category", "qcpx_category"], + Metrics: ["stat_cost"], + StartTime: start_time, + EndTime: end_time, + Filters: { + Conditions: [ + { + Field: "advertiser_id", + Values: [aadvid], + Operator: 7, + }, + { + Field: "ecp_app_id", + Values: ["1", "2"], + Operator: 7, + }, + { + Field: "query_self_data", + Values: ["off"], + Operator: 7, + }, + ], + ConditionRelationshipType: 1, + }, + }; + + try { + const response = await sendHttpRequest( + "POST", + `https://qianchuan.jinritemai.com/ad/api/data/v1/common/statQuery?reqFrom=data-summary&aavid=${aadvid}`, + JSON.stringify(body), + { + "Content-Type": "application/json", + } + ); + return response.data.StatsData.Totals.stat_cost.Value; + } catch (error) { + log("Error fetching Qianchuan data: " + error.message); + return 0; + } +} + +// 获取直播间转化漏斗数据 +async function fetchConvertData(roomId) { + try { + const url = CONFIG.urls.fetchConvertData(roomId); + const response = await sendHttpRequest("GET", url); + return { + exposure_ucnt: response.data.gmv_change.find( + (d) => d.index_name === "直播间曝光人数" + ).value, + watch_ucnt: response.data.gmv_change.find( + (d) => d.index_name === "直播间观看人数" + ).value, + product_exposure_ucnt: response.data.gmv_change.find( + (d) => d.index_name === "商品曝光人数" + ).value, + product_click_ucnt: response.data.gmv_change.find( + (d) => d.index_name === "商品点击人数" + ).value, + }; + } catch (error) { + log("Error fetching flow data: " + error.message); + return { + exposure_ucnt: 0, + watch_ucnt: 0, + product_exposure_ucnt: 0, + product_click_ucnt: 0, + }; + } +} + +// 存储数据,每个直播间单独一个key,不覆盖之前的数据 +function saveDataUsingGM(roomId, data) { + let existingData = GM_getValue(roomId, []); + if (!Array.isArray(existingData)) { + existingData = []; + } + existingData.push(data); // 将新的数据推入到现有数据数组中 + GM_setValue(roomId, existingData); +} + +// 读取数据 +function getDataUsingGM(roomId) { + let data = GM_getValue(roomId, []); + return Array.isArray(data) ? data : []; +} + +//延迟函数 +function delay(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +//计算直播场次所在的日期 +function calculateSessionDate(timestamp) { + const date = new Date(timestamp); + + // 直播时间在凌晨2点前,则认为是前一天的场次 + if (date.getHours() <= 2) { + date.setDate(date.getDate() - 1); + } + + // 创建一个新的日期对象,表示计算后的日期,并返回其时间戳 + const sessionDate = new Date( + date.getFullYear(), + date.getMonth(), + date.getDate() + ); + return sessionDate.getTime(); +} + +async function fetchAndUploadLiveRoomData() { + for (const room of CONFIG.rooms) { + try { + log(`Fetching live room id for ${room.name}`); + const liveRoomId = await fetchLiveRoomId(room.name); + + if (!liveRoomId) { + log(`未找到符合条件的直播间 ${room.name},跳过执行`); + continue; // 未找到符合条件的直播间,跳过执行 + } + + log(`Fetching live room start time for ${room.name}...`); + const start_time = await fetchLiveRoomStartTime(liveRoomId); + if (!start_time) { + Swal.fire({ + icon: "error", + title: `无法获取直播间 ${room.name} 开始时间,请检查直播间ID`, + text: "使用默认开始时间 06:00:00", + }); + } + + log(`Fetching live room data for ${room.name}...`); + const liveRoomData = await fetchLiveRoomData(liveRoomId, room.type); + log("Live room data fetched successfully."); + + await delay(2000); + + log(`Fetching Qianchuan data for ${room.name}...`); + const qianchuanData = await fetchQianchuanData(room.aadvid); + log("Qianchuan data fetched successfully."); + + await delay(2000); + + log(`Fetching flow data for ${room.name}...`); + const convertData = await fetchConvertData(liveRoomId); + log("Flow data fetched successfully."); + + const previousData = getDataUsingGM(liveRoomId); + + const currentTime = new Date().toLocaleString("zh-CN", { + timeZone: "Asia/Shanghai", + }); + + let incrementData = {}; + + if (previousData.length > 0) { + const lastData = previousData[previousData.length - 1]; + incrementData = { + 直播间: room.name, // 添加直播间名字字段 + GMV: liveRoomData.pay_amt / 100 - lastData.pay_amt, + 商品成交人数: liveRoomData.pay_ucnt - lastData.pay_ucnt, + 实时在线人数: liveRoomData.current_cnt, + 退款金额: + liveRoomData.real_refund_amt / 100 - lastData.real_refund_amt, + 千川消耗: qianchuanData - lastData.qianchuan_cost, + 直播间曝光人数: convertData.exposure_ucnt - lastData.exposure_ucnt, + 直播观看人数: convertData.watch_ucnt - lastData.watch_ucnt, + 商品曝光人数: + convertData.product_exposure_ucnt - lastData.product_exposure_ucnt, + 商品点击人数: + convertData.product_click_ucnt - lastData.product_click_ucnt, + 直播间ID: liveRoomId, + 时间: new Date(currentTime).getTime(), + 直播间曝光次数: liveRoomData.live_show_cnt - lastData.live_show_cnt, + 直播观看次数: + liveRoomData.live_show_watch_cnt_ratio * + liveRoomData.live_show_cnt - + lastData.live_show_cnt * lastData.live_show_watch_cnt_ratio, + 人均停留时长: + (liveRoomData.avg_watch_duration * convertData.watch_ucnt - + lastData.avg_watch_duration * lastData.watch_ucnt) / + (convertData.watch_ucnt - lastData.watch_ucnt), + 直播场次日期: calculateSessionDate(new Date(currentTime).getTime()), + 差值时间: lastData.timestamp, + }; + } else { + incrementData = { + 直播间: room.name, // 添加直播间名字字段 + GMV: liveRoomData.pay_amt / 100, + 商品成交人数: liveRoomData.pay_ucnt, + 实时在线人数: liveRoomData.current_cnt, + 退款金额: liveRoomData.real_refund_amt / 100, + 千川消耗: qianchuanData, + 直播间曝光人数: convertData.exposure_ucnt, + 直播观看人数: convertData.watch_ucnt, + 商品曝光人数: convertData.product_exposure_ucnt, + 商品点击人数: convertData.product_click_ucnt, + 直播间ID: liveRoomId, + 时间: new Date(currentTime).getTime(), + 直播间曝光次数: liveRoomData.live_show_cnt, + 直播观看次数: + liveRoomData.live_show_watch_cnt_ratio * liveRoomData.live_show_cnt, + 人均停留时长: liveRoomData.avg_watch_duration, + 直播场次日期: calculateSessionDate(new Date(currentTime).getTime()), + 差值时间: new Date(start_time).getTime(), + }; + } + log("Data extracted successfully"); + log(incrementData); + + // 获取tenant access token + log("Fetching tenant access token..."); + const accessToken = await fetchTenantAccessToken( + CONFIG.id, + CONFIG.secret + ); + log("Tenant access token fetched successfully."); + + // 把计算后的数据写入到多维表格 + log(`Uploading live room data to bitable for ${room.name}...`); + const itemsToWrite = { + fields: incrementData, + }; + await updateBitableRecords( + accessToken, + CONFIG.appId, + CONFIG.tableId, + itemsToWrite + ); + log("Live room data uploaded successfully."); + + log(`Uploading live room data to localstorage for ${room.name}...`); + + // 写入localStorage + const dataToSave = { + room_name: room.name, + pay_amt: liveRoomData.pay_amt / 100, + pay_ucnt: liveRoomData.pay_ucnt, + real_refund_amt: liveRoomData.real_refund_amt / 100, + avg_watch_duration: liveRoomData.avg_watch_duration, + current_cnt: liveRoomData.current_cnt, + qianchuan_cost: qianchuanData, + exposure_ucnt: convertData.exposure_ucnt, + watch_ucnt: convertData.watch_ucnt, + product_exposure_ucnt: convertData.product_exposure_ucnt, + product_click_ucnt: convertData.product_click_ucnt, + liveRoomId: liveRoomId, + timestamp: new Date(currentTime).getTime(), + timestring: currentTime, + live_show_watch_cnt_ratio: liveRoomData.live_show_watch_cnt_ratio, + live_show_cnt: liveRoomData.live_show_cnt, + }; + log(dataToSave); + saveDataUsingGM(liveRoomId, dataToSave); + log("Writing to localstorage successfully"); + + // 写入rawdata多维表格 + log(`Uploading saved data to another bitable table for ${room.name}...`); + await updateBitableRecords( + accessToken, + CONFIG.appId, + CONFIG.rawDataTableId, + { + fields: dataToSave, + } + ); + log("Saved data uploaded to another bitable table successfully."); + } catch (error) { + log(`Error for ${room.name}: ` + error.message); + Swal.fire({ + icon: "error", + title: `直播间 ${room.name} 出错了,请联系@汪喜`, + text: error.message, + }); + } + } +} + +(() => { + let runResult = "成功"; // 默认值为成功 + let logs = []; + + const log = (message) => { + logs.push(message); + console.log(message); + }; + + async function logRunResult() { + try { + const accessToken = await fetchTenantAccessToken( + CONFIG.id, + CONFIG.secret + ); + const runTime = new Date().getTime(); + const logData = { + fields: { + 表名: "直播间数据", + 表格id: CONFIG.tableId, + 最近运行时间: runTime, + 运行结果: runResult, + 日志: logs.join("\n"), + }, + }; + await updateBitableRecords( + accessToken, + CONFIG.appId, + CONFIG.logTableId, + logData + ); + log("Run result logged successfully."); + } catch (error) { + log("Failed to log run result: " + error.message); + } + } + + (async function () { + try { + log("Script started"); + await fetchAndUploadLiveRoomData(); + log("Data fetched and uploaded"); + } catch (error) { + runResult = "失败"; + log("Script execution failed: " + error.message); + } finally { + await logRunResult(); + log("Run result logged"); + } + })(); +})(); diff --git a/douyinShop/douyinAftersale/douyinAftersale.js b/douyinShop/douyinAftersale/douyinAftersale.js new file mode 100644 index 0000000..3c5afba --- /dev/null +++ b/douyinShop/douyinAftersale/douyinAftersale.js @@ -0,0 +1,287 @@ +// ==UserScript== +// @name 抖音店铺售后监控 +// @namespace https://bbs.tampermonkey.net.cn/ +// @version 0.2.0 +// @description 每小时获取前2小时的店铺售后数据,并去重后上传到飞书多维表格 +// @author wanxi +// @crontab 5 * * * * +// @grant GM_xmlhttpRequest +// ==/UserScript== + +const CONFIG = { + id: "", + secret: "", + appId: "", + tableId: "", // 表格ID + logTableId: "", // 运行记录表 + 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`, + fetchAfterSales: () => `https://fxg.jinritemai.com/after_sale/pc/list`, + }, +}; + +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, afterSaleId) { + const url = CONFIG.urls.bitableSearchRecords(appId, tableId); + const headers = { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }; + const body = JSON.stringify({ + field_names: ["售后单号"], + filter: { + conjunction: "and", + conditions: [ + { + field_name: "售后单号", + operator: "is", + value: [afterSaleId], + }, + ], + }, + 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}`); + } +} + +async function fetchAfterSales(page, pageSize = 50, startTime, endTime) { + const url = CONFIG.urls.fetchAfterSales(); + const headers = { + accept: "application/json, text/plain, */*", + "content-type": "application/json;charset=UTF-8", + }; + const body = JSON.stringify({ + pageSize: pageSize, + page: page, + apply_time_start: startTime, + apply_time_end: endTime, + }); + + try { + const response = await sendHttpRequest("POST", url, body, headers); + return response; + } catch (error) { + console.error("Error fetching after sales:", error); + throw error; + } +} + +async function fetchAllAfterSales() { + let allAfterSales = []; + let page = 0; + let hasMore = true; + + //秒级时间戳 + const endTime = Math.floor(Date.now() / 1000); + const startTime = endTime - 2 * 3600; + //const startTime = 1721516400; + //const endTime = 1721523600; + + while (hasMore) { + const data = await fetchAfterSales(page, 50, startTime, endTime); + if (data.code !== 0) { + result = "Failed"; + throw new Error(`Error fetching after sales: ${data.msg}`); + } + if (data.data && data.data.items.length > 0) { + allAfterSales = allAfterSales.concat(data.data.items); + page++; + hasMore = data.data.has_more; + } else { + hasMore = false; + } + } + + return allAfterSales; +} + +async function fetchAndUploadAfterSales() { + try { + log("Fetching after sales data..."); + const afterSales = await fetchAllAfterSales(); + log("After sales data fetched successfully."); + + log("Fetching tenant access token..."); + const accessToken = await fetchTenantAccessToken(CONFIG.id, CONFIG.secret); + log("Tenant access token fetched successfully."); + + log("Uploading after sales data to bitable..."); + for (const afterSale of afterSales) { + const exists = await checkIfRecordExists( + accessToken, + CONFIG.appId, + CONFIG.tableId, + afterSale.after_sale_info.after_sale_id + ); + if (!exists) { + const fields = { + 售后单号: afterSale.after_sale_info.after_sale_id, + 售后类型: afterSale.after_sale_info.after_sale_tags[0].text, + 售后原因: afterSale.text_part.reason_text, + 申请时间: afterSale.after_sale_info.apply_time * 1000, + 退款金额: afterSale.after_sale_info.refund_amount / 100, + 订单创建时间: + afterSale.order_info.related_order_info[0].create_time * 1000, + 订单id: afterSale.order_info.related_order_info[0].sku_order_id, + 订单商品id: afterSale.order_info.related_order_info[0].product_id, + 订单商品名称: afterSale.order_info.related_order_info[0].product_name, + 店铺: "夸迪官方旗舰店", + }; + const itemsToWrite = { fields: fields }; + await updateBitableRecords( + accessToken, + CONFIG.appId, + CONFIG.tableId, + itemsToWrite + ); + } + } + log("After sales data uploaded successfully."); + } catch (error) { + log("Error: " + error.message); + result = "Failed"; + throw error; + } +} + +async function logRunResult() { + try { + const accessToken = await fetchTenantAccessToken(CONFIG.id, CONFIG.secret); + const runTime = new Date().getTime(); + const logData = { + fields: { + 表名: "售后", + 表格id: CONFIG.tableId, + 最近运行时间: runTime, + 运行结果: result, + 日志: logs.join("\n"), + }, + }; + await updateBitableRecords( + accessToken, + CONFIG.appId, + CONFIG.logTableId, + logData + ); + log("Run result logged successfully."); + } catch (error) { + log("Failed to log run result: " + error.message); + } +} + +(async function () { + try { + await fetchAndUploadAfterSales(); + } catch (error) { + log("Script execution failed: " + error.message); + } finally { + await logRunResult(); + } +})(); diff --git a/douyinShop/douyinComment/douyinComment.js b/douyinShop/douyinComment/douyinComment.js new file mode 100644 index 0000000..c284256 --- /dev/null +++ b/douyinShop/douyinComment/douyinComment.js @@ -0,0 +1,285 @@ +// ==UserScript== +// @name 抖音店铺评价监控 +// @namespace https://bbs.tampermonkey.net.cn/ +// @version 0.2.0 +// @description 每小时获取前2小时的店铺评价数据,并去重后上传到飞书多维表格 +// @author wanxi +// @crontab 1 * * * * +// @grant GM_xmlhttpRequest +// @connect open.feishu.cn +// @connect fxg.jinritemai.com +// ==/UserScript== + +const CONFIG = { + id: "cli_a6f25876ea28100d", + secret: "raLC56ZLIara07nKigpysfoDxHTAeyJf", + appId: "GyUUbEzuxajfU4sGieIcNKxvnXd", + tableId: "tblfMlqE1lKBEnR4", // 表格ID + logTableId: "tbl6eZJpt9GkZjWO", // 运行记录表 + 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`, + fetchComments: (page, pageSize, startTime, endTime) => + `https://fxg.jinritemai.com/product/tcomment/commentList?rank=0&content_search=0&reply_search=0&appeal_search=0&comment_time_from=${startTime}&comment_time_to=${endTime}&pageSize=${pageSize}&page=${page}`, + }, +}; + +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, commentId) { + 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: [commentId], + }, + ], + }, + 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}`); + } +} + +async function fetchComments(page, pageSize = 50, startTime, endTime) { + const url = CONFIG.urls.fetchComments(page, pageSize, startTime, endTime); + const headers = { + accept: "application/json, text/plain, */*", + }; + + try { + const response = await sendHttpRequest("GET", url, null, headers); + return response; + } catch (error) { + console.error("Error fetching comments:", error); + throw error; + } +} + +async function fetchAllComments() { + let allComments = []; + let page = 0; + let hasMore = true; + //秒级时间戳 + + const endTime = Math.floor(Date.now() / 1000); + const startTime = endTime - 2 * 3600; + + //const startTime = 1721516400; + //const endTime = 1721523600; + + while (hasMore) { + const data = await fetchComments(page, 50, startTime, endTime); + if (data.code !== 0) { + result = "Failed"; + throw new Error(`Error fetching comments: ${data.msg}`); + } + if (data.data && data.data.length > 0) { + allComments = allComments.concat(data.data); + page++; + } else { + hasMore = false; + } + } + + return allComments; +} + +async function fetchAndUploadComments() { + try { + log("Fetching comments data..."); + const comments = await fetchAllComments(); + log("Comments data fetched successfully."); + + log("Fetching tenant access token..."); + const accessToken = await fetchTenantAccessToken(CONFIG.id, CONFIG.secret); + log("Tenant access token fetched successfully."); + + log("Uploading comments data to bitable..."); + for (const comment of comments) { + const exists = await checkIfRecordExists( + accessToken, + CONFIG.appId, + CONFIG.tableId, + comment.id + ); + if (!exists) { + const fields = { + 评价id: comment.id, + 评价时间: comment.comment_time * 1000, + 商品id: comment.product_id, + 店铺评分: comment.rank_shop, + 物流评分: comment.rank_logistic, + 商品评分: comment.rank_product, + 综合评分: comment.rank, + 评价标签: comment.tags.rank_info.name, + SKU: comment.sku, + 店铺名: "夸迪官方旗舰店", + 订单id: comment.order_id, + 商品名称: comment.product.name, + 评价内容: comment.content, + }; + const itemsToWrite = { fields: fields }; + await updateBitableRecords( + accessToken, + CONFIG.appId, + CONFIG.tableId, + itemsToWrite + ); + } + } + log("Comments data uploaded successfully."); + } catch (error) { + log("Error: " + error.message); + result = "Failed"; + throw error; + } +} + +async function logRunResult() { + try { + const accessToken = await fetchTenantAccessToken(CONFIG.id, CONFIG.secret); + const runTime = new Date().getTime(); + const logData = { + fields: { + 表名: "评价", + 表格id: CONFIG.tableId, + 最近运行时间: runTime, + 运行结果: result, + 日志: logs.join("\n"), + }, + }; + await updateBitableRecords( + accessToken, + CONFIG.appId, + CONFIG.logTableId, + logData + ); + log("Run result logged successfully."); + } catch (error) { + log("Failed to log run result: " + error.message); + } +} + +(async function () { + try { + await fetchAndUploadComments(); + } catch (error) { + log("Script execution failed: " + error.message); + } finally { + await logRunResult(); + } +})(); diff --git a/douyinShop/douyinExperienceScore/douyinExperienceScore.js b/douyinShop/douyinExperienceScore/douyinExperienceScore.js new file mode 100644 index 0000000..ab179f7 --- /dev/null +++ b/douyinShop/douyinExperienceScore/douyinExperienceScore.js @@ -0,0 +1,361 @@ +// ==UserScript== +// @name 抖音商家体验分监控 +// @namespace https://bbs.tampermonkey.net.cn/ +// @version 0.4.0 +// @description 每天更新前一日的体验分数据 +// @author wanxi +// @crontab 0 8-19 * * * +// @grant GM_xmlhttpRequest +// ==/UserScript== + +const CONFIG = { + id: "cli_a6f25876ea28100d", + secret: "raLC56ZLIara07nKigpysfoDxHTAeyJf", + appId: "GyUUbEzuxajfU4sGieIcNKxvnXd", + tableId1: "tblyXVQVqwfcyaoK", // 维度表 + tableId2: "tbl8JEts30wvgWv3", // 指标表 + logTableId: "tbl6eZJpt9GkZjWO", // 运行记录表 + dimensionDict: [ + { + 维度: "商品体验", + 指标: ["商品差评率", "商品品质退货率"], + }, + { + 维度: "物流体验", + 指标: ["运单配送时效达成率", "24小时支付-揽收率", "发货问题负向反馈率"], + }, + { + 维度: "服务体验", + 指标: [ + "仅退款自主完结时长", + "退货退款自主完结时长", + "飞鸽平均响应时长", + "飞鸽不满意率", + "平台求助率", + "售后拒绝率", + ], + }, + ], + urls: { + overview: + "https://fxg.jinritemai.com/governance/shop/experiencescore/getOverviewByVersion?exp_version=8.0&source=1", + analysisScore: + "https://fxg.jinritemai.com/governance/shop/experiencescore/getAnalysisScore?new_dimension=true&time=30&filter_by_industry=true&number_type=30&exp_version=8.0", + tenantAccessToken: + "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal", + bitableRecords: (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`, + }, +}; + +let logs = []; +let result = "Success"; + +function log(message) { + logs.push(message); + console.log(message); +} + +async function sendHttpRequest( + method, + url, + body = null, + headers = { + "Content-Type": "application/json", + } +) { + return 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 checkIfRecordExists(accessToken, appId, tableId, date) { + try { + const response = await sendHttpRequest( + "POST", + CONFIG.urls.bitableSearchRecords(appId, tableId), + JSON.stringify({ + field_names: ["数据时间"], + filter: { + conjunction: "and", + conditions: [ + { + field_name: "数据时间", + operator: "is", + value: [date], + }, + ], + }, + }), + { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + } + ); + return response.data.total > 0; + } catch (error) { + throw new Error(`Failed to check if record exists in Bitable: ${error}`); + } +} + +async function updateBitableRecords(accessToken, appId, tableId, items) { + try { + const response = await sendHttpRequest( + "POST", + CONFIG.urls.bitableRecords(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}`); + } +} + +function getDimension(indicator) { + for (const dimension of CONFIG.dimensionDict) { + if (dimension.指标.includes(indicator)) { + return dimension.维度; + } + } + return null; +} + +function isYesterday(date) { + const today = new Date(); + const yesterday = new Date(today); + yesterday.setDate(today.getDate() - 1); + return date.toDateString() === yesterday.toDateString(); +} + +async function fetchAndUploadData() { + try { + // 获取TenantAccessToken + log("Fetching tenant access token..."); + const accessToken = await fetchTenantAccessToken(CONFIG.id, CONFIG.secret); + log("Tenant access token fetched successfully."); + + /* + // 获取昨天的日期 + const today = new Date(); + const yesterday = new Date(today); + yesterday.setDate(today.getDate() - 1); + const yesterdayStr = yesterday.toISOString().split('T')[0]; + */ + + // 检查昨天的数据是否已经存在 + log("Checking if yesterday's data already exists..."); + //https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-record/record-filter-guide + const recordExists = await checkIfRecordExists( + accessToken, + CONFIG.appId, + CONFIG.tableId1, + "Yesterday" + ); + + if (recordExists) { + log("Yesterday's data already exists. Skipping data upload."); + return; + } + + // 细分指标里面有current_date,先请求细分指标 + log("Fetching analysis score data..."); + const analysisData = await sendHttpRequest( + "GET", + CONFIG.urls.analysisScore + ); + log("analysisData: " + JSON.stringify(analysisData)); + + if (!analysisData || !analysisData.data) { + throw new Error("Invalid analysisData structure"); + } + + const currentDate = new Date(Date.parse(analysisData.data.current_date)); + + if (!isYesterday(currentDate)) { + log("Current date is not yesterday. Skipping data upload."); + return; + } + + const analysisScore = analysisData.data.shop_analysis.map((item) => ({ + 指标: item.title, + 维度: getDimension(item.title), + 数据时间: new Date(currentDate).getTime(), + 数值无单位: item.value.value_figure, + 环比: item.compare_with_self.rise_than_yesterday, + 超越同行: item.surpass_peers.value_figure, + 等级: item.level, + })); + log("Analysis score data fetched successfully."); + + // 将指标分写入多维表格 + log("Uploading analysis score data to bitable..."); + for (const item of analysisScore) { + const itemsToWrite = { + fields: item, + }; + await updateBitableRecords( + accessToken, + CONFIG.appId, + CONFIG.tableId2, + itemsToWrite + ); + } + log("Analysis score data uploaded successfully."); + + // 维度分 + log("Fetching overview data..."); + const overviewData = await sendHttpRequest("GET", CONFIG.urls.overview); + log("overviewData: " + JSON.stringify(overviewData)); + + if (!overviewData || !overviewData.data) { + throw new Error("Invalid overviewData structure"); + } + + const scores = [ + { + 维度: "商品体验分", + 得分: overviewData.data.goods_score.value, + 较前一日: overviewData.data.goods_score.rise_than_yesterday, + 数据时间: new Date(currentDate).getTime(), + }, + { + 维度: "物流体验分", + 得分: overviewData.data.logistics_score.value, + 较前一日: overviewData.data.logistics_score.rise_than_yesterday, + 数据时间: new Date(currentDate).getTime(), + }, + { + 维度: "服务体验分", + 得分: overviewData.data.service_score.value, + 较前一日: overviewData.data.service_score.rise_than_yesterday, + 数据时间: new Date(currentDate).getTime(), + }, + { + 维度: "商家体验分", + 得分: overviewData.data.experience_score.value, + 较前一日: overviewData.data.experience_score.rise_than_yesterday, + 数据时间: new Date(currentDate).getTime(), + }, + ]; + log("Overview data fetched successfully."); + + log("Uploading overview data to bitable..."); + for (const item of scores) { + const itemsToWrite = { + fields: item, + }; + await updateBitableRecords( + accessToken, + CONFIG.appId, + CONFIG.tableId1, + itemsToWrite + ); + } + log("Overview data uploaded successfully."); + } catch (error) { + log("Error: " + error.message); + result = "Failed"; + throw error; + } +} + +async function logRunResult() { + try { + const accessToken = await fetchTenantAccessToken(CONFIG.id, CONFIG.secret); + const runTime = new Date().getTime(); + + const logData1 = { + fields: { + 表名: "商家体验分-维度", + 表格id: CONFIG.tableId1, + 最近运行时间: runTime, + 运行结果: result, + 日志: logs.join("\n"), + }, + }; + + const logData2 = { + fields: { + 表名: "商家体验分-指标", + 表格id: CONFIG.tableId2, + 最近运行时间: runTime, + 运行结果: result, + 日志: logs.join("\n"), + }, + }; + + await updateBitableRecords( + accessToken, + CONFIG.appId, + CONFIG.logTableId, + logData1 + ); + await updateBitableRecords( + accessToken, + CONFIG.appId, + CONFIG.logTableId, + logData2 + ); + + log("Run result logged successfully."); + } catch (error) { + log("Failed to log run result: " + error.message); + } +} + +(async function () { + try { + await fetchAndUploadData(); + } catch (error) { + log("Script execution failed: " + error.message); + } finally { + await logRunResult(); + } +})(); diff --git a/yuntu/yuntuHotItems/yuntuHotItems.js b/yuntu/yuntuHotItems/yuntuHotItems.js new file mode 100644 index 0000000..43b57f7 --- /dev/null +++ b/yuntu/yuntuHotItems/yuntuHotItems.js @@ -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(); + } +})(); \ No newline at end of file diff --git a/yuntu/yuntuLowCPMItems/yuntuLowCPMItems.js b/yuntu/yuntuLowCPMItems/yuntuLowCPMItems.js new file mode 100644 index 0000000..b7dc13c --- /dev/null +++ b/yuntu/yuntuLowCPMItems/yuntuLowCPMItems.js @@ -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小于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(); +})();