Files
zhuiguang-ai/src/app/api/bots/route.ts
T

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 });
}
}