192 lines
7.3 KiB
TypeScript
192 lines
7.3 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { readFileSync } from "fs";
|
|
import { resolve } from "path";
|
|
import { prisma } from "@/lib/prisma";
|
|
|
|
const BOT_DATA_PATH = resolve(process.cwd(), "data", "bot-characters.json");
|
|
|
|
const FORUM_NAMES: Record<string, string> = {
|
|
"ec-platform": "平台电商", "ec-livestream": "直播带货", "ec-private": "私域电商",
|
|
"ec-group": "社区团购", "ec-dtc": "独立站/DTC", "ec-supply": "选品与供应链",
|
|
"ai-tools-app": "AI工具与应用", "ai-startup-forum": "AI创业", "ai-saas": "SaaS出海",
|
|
"ai-hardware": "智能硬件", "ai-llm": "大模型与开源",
|
|
"ct-short-video": "短视频创作", "ct-live": "直播运营", "ct-writing": "图文内容",
|
|
"ct-mcn": "MCN机构", "ct-podcast": "播客与音频",
|
|
"fi-stock": "股票与基金", "fi-crypto": "加密货币/Web3", "fi-insurance": "保险与理财",
|
|
"fi-pe": "PE与VC", "fi-forex": "外汇与期货",
|
|
"cb-ecommerce": "跨境电商平台", "cb-factory": "外贸工厂/B2B", "cb-logistics": "海外仓与物流",
|
|
"cb-payment": "跨境支付合规", "cb-brand": "品牌出海/DTC",
|
|
"re-residential": "住宅买卖", "re-commercial": "商业地产",
|
|
"re-renovation": "装修与家居", "re-property": "物业与租赁",
|
|
"fd-restaurant": "餐饮开店", "fd-tea": "茶饮与咖啡", "fd-prepared": "预制菜加工",
|
|
"fd-supply": "食品供应链", "fd-delivery": "外卖运营",
|
|
"ll-beauty": "美容美发", "ll-fitness": "健身运动", "ll-pet": "宠物经济",
|
|
"ll-housekeeping": "家政服务", "ll-repair": "维修与数码", "ll-edu-local": "本地教培",
|
|
"hl-cosmetic": "医疗美容", "hl-wellness": "健康管理", "hl-elderly": "养老服务",
|
|
"hl-rehab": "运动康复", "hl-mental": "心理健康",
|
|
"edu-skills": "职业技能", "edu-knowledge": "知识付费", "edu-abroad": "留学与语言",
|
|
"edu-corporate": "企业培训", "edu-k12": "K12与家庭",
|
|
};
|
|
|
|
interface BotCharacter {
|
|
key: string;
|
|
displayName: string;
|
|
email: string;
|
|
role?: string;
|
|
avatarPrompt: string;
|
|
personality: {
|
|
identity: string;
|
|
expertise: string[];
|
|
weakness: string[];
|
|
stance: string;
|
|
speakingStyle: string;
|
|
catchphrase: string;
|
|
forbiddenPatterns: string[];
|
|
wordLimit: { min: number; max: number };
|
|
passerbyType?: string;
|
|
};
|
|
primaryForums: string[];
|
|
activeHours: { weekday: string[]; weekend: string[] };
|
|
activityLevel: string;
|
|
replyChance: number;
|
|
}
|
|
|
|
function loadBotCharacters(): BotCharacter[] {
|
|
try {
|
|
const raw = readFileSync(BOT_DATA_PATH, "utf-8");
|
|
const data = JSON.parse(raw);
|
|
return (data.characters || []) as BotCharacter[];
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export async function GET(req: NextRequest) {
|
|
try {
|
|
const { searchParams } = new URL(req.url);
|
|
const role = searchParams.get("role");
|
|
const forumSlug = searchParams.get("forum");
|
|
const search = (searchParams.get("q") || "").toLowerCase().trim();
|
|
|
|
const characters = loadBotCharacters();
|
|
let filtered = characters;
|
|
if (role === "passerby") filtered = filtered.filter(c => c.role === "passerby");
|
|
if (role === "expert") filtered = filtered.filter(c => c.role !== "passerby");
|
|
if (forumSlug) filtered = filtered.filter(c => (c.primaryForums || []).includes(forumSlug));
|
|
if (search) {
|
|
filtered = filtered.filter(c => {
|
|
const p = c.personality || {};
|
|
const text = [
|
|
c.displayName || "",
|
|
c.key || "",
|
|
p.identity || "",
|
|
p.stance || "",
|
|
(p.expertise || []).join(" "),
|
|
].join(" ").toLowerCase();
|
|
return text.includes(search);
|
|
});
|
|
}
|
|
|
|
const emails = filtered.map(c => `bot_${c.key}@zhuiguang.ai`);
|
|
const userRows = emails.length > 0
|
|
? await prisma.user.findMany({
|
|
where: { email: { in: emails } },
|
|
select: { id: true, name: true, email: true, avatarUrl: true, points: true, level: true, createdAt: true },
|
|
})
|
|
: [];
|
|
const userByEmail = new Map(userRows.map(u => [u.email, u]));
|
|
|
|
const userIds = userRows.map(u => u.id);
|
|
const [topicGroups, postGroups, recentTopics] = await Promise.all([
|
|
userIds.length > 0
|
|
? prisma.forumTopic.groupBy({
|
|
by: ["userId"],
|
|
where: { userId: { in: userIds }, isHidden: false },
|
|
_count: { _all: true },
|
|
})
|
|
: Promise.resolve([] as Array<{ userId: number; _count: { _all: number } }>),
|
|
userIds.length > 0
|
|
? prisma.forumPost.groupBy({
|
|
by: ["userId"],
|
|
where: { userId: { in: userIds }, isHidden: false },
|
|
_count: { _all: true },
|
|
})
|
|
: Promise.resolve([] as Array<{ userId: number; _count: { _all: number } }>),
|
|
userIds.length > 0
|
|
? prisma.forumTopic.findMany({
|
|
where: {
|
|
userId: { in: userIds },
|
|
createdAt: { gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) },
|
|
isHidden: false,
|
|
},
|
|
select: {
|
|
userId: true,
|
|
createdAt: true,
|
|
title: true,
|
|
id: true,
|
|
category: { select: { name: true, slug: true } },
|
|
},
|
|
orderBy: { createdAt: "desc" },
|
|
take: 200,
|
|
})
|
|
: Promise.resolve([] as Array<any>),
|
|
]);
|
|
|
|
const topicMap = new Map(topicGroups.map(t => [t.userId, t._count._all]));
|
|
const postMap = new Map(postGroups.map(p => [p.userId, p._count._all]));
|
|
const recentMap = new Map<number, any[]>();
|
|
for (const t of recentTopics) {
|
|
const arr = recentMap.get(t.userId) || [];
|
|
arr.push(t);
|
|
recentMap.set(t.userId, arr);
|
|
}
|
|
|
|
const enriched = filtered.map(c => {
|
|
const u = userByEmail.get(`bot_${c.key}@zhuiguang.ai`);
|
|
const isPasserby = c.role === "passerby";
|
|
return {
|
|
key: c.key,
|
|
displayName: c.displayName,
|
|
role: isPasserby ? "passerby" : "expert",
|
|
avatarUrl: u?.avatarUrl || null,
|
|
avatarPrompt: c.avatarPrompt,
|
|
primaryForums: c.primaryForums || [],
|
|
primaryForumNames: (c.primaryForums || []).map(s => FORUM_NAMES[s] || s),
|
|
activityLevel: c.activityLevel,
|
|
activeHours: c.activeHours,
|
|
expertise: c.personality?.expertise || [],
|
|
identity: c.personality?.identity || "",
|
|
catchphrase: c.personality?.catchphrase || "",
|
|
stance: c.personality?.stance || "",
|
|
speakingStyle: c.personality?.speakingStyle || "",
|
|
passerbyType: c.personality?.passerbyType || null,
|
|
userId: u?.id || null,
|
|
points: u?.points || 0,
|
|
level: u?.level || null,
|
|
createdAt: u?.createdAt || null,
|
|
stats: {
|
|
topics: u ? topicMap.get(u.id) || 0 : 0,
|
|
posts: u ? postMap.get(u.id) || 0 : 0,
|
|
},
|
|
recentTopics: u ? (recentMap.get(u.id) || []).slice(0, 3) : [],
|
|
};
|
|
});
|
|
|
|
enriched.sort((a, b) => {
|
|
const aa = a.stats.topics + a.stats.posts;
|
|
const bb = b.stats.topics + b.stats.posts;
|
|
if (bb !== aa) return bb - aa;
|
|
return a.displayName.localeCompare(b.displayName);
|
|
});
|
|
|
|
return NextResponse.json({
|
|
bots: enriched,
|
|
total: enriched.length,
|
|
forums: Object.entries(FORUM_NAMES).map(([slug, name]) => ({ slug, name })),
|
|
});
|
|
} catch (error: unknown) {
|
|
const err = error as { message?: string };
|
|
return NextResponse.json({ error: err.message || "获取Bot列表失败" }, { status: 500 });
|
|
}
|
|
}
|