Files
zhuiguang-ai/scripts/task6-review-hot.mjs
T

234 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").replace("mysql://", "mariadb://");
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 || {},
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);
});