|
|
@@ -77,33 +77,47 @@ export async function generateRecommendations(
|
|
|
console.log('[Recommend] ========== 开始推荐流程 ==========');
|
|
|
console.log('[Recommend] 搜索条件:', JSON.stringify(criteria, null, 2));
|
|
|
const targetCandidatePool = resolveTargetCandidatePool(criteria);
|
|
|
+ const requestedPlatforms = uniquePlatforms(criteria.platforms);
|
|
|
+ const targetPerPlatform = Math.max(1, Math.ceil(targetCandidatePool / Math.max(1, requestedPlatforms.length)));
|
|
|
onProgress?.('search', `正在检索候选达人,目标候选池 ${targetCandidatePool} 位...`);
|
|
|
|
|
|
// Step 1: 优先从本地/自有达人库召回候选
|
|
|
const allCandidates: JustOneCreator[] = [];
|
|
|
- const localCandidates = await searchLocalCreators(criteria, Math.min(config.recommendation.maxCandidates, targetCandidatePool));
|
|
|
+ const localCandidates: JustOneCreator[] = [];
|
|
|
+ for (const platform of requestedPlatforms) {
|
|
|
+ const platformLocalCandidates = await searchLocalCreators(
|
|
|
+ { ...criteria, platforms: [platform] },
|
|
|
+ Math.min(config.recommendation.maxCandidates, targetPerPlatform),
|
|
|
+ );
|
|
|
+ localCandidates.push(...platformLocalCandidates);
|
|
|
+ allCandidates.push(...platformLocalCandidates);
|
|
|
+ console.log(`[Recommend] 本地达人库 ${platform} 命中: ${platformLocalCandidates.length}/${targetPerPlatform} 位`);
|
|
|
+ }
|
|
|
if (localCandidates.length > 0) {
|
|
|
console.log(`[Recommend] 本地达人库命中: ${localCandidates.length} 位`);
|
|
|
onProgress?.('search', `本地达人库命中 ${localCandidates.length}/${targetCandidatePool} 位候选达人`);
|
|
|
- allCandidates.push(...localCandidates);
|
|
|
}
|
|
|
|
|
|
// JustOne API 仅支持小红书和抖音
|
|
|
const supportedPlatforms = ['xiaohongshu', '小红书', 'douyin', '抖音'];
|
|
|
|
|
|
let providerCandidateCount = 0;
|
|
|
- if (allCandidates.length < targetCandidatePool) {
|
|
|
+ if (platformsNeedSupplement(allCandidates, requestedPlatforms, targetPerPlatform)) {
|
|
|
const shortage = targetCandidatePool - allCandidates.length;
|
|
|
console.log(`[Recommend] 本地候选不足,继续调用 JustOne 补足: 缺口 ${shortage} 位`);
|
|
|
onProgress?.('search', `本地候选不足,继续调用 JustOne 补足 ${shortage} 位...`);
|
|
|
|
|
|
const searchKeywords = buildCompactSearchKeywords(criteria);
|
|
|
- for (const platform of uniquePlatforms(criteria.platforms)) {
|
|
|
+ for (const platform of requestedPlatforms) {
|
|
|
const platformName = mapPlatformName(platform);
|
|
|
if (!supportedPlatforms.includes(platform) && !supportedPlatforms.includes(platformName)) {
|
|
|
console.log(`[Recommend] 跳过不支持的平台: ${platform} (${platformName})`);
|
|
|
continue;
|
|
|
}
|
|
|
+ if (countCreatorsByPlatform(allCandidates, platformName) >= targetPerPlatform) {
|
|
|
+ console.log(`[Recommend] ${platformName} 已满足平台候选目标 ${targetPerPlatform} 位,跳过补量`);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
for (const keyword of searchKeywords) {
|
|
|
for (let page = 1; page <= PAGES_PER_PLATFORM; page++) {
|
|
|
console.log(`[Recommend] 搜索: 平台=${platform} -> ${platformName}, 关键词=${keyword}, 页=${page}`);
|
|
|
@@ -125,12 +139,11 @@ export async function generateRecommendations(
|
|
|
console.log(`[Recommend] ${platformName}/${keyword}/page-${page} 返回 ${results.length} 条结果`);
|
|
|
allCandidates.push(...results);
|
|
|
providerCandidateCount += results.length;
|
|
|
- if (allCandidates.length >= targetCandidatePool) break;
|
|
|
+ if (countCreatorsByPlatform(allCandidates, platformName) >= targetPerPlatform) break;
|
|
|
if (results.length === 0) break;
|
|
|
}
|
|
|
- if (allCandidates.length >= targetCandidatePool) break;
|
|
|
+ if (countCreatorsByPlatform(allCandidates, platformName) >= targetPerPlatform) break;
|
|
|
}
|
|
|
- if (allCandidates.length >= targetCandidatePool) break;
|
|
|
}
|
|
|
} else {
|
|
|
console.log(`[Recommend] 本地候选已满足目标候选池 ${targetCandidatePool} 位,跳过 JustOne`);
|
|
|
@@ -142,8 +155,9 @@ export async function generateRecommendations(
|
|
|
// Step 2: 去重
|
|
|
const uniqueMap = new Map<string, JustOneCreator>();
|
|
|
for (const c of allCandidates) {
|
|
|
- if (!uniqueMap.has(c.userId)) {
|
|
|
- uniqueMap.set(c.userId, c);
|
|
|
+ const key = `${mapPlatformName(c.platform)}:${c.userId}`;
|
|
|
+ if (!uniqueMap.has(key)) {
|
|
|
+ uniqueMap.set(key, c);
|
|
|
}
|
|
|
}
|
|
|
const uniqueCandidates = Array.from(uniqueMap.values());
|
|
|
@@ -413,6 +427,21 @@ function uniquePlatforms(platforms: string[]): string[] {
|
|
|
return [...new Set(supported.length > 0 ? supported : ['xiaohongshu'])];
|
|
|
}
|
|
|
|
|
|
+function platformsNeedSupplement(candidates: JustOneCreator[], platforms: string[], targetPerPlatform: number): boolean {
|
|
|
+ return platforms.some((platform) => countCreatorsByPlatform(candidates, platform) < targetPerPlatform);
|
|
|
+}
|
|
|
+
|
|
|
+function countCreatorsByPlatform(candidates: JustOneCreator[], platform: string): number {
|
|
|
+ const normalizedPlatform = mapPlatformName(platform);
|
|
|
+ const uniqueIds = new Set<string>();
|
|
|
+ for (const candidate of candidates) {
|
|
|
+ if (mapPlatformName(candidate.platform) === normalizedPlatform && candidate.userId) {
|
|
|
+ uniqueIds.add(candidate.userId);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return uniqueIds.size;
|
|
|
+}
|
|
|
+
|
|
|
function mapPlatformName(platform: string): string {
|
|
|
const map: Record<string, string> = {
|
|
|
xiaohongshu: 'xiaohongshu',
|