// ==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(); } })();