全项目扫描修复: Docker数据卷修复+安全requireAdmin+12页SEO+API白名单+脚本超时+常量提取+假数据删除
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
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 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user