// ==UserScript==
// @name 蝉妈妈产品历史KOC助手
// @namespace https://github.com/scriptCat/
// @version 1.0.0
// @description 蝉妈妈平台产品历史KOC数据分析和导出助手
// @author wangxi
// @match https://www.chanmama.com/promotionRank/*
// @grant GM_xmlhttpRequest
// @grant GM_notification
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_log
// @grant GM_addStyle
// @grant GM_setClipboard
// @connect chanmama.com
// @connect api-service.chanmama.com
// @connect open.feishu.cn
// @require https://unpkg.com/sweetalert2@11
// @license MIT
// ==/UserScript==
/* ==UserConfig==
basic:
enabled:
title: 启用功能
description: 是否启用蝉妈妈产品历史KOC助手功能
type: checkbox
default: true
timeout:
title: 超时时间
description: 网络请求超时时间
type: number
default: 30
min: 10
max: 120
unit: 秒
debug:
title: 调试模式
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: 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: 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`
}
}
};
// 工具函数
function log(message, level = 'info') {
const logMessage = `[${GM_info.script.name}] ${message}`;
// 强制输出所有日志到GM日志和控制台,便于调试
GM_log(logMessage, level);
console.log(logMessage);
}
function showNotification(title, text, type = 'info') {
GM_notification({
title: title,
text: text,
timeout: 5000,
onclick: () => log('通知被点击')
});
}
function makeRequest(url, options = {}) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: options.method || 'GET',
url: url,
headers: {
'Content-Type': 'application/json',
...options.headers
},
data: options.data,
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);
resolve(data);
} catch (error) {
resolve(response.responseText);
}
} else {
// 解析错误响应
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('网络请求失败')),
ontimeout: () => reject(new Error('请求超时'))
});
});
}
// 重试函数
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) {
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) {
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) {
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) {
if (typeof Swal !== 'undefined') {
const result = await Swal.fire({
title: title,
text: text,
icon: 'question',
showCancelButton: true,
confirmButtonText: '确定',
cancelButtonText: '取消'
});
return result.isConfirmed;
} else {
return confirm(`${title}\n${text}`);
}
}
/**
* 解析区间文本为平均值
* 例如: "750~1000" -> 875, "1w~2.5w" -> 17500
*/
function parseRangeText(rangeText) {
if (!rangeText || rangeText === '0' || rangeText === '') return 0;
// 正确处理万单位:先解析每个数值,再转换单位
const split = rangeText.split('~');
const values = split.map(s => {
const trimmed = s.trim();
if (trimmed.includes('w')) {
// 提取数字部分,乘以10000
const numPart = parseFloat(trimmed.replace('w', '')) || 0;
return numPart * 10000;
} else {
return parseFloat(trimmed) || 0;
}
});
const result = values.reduce((sum, val) => sum + val, 0) / values.length;
return result;
}
/**
* 解析KOC数据
*/
function parseKOCData(apiResponse) {
if (!apiResponse || !apiResponse.data || !apiResponse.data.list) {
log('数据格式错误', 'error');
return [];
}
const results = [];
const kocList = apiResponse.data.list;
kocList.forEach((koc, index) => {
// 基础信息
const baseInfo = {
昵称: koc.nickname || '',
达人UID: koc.unique_id || koc.short_id || '',
粉丝数: koc.follower_count || 0,
类型: koc.label || '',
口碑分: (koc.reputation && koc.reputation.score) || 0,
关联商品销售量区间: koc.volume_text || '',
关联商品销售金额区间: koc.amount_text || '',
预估关联商品销售量: parseRangeText(koc.volume_text),
预估关联商品销售金额: parseRangeText(koc.amount_text)
};
// 计算关联商品视频的总销售量和金额
let totalVideoVolume = 0;
let totalVideoAmount = 0;
const videoProductIds = new Set();
if (koc.a && Array.isArray(koc.a)) {
koc.a.forEach((video, videoIndex) => {
if (video.product_id) {
videoProductIds.add(video.product_id);
}
const volumeValue = parseRangeText(video.volume_text);
const amountValue = parseRangeText(video.amount_text);
totalVideoVolume += volumeValue;
totalVideoAmount += amountValue;
});
}
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);
});
return results;
}
// UI相关函数 - 创建简洁美观的悬浮窗
function createFloatingWindow() {
// 检查悬浮窗是否已存在
if (document.querySelector('.koc-floating-window')) {
log('悬浮窗已存在');
return true;
}
// 创建简洁的悬浮窗
const floatingWindow = document.createElement('div');
floatingWindow.className = 'koc-floating-window';
floatingWindow.innerHTML = `
${CONFIG.feishu.enabled ? `
` : ''}
`;
// 设置悬浮窗样式
floatingWindow.style.cssText = `
position: fixed;
bottom: 30px;
right: 30px;
width: 180px;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
z-index: 9999;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
user-select: none;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
opacity: 0;
transform: translateY(20px) scale(0.95);
`;
// 添加美观的样式
GM_addStyle(`
.koc-floating-window {
animation: kocSlideIn 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
@keyframes kocSlideIn {
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.koc-window-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px 8px;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
cursor: move;
user-select: none;
}
.koc-window-title {
font-size: 13px;
font-weight: 600;
color: #374151;
letter-spacing: 0.025em;
}
.koc-close-btn {
width: 20px;
height: 20px;
border: none;
background: transparent;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
color: #6b7280;
transition: all 0.2s ease;
font-weight: 300;
}
.koc-close-btn:hover {
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
transform: scale(1.1);
}
.koc-window-content {
padding: 16px;
}
.koc-api-status {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: rgba(16, 185, 129, 0.05);
border-radius: 6px;
border: 1px solid rgba(16, 185, 129, 0.1);
margin-bottom: 12px;
}
.koc-actions {
display: flex;
flex-direction: column;
gap: 8px;
}
.koc-download-btn {
width: 100%;
padding: 12px 16px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
position: relative;
overflow: hidden;
}
.koc-download-btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: left 0.5s;
}
.koc-download-btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
}
.koc-download-btn:hover::before {
left: 100%;
}
.koc-download-btn:active {
transform: translateY(0);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
}
.koc-download-btn:disabled {
background: linear-gradient(135deg, #d1d5db 0%, #9ca3af 100%);
cursor: not-allowed;
transform: none;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.koc-btn-icon {
width: 16px;
height: 16px;
flex-shrink: 0;
}
.koc-status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
background: #10b981;
animation: kocPulse 2s infinite;
}
@keyframes kocPulse {
0%, 100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.5;
transform: scale(1.2);
}
}
.koc-status-text {
font-size: 11px;
color: #6b7280;
font-weight: 500;
}
.koc-feishu-status {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
background: rgba(59, 130, 246, 0.05);
border-radius: 6px;
border: 1px solid rgba(59, 130, 246, 0.1);
margin-bottom: 12px;
}
.koc-feishu-icon {
width: 16px;
height: 16px;
color: #3b82f6;
flex-shrink: 0;
}
.koc-feishu-text {
font-size: 11px;
color: #3b82f6;
font-weight: 500;
}
.koc-floating-window.dragging {
transition: none !important;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
transform: scale(1.02);
}
/* 状态颜色 */
.koc-status.waiting .koc-status-indicator {
background: #10b981;
}
.koc-status.processing .koc-status-indicator {
background: #f59e0b;
animation: kocSpin 1s linear infinite;
}
.koc-status.success .koc-status-indicator {
background: #10b981;
}
.koc-status.error .koc-status-indicator {
background: #ef4444;
}
@keyframes kocSpin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* 响应式设计 */
@media (max-width: 768px) {
.koc-floating-window {
bottom: 20px;
right: 20px;
width: 160px;
}
}
`);
// 添加到页面
document.body.appendChild(floatingWindow);
// 设置拖拽功能
setupSimpleDrag(floatingWindow);
// 设置事件监听器
setupFloatingWindowEvents(floatingWindow);
// 显示动画
setTimeout(() => {
floatingWindow.style.opacity = '1';
floatingWindow.style.transform = 'translateY(0) scale(1)';
}, 100);
return true;
}
// 更新悬浮窗状态
function updateFloatingWindowStatus(message, type = 'info') {
const floatingWindow = document.querySelector('.koc-floating-window');
if (!floatingWindow) return;
const statusText = floatingWindow.querySelector('.koc-status-text');
const statusIndicator = floatingWindow.querySelector('.koc-status-indicator');
if (statusText) {
statusText.textContent = message;
}
if (statusIndicator) {
statusIndicator.className = 'koc-status-indicator';
statusIndicator.classList.add(`koc-status-${type}`);
}
}
// 处理下载按钮点击
async function handleDownloadClick() {
const floatingWindow = document.querySelector('.koc-floating-window');
if (!floatingWindow) {
log('悬浮窗不存在', 'error');
return;
}
const downloadBtn = floatingWindow.querySelector('.koc-download-btn');
if (!downloadBtn) {
log('下载按钮不存在', 'error');
return;
}
if (downloadBtn.disabled) {
log('按钮已禁用,请等待当前操作完成', 'warn');
return;
}
try {
log('=== 开始KOC数据采集 ===', 'info');
// 设置按钮状态为处理中
setStatus(floatingWindow, 'processing', '正在采集数据...');
downloadBtn.disabled = true;
downloadBtn.innerHTML = `
采集中...
`;
// 从当前页面URL提取product_id
function extractProductIdFromUrl() {
const url = window.location.href;
const match = url.match(/promotionRank\/([^\.]+)\.html/);
return match ? match[1] : "";
}
// 计算时间范围(近365天,T-1结束)
function getDateRange() {
const endDate = new Date();
endDate.setDate(endDate.getDate() - 1); // T-1
const startDate = new Date();
startDate.setDate(startDate.getDate() - 365); // 近365天
return {
start_date: startDate.toISOString().split('T')[0],
end_date: endDate.toISOString().split('T')[0]
};
}
const dateRange = getDateRange();
const productId = extractProductIdFromUrl();
const apiUrl = `${CONFIG.api.baseUrl}${CONFIG.api.endpoint}`;
log(`产品ID: ${productId}`, 'debug');
log(`时间范围: ${dateRange.start_date} 到 ${dateRange.end_date}`, 'debug');
let allData = [];
let currentPage = 1;
let shouldContinue = true;
while (shouldContinue) {
log(`请求第 ${currentPage} 页数据`, 'info');
// 构造请求参数
const requestParams = new URLSearchParams({
product_id: productId,
sort: 'amount',
order_by: 'desc',
has_contact: '0',
is_new_corp: '0',
self_play: '0',
shop_play: '0',
author_play: '1',
live_room_status: '0',
category: '',
reputation_level: '-1',
follower_count: '',
keyword: '',
page: currentPage.toString(),
size: CONFIG.pagination.pageSize.toString(),
take_product_method: '1',
start_date: dateRange.start_date,
end_date: dateRange.end_date
}).toString();
log(`请求参数: ${requestParams}`, 'debug');
// 发起API请求
const response = await makeRequest(`${apiUrl}?${requestParams}`, {
method: 'GET'
});
// 解析数据
const parsedData = parseKOCData(response);
if (parsedData.length === 0) {
log(`第 ${currentPage} 页无数据,停止采集`, 'info');
break;
}
// 应用筛选条件
const filteredData = parsedData.filter(author => {
// 检查金额门槛
if (author.预估关联商品视频销售金额 < CONFIG.pagination.amountThreshold) {
log(`筛选掉达人 ${author.昵称}: 视频销售金额 ${author.预估关联商品视频销售金额} < ${CONFIG.pagination.amountThreshold}`, 'debug');
return false;
}
// 检查粉丝数上限
const followers = author.粉丝数 || 0;
if (followers >= CONFIG.filters.maxFollowers) {
log(`筛选掉达人 ${author.昵称}: 粉丝数 ${followers} >= ${CONFIG.filters.maxFollowers}`, 'debug');
return false;
}
// 检查视频带货占比(使用已计算的值)
const videoRatio = author.视频带货占比 || 0;
if (videoRatio < CONFIG.filters.videoRatioThreshold) {
log(`筛选掉达人 ${author.昵称}: 视频带货占比 ${videoRatio.toFixed(3)} < ${CONFIG.filters.videoRatioThreshold}`, 'debug');
return false;
}
return true;
});
log(`第 ${currentPage} 页原始数据 ${parsedData.length} 条,筛选后 ${filteredData.length} 条`, 'info');
// 检查金额门槛 - 使用预估关联商品销售金额的最大值来决定是否停止采集
const amountValues = parsedData.map(author => author.预估关联商品销售金额 || 0);
const maxAmount = Math.max(...amountValues);
log(`第 ${currentPage} 页最大金额: ${maxAmount}`, 'debug');
if (maxAmount < CONFIG.pagination.amountThreshold) {
log(`达到金额门槛 ${CONFIG.pagination.amountThreshold},停止采集`, 'info');
allData = allData.concat(filteredData);
shouldContinue = false;
} else {
allData = allData.concat(filteredData);
currentPage++;
// 防止无限循环,最多采集100页
if (currentPage > 100) {
log('达到最大页数限制,停止采集', 'warn');
shouldContinue = false;
}
}
// 更新状态
setStatus(floatingWindow, 'processing', `已采集 ${allData.length} 条数据`);
// 延迟1秒,避免请求过于频繁
await new Promise(resolve => setTimeout(resolve, 1000));
}
log(`=== 数据采集完成 ===`, 'info');
log(`总共采集 ${allData.length} 条符合条件的达人数据`, 'info');
// 显示采集结果的toast提示
showToast(`📊 采集完成:找到 ${allData.length} 个满足条件的达人`, 'success');
// 打印完整数据到日志
log('📊 KOC数据采集结果:', 'info');
log(`📈 共采集 ${allData.length} 条达人数据`, 'info');
log('📋 数据详情:', 'info');
log(JSON.stringify(allData, null, 2), 'info');
// 飞书同步处理
let feishuResult = 'Success';
let syncLogs = [];
let syncedCount = 0;
if (CONFIG.feishu.enabled && allData.length > 0) {
try {
log(`=== 开始同步到飞书多维表格 ===`, 'info');
setStatus(floatingWindow, 'processing', '同步到飞书中...');
// 检查飞书配置
if (!CONFIG.feishu.bitableAppId || !CONFIG.feishu.tableId) {
throw new Error('飞书多维表格配置不完整,请检查配置');
}
const accessToken = await fetchFeishuTenantAccessToken();
syncLogs.push('飞书访问令牌获取成功');
setStatus(floatingWindow, 'processing', '检查已存在记录...');
// 获取已存在的达人UID
const existingUIDs = await getExistingKOCUIDs(accessToken);
syncLogs.push(`查询到 ${existingUIDs.size} 个已存在的达人UID`);
// 过滤掉已存在的记录
const newRecords = allData.filter(kocData => !existingUIDs.has(kocData.达人UID));
const skippedCount = allData.length - newRecords.length;
log(`原始数据 ${allData.length} 条,已存在 ${skippedCount} 条,需要上传 ${newRecords.length} 条新记录`);
syncLogs.push(`总计 ${allData.length} 条记录,跳过 ${skippedCount} 条已存在,上传 ${newRecords.length} 条新记录`);
if (newRecords.length > 0) {
setStatus(floatingWindow, 'processing', `批量上传 ${newRecords.length} 条新记录...`);
const result = await batchUploadKOCRecordsToFeishu(accessToken, newRecords);
syncedCount = result.uploaded || newRecords.length;
syncLogs.push(`✅ 批量上传完成: ${syncedCount} 条记录`);
setStatus(floatingWindow, 'success', `上传完成: ${syncedCount} 条新记录`);
// 显示上传结果的toast提示
showToast(`📤 飞书同步:${skippedCount} 个重复跳过,${syncedCount} 个新增写入`, 'info');
} else {
syncLogs.push('✅ 没有需要上传的新记录');
setStatus(floatingWindow, 'success', '所有记录均已存在,无需上传');
// 显示全部重复的toast提示
showToast(`📋 飞书同步:${allData.length} 个达人均已存在,无新增记录`, 'warning');
}
log(`=== 飞书同步完成 ===`, 'info');
log(`成功同步 ${syncedCount} 条新数据到飞书多维表格`, 'info');
} catch (error) {
feishuResult = 'Failed';
syncLogs.push(`❌ 飞书同步失败: ${error.message}`);
log(`飞书同步失败: ${error.message}`, 'error');
setStatus(floatingWindow, 'error', '飞书同步失败');
showError('飞书同步失败', error.message);
// 显示同步失败的toast提示
showToast(`❌ 飞书同步失败:${error.message}`, 'error');
}
}
// 显示最终结果
const successMessage = CONFIG.feishu.enabled
? `已采集 ${allData.length} 条数据${syncedCount > 0 ? `,同步 ${syncedCount} 条新数据到飞书` : ',所有数据均已存在'}`
: `已采集 ${allData.length} 条符合条件的达人数据,请查看控制台`;
setStatus(floatingWindow, 'success', `采集完成 (${allData.length}条)`);
showSuccess('采集成功', successMessage);
// 显示最终结果的详细toast
if (CONFIG.feishu.enabled) {
if (syncedCount > 0) {
showToast(`✅ 任务完成:采集 ${allData.length} 个达人,新增 ${syncedCount} 个到飞书`, 'success');
} else {
showToast(`✅ 任务完成:采集 ${allData.length} 个达人,全部已存在`, 'info');
}
} else {
showToast(`✅ 任务完成:采集 ${allData.length} 个符合条件的达人`, 'success');
}
// 5秒后恢复按钮状态
setTimeout(() => {
resetButton(floatingWindow);
}, 5000);
} catch (error) {
log(`=== 数据采集失败 ===`, 'error');
log(`错误详情: ${error.message}`, 'error');
log(`错误堆栈: ${error.stack}`, 'error');
setStatus(floatingWindow, 'error', '采集失败');
showError('采集失败', error.message);
// 5秒后恢复按钮状态
setTimeout(() => {
resetButton(floatingWindow);
}, 5000);
}
}
// 主逻辑
async function init() {
if (!CONFIG.enabled) {
log('脚本已禁用');
return;
}
try {
// 检测依赖库状态
checkLibraryStatus();
// 检查飞书配置
if (CONFIG.feishu.enabled) {
if (!CONFIG.feishu.appId || !CONFIG.feishu.appSecret) {
log('⚠️ 飞书同步已启用但应用ID或密钥未配置', 'warn');
showToast('飞书配置不完整,请检查应用ID和密钥', 'warning');
} else if (!CONFIG.feishu.bitableAppId || !CONFIG.feishu.tableId) {
log('⚠️ 飞书多维表格配置不完整', 'warn');
showToast('飞书多维表格配置不完整,请检查多维表格App ID和数据表ID', 'warning');
} else {
log('✅ 飞书同步配置完整', 'info');
}
} else {
log('ℹ️ 飞书同步未启用', 'info');
}
// 输出启动信息
log('🎯 KOC数据采集工具启动', 'info');
log(`📋 筛选条件:`, 'info');
log(` - 金额门槛: ${CONFIG.pagination.amountThreshold}元`, 'info');
log(` - 粉丝数上限: <${CONFIG.filters.maxFollowers.toLocaleString()}`, 'info');
log(` - 视频带货占比: ≥${(CONFIG.filters.videoRatioThreshold * 100).toFixed(1)}%`, 'info');
// 等待页面加载完成
await waitForPageLoad();
// 检查是否在正确的页面
if (!isValidPage()) {
log('当前页面不支持此功能');
return;
}
// 创建悬浮窗
let windowCreated = createFloatingWindow();
if (!windowCreated) {
log('首次创建悬浮窗失败,延迟重试...', 'warn');
setTimeout(() => {
windowCreated = createFloatingWindow();
if (!windowCreated) {
log('延迟重试失败,设置DOM观察器', 'warn');
setupWindowObserver();
}
}, 2000);
}
showToast('KOC数据采集工具已就绪', 'success');
} catch (error) {
log(`初始化失败: ${error.message}`, 'error');
showToast('初始化失败', 'error');
}
}
function setupWindowObserver() {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
// 检查是否已经有悬浮窗
if (document.querySelector('.koc-floating-window')) {
observer.disconnect();
return;
}
// 简单创建悬浮窗
createFloatingWindow();
observer.disconnect();
}
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
log('DOM观察器已设置');
}
function waitForPageLoad() {
return new Promise((resolve) => {
if (document.readyState === 'complete') {
resolve();
} else {
window.addEventListener('load', resolve);
}
});
}
function isValidPage() {
// 检查是否在蝉妈妈的推广排名页面
return window.location.hostname.includes('chanmama.com') &&
window.location.pathname.includes('/promotionRank');
}
// 备用拖拽方案
function setupSimpleDrag(floatingWindow) {
const header = floatingWindow.querySelector('.koc-window-header');
let isDragging = false;
let startX, startY, startLeft, startTop;
header.addEventListener('mousedown', (e) => {
if (e.target.closest('.koc-close-btn')) return;
isDragging = true;
startX = e.clientX;
startY = e.clientY;
// 获取当前位置,正确处理bottom/right定位
const rect = floatingWindow.getBoundingClientRect();
startLeft = rect.left;
startTop = rect.top;
floatingWindow.classList.add('dragging');
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const deltaX = e.clientX - startX;
const deltaY = e.clientY - startY;
let newLeft = startLeft + deltaX;
let newTop = startTop + deltaY;
// 限制在视窗内
const maxX = window.innerWidth - floatingWindow.offsetWidth;
const maxY = window.innerHeight - floatingWindow.offsetHeight;
newLeft = Math.max(0, Math.min(newLeft, maxX));
newTop = Math.max(0, Math.min(newTop, maxY));
// 设置新位置,清除bottom/right定位
floatingWindow.style.left = newLeft + 'px';
floatingWindow.style.top = newTop + 'px';
floatingWindow.style.right = 'auto';
floatingWindow.style.bottom = 'auto';
});
document.addEventListener('mouseup', () => {
if (isDragging) {
isDragging = false;
floatingWindow.classList.remove('dragging');
}
});
}
// 设置悬浮窗事件
function setupFloatingWindowEvents(floatingWindow) {
const downloadBtn = floatingWindow.querySelector('.koc-download-btn');
const closeBtn = floatingWindow.querySelector('.koc-close-btn');
// 关闭功能
closeBtn.addEventListener('click', () => {
floatingWindow.style.opacity = '0';
floatingWindow.style.transform = 'translateY(20px) scale(0.95)';
setTimeout(() => {
floatingWindow.remove();
}, 300);
});
// 下载按钮点击 - 直接调用handleDownloadClick
downloadBtn.addEventListener('click', handleDownloadClick);
}
// 设置状态
function setStatus(floatingWindow, status, text) {
const apiStatusContainer = floatingWindow.querySelector('.koc-api-status');
const apiStatusText = floatingWindow.querySelector('.koc-api-status .koc-status-text');
if (apiStatusContainer && apiStatusText) {
// 移除所有状态类
apiStatusContainer.classList.remove('waiting', 'processing', 'success', 'error');
// 添加新状态类
apiStatusContainer.classList.add(status);
apiStatusText.textContent = text;
// 根据状态更新样式
switch (status) {
case 'processing':
apiStatusContainer.style.background = 'rgba(245, 158, 11, 0.1)';
apiStatusContainer.style.borderColor = 'rgba(245, 158, 11, 0.2)';
break;
case 'success':
apiStatusContainer.style.background = 'rgba(16, 185, 129, 0.1)';
apiStatusContainer.style.borderColor = 'rgba(16, 185, 129, 0.2)';
break;
case 'error':
apiStatusContainer.style.background = 'rgba(239, 68, 68, 0.1)';
apiStatusContainer.style.borderColor = 'rgba(239, 68, 68, 0.2)';
break;
default:
apiStatusContainer.style.background = 'rgba(16, 185, 129, 0.05)';
apiStatusContainer.style.borderColor = 'rgba(16, 185, 129, 0.1)';
}
}
}
// 重置按钮
function resetButton(floatingWindow) {
const downloadBtn = floatingWindow.querySelector('.koc-download-btn');
if (downloadBtn) {
downloadBtn.disabled = false;
downloadBtn.innerHTML = `
开始采集
`;
}
// 恢复状态显示
const apiStatusText = floatingWindow.querySelector('.koc-api-status .koc-status-text');
if (apiStatusText) {
apiStatusText.textContent = '准备采集...';
}
setStatus(floatingWindow, 'waiting', '准备采集...');
}
// 创建简洁的toast提示
function showToast(message, type = 'info') {
// 移除已存在的toast
const existingToast = document.querySelector('.koc-toast');
if (existingToast) {
existingToast.remove();
}
const toast = document.createElement('div');
toast.className = `koc-toast koc-toast-${type}`;
toast.innerHTML = `
${getToastIcon(type)}
${message}
`;
// 添加toast样式
GM_addStyle(`
.koc-toast {
position: fixed;
top: 20px;
right: 20px;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 12px;
padding: 16px 20px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
z-index: 10000;
display: flex;
align-items: center;
gap: 12px;
font-size: 15px;
font-weight: 600;
max-width: 350px;
min-width: 280px;
animation: kocToastSlideIn 0.3s ease;
transition: all 0.3s ease;
}
@keyframes kocToastSlideIn {
from {
opacity: 0;
transform: translateX(100%);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.koc-toast-icon {
width: 20px;
height: 20px;
flex-shrink: 0;
}
.koc-toast-message {
color: #374151;
line-height: 1.4;
}
.koc-toast-success {
border-left: 5px solid #10b981;
}
.koc-toast-error {
border-left: 5px solid #ef4444;
}
.koc-toast-info {
border-left: 5px solid #3b82f6;
}
.koc-toast-warning {
border-left: 5px solid #f59e0b;
}
.koc-toast.fade-out {
opacity: 0;
transform: translateX(100%);
}
`);
document.body.appendChild(toast);
// 3秒后自动消失
setTimeout(() => {
toast.classList.add('fade-out');
setTimeout(() => {
if (toast.parentNode) {
toast.remove();
}
}, 300);
}, 3000);
// 点击关闭
toast.addEventListener('click', () => {
toast.classList.add('fade-out');
setTimeout(() => {
if (toast.parentNode) {
toast.remove();
}
}, 300);
});
}
// 获取toast图标
function getToastIcon(type) {
switch (type) {
case 'success':
return ``;
case 'error':
return ``;
case 'warning':
return ``;
default:
return ``;
}
}
// 页面加载完成后运行
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();