Files
zhuiguang-ai/scripts/task6-review-hot.mjs
T
ZhuiGuangAI Dev e263e28c81 全面审查优化: 43项修复(安全/性能/可靠性)+ 文档更新
🔴 P0 安全: .env入gitignore, CSP/HSTS, Logo安全(5MB限制+SVG消毒+SHA256), middleware权限收紧(VIP不能访问admin), 发布去重, GitHub认证, 删除危险API, skill-discoverer绕过审核修复, prisma默认连接修复
🔴 P0 可靠性: deepseek/github超时重试, 脚本层DeepSeek保护, 脚本启动验证, OAuth竞态修复
🔴 P0 性能: skill-discoverer N+1优化(↓94%), 后台API分页限制, getUserStats聚合优化
🟡 P1: 分类动态加载, 星级字段统一, fetch-logos并发Bug修复, 批量发布冲突不删除, check-tools重试, 前端空指针修复(13处), logo-fetcher日志, rate-limit清理, 公开API缓存
2026-05-26 00:30:36 +08:00

235 lines
7.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { PrismaClient } from "@prisma/client";
import { PrismaMariaDb } from "@prisma/adapter-mariadb";
import { Octokit } from "octokit";
import OpenAI from "openai";
import "dotenv/config";
import { withRetry } from "./lib/retry.mjs";
const base = process.env.DATABASE_URL || "mysql://localhost:3306/zhuiguang_ai?charset=utf8mb4";
const sep = base.includes("?") ? "&" : "?";
const connectionString = `${base}${sep}connection_limit=20&pool_timeout=30`;
const adapter = new PrismaMariaDb(connectionString);
const prisma = new PrismaClient({ adapter });
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
const openai = new OpenAI({ apiKey: process.env.DEEPSEEK_API_KEY, baseURL: "https://api.deepseek.com/v1" });
const REVIEW_PROMPT = `你是AI技术资深评测师。请对以下AI开源项目进行五维评测,每个维度1-5分。
评测维度:
- capability:场景与能力(功能覆盖度、场景适配性、输出质量)
- devExp:开发体验(安装便捷度、文档质量、API设计、调试友好度)
- costLicense:成本与许可(开源协议友好度、部署成本、资源消耗)
- community:社区健康(Stars趋势、贡献者数量、Issue响应速度、更新频率)
- performance:性能稳定(推理速度、并发能力、稳定性、可扩展性)
项目信息:
名称:{name}
描述:{description}
GitHub Stars:{stars}
README摘要:{readme}
返回纯JSON:
{
"capability": 4.5,
"devExp": 4.0,
"costLicense": 5.0,
"community": 4.5,
"performance": 4.0,
"overall": 4.4,
"reviews": {
"capability": {"score": 4.5, "summary": "一句话总结", "detail": "详细分析"},
"devExp": {"score": 4.0, "summary": "一句话总结", "detail": "详细分析"},
"costLicense": {"score": 5.0, "summary": "一句话总结", "detail": "详细分析"},
"community": {"score": 4.5, "summary": "一句话总结", "detail": "详细分析"},
"performance": {"score": 4.0, "summary": "一句话总结", "detail": "详细分析"}
},
"tags": ["标签1", "标签2"]
}`;
function extractJSON(str) {
const trimmed = str.trim();
const codeBlockMatch = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/);
let jsonStr = codeBlockMatch ? codeBlockMatch[1].trim() : trimmed;
const braceMatch = jsonStr.match(/\{[\s\S]*\}/);
if (braceMatch) { try { return JSON.parse(braceMatch[0]); } catch {} }
return JSON.parse(jsonStr);
}
function extractOwnerRepo(sourceUrl) {
const match = sourceUrl.match(/github\.com\/([^/]+)\/([^/]+)/);
if (!match) return null;
return { owner: match[1], repo: match[2].replace(/\.git$/, "") };
}
async function getReadme(owner, repo) {
try {
const { data } = await octokit.rest.repos.getReadme({ owner, repo });
const content = Buffer.from(data.content, "base64").toString("utf-8");
return content.substring(0, 3000);
} catch {
return "(无法获取README)";
}
}
async function main() {
if (!process.env.DEEPSEEK_API_KEY) throw new Error("缺少环境变量: DEEPSEEK_API_KEY");
if (!process.env.DATABASE_URL) throw new Error("缺少环境变量: DATABASE_URL");
const startTime = new Date();
const now = new Date();
const cnOffset = 8 * 60 * 60 * 1000;
const cnTime = new Date(now.getTime() + cnOffset);
const today = cnTime.toISOString().split("T")[0];
console.log(`🔥 [Task6] ${today} 评测最热工具...`);
const unreviewed = await prisma.skill.findMany({
where: {
status: "published",
sourceUrl: { contains: "github.com" },
skillReview: null,
},
orderBy: [{ githubStars: "desc" }, { viewCount: "desc" }],
take: 2,
select: { id: true, name: true, description: true, sourceUrl: true, githubStars: true },
});
if (unreviewed.length === 0) {
console.log(" 所有热门技能已有评测,跳过");
await prisma.taskLog.create({
data: {
taskKey: "task6-review-hot",
taskName: "评测热门技能",
status: "completed",
startedAt: startTime,
finishedAt: new Date(),
duration: new Date().getTime() - startTime.getTime(),
result: { reviewed: 0, message: "所有热门技能已有评测" },
},
});
await prisma.$disconnect();
return;
}
console.log(` 待评测: ${unreviewed.map((s) => s.name).join(", ")}`);
let reviewed = 0;
let failed = 0;
const errors = [];
for (const skill of unreviewed) {
console.log(`\n 📦 评测: ${skill.name} (${skill.githubStars}⭐)`);
const parsed = extractOwnerRepo(skill.sourceUrl);
let readme = "(无README)";
let stars = skill.githubStars;
if (parsed) {
readme = await getReadme(parsed.owner, parsed.repo);
try {
const { data } = await octokit.rest.repos.get({ owner: parsed.owner, repo: parsed.repo });
stars = data.stargazers_count;
} catch {}
}
const prompt = REVIEW_PROMPT
.replace("{name}", skill.name)
.replace("{description}", skill.description || "")
.replace("{stars}", String(stars))
.replace("{readme}", readme.substring(0, 2000));
try {
const completion = await withRetry(async () => {
return await openai.chat.completions.create({
model: process.env.DEEPSEEK_MODEL || "deepseek-v4-pro",
messages: [{ role: "user", content: prompt }],
temperature: 0.3,
}, { signal: AbortSignal.timeout(120000) });
}, { maxRetries: 3, label: "DeepSeek Review API" });
const content = completion.choices[0]?.message?.content;
if (!content) throw new Error("DeepSeek返回为空");
const parsed_review = extractJSON(content);
const scores = [parsed_review.capability, parsed_review.devExp, parsed_review.costLicense, parsed_review.community, parsed_review.performance].filter((s) => typeof s === "number");
const overall = parsed_review.overall || (scores.length > 0 ? Math.round((scores.reduce((a, b) => a + b, 0) / scores.length) * 10) / 10 : 3.0);
await prisma.skillReview.create({
data: {
skillId: skill.id,
capability: parsed_review.capability || 3.0,
devExp: parsed_review.devExp || 3.0,
costLicense: parsed_review.costLicense || 3.0,
community: parsed_review.community || 3.0,
performance: parsed_review.performance || 3.0,
overall,
reviews: parsed_review.reviews || {},
tags: parsed_review.tags || [],
status: "PUBLISHED",
reviewedAt: new Date(),
},
});
await prisma.skill.update({
where: { id: skill.id },
data: { lastReviewedAt: new Date() },
});
reviewed++;
console.log(` ✅ 评测完成: overall=${overall}`);
} catch (e) {
failed++;
errors.push(`${skill.name}: ${e.message}`);
console.error(` ❌ 评测失败: ${e.message}`);
}
}
const endTime = new Date();
const duration = endTime.getTime() - startTime.getTime();
const result = {
total: unreviewed.length,
reviewed,
failed,
duration: `${(duration / 1000).toFixed(1)}s`,
};
console.log(`\n🏁 [Task6] 完成: 评测${reviewed}个, 失败${failed}个`);
await prisma.taskLog.create({
data: {
taskKey: "task6-review-hot",
taskName: "评测热门技能",
status: "completed",
startedAt: startTime,
finishedAt: endTime,
duration,
result,
error: errors.length > 0 ? errors.slice(0, 5).join("\n") : null,
},
});
await prisma.$disconnect();
}
main().catch(async (e) => {
console.error("❌ 任务六失败:", e.message);
if (prisma) {
try {
await prisma.taskLog.create({
data: {
taskKey: "task6-review-hot",
taskName: "评测热门技能",
status: "failed",
startedAt: new Date(),
finishedAt: new Date(),
duration: 0,
error: e.message,
},
});
} catch {}
await prisma.$disconnect();
}
process.exit(1);
});