2026-05-25: 等级体系升级(20级)+论坛二级分类+会员空间

This commit is contained in:
ZhuiGuangAI Dev
2026-05-24 23:05:38 +08:00
parent 6c78c32157
commit 9bf3e7574a
118 changed files with 11842 additions and 3262 deletions
+26
View File
@@ -0,0 +1,26 @@
import { PrismaClient } from "@prisma/client";
import { PrismaMariaDb } from "@prisma/adapter-mariadb";
import "dotenv/config";
const base = process.env.DATABASE_URL;
const sep = base.includes("?") ? "&" : "?";
const connectionString = `${base}${sep}connection_limit=5&pool_timeout=10`;
const adapter = new PrismaMariaDb(connectionString);
const prisma = new PrismaClient({ adapter });
async function main() {
const result = await prisma.$queryRawUnsafe("SELECT migration_name, finished_at FROM _prisma_migrations ORDER BY started_at");
console.log("Applied migrations:");
for (const row of result) {
console.log(` ${row.migration_name} (${row.finished_at})`);
}
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
cd /home/ubuntu/zhuiguang-ai
export $(grep -v '^#' .env | xargs)
export NODE_TLS_REJECT_UNAUTHORIZED=0
node scripts/task1-discover-tools.mjs >> /home/ubuntu/zhuiguang-ai/logs/task1-discover-tools.log 2>&1
+70 -31
View File
@@ -3,6 +3,7 @@ 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("?") ? "&" : "?";
@@ -165,13 +166,17 @@ function genSlug(name) {
async function searchRepos(query, minStars, max) {
const q = `${query} stars:>${minStars} pushed:>2024-01-01`;
const { data } = await octokit.rest.search.repos({ q, sort: "stars", order: "desc", per_page: max });
const { data } = await withRetry(async () => {
return await octokit.rest.search.repos({ q, sort: "stars", order: "desc", per_page: max });
}, { maxRetries: 3, label: `GitHub Search "${query}"` });
return data.items;
}
async function getReadme(owner, repo) {
try {
const { data } = await octokit.rest.repos.getReadme({ owner, repo });
const { data } = await withRetry(async () => {
return await octokit.rest.repos.getReadme({ owner, repo });
}, { maxRetries: 3, label: `GitHub README ${owner}/${repo}` });
return Buffer.from(data.content, "base64").toString("utf-8");
} catch { return ""; }
}
@@ -181,9 +186,11 @@ async function genReview(repoName, desc, stars, license, readme) {
.replace("{repoName}", repoName).replace("{description}", desc || "暂无描述")
.replace("{stars}", stars.toString()).replace("{license}", license || "未知")
.replace("{readmeSummary}", readme.substring(0, 1500));
const { choices } = await openai.chat.completions.create({
model: "deepseek-v4-pro", messages: [{ role: "user", content: prompt }], temperature: 0.3,
});
const { choices } = await withRetry(async () => {
return await openai.chat.completions.create({
model: "deepseek-v4-pro", messages: [{ role: "user", content: prompt }], temperature: 0.3,
});
}, { maxRetries: 3, label: "DeepSeek Review API" });
const content = choices[0]?.message?.content;
if (!content) throw new Error("Empty response");
let review = extractJSON(content);
@@ -199,15 +206,16 @@ async function genReview(repoName, desc, stars, license, readme) {
}
async function main() {
console.log(`🚀 [Daily Discover] ${new Date().toISOString().split("T")[0]} 开始...\n`);
const DAILY_LIMIT = 10;
const startTime = new Date();
console.log(`🚀 [Task4] ${new Date().toISOString().split("T")[0]} 发现新AI技能...\n`);
const DAILY_LIMIT = 30;
let discovered = 0, skipped = 0, reviewed = 0, failed = 0;
for (const query of SEARCH_QUERIES) {
if (discovered >= DAILY_LIMIT) break;
console.log(`🔍 "${query}"`);
let repos;
try { repos = await searchRepos(query, 500, 5); } catch (e) { console.error(` ❌ 搜索失败: ${e.message}`); failed++; continue; }
try { repos = await searchRepos(query, 1000, 5); } catch (e) { console.error(` ❌ 搜索失败: ${e.message}`); failed++; continue; }
for (const repo of repos) {
if (discovered >= DAILY_LIMIT) break;
@@ -225,36 +233,31 @@ async function main() {
const [owner, name] = repo.full_name.split("/");
const readme = await getReadme(owner, name);
let reviewData = null;
try { reviewData = await genReview(repo.full_name, repo.description || "", repo.stargazers_count, repo.license?.spdx_id || "Unknown", readme); console.log(` ✅ 评测: ${reviewData.overall}`); reviewed++; }
catch (e) { console.log(` ⚠️ 评测失败: ${e.message}`); }
try { reviewData = await genReview(repo.full_name, repo.description || "", repo.stargazers_count, repo.license?.spdx_id || "Unknown", readme); console.log(` ✅ 预评测: ${reviewData.overall}`); reviewed++; }
catch (e) { console.log(` ⚠️ 预评测失败: ${e.message}`); }
const categoryId = await guessCategory(repo.full_name, repo.description || "", reviewData?.tags || []);
const skill = await prisma.skill.create({
const pendingSkill = await prisma.pendingSkill.create({
data: {
name: repo.full_name.substring(0, 100), slug, categoryId,
description: repo.description || "", sourceUrl: repo.html_url, sourceType: "github",
rating: reviewData?.overall || 0,
features: { forks: repo.forks_count, language: repo.language, license: repo.license?.spdx_id || null, topics: repo.topics || [] },
tags: reviewData?.tags || [], status: "draft",
lastReviewedAt: reviewData ? new Date() : null,
githubStarsHistory: [{ date: new Date().toISOString().split("T")[0], stars: repo.stargazers_count }],
features: {
forks: repo.forks_count,
language: repo.language,
license: repo.license?.spdx_id || null,
topics: repo.topics || [],
stars: repo.stargazers_count,
reviewData: reviewData || null,
},
tags: reviewData?.tags || [],
status: "pending",
},
});
console.log(` ✅ Skill #${skill.id}`);
if (reviewData) {
await prisma.skillReview.create({
data: {
skillId: skill.id, capability: reviewData.capability.score, devExp: reviewData.devExp.score,
costLicense: reviewData.costLicense.score, community: reviewData.community.score,
performance: reviewData.performance.score, overall: reviewData.overall,
reviews: reviewData, status: "DRAFT",
},
});
}
console.log(` ✅ PendingSkill #${pendingSkill.id}`);
await prisma.reviewGenerationLog.create({
data: { skillId: skill.id, status: "SUCCESS", prompt: `daily-discover: ${repo.full_name}`, response: reviewData ? JSON.stringify(reviewData) : null },
data: { skillId: pendingSkill.id, status: "SUCCESS", prompt: `task4-discover: ${repo.full_name}`, response: reviewData ? JSON.stringify(reviewData) : null },
});
discovered++;
} catch (e) {
@@ -262,14 +265,50 @@ async function main() {
failed++;
try {
await prisma.reviewGenerationLog.create({
data: { skillId: 0, status: "FAILED", prompt: `daily-discover: ${repo.full_name}`, error: e.message?.substring(0, 500) || "Unknown" },
data: { skillId: 0, status: "FAILED", prompt: `task4-discover: ${repo.full_name}`, error: e.message?.substring(0, 500) || "Unknown" },
});
} catch {}
}
}
}
console.log(`\n🏁 [Daily Discover] 完成: 发现${discovered} / 跳过${skipped} / 评测${reviewed} / 失败${failed}`);
const endTime = new Date();
const duration = endTime.getTime() - startTime.getTime();
const result = { discovered, skipped, reviewed, failed, duration: `${(duration / 1000).toFixed(1)}s` };
console.log(`\n🏁 [Task4] 完成: 发现${discovered} / 跳过${skipped} / 评测${reviewed} / 失败${failed}`);
await prisma.taskLog.create({
data: {
taskKey: "task4-discover-skills",
taskName: "发现AI技能",
status: "completed",
startedAt: startTime,
finishedAt: endTime,
duration,
result,
error: failed > discovered * 2 ? `失败率过高: ${failed}/${discovered}` : null,
},
});
}
main().catch((e) => { console.error("💥 异常:", e); process.exit(1); }).finally(() => prisma.$disconnect());
main().catch(async (e) => {
console.error("💥 异常:", e);
if (prisma) {
try {
await prisma.taskLog.create({
data: {
taskKey: "task4-discover-skills",
taskName: "发现AI技能",
status: "failed",
startedAt: new Date(),
finishedAt: new Date(),
duration: 0,
error: e.message,
},
});
} catch {}
await prisma.$disconnect();
}
process.exit(1);
}).finally(() => prisma.$disconnect());
+34 -4
View File
@@ -244,13 +244,16 @@ function extractJSON(str) {
}
async function generateDailyNews() {
const today = new Date();
const dateStr = today.toLocaleDateString("zh-CN", {
const now = new Date();
const cnOffset = 8 * 60 * 60 * 1000;
const cnTime = new Date(now.getTime() + cnOffset);
const dateStr = cnTime.toLocaleDateString("zh-CN", {
year: "numeric",
month: "long",
day: "numeric",
timeZone: "Asia/Shanghai",
});
const dateISO = today.toISOString().split("T")[0];
const dateISO = cnTime.toISOString().split("T")[0];
console.log(`📰 开始生成 ${dateStr} 的AI日报...`);
@@ -344,10 +347,37 @@ async function generateDailyNews() {
}
async function main() {
const startTime = new Date();
try {
await generateDailyNews();
const report = await generateDailyNews();
const endTime = new Date();
const duration = endTime.getTime() - startTime.getTime();
await prisma.taskLog.create({
data: {
taskKey: "daily-news",
taskName: "AI日报生成",
status: "completed",
startedAt: startTime,
finishedAt: endTime,
duration,
result: { reportId: report.id, itemCount: report.items.length, title: report.title },
},
});
} catch (error) {
console.error("❌ 日报生成失败:", error.message);
try {
await prisma.taskLog.create({
data: {
taskKey: "daily-news",
taskName: "AI日报生成",
status: "failed",
startedAt: startTime,
finishedAt: new Date(),
duration: new Date().getTime() - startTime.getTime(),
error: error.message,
},
});
} catch {}
process.exit(1);
} finally {
await prisma.$disconnect();
+124
View File
@@ -0,0 +1,124 @@
import { PrismaClient } from "@prisma/client";
import { PrismaMariaDb } from "@prisma/adapter-mariadb";
import "dotenv/config";
const base = process.env.DATABASE_URL;
const sep = base.includes("?") ? "&" : "?";
const connectionString = `${base}${sep}connection_limit=5&pool_timeout=10`;
const adapter = new PrismaMariaDb(connectionString);
const prisma = new PrismaClient({ adapter });
const CATEGORIES = [
{
name: "综合讨论区", slug: "general",
children: [
{ name: "新手入门", slug: "newbie" },
{ name: "AI资讯", slug: "ai-news" },
{ name: "观点讨论", slug: "discuss" },
],
},
{
name: "AI工具", slug: "ai-tools",
children: [
{ name: "聊天写作", slug: "chat-writing" },
{ name: "图像创作", slug: "image-gen" },
{ name: "视频音频", slug: "video-audio" },
{ name: "代码编程", slug: "code-tools" },
{ name: "效率办公", slug: "productivity" },
{ name: "数据分析", slug: "data-analysis" },
{ name: "教育学习", slug: "edu-tools" },
],
},
{
name: "开源技能", slug: "skills",
children: [
{ name: "LLM框架", slug: "llm-framework" },
{ name: "Agent框架", slug: "agent-framework" },
{ name: "RAG技术", slug: "rag" },
{ name: "模型训练", slug: "model-training" },
{ name: "部署推理", slug: "deploy-infer" },
{ name: "多模态", slug: "multimodal" },
{ name: "数据工程", slug: "data-eng" },
],
},
{
name: "技术深度", slug: "tech",
children: [
{ name: "算法原理", slug: "algorithms" },
{ name: "模型评测", slug: "model-review" },
{ name: "安全对齐", slug: "safety" },
{ name: "硬件加速", slug: "hardware" },
],
},
{
name: "教程与分享", slug: "tutorials",
children: [
{ name: "入门教程", slug: "beginner-guide" },
{ name: "实战项目", slug: "projects" },
{ name: "最佳实践", slug: "best-practice" },
],
},
{
name: "社区互动", slug: "social",
children: [
{ name: "求助问答", slug: "qa" },
{ name: "求职招聘", slug: "jobs" },
{ name: "闲聊灌水", slug: "random" },
{ name: "反馈建议", slug: "feedback" },
],
},
];
async function main() {
console.log("Fixing forum category parent-child relationships...\n");
for (const cat of CATEGORIES) {
const parent = await prisma.forumCategory.findUnique({
where: { slug: cat.slug },
});
if (!parent) {
console.log(` ✗ Parent "${cat.name}" not found, skipping`);
continue;
}
console.log(` Parent: ${cat.name} (id=${parent.id})`);
for (const child of cat.children) {
const existing = await prisma.forumCategory.findUnique({
where: { slug: child.slug },
});
if (!existing) {
await prisma.forumCategory.create({
data: {
name: child.name,
slug: child.slug,
parentId: parent.id,
},
});
console.log(` + Created: ${child.name}`);
} else if (existing.parentId !== parent.id) {
await prisma.forumCategory.update({
where: { id: existing.id },
data: { parentId: parent.id },
});
console.log(` ✓ Fixed: ${child.name} (parentId: ${existing.parentId} → ${parent.id})`);
} else {
console.log(` ✓ OK: ${child.name}`);
}
}
console.log("");
}
console.log("Done! All category relationships are correct.");
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});
+18
View File
@@ -0,0 +1,18 @@
async function withRetry(fn, { maxRetries = 3, baseDelay = 1000, label = "operation" } = {}) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
if (attempt < maxRetries) {
const delay = baseDelay * Math.pow(2, attempt - 1);
console.warn(` ⚠ ${label} 第${attempt}次失败,${delay / 1000}s后重试...`);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}
throw lastError;
}
export { withRetry };
+28
View File
@@ -0,0 +1,28 @@
import { PrismaClient } from "@prisma/client";
import { PrismaMariaDb } from "@prisma/adapter-mariadb";
import "dotenv/config";
const base = process.env.DATABASE_URL;
const sep = base.includes("?") ? "&" : "?";
const connectionString = `${base}${sep}connection_limit=5&pool_timeout=10`;
const adapter = new PrismaMariaDb(connectionString);
const prisma = new PrismaClient({ adapter });
async function main() {
console.log("Updating user levels from old enum to new...");
const result = await prisma.$executeRawUnsafe(
"UPDATE users SET level = 'LV01' WHERE level NOT REGEXP '^LV[0-9][0-9]$'"
);
console.log(`Updated: ${result} rows`);
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});
+93
View File
@@ -0,0 +1,93 @@
import { PrismaClient } from "@prisma/client";
import { PrismaMariaDb } from "@prisma/adapter-mariadb";
import "dotenv/config";
const base = process.env.DATABASE_URL;
const sep = base.includes("?") ? "&" : "?";
const connectionString = `${base}${sep}connection_limit=5&pool_timeout=10`;
const adapter = new PrismaMariaDb(connectionString);
const prisma = new PrismaClient({ adapter });
async function main() {
try {
console.log('Starting manual migration...');
// Step 1: Change level column to VARCHAR
await prisma.$executeRawUnsafe(
"ALTER TABLE `users` MODIFY COLUMN `level` VARCHAR(20) NOT NULL DEFAULT 'LV01'"
);
console.log('✓ Step 1: level column changed to VARCHAR');
// Step 2: Map old enum values
await prisma.$executeRawUnsafe("UPDATE `users` SET `level` = 'LV01' WHERE `level` = 'BRONZE'");
await prisma.$executeRawUnsafe("UPDATE `users` SET `level` = 'LV03' WHERE `level` = 'SILVER'");
await prisma.$executeRawUnsafe("UPDATE `users` SET `level` = 'LV06' WHERE `level` = 'GOLD'");
await prisma.$executeRawUnsafe("UPDATE `users` SET `level` = 'LV10' WHERE `level` = 'PLATINUM'");
console.log('✓ Step 2: Old enum values mapped to new levels');
// Step 3: Add bio column (if not exists)
try {
await prisma.$executeRawUnsafe("ALTER TABLE `users` ADD COLUMN `bio` TEXT NULL");
console.log('✓ Step 3: bio column added');
} catch (e) {
console.log('✓ Step 3: bio column may already exist, skipping');
}
// Step 4: Add parent_id column (if not exists)
try {
await prisma.$executeRawUnsafe("ALTER TABLE `forum_categories` ADD COLUMN `parent_id` INT NULL");
console.log('✓ Step 4: parent_id column added');
} catch (e) {
console.log('✓ Step 4: parent_id column may already exist, skipping');
}
// Step 5: Add foreign key (if not exists)
try {
await prisma.$executeRawUnsafe(
"ALTER TABLE `forum_categories` ADD CONSTRAINT `forum_categories_parent_id_fkey` " +
"FOREIGN KEY (`parent_id`) REFERENCES `forum_categories`(`id`) ON DELETE SET NULL ON UPDATE CASCADE"
);
console.log('✓ Step 5: Foreign key added');
} catch (e) {
console.log('✓ Step 5: Foreign key may already exist, skipping');
}
// Step 6: Add points_history table (if not exists)
try {
await prisma.$executeRawUnsafe(
`CREATE TABLE IF NOT EXISTS \`points_history\` (
\`id\` INT AUTO_INCREMENT PRIMARY KEY,
\`user_id\` INT NOT NULL,
\`points\` INT NOT NULL,
\`reason\` VARCHAR(50) NOT NULL,
\`detail\` VARCHAR(200) NULL,
\`ref_type\` VARCHAR(50) NULL,
\`ref_id\` INT NULL,
\`created_at\` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX \`idx_user_created\` (\`user_id\`, \`created_at\`),
FOREIGN KEY (\`user_id\`) REFERENCES \`users\`(\`id\`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`
);
console.log('✓ Step 6: points_history table created');
} catch (e) {
console.log('✓ Step 6: points_history table may already exist, skipping');
}
// Step 7: Add is_answer column to forum_posts (if not exists)
try {
await prisma.$executeRawUnsafe("ALTER TABLE `forum_posts` ADD COLUMN `is_answer` BOOLEAN NOT NULL DEFAULT false");
console.log('✓ Step 7: is_answer column added');
} catch (e) {
console.log('✓ Step 7: is_answer column may already exist, skipping');
}
console.log('\n✓ Migration completed successfully!');
} catch (e) {
console.error('Error:', e.message);
process.exit(1);
} finally {
await prisma.$disconnect();
}
}
main();
+130
View File
@@ -0,0 +1,130 @@
import { PrismaClient } from "@prisma/client";
import { PrismaMariaDb } from "@prisma/adapter-mariadb";
import "dotenv/config";
const base = process.env.DATABASE_URL;
const sep = base.includes("?") ? "&" : "?";
const connectionString = `${base}${sep}connection_limit=5&pool_timeout=10`;
const adapter = new PrismaMariaDb(connectionString);
const prisma = new PrismaClient({ adapter });
const CATEGORIES = [
{
name: "综合讨论区", slug: "general", icon: "💬", sortOrder: 1,
description: "AI话题综合交流",
children: [
{ name: "新手入门", slug: "newbie", icon: "🌱", description: "新人报到,快速上手" },
{ name: "AI资讯", slug: "ai-news", icon: "📰", description: "行业动态与新闻" },
{ name: "观点讨论", slug: "discuss", icon: "💡", description: "AI发展趋势讨论" },
],
},
{
name: "AI工具", slug: "ai-tools", icon: "🤖", sortOrder: 2,
description: "AI工具推荐、测评与讨论",
children: [
{ name: "聊天写作", slug: "chat-writing", icon: "✍️", description: "ChatGPT/Claude/文心一言等" },
{ name: "图像创作", slug: "image-gen", icon: "🎨", description: "Midjourney/Stable Diffusion等" },
{ name: "视频音频", slug: "video-audio", icon: "🎬", description: "Sora/Runway/Suno等" },
{ name: "代码编程", slug: "code-tools", icon: "💻", description: "GitHub Copilot/Cursor等" },
{ name: "效率办公", slug: "productivity", icon: "📊", description: "Notion AI/办公自动化等" },
{ name: "数据分析", slug: "data-analysis", icon: "📈", description: "商业智能与数据分析工具" },
{ name: "教育学习", slug: "edu-tools", icon: "📚", description: "在线教育与学习辅助" },
],
},
{
name: "开源技能", slug: "skills", icon: "⚡", sortOrder: 3,
description: "开源AI框架、模型与工具技能",
children: [
{ name: "LLM框架", slug: "llm-framework", icon: "🧠", description: "LangChain/LlamaIndex等" },
{ name: "Agent框架", slug: "agent-framework", icon: "🤖", description: "AutoGPT/CrewAI/MetaGPT等" },
{ name: "RAG技术", slug: "rag", icon: "📖", description: "向量数据库/检索增强生成" },
{ name: "模型训练", slug: "model-training", icon: "🔬", description: "LoRA/Fine-tune/PEFT等" },
{ name: "部署推理", slug: "deploy-infer", icon: "🚀", description: "vLLM/Ollama/TensorRT等" },
{ name: "多模态", slug: "multimodal", icon: "🎯", description: "视觉/语音/多模态技术" },
{ name: "数据工程", slug: "data-eng", icon: "🗃️", description: "数据清洗/标注/管理工具" },
],
},
{
name: "技术深度", slug: "tech", icon: "🔧", sortOrder: 4,
description: "AI技术深度探讨",
children: [
{ name: "算法原理", slug: "algorithms", icon: "🧮", description: "Transformer/注意力机制等" },
{ name: "模型评测", slug: "model-review", icon: "📋", description: "Benchmark/对比评测" },
{ name: "安全对齐", slug: "safety", icon: "🛡️", description: "RLHF/安全/伦理" },
{ name: "硬件加速", slug: "hardware", icon: "🔌", description: "GPU/TPU/NPU优化" },
],
},
{
name: "教程与分享", slug: "tutorials", icon: "📚", sortOrder: 5,
description: "教程、指南与经验分享",
children: [
{ name: "入门教程", slug: "beginner-guide", icon: "🌟", description: "从零开始学AI" },
{ name: "实战项目", slug: "projects", icon: "🏗️", description: "完整项目实战" },
{ name: "最佳实践", slug: "best-practice", icon: "✅", description: "经验分享与踩坑记录" },
],
},
{
name: "社区互动", slug: "social", icon: "🎪", sortOrder: 6,
description: "社区活动与交流",
children: [
{ name: "求助问答", slug: "qa", icon: "❓", description: "有问题来这里" },
{ name: "求职招聘", slug: "jobs", icon: "💼", description: "AI领域求职招聘" },
{ name: "闲聊灌水", slug: "random", icon: "🎲", description: "轻松交流区" },
{ name: "反馈建议", slug: "feedback", icon: "📢", description: "对追光AI的建议" },
],
},
];
async function main() {
console.log("Seeding forum categories (2-level)...");
for (const cat of CATEGORIES) {
let parent = await prisma.forumCategory.findUnique({
where: { slug: cat.slug },
include: { children: true },
});
if (!parent) {
parent = await prisma.forumCategory.create({
data: {
name: cat.name,
slug: cat.slug,
icon: cat.icon,
description: cat.description,
sortOrder: cat.sortOrder,
},
include: { children: true },
});
console.log(` + ${cat.name}`);
} else {
console.log(` ✓ ${cat.name} (exists, checking children)`);
}
const existingSlugs = new Set(parent.children.map(c => c.slug));
for (const child of cat.children) {
if (existingSlugs.has(child.slug)) {
console.log(` - ${child.name} (exists)`);
} else {
await prisma.forumCategory.create({
data: {
...child,
parentId: parent.id,
},
});
console.log(` + ${child.name}`);
}
}
}
console.log("Done!");
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});
+109
View File
@@ -0,0 +1,109 @@
import { PrismaClient } from "@prisma/client";
import { PrismaMariaDb } from "@prisma/adapter-mariadb";
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 TASK_DEFS = [
{
taskKey: "task1-discover-tools",
taskName: "发现AI工具",
enabled: true,
cronExpr: "0 6 * * *",
description: "每天早上6点从36kr/IT之家/开源中国RSS抓取新闻,由DeepSeek提取AI工具信息并入库待审核",
config: { sources: ["36kr", "IT之家", "开源中国"], model: "deepseek-v4-pro" },
},
{
taskKey: "task3-check-tools",
taskName: "工具巡检",
enabled: true,
cronExpr: "0 3 * * *",
description: "每天凌晨3点检查所有已发布工具的网站可访问性和Logo有效性,自动发布通过检查的待审核工具",
config: { checkWebsite: true, checkLogo: true, autoPublish: true },
},
{
taskKey: "task4-discover-skills",
taskName: "发现AI技能",
enabled: true,
cronExpr: "0 5 * * *",
description: "每天凌晨5点从GitHub搜索高星AI项目(stars≥1000),DeepSeek预评测后入库PendingSkill待审核",
config: { minStars: 1000, dailyLimit: 30, model: "deepseek-v4-pro" },
},
{
taskKey: "task5-update-stars",
taskName: "更新技能星值",
enabled: true,
cronExpr: "0 1 * * *",
description: "每天凌晨1点更新所有GitHub技能的最新Stars数,记录日/周/月变化趋势",
config: { updateAll: true, historyDays: 90 },
},
{
taskKey: "task6-review-hot",
taskName: "评测热门技能",
enabled: true,
cronExpr: "0 4 * * *",
description: "每天凌晨4点对2个最热门且未评测的GitHub技能进行五维深度评测,自动生成SkillReview",
config: { dailyLimit: 2, model: "deepseek-v4-pro" },
},
{
taskKey: "daily-news",
taskName: "AI日报生成",
enabled: true,
cronExpr: "30 7 * * *",
description: "每天早上7:30从多源RSS搜索真实AI新闻,由DeepSeek整理格式生成当日日报",
config: { sources: ["36kr", "IT之家", "开源中国", "GoogleNews", "HackerNews"], model: "deepseek-v4-pro" },
},
];
async function main() {
console.log("🌱 初始化任务配置种子数据...\n");
let created = 0;
let updated = 0;
for (const def of TASK_DEFS) {
const existing = await prisma.taskConfig.findUnique({
where: { taskKey: def.taskKey },
});
if (existing) {
await prisma.taskConfig.update({
where: { taskKey: def.taskKey },
data: {
taskName: def.taskName,
cronExpr: def.cronExpr,
description: def.description,
config: def.config,
},
});
updated++;
console.log(` 🔄 更新: ${def.taskName} (${def.taskKey})`);
} else {
await prisma.taskConfig.create({
data: {
taskKey: def.taskKey,
taskName: def.taskName,
enabled: def.enabled,
cronExpr: def.cronExpr,
description: def.description,
config: def.config,
},
});
created++;
console.log(` ✅ 创建: ${def.taskName} (${def.taskKey})`);
}
}
console.log(`\n🏁 完成: 创建${created}个, 更新${updated}个`);
await prisma.$disconnect();
}
main().catch(async (e) => {
console.error("❌ 种子数据初始化失败:", e.message);
if (prisma) await prisma.$disconnect();
process.exit(1);
});
+231
View File
@@ -0,0 +1,231 @@
import { PrismaClient } from "@prisma/client";
import { PrismaMariaDb } from "@prisma/adapter-mariadb";
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 openai = new OpenAI({ apiKey: process.env.DEEPSEEK_API_KEY, baseURL: "https://api.deepseek.com/v1" });
const EXTRACT_PROMPT = `你是一个AI工具信息提取助手。从以下科技新闻内容中,提取出现的AI工具/产品信息。只提取明确提到的AI软件工具、AI平台、AI产品,不要提取开源项目库(GitHub仓库)、不要提取硬件、芯片。每个工具返回以下信息(如果信息不足则跳过该工具):
新闻内容:
{content}
返回纯JSON数组:
[{
"name": "工具名称",
"description": "一句话简介(20字以内)",
"websiteUrl": "官网或产品主页URL",
"category": "分类关键词(只能从以下选一):文本生成/图像生成/视频生成/语音音频/代码编程/办公效率/营销创作/数据AI/教育学习/设计制作"
}]`;
function extractJSON(str) {
const trimmed = str.trim();
const codeBlockMatch = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/);
let jsonStr = codeBlockMatch ? codeBlockMatch[1].trim() : trimmed;
const arrayMatch = jsonStr.match(/\[[\s\S]*\]/);
if (arrayMatch) {
try { return JSON.parse(arrayMatch[0]); } catch {}
}
return JSON.parse(jsonStr);
}
async function fetchRSS(url) {
try {
const res = await withRetry(async () => {
return await fetch(url, {
headers: { "User-Agent": "Mozilla/5.0 (compatible; ToolBot/1.0)" },
signal: AbortSignal.timeout(10000),
});
}, { maxRetries: 3, label: `RSS ${url}` });
if (!res.ok) return [];
const xml = await res.text();
const items = [];
const regex = /<item>([\s\S]*?)<\/item>/g;
let m;
while ((m = regex.exec(xml)) !== null) {
const b = m[1];
const t = (b.match(/<title><!\[CDATA\[([\s\S]*?)\]\]><\/title>/) || b.match(/<title>([\s\S]*?)<\/title>/))?.[1] || "";
const d = (b.match(/<description><!\[CDATA\[([\s\S]*?)\]\]><\/description>/) || b.match(/<description>([\s\S]*?)<\/description>/))?.[1] || "";
items.push({ title: t.trim(), content: d.replace(/<[^>]+>/g, "").trim().substring(0, 500) });
}
return items.slice(0, 20);
} catch { return []; }
}
async function checkWebsite(url) {
try {
const res = await withRetry(async () => {
return await fetch(url, { method: "GET", signal: AbortSignal.timeout(8000), redirect: "follow" });
}, { maxRetries: 2, label: `Website ${url}` });
return res.ok;
} catch { return false; }
}
async function checkLogo(logoUrl) {
if (!logoUrl) return false;
if (logoUrl.startsWith("/logos/") || logoUrl.startsWith("data:")) return true;
try {
const res = await withRetry(async () => {
return await fetch(logoUrl, { method: "HEAD", signal: AbortSignal.timeout(5000) });
}, { maxRetries: 2, label: `Logo ${logoUrl}` });
return res.ok;
} catch { return false; }
}
async function main() {
const startTime = new Date();
console.log(`🔍 [Task1] ${startTime.toISOString().split("T")[0]} 发现新AI工具...`);
const rssSources = [
{ url: "https://36kr.com/feed", name: "36kr" },
{ url: "https://www.ithome.com/rss/", name: "IT之家" },
{ url: "https://www.oschina.net/news/rss", name: "开源中国" },
];
let totalNews = 0;
let foundTools = 0;
let importedTools = 0;
const errors: string[] = [];
const existingNames = new Set([
...(await prisma.tool.findMany({ select: { name: true } })).map((t) => t.name.toLowerCase()),
...(await prisma.pendingTool.findMany({ select: { name: true } })).map((t) => t.name.toLowerCase()),
]);
const existingUrls = new Set([
...(await prisma.tool.findMany({ select: { websiteUrl: true } })).map((t) => t.websiteUrl || ""),
...(await prisma.pendingTool.findMany({ select: { websiteUrl: true } })).map((t) => t.websiteUrl || ""),
]);
const categoryMap: Record<string, number> = {};
const categories = await prisma.category.findMany({ select: { id: true, name: true, slug: true } });
for (const c of categories) {
const name = c.name.toLowerCase();
const slugLower = c.slug.toLowerCase();
if (name.includes("文本") || slugLower.includes("text")) categoryMap["文本生成"] = c.id;
else if (name.includes("图像") || slugLower.includes("image")) categoryMap["图像生成"] = c.id;
else if (name.includes("视频") || slugLower.includes("video")) categoryMap["视频生成"] = c.id;
else if (name.includes("语音") || name.includes("音频") || slugLower.includes("audio")) categoryMap["语音音频"] = c.id;
else if (name.includes("编程") || name.includes("代码") || slugLower.includes("code")) categoryMap["代码编程"] = c.id;
else if (name.includes("办公") || slugLower.includes("office")) categoryMap["办公效率"] = c.id;
else if (name.includes("营销") || slugLower.includes("marketing")) categoryMap["营销创作"] = c.id;
else if (name.includes("数据") || name.includes("AI") || slugLower.includes("data")) categoryMap["数据AI"] = c.id;
else if (name.includes("教育") || slugLower.includes("education")) categoryMap["教育学习"] = c.id;
else if (name.includes("设计") || slugLower.includes("design")) categoryMap["设计制作"] = c.id;
}
for (const src of rssSources) {
console.log(` 📡 ${src.name}...`);
const news = await fetchRSS(src.url);
totalNews += news.length;
console.log(` 获取 ${news.length} 条`);
for (const item of news) {
const text = `${item.title} ${item.content}`;
try {
const completion = await withRetry(async () => {
return await openai.chat.completions.create({
model: "deepseek-v4-pro",
messages: [{ role: "user", content: EXTRACT_PROMPT.replace("{content}", text.substring(0, 1500)) }],
temperature: 0.2,
});
}, { maxRetries: 3, label: "DeepSeek API" });
const content = completion.choices[0]?.message?.content;
if (!content) continue;
let tools;
try { tools = extractJSON(content); } catch { continue; }
if (!Array.isArray(tools)) continue;
foundTools += tools.length;
for (const tool of tools) {
if (!tool.name || !tool.websiteUrl) continue;
if (existingNames.has(tool.name.toLowerCase()) || existingUrls.has(tool.websiteUrl)) continue;
const websiteOk = await checkWebsite(tool.websiteUrl);
const logoOk = await checkLogo(tool.logoUrl || "");
const catId = categoryMap[tool.category] || categoryMap["数据AI"] || categories[0]?.id || 1;
await prisma.pendingTool.create({
data: {
name: tool.name,
description: tool.description || "",
websiteUrl: tool.websiteUrl,
logoUrl: tool.logoUrl,
categoryId: catId,
sourceUrl: "AI自动发现",
status: "pending",
checkWebsiteOk: websiteOk,
checkLogoOk: logoOk,
checkDescOk: true,
checkCategoryOk: true,
checkDetail: { source: src.name, extractedAt: new Date().toISOString() },
},
});
existingNames.add(tool.name.toLowerCase());
existingUrls.add(tool.websiteUrl);
importedTools++;
console.log(` ✅ ${tool.name} (网站:${websiteOk ? "OK" : "FAIL"}, Logo:${logoOk ? "OK" : "FAIL"})`);
}
} catch (e) {
errors.push(`${src.name}/${item.title}: ${e.message}`);
}
await new Promise((r) => setTimeout(r, 300));
}
}
const endTime = new Date();
const duration = endTime.getTime() - startTime.getTime();
const result = {
totalNews,
foundTools,
importedTools,
duration: `${(duration / 1000).toFixed(1)}s`,
};
console.log(`\n🏁 [Task1] 完成: 新闻${totalNews}条, 发现${foundTools}个, 入库${importedTools}个, 耗时${result.duration}`);
if (errors.length > 0) console.log(` 错误: ${errors.slice(0, 3).join("; ")}`);
await prisma.taskLog.create({
data: {
taskKey: "task1-discover-tools",
taskName: "发现AI工具",
status: errors.length > 0 && importedTools === 0 ? "failed" : "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: "task1-discover-tools",
taskName: "发现AI工具",
status: "failed",
startedAt: new Date(),
finishedAt: new Date(),
duration: 0,
error: e.message,
},
});
} catch {}
await prisma.$disconnect();
}
process.exit(1);
});
+186
View File
@@ -0,0 +1,186 @@
import { PrismaClient } from "@prisma/client";
import { PrismaMariaDb } from "@prisma/adapter-mariadb";
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 });
async function checkWebsite(url) {
try {
const res = await withRetry(async () => {
return await fetch(url, {
method: "GET",
signal: AbortSignal.timeout(8000),
redirect: "follow",
headers: { "User-Agent": "Mozilla/5.0 (compatible; CheckBot/1.0)" },
});
}, { maxRetries: 2, label: `Website ${url}` });
return res.status >= 200 && res.status < 400;
} catch {
return false;
}
}
async function checkLogo(logoUrl) {
if (!logoUrl) return false;
if (logoUrl.startsWith("/logos/")) return true;
if (logoUrl.startsWith("data:")) return true;
try {
const res = await withRetry(async () => {
return await fetch(logoUrl, { method: "HEAD", signal: AbortSignal.timeout(5000) });
}, { maxRetries: 2, label: `Logo ${logoUrl}` });
return res.ok;
} catch {
return false;
}
}
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(`🔍 [Task3] ${today} 巡检已入库工具...`);
const tools = await prisma.tool.findMany({
select: { id: true, name: true, websiteUrl: true, logoUrl: true, status: true },
});
const pendingTools = await prisma.pendingTool.findMany({
where: { status: "pending" },
select: { id: true, name: true, websiteUrl: true, logoUrl: true, checkWebsiteOk: true, checkLogoOk: true },
});
console.log(` 已发布工具: ${tools.length}, 待审核: ${pendingTools.length}`);
let checked = 0;
let websiteFails = 0;
let logoFails = 0;
let autoPublished = 0;
const errors = [];
for (const tool of tools) {
try {
const websiteOk = await checkWebsite(tool.websiteUrl);
const logoOk = await checkLogo(tool.logoUrl);
await prisma.tool.update({
where: { id: tool.id },
data: { lastCheckedAt: new Date(), websiteOk, logoOk },
});
if (!websiteOk) websiteFails++;
if (!logoOk) logoFails++;
checked++;
} catch (e) {
errors.push(`Tool ${tool.name}: ${e.message}`);
}
if (checked % 10 === 0) {
console.log(` 进度: ${checked}/${tools.length}`);
}
}
console.log(`\n 已发布工具巡检完成: ${checked}个, 网站失效${websiteFails}个, Logo异常${logoFails}个`);
checked = 0;
for (const pt of pendingTools) {
try {
const websiteOk = pt.websiteUrl ? await checkWebsite(pt.websiteUrl) : false;
const logoOk = await checkLogo(pt.logoUrl);
await prisma.pendingTool.update({
where: { id: pt.id },
data: { checkWebsiteOk: websiteOk, checkLogoOk: logoOk, checkDescOk: true, checkCategoryOk: true },
});
if (websiteOk && logoOk) {
const slug = pt.slug || pt.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") + "-" + Date.now().toString(36);
const existingSlug = await prisma.tool.findUnique({ where: { slug } });
if (!existingSlug) {
await prisma.tool.create({
data: {
name: pt.name,
slug,
categoryId: pt.categoryId || 1,
description: pt.description || "",
logoUrl: pt.logoUrl,
websiteUrl: pt.websiteUrl || "",
pricingModel: pt.pricingModel || "freemium",
pricingDetail: pt.pricingDetail,
features: pt.features,
tags: pt.tags,
status: "published",
websiteOk: true,
logoOk: true,
},
});
await prisma.pendingTool.delete({ where: { id: pt.id } });
autoPublished++;
console.log(` ✅ 自动发布: ${pt.name}`);
}
}
checked++;
} catch (e) {
errors.push(`PendingTool ${pt.name}: ${e.message}`);
}
if (checked % 5 === 0) {
console.log(` 待审核进度: ${checked}/${pendingTools.length}`);
}
}
const endTime = new Date();
const duration = endTime.getTime() - startTime.getTime();
const result = {
totalTools: tools.length,
totalPending: pendingTools.length,
websiteFails,
logoFails,
autoPublished,
duration: `${(duration / 1000).toFixed(1)}s`,
};
console.log(`\n🏁 [Task3] 完成: 巡检${tools.length + pendingTools.length}个, 自动发布${autoPublished}个, 网站失效${websiteFails}个`);
await prisma.taskLog.create({
data: {
taskKey: "task3-check-tools",
taskName: "工具巡检",
status: errors.length > 0 ? "completed" : "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: "task3-check-tools",
taskName: "工具巡检",
status: "failed",
startedAt: new Date(),
finishedAt: new Date(),
duration: 0,
error: e.message,
},
});
} catch {}
await prisma.$disconnect();
}
process.exit(1);
});
+148
View File
@@ -0,0 +1,148 @@
import { PrismaClient } from "@prisma/client";
import { PrismaMariaDb } from "@prisma/adapter-mariadb";
import { Octokit } from "octokit";
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 });
function extractOwnerRepo(sourceUrl) {
const match = sourceUrl.match(/github\.com\/([^/]+)\/([^/]+)/);
if (!match) return null;
return { owner: match[1], repo: match[2].replace(/\.git$/, "") };
}
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(`📈 [Task5] ${today} 更新GitHub星值...`);
const skills = await prisma.skill.findMany({
where: { sourceUrl: { contains: "github.com" } },
select: { id: true, name: true, sourceUrl: true, githubStars: true, githubStarsHistory: true },
});
console.log(` 共 ${skills.length} 个GitHub技能需要更新`);
let updated = 0;
let failed = 0;
let skipped = 0;
const errors = [];
for (const skill of skills) {
const parsed = extractOwnerRepo(skill.sourceUrl);
if (!parsed) {
skipped++;
console.log(` ⏭️ ${skill.name} - 无法解析GitHub URL`);
continue;
}
try {
const { data } = await octokit.rest.repos.get({ owner: parsed.owner, repo: parsed.repo });
const currentStars = data.stargazers_count;
const history = Array.isArray(skill.githubStarsHistory) ? [...skill.githubStarsHistory] : [];
const prevEntry = history.length > 0 ? history[history.length - 1] : null;
const prevStars = prevEntry ? prevEntry.stars : currentStars;
history.push({ date: today, stars: currentStars });
if (history.length > 90) history.splice(0, history.length - 90);
const dayChange = currentStars - prevStars;
const weekAgo = history.find((h) => {
const d = new Date(h.date);
const diff = (cnTime.getTime() - d.getTime()) / (24 * 60 * 60 * 1000);
return diff >= 6 && diff <= 8;
});
const weekChange = weekAgo ? currentStars - weekAgo.stars : dayChange * 7;
const monthAgo = history.find((h) => {
const d = new Date(h.date);
const diff = (cnTime.getTime() - d.getTime()) / (24 * 60 * 60 * 1000);
return diff >= 28 && diff <= 32;
});
const monthChange = monthAgo ? currentStars - monthAgo.stars : weekChange * 4;
await prisma.skill.update({
where: { id: skill.id },
data: {
githubStars: currentStars,
starsChangeDay: dayChange,
starsChangeWeek: weekChange,
starsChangeMonth: monthChange,
githubStarsHistory: history,
},
});
updated++;
const arrow = dayChange >= 0 ? "↑" : "↓";
console.log(` ✅ ${skill.name}: ${currentStars}⭐ (${arrow}${Math.abs(dayChange)}/日)`);
} catch (e) {
failed++;
errors.push(`${skill.name}: ${e.message}`);
console.log(` ❌ ${skill.name}: ${e.message}`);
}
if (updated % 5 === 0) {
await new Promise((r) => setTimeout(r, 1000));
}
}
const endTime = new Date();
const duration = endTime.getTime() - startTime.getTime();
const result = {
total: skills.length,
updated,
failed,
skipped,
duration: `${(duration / 1000).toFixed(1)}s`,
};
console.log(`\n🏁 [Task5] 完成: 更新${updated} / 失败${failed}`);
await prisma.taskLog.create({
data: {
taskKey: "task5-update-stars",
taskName: "更新技能星值",
status: failed > skills.length * 0.5 ? "failed" : "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: "task5-update-stars",
taskName: "更新技能星值",
status: "failed",
startedAt: new Date(),
finishedAt: new Date(),
duration: 0,
error: e.message,
},
});
} catch {}
await prisma.$disconnect();
}
process.exit(1);
});
+228
View File
@@ -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);
});