diff --git a/chanmama/productHistoryKOCHelper.user.js b/chanmama/productHistoryKOCHelper.user.js index 57df6db..a146d02 100644 --- a/chanmama/productHistoryKOCHelper.user.js +++ b/chanmama/productHistoryKOCHelper.user.js @@ -14,12 +14,13 @@ // @grant GM_setClipboard // @connect chanmama.com // @connect api-service.chanmama.com -// @require https://cdn.jsdelivr.net/npm/sweetalert2@11 +// @connect open.feishu.cn +// @require https://unpkg.com/sweetalert2@11 // @license MIT // ==/UserScript== /* ==UserConfig== -settings: +basic: enabled: title: 启用功能 description: 是否启用蝉妈妈产品历史KOC助手功能 @@ -38,23 +39,94 @@ settings: description: 启用详细日志输出 type: checkbox default: false +--- +feishu: + enableSync: + title: 启用飞书同步 + description: 是否将采集到的数据同步到飞书多维表格 + type: checkbox + default: false + appId: + title: 飞书应用ID + description: 飞书开放平台应用的App ID + type: text + default: "cli_a73b734d0564d01c" + appSecret: + title: 飞书应用密钥 + description: 飞书开放平台应用的App Secret + type: text + default: "xeKSIOpQiVW6oHeJPH4EvlZMZ1QmnCxx" + password: true + bitableAppId: + title: 多维表格App ID + description: 飞书多维表格的App ID + type: text + default: "ILIYb7MUCatbfNsdUMccHsgjnLf" + tableId: + title: 数据表ID + description: 存储KOC数据的表格ID + type: text + default: "tblK71li9kOfOc7C" +filters: + amountThreshold: + title: 金额门槛 + description: 预估关联商品视频销售金额的最低门槛(元),低于此值停止采集 + type: number + default: 10000 + videoRatioThreshold: + title: 视频带货占比门槛 + description: 视频销售金额占总销售金额的比例门槛,如0.5表示视频销售需占总销售的50%以上 + type: number + default: 0.5 + maxFollowers: + title: 粉丝数上限 + description: 达人粉丝数上限,只采集粉丝数少于此数值的达人 + type: number + default: 500000 ==/UserConfig== */ (function() { 'use strict'; + // 检测依赖库加载状态 + function checkLibraryStatus() { + if (typeof Swal !== 'undefined') { + log('SweetAlert2 库加载成功'); + } else { + log('SweetAlert2 库未加载,将使用备用方案', 'warn'); + } + } + // 配置常量 const CONFIG = { - debug: true, // 强制开启调试模式 - enabled: GM_getValue('settings.enabled', true), - timeout: GM_getValue('settings.timeout', 30) * 1000, + debug: GM_getValue('basic.debug', false), + enabled: GM_getValue('basic.enabled', true), + timeout: GM_getValue('basic.timeout', 30) * 1000, api: { baseUrl: 'https://api-service.chanmama.com', endpoint: '/v1/product/author/analysis' }, pagination: { pageSize: 30, - amountThreshold: 10000 // 金额门槛,低于此值停止翻页 + amountThreshold: GM_getValue('filters.amountThreshold', 10000) + }, + filters: { + videoRatioThreshold: GM_getValue('filters.videoRatioThreshold', 0.5), + maxFollowers: GM_getValue('filters.maxFollowers', 500000) + }, + feishu: { + enabled: GM_getValue('feishu.enableSync', false), + appId: GM_getValue('feishu.appId', ''), + appSecret: GM_getValue('feishu.appSecret', ''), + bitableAppId: GM_getValue('feishu.bitableAppId', ''), + tableId: GM_getValue('feishu.tableId', ''), + urls: { + tenantAccessToken: "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal", + bitableBatchCreate: (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` + } } }; @@ -88,6 +160,13 @@ settings: timeout: CONFIG.timeout, anonymous: false, onload: (response) => { + // 调试信息:记录详细的请求和响应 + if (CONFIG.debug) { + log(`请求URL: ${url}`); + log(`响应状态: ${response.status} ${response.statusText}`); + log(`响应内容: ${response.responseText.substring(0, 500)}...`); + } + if (response.status >= 200 && response.status < 300) { try { const data = JSON.parse(response.responseText); @@ -96,7 +175,16 @@ settings: resolve(response.responseText); } } else { - reject(new Error(`HTTP ${response.status}: ${response.statusText}`)); + // 解析错误响应 + let errorDetail = response.statusText; + try { + const errorData = JSON.parse(response.responseText); + errorDetail = errorData.msg || errorData.message || errorData.error || response.statusText; + } catch (e) { + // 如果无法解析JSON,使用原始响应 + errorDetail = response.responseText || response.statusText; + } + reject(new Error(`HTTP ${response.status}: ${errorDetail}`)); } }, onerror: () => reject(new Error('网络请求失败')), @@ -104,69 +192,308 @@ settings: }); }); } - - // SweetAlert2 弹窗函数 + + // 重试函数 + 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) { + log(`重试中... (${i + 1}/${retries})`, 'warn'); + await new Promise((resolve) => setTimeout(resolve, delay)); + } else { + throw error; + } + } + } + }; + + // 飞书API相关函数 + async function fetchFeishuTenantAccessToken() { + if (!CONFIG.feishu.appId || !CONFIG.feishu.appSecret) { + throw new Error('飞书应用ID或密钥未配置'); + } + + log(`正在获取飞书访问令牌,App ID: ${CONFIG.feishu.appId.substring(0, 8)}...`); + + try { + const response = await retry(() => makeRequest( + CONFIG.feishu.urls.tenantAccessToken, + { + method: 'POST', + data: JSON.stringify({ + app_id: CONFIG.feishu.appId, + app_secret: CONFIG.feishu.appSecret, + }) + } + )); + + log(`飞书API响应: code=${response.code}, msg=${response.msg || '无'}`); + + if (response.code !== 0) { + throw new Error(`获取飞书访问令牌失败: ${response.msg || response.message || '未知错误'}`); + } + + if (!response.tenant_access_token) { + throw new Error('飞书返回的访问令牌为空'); + } + + log('飞书访问令牌获取成功'); + return response.tenant_access_token; + } catch (error) { + log(`获取飞书访问令牌失败: ${error.message}`, 'error'); + log(`请检查:1.应用ID和密钥是否正确 2.应用是否有多维表格权限 3.网络连接是否正常`, 'error'); + throw error; + } + } + + /** + * 获取飞书表格中所有已存在的达人UID(支持自动翻页) + */ + async function getExistingKOCUIDs(accessToken) { + try { + const url = CONFIG.feishu.urls.bitableSearchRecords(CONFIG.feishu.bitableAppId, CONFIG.feishu.tableId); + const headers = { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }; + + const existingUIDs = new Set(); + let hasMore = true; + let pageToken = null; + let pageCount = 0; + + log('开始获取飞书表格中已存在的达人UID...'); + + while (hasMore) { + pageCount++; + log(`获取第 ${pageCount} 页已存在的UID...`); + + // 构造请求体 + const requestBody = { + field_names: ['达人UID'], // 只获取达人UID字段 + automatic_fields: false + }; + + // 构造URL参数 + let requestUrl = url + '?page_size=500&user_id_type=open_id'; + if (pageToken) { + requestUrl += `&page_token=${encodeURIComponent(pageToken)}`; + } + + const response = await retry(() => makeRequest(requestUrl, { + method: 'POST', + data: JSON.stringify(requestBody), + headers: headers + })); + + if (response.code !== 0) { + throw new Error(`获取已存在UID失败: ${response.msg || response.message || '未知错误'}`); + } + + const data = response.data || {}; + const items = data.items || []; + + // 提取UID - 处理富文本格式 + items.forEach(item => { + const fields = item.fields || {}; + const uidField = fields['达人UID']; + + let uid = ''; + if (uidField) { + if (Array.isArray(uidField) && uidField.length > 0) { + // 处理富文本格式 [{"text": "值", "type": "text"}] + for (const item of uidField) { + if (item.type === 'text' && item.text) { + uid += item.text; + } + } + } else if (typeof uidField === 'string') { + // 处理普通文本格式 + uid = uidField; + } + + if (uid) { + existingUIDs.add(uid); + } + } + }); + + log(`第 ${pageCount} 页获取到 ${items.length} 条记录`); + + // 检查是否还有更多数据 + hasMore = data.has_more || false; + pageToken = data.page_token || null; + + // 防止无限循环 + if (pageCount > 1000) { + log('达到最大翻页限制,停止获取', 'warn'); + break; + } + + // 批次间延迟,避免频率限制 + if (hasMore) { + await new Promise(resolve => setTimeout(resolve, 200)); + } + } + + log(`总共获取 ${pageCount} 页,找到 ${existingUIDs.size} 个已存在的达人UID`); + return existingUIDs; + + } catch (error) { + log(`获取已存在UID失败: ${error.message}`, 'error'); + // 如果获取失败,返回空集合,继续处理 + return new Set(); + } + } + + async function batchUploadKOCRecordsToFeishu(accessToken, kocDataList) { + try { + const url = CONFIG.feishu.urls.bitableBatchCreate(CONFIG.feishu.bitableAppId, CONFIG.feishu.tableId); + const headers = { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }; + + log(`批量上传URL: ${url}`); + log(`上传记录数: ${kocDataList.length}`); + + // 构造批量记录数据 + const records = kocDataList.map(kocData => ({ + fields: { + '昵称': kocData.昵称, + '达人UID': kocData.达人UID, + '粉丝数': kocData.粉丝数, + '类型': kocData.类型, + '口碑分': kocData.口碑分, + '关联商品销售量区间': kocData.关联商品销售量区间, + '关联商品销售金额区间': kocData.关联商品销售金额区间, + '预估关联商品销售量': kocData.预估关联商品销售量, + '预估关联商品销售金额': kocData.预估关联商品销售金额, + '预估关联商品视频销售量': kocData.预估关联商品视频销售量, + '预估关联商品视频销售金额': kocData.预估关联商品视频销售金额, + '视频带货占比': kocData.视频带货占比, + '关联商品蝉妈妈ID': kocData.关联商品蝉妈妈ID, + '采集时间': new Date().getTime(), // 毫秒级时间戳 + } + })); + + // 分批上传,每批最多100条记录 + const batchSize = 100; + const batches = []; + for (let i = 0; i < records.length; i += batchSize) { + batches.push(records.slice(i, i + batchSize)); + } + + log(`分为 ${batches.length} 批上传,每批最多 ${batchSize} 条记录`); + + let totalUploaded = 0; + for (let i = 0; i < batches.length; i++) { + const batch = batches[i]; + const body = JSON.stringify({ records: batch }); + + log(`上传第 ${i + 1}/${batches.length} 批,包含 ${batch.length} 条记录`); + + const response = await retry(() => makeRequest(url, { + method: 'POST', + data: body, + headers: headers + })); + + if (response.code !== 0) { + throw new Error(`批量上传到飞书失败: ${response.msg || response.message || '未知错误'}`); + } + + totalUploaded += batch.length; + log(`第 ${i + 1} 批上传成功: ${batch.length} 条记录`); + + // 批次间延迟500ms避免频率限制 + if (i < batches.length - 1) { + await new Promise(resolve => setTimeout(resolve, 500)); + } + } + + log(`批量上传完成: 总共 ${totalUploaded} 条记录`); + return { code: 0, uploaded: totalUploaded }; + } catch (error) { + // 特殊处理403权限错误 + if (error.message.includes('HTTP 403')) { + log('HTTP 403权限错误诊断:', 'error'); + log(`- App ID: ${CONFIG.feishu.appId}`, 'error'); + log(`- Bitable App ID: ${CONFIG.feishu.bitableAppId}`, 'error'); + log(`- Table ID: ${CONFIG.feishu.tableId}`, 'error'); + log('请检查以下几点:', 'error'); + log('1. 飞书应用是否有多维表格权限', 'error'); + log('2. App ID和App Secret是否正确', 'error'); + log('3. 多维表格App ID和表格ID是否正确', 'error'); + log('4. 应用是否已发布并获得权限', 'error'); + throw new Error(`飞书权限错误 (403): 请检查应用权限配置`); + } + log(`批量上传到飞书失败: ${error.message}`, 'error'); + throw error; + } + } + + + + // 弹窗函数 - 带 SweetAlert2 备用方案 function showInfo(title, text) { - Swal.fire({ - title: title, - text: text, - icon: 'info', - confirmButtonText: '确定' - }); + if (typeof Swal !== 'undefined') { + Swal.fire({ + title: title, + text: text, + icon: 'info', + confirmButtonText: '确定' + }); + } else { + alert(`${title}\n${text}`); + log(`Info: ${title} - ${text}`); + } } function showSuccess(title, text) { - Swal.fire({ - title: title, - text: text, - icon: 'success', - timer: 3000, - showConfirmButton: false - }); + if (typeof Swal !== 'undefined') { + Swal.fire({ + title: title, + text: text, + icon: 'success', + timer: 3000, + showConfirmButton: false + }); + } else { + showToast(`${title}: ${text}`, 'success'); + log(`Success: ${title} - ${text}`); + } } function showError(title, text) { - Swal.fire({ - title: title, - text: text, - icon: 'error', - confirmButtonText: '确定' - }); + if (typeof Swal !== 'undefined') { + Swal.fire({ + title: title, + text: text, + icon: 'error', + confirmButtonText: '确定' + }); + } else { + alert(`错误: ${title}\n${text}`); + log(`Error: ${title} - ${text}`, 'error'); + } } async function showConfirm(title, text) { - const result = await Swal.fire({ - title: title, - text: text, - icon: 'question', - showCancelButton: true, - confirmButtonText: '确定', - cancelButtonText: '取消' - }); - return result.isConfirmed; - } - - // 数据导出函数 - function exportToJSON(data, filename = 'koc_history') { - // JSON导出逻辑 - log('导出JSON格式数据'); - const jsonStr = JSON.stringify(data, null, 2); - GM_setClipboard(jsonStr); - showSuccess('导出成功', 'JSON数据已复制到剪贴板'); - } - - // KOC数据处理函数 - async function fetchKOCHistory(productId) { - try { - log(`获取产品 ${productId} 的KOC历史数据`); - const response = await makeRequest(`${CONFIG.api.baseUrl}${CONFIG.api.endpoint}`, { - method: 'POST', - data: JSON.stringify({ productId }) + if (typeof Swal !== 'undefined') { + const result = await Swal.fire({ + title: title, + text: text, + icon: 'question', + showCancelButton: true, + confirmButtonText: '确定', + cancelButtonText: '取消' }); - return response; - } catch (error) { - log(`获取KOC历史数据失败: ${error.message}`, 'error'); - throw error; + return result.isConfirmed; + } else { + return confirm(`${title}\n${text}`); } } @@ -240,6 +567,12 @@ settings: baseInfo.预估关联商品视频销售量 = totalVideoVolume; baseInfo.预估关联商品视频销售金额 = totalVideoAmount; baseInfo.关联商品蝉妈妈ID = Array.from(videoProductIds).join(','); + + // 计算视频带货占比(限制最大值为1) + const totalAmount = baseInfo.预估关联商品销售金额 || 0; + let videoRatio = totalAmount > 0 ? totalVideoAmount / totalAmount : 0; + videoRatio = Math.min(videoRatio, 1); // 限制最大值为1 + baseInfo.视频带货占比 = videoRatio; results.push(baseInfo); }); @@ -270,6 +603,14 @@ settings:
准备采集... + ${CONFIG.feishu.enabled ? ` +