234 lines
7.8 KiB
JavaScript
234 lines
7.8 KiB
JavaScript
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" });
|
||
|
||
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);
|
||
});
|