2026-05-25: 等级体系升级(20级)+论坛二级分类+会员空间
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { PrismaMariaDb } from "@prisma/adapter-mariadb";
|
||||
import { Octokit } from "octokit";
|
||||
import OpenAI from "openai";
|
||||
import "dotenv/config";
|
||||
|
||||
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() {
|
||||
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 openai.chat.completions.create({
|
||||
model: "deepseek-v4-pro",
|
||||
messages: [{ role: "user", content: prompt }],
|
||||
temperature: 0.3,
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user