refactor: 主题系统 → monorepo 五个独立网站
- packages/core: @aidir/core 底座库(src/pages 页面组件体 1:1 镜像路由树 / kit 站点外壳工厂 / content/blog / scripts) - sites/: 5 个独立 Next.js 网站(ai-stack / launchai / the-ai-ledger / devai-tools / findai) - 每站独立 package.json / next.config / tailwind / messages / .env / 构建 - src/app 约 56 个薄路由文件(re-export 底座页面 + 内联路由段配置 revalidate/dynamic) - src/site/brand.tsx 站点配置 + src/site/home.tsx 站点首页(createMarketingLayout / createHomePage 工厂) - generate-site.mjs: 站点生成器(新增网站 = SITES 加条目,零 Core 修改) - start-all-sites.sh: 一键启动 3001-3005 - next-config.mjs: 底座统一 Next 配置工厂(next-intl + MDX + standalone) - 五站共享同一套 PostgreSQL / Better Auth / Admin(设计文档 §42) - 验证: 5 站 build OK / typecheck OK / 55 路由冒烟 200 / 品牌-内容-截图 差异确认 / 登录-投票-重复检测 e2e PASS
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* i18n 键一致性检查:对比 messages/ 下所有语言文件的键集合,
|
||||
* 存在差异时打印缺失键并以非零码退出(可挂进 CI)。
|
||||
*
|
||||
* 用法: pnpm i18n:check
|
||||
*/
|
||||
import { readFileSync, readdirSync } from "node:fs";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const messagesDir = join(dirname(fileURLToPath(import.meta.url)), "..", "messages");
|
||||
|
||||
function flattenKeys(obj, prefix = "") {
|
||||
return Object.entries(obj).flatMap(([key, value]) => {
|
||||
const path = prefix ? `${prefix}.${key}` : key;
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? flattenKeys(value, path)
|
||||
: [path];
|
||||
});
|
||||
}
|
||||
|
||||
const files = readdirSync(messagesDir).filter((f) => f.endsWith(".json"));
|
||||
const keySets = new Map(
|
||||
files.map((f) => [
|
||||
f,
|
||||
new Set(flattenKeys(JSON.parse(readFileSync(join(messagesDir, f), "utf8")))),
|
||||
])
|
||||
);
|
||||
|
||||
const allKeys = new Set([...keySets.values()].flatMap((s) => [...s]));
|
||||
let hasError = false;
|
||||
|
||||
for (const [file, keys] of keySets) {
|
||||
const missing = [...allKeys].filter((k) => !keys.has(k));
|
||||
if (missing.length > 0) {
|
||||
hasError = true;
|
||||
console.error(`\n❌ ${file} 缺失 ${missing.length} 个键:`);
|
||||
for (const key of missing.sort()) console.error(` - ${key}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (hasError) {
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`✅ i18n 键一致 (${files.join(", ")},共 ${allKeys.size} 个键)`);
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* # 脚本 e2e-interaction
|
||||
* 说明:端到端交互验证 —— 登录 → 投票 → 计数变化 → 退出登录态下投票被拦截。
|
||||
*/
|
||||
import { chromium } from "playwright";
|
||||
|
||||
const BASE = "http://localhost:3001";
|
||||
const results: string[] = [];
|
||||
|
||||
const browser = await chromium.launch();
|
||||
const context = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await context.newPage();
|
||||
|
||||
// 1) 登录
|
||||
await page.goto(`${BASE}/login`, { waitUntil: "domcontentloaded" });
|
||||
await page.fill('input[name="email"], input[type="email"]', "alice@demo.dev");
|
||||
await page.fill('input[name="password"], input[type="password"]', "password123");
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForURL(/dashboard|\/$/, { timeout: 15000 }).catch(() => {});
|
||||
results.push(`登录后 URL: ${page.url()}`);
|
||||
|
||||
// 2) 打开工具详情页,记录票数
|
||||
await page.goto(`${BASE}/tools/claude`, { waitUntil: "domcontentloaded" });
|
||||
await page.waitForTimeout(1000);
|
||||
const voteButton = page.locator("button", { hasText: /Upvote/ }).first();
|
||||
const before = await voteButton.textContent();
|
||||
results.push(`投票前按钮: ${(before ?? "").trim()}`);
|
||||
|
||||
// 3) 投票
|
||||
await voteButton.click();
|
||||
await page.waitForTimeout(1500);
|
||||
const after = await voteButton.textContent();
|
||||
results.push(`投票后按钮: ${(after ?? "").trim()}`);
|
||||
results.push(`投票生效: ${(before ?? "") !== (after ?? "") ? "PASS" : "FAIL"}`);
|
||||
|
||||
// 4) 写评论
|
||||
try {
|
||||
const hasReviewForm = await page.locator('button:has-text("Write a review")').count();
|
||||
results.push(`找到 Write a review 按钮: ${hasReviewForm}`);
|
||||
if (hasReviewForm > 0) {
|
||||
await page.locator('button:has-text("Write a review")').first().click();
|
||||
await page.waitForTimeout(800);
|
||||
await page.screenshot({ path: "/tmp/aidir-shots/e2e-review-form.png" });
|
||||
// 选 5 星(表单内的大号星级)
|
||||
await page.locator('form button[aria-label="5"]').first().click();
|
||||
await page.locator("form textarea").first().fill("Interactive e2e test review - Claude handles long documents exceptionally well.");
|
||||
await page.locator('form button:has-text("Publish review")').click();
|
||||
await page.waitForTimeout(2000);
|
||||
const reviewVisible = await page.locator("text=Interactive e2e test review").count();
|
||||
results.push(`评论发布: ${reviewVisible > 0 ? "PASS" : "FAIL"}`);
|
||||
}
|
||||
} catch (error) {
|
||||
await page.screenshot({ path: "/tmp/aidir-shots/e2e-debug.png" });
|
||||
results.push(`评论步骤异常: ${String(error).slice(0, 100)}`);
|
||||
}
|
||||
|
||||
// 5) 提交工具(重复检测)
|
||||
await page.goto(`${BASE}/submit`, { waitUntil: "domcontentloaded" });
|
||||
await page.fill('input[placeholder="ChatGPT"]', "ChatGPT");
|
||||
await page.fill('input[type="url"]', "https://chatgpt.com");
|
||||
await page.locator('input[type="url"]').blur();
|
||||
await page.waitForTimeout(1200);
|
||||
const dupVisible = await page.locator("text=already be listed").count();
|
||||
results.push(`重复检测: ${dupVisible > 0 ? "PASS" : "FAIL"}`);
|
||||
|
||||
await browser.close();
|
||||
console.log(results.join("\n"));
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* # 脚本 e2e-interaction
|
||||
* 说明:端到端交互验证 —— 登录 → 投票(DB 前后对比)→ 重复提交检测。
|
||||
* 在 monorepo 多站点架构下运行(共享同一套 Auth/DB)。
|
||||
*/
|
||||
import { chromium } from "playwright";
|
||||
import postgres from "postgres";
|
||||
|
||||
const BASE = process.env.E2E_BASE || "http://localhost:3001";
|
||||
const sql = postgres(
|
||||
process.env.DATABASE_URL || "postgresql://postgres:postgres@127.0.0.1:15433/ai_directory",
|
||||
{ prepare: false }
|
||||
);
|
||||
const results: string[] = [];
|
||||
|
||||
const browser = await chromium.launch();
|
||||
const context = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await context.newPage();
|
||||
|
||||
const countClaudeVotes = async () =>
|
||||
Number((await sql`SELECT COUNT(*) FROM vote WHERE tool_id='tool_claude'`)[0].count);
|
||||
|
||||
// 1) 登录
|
||||
await page.goto(`${BASE}/login`, { waitUntil: "domcontentloaded" });
|
||||
await page.fill('input[type="email"]', "bob@demo.dev");
|
||||
await page.fill('input[type="password"]', "password123");
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForTimeout(2500);
|
||||
results.push(`1) 登录后 URL: ${page.url()}`);
|
||||
|
||||
await page.screenshot({ path: "/tmp/aidir-mono/e2e-after-login.png" });
|
||||
results.push(`1b) 登录后用户菜单存在: ${await page.locator("header img, header button").count() > 0}`);
|
||||
|
||||
// 2) 投票 toggle(DB 前后对比)
|
||||
const before = await countClaudeVotes();
|
||||
await page.goto(`${BASE}/tools/claude`, { waitUntil: "domcontentloaded" });
|
||||
await page.waitForTimeout(1500);
|
||||
await page.screenshot({ path: "/tmp/aidir-mono/e2e-detail.png" });
|
||||
const btnCount = await page.locator("button", { hasText: /Upvote|Upvoted/ }).count();
|
||||
results.push(`2a) 找到投票按钮数: ${btnCount}`);
|
||||
const btn = page.locator("button", { hasText: /Upvote|Upvoted/ }).first();
|
||||
await btn.click();
|
||||
await page.waitForTimeout(2500);
|
||||
const after = await countClaudeVotes();
|
||||
results.push(`2) 投票: DB ${before}→${after},按钮 "${((await btn.textContent()) ?? "").trim()}" → ${before !== after ? "PASS" : "FAIL"}`);
|
||||
await btn.click();
|
||||
await page.waitForTimeout(2500);
|
||||
const restored = await countClaudeVotes();
|
||||
results.push(`3) 取消投票: DB 回到 ${restored} → ${restored === before ? "PASS" : "FAIL"}`);
|
||||
|
||||
// 4) 重复提交检测
|
||||
await page.goto(`${BASE}/submit`, { waitUntil: "domcontentloaded" });
|
||||
await page.fill('input[placeholder="ChatGPT"]', "ChatGPT");
|
||||
await page.fill('input[type="url"]', "https://chatgpt.com");
|
||||
await page.locator('input[type="url"]').blur();
|
||||
await page.waitForTimeout(1500);
|
||||
const dupVisible = await page.locator("text=already be listed").count();
|
||||
results.push(`4) 重复检测: ${dupVisible > 0 ? "PASS" : "FAIL"}`);
|
||||
await page.screenshot({ path: "/tmp/aidir-mono/e2e-duplicate.png" });
|
||||
|
||||
await browser.close();
|
||||
await sql.end();
|
||||
console.log(results.join("\n"));
|
||||
@@ -0,0 +1,74 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import glob from "fast-glob";
|
||||
|
||||
async function main() {
|
||||
const blogRoot = path.join(process.cwd(), "src/content/blog");
|
||||
const entries = await glob("**/*.mdx", { cwd: blogRoot });
|
||||
const manifest = new Map();
|
||||
|
||||
for (const entry of entries) {
|
||||
const segments = entry.split("/");
|
||||
|
||||
if (segments.length < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const [slug, filename] = segments;
|
||||
|
||||
if (!slug || slug.startsWith("[")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const locale = filename.replace(/\.mdx$/, "");
|
||||
|
||||
if (!locale) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!manifest.has(slug)) {
|
||||
manifest.set(slug, new Set());
|
||||
}
|
||||
|
||||
manifest.get(slug).add(locale);
|
||||
}
|
||||
|
||||
const sortedManifest = Array.from(manifest.entries()).sort(([a], [b]) =>
|
||||
a.localeCompare(b)
|
||||
);
|
||||
|
||||
const lines = [];
|
||||
lines.push("// This file is auto-generated by scripts/generate-blog-manifest.mjs");
|
||||
lines.push("// Do not edit manually.\n");
|
||||
lines.push("export const blogModuleLoaders = {");
|
||||
|
||||
for (const [slug, locales] of sortedManifest) {
|
||||
lines.push(` "${slug}": {`);
|
||||
|
||||
const sortedLocales = Array.from(locales).sort((a, b) => a.localeCompare(b));
|
||||
|
||||
for (const locale of sortedLocales) {
|
||||
// 使用 path.posix 确保导入路径始终使用 / (跨平台兼容)
|
||||
const importPath = path.posix.join(
|
||||
"..",
|
||||
"content",
|
||||
"blog",
|
||||
slug,
|
||||
`${locale}.mdx`
|
||||
);
|
||||
lines.push(` "${locale}": () => import("${importPath}"),`);
|
||||
}
|
||||
|
||||
lines.push(" },");
|
||||
}
|
||||
|
||||
lines.push("} as const;\n");
|
||||
|
||||
const targetPath = path.join(process.cwd(), "src/lib/blog-manifest.generated.ts");
|
||||
fs.writeFileSync(targetPath, lines.join("\n"), "utf-8");
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
@@ -0,0 +1,705 @@
|
||||
/**
|
||||
* # 脚本 generate-site
|
||||
* 说明:从底座生成一个独立网站应用(sites/<id>/)。
|
||||
* - src/app/**:全部为薄路由(re-export core/src/pages 的同名模块)
|
||||
* - (marketing)/layout + 首页:站点组合器(品牌 + 站点首页组件)
|
||||
* - messages/:core 基础文案 + 站点品牌 SEO 覆盖
|
||||
* - 配置:独立 package.json / next.config / tsconfig / tailwind / postcss
|
||||
*
|
||||
* 用法:bun scripts/generate-site.mjs <siteKey>
|
||||
* 站点定义见本文件 SITES 常量。
|
||||
*/
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const CORE_ROOT = path.resolve(__dirname, "..");
|
||||
const REPO_ROOT = path.resolve(CORE_ROOT, "..", "..");
|
||||
const SITES_ROOT = path.join(REPO_ROOT, "sites");
|
||||
|
||||
// ====================================================================
|
||||
// 站点定义:一个条目 = 一个独立网站(品牌 / 导航 / 页脚 / 首页 / 端口)
|
||||
// ====================================================================
|
||||
const SITES = {
|
||||
"ai-stack": {
|
||||
port: 3001,
|
||||
theme: "classic",
|
||||
brand: {
|
||||
name: "AI Stack",
|
||||
tagline: "The professional AI tools directory — ranked by the community.",
|
||||
headline: "Find the best AI tools, ranked by real users",
|
||||
subheadline:
|
||||
"1,000+ AI tools across 14 categories. Compare votes, reviews and pricing to pick the right one.",
|
||||
},
|
||||
navItems: [
|
||||
{ label: "Discover", href: "/tools" },
|
||||
{ label: "Use Cases", href: "/ai-tools-for" },
|
||||
{ label: "Best Lists", href: "/best" },
|
||||
{ label: "Rankings", href: "/rankings" },
|
||||
{ label: "Blog", href: "/blog" },
|
||||
{ label: "Submit", href: "/submit" },
|
||||
],
|
||||
footerGroups: [
|
||||
{
|
||||
title: "Discover",
|
||||
links: [
|
||||
{ label: "All Tools", href: "/tools" },
|
||||
{ label: "Use Cases", href: "/ai-tools-for" },
|
||||
{ label: "Best Lists", href: "/best" },
|
||||
{ label: "Rankings", href: "/rankings" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Community",
|
||||
links: [
|
||||
{ label: "Submit a Tool", href: "/submit" },
|
||||
{ label: "Blog", href: "/blog" },
|
||||
{ label: "Dashboard", href: "/dashboard" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Legal",
|
||||
links: [
|
||||
{ label: "Terms", href: "/terms" },
|
||||
{ label: "Privacy", href: "/privacy" },
|
||||
{ label: "Contact", href: "/contact" },
|
||||
],
|
||||
},
|
||||
],
|
||||
searchPlaceholder: "Search AI tools...",
|
||||
seo: {
|
||||
en: {
|
||||
title: "AI Stack — Find the Best AI Tools, Ranked by Real Users",
|
||||
description:
|
||||
"The professional AI tools directory. 1,000+ AI tools across 14 categories ranked by community votes, reviews and engagement.",
|
||||
},
|
||||
zh: {
|
||||
title: "AI Stack — 由真实用户排名的 AI 工具目录",
|
||||
description: "专业 AI 工具导航:14 个分类、1000+ 工具,由社区投票、评论与互动数据实时排名。",
|
||||
},
|
||||
},
|
||||
},
|
||||
launchai: {
|
||||
port: 3002,
|
||||
theme: "product",
|
||||
brand: {
|
||||
name: "LaunchAI",
|
||||
tagline: "Discover and upvote today's AI products.",
|
||||
headline: "Today's AI product launches",
|
||||
subheadline: "Fresh AI products, every day. Upvote the best ones to the top.",
|
||||
},
|
||||
navItems: [
|
||||
{ label: "Today", href: "/" },
|
||||
{ label: "Leaderboard", href: "/rankings" },
|
||||
{ label: "Categories", href: "/tools" },
|
||||
{ label: "Collections", href: "/best" },
|
||||
{ label: "Blog", href: "/blog" },
|
||||
{ label: "Submit", href: "/submit" },
|
||||
],
|
||||
footerGroups: [
|
||||
{
|
||||
title: "Community",
|
||||
links: [
|
||||
{ label: "Submit a Product", href: "/submit" },
|
||||
{ label: "Leaderboard", href: "/rankings" },
|
||||
{ label: "Dashboard", href: "/dashboard" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Explore",
|
||||
links: [
|
||||
{ label: "All Products", href: "/tools" },
|
||||
{ label: "Use Cases", href: "/ai-tools-for" },
|
||||
{ label: "Collections", href: "/best" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "About",
|
||||
links: [
|
||||
{ label: "Blog", href: "/blog" },
|
||||
{ label: "Terms", href: "/terms" },
|
||||
{ label: "Privacy", href: "/privacy" },
|
||||
],
|
||||
},
|
||||
],
|
||||
searchPlaceholder: "Search AI products...",
|
||||
seo: {
|
||||
en: {
|
||||
title: "LaunchAI — Discover and Upvote Today's AI Products",
|
||||
description:
|
||||
"A daily launch feed for AI products. Upvote the best AI tools, follow makers and climb the leaderboard.",
|
||||
},
|
||||
zh: {
|
||||
title: "LaunchAI — 发现并投票今日 AI 新品",
|
||||
description: "AI 产品每日发布社区:为最好的 AI 工具投票,登上社区排行榜。",
|
||||
},
|
||||
},
|
||||
},
|
||||
"the-ai-ledger": {
|
||||
port: 3003,
|
||||
theme: "editorial",
|
||||
brand: {
|
||||
name: "The AI Ledger",
|
||||
tagline: "Independent journalism on AI tools, with a community-ranked directory.",
|
||||
headline: "AI tools, reviewed and ranked",
|
||||
subheadline:
|
||||
"Guides, comparisons and a community-powered directory of the best AI tools.",
|
||||
},
|
||||
navItems: [
|
||||
{ label: "Stories", href: "/blog" },
|
||||
{ label: "Guides", href: "/ai-tools-for" },
|
||||
{ label: "Best Picks", href: "/best" },
|
||||
{ label: "Directory", href: "/tools" },
|
||||
{ label: "Rankings", href: "/rankings" },
|
||||
{ label: "Submit", href: "/submit" },
|
||||
],
|
||||
footerGroups: [
|
||||
{
|
||||
title: "Read",
|
||||
links: [
|
||||
{ label: "Latest Stories", href: "/blog" },
|
||||
{ label: "Buying Guides", href: "/best" },
|
||||
{ label: "Use Case Guides", href: "/ai-tools-for" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Directory",
|
||||
links: [
|
||||
{ label: "All Tools", href: "/tools" },
|
||||
{ label: "Community Rankings", href: "/rankings" },
|
||||
{ label: "Submit a Tool", href: "/submit" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Company",
|
||||
links: [
|
||||
{ label: "Contact", href: "/contact" },
|
||||
{ label: "Terms", href: "/terms" },
|
||||
{ label: "Privacy", href: "/privacy" },
|
||||
],
|
||||
},
|
||||
],
|
||||
searchPlaceholder: "Search the directory...",
|
||||
seo: {
|
||||
en: {
|
||||
title: "The AI Ledger — AI Tools, Reviewed and Ranked",
|
||||
description:
|
||||
"Independent journalism on AI: buying guides, comparisons and a community-ranked directory of the best AI tools.",
|
||||
},
|
||||
zh: {
|
||||
title: "The AI Ledger — AI 工具评测与排名",
|
||||
description: "独立 AI 媒体:深度指南、横向对比,以及由社区排名的 AI 工具目录。",
|
||||
},
|
||||
},
|
||||
},
|
||||
"devai-tools": {
|
||||
port: 3004,
|
||||
theme: "niche",
|
||||
brand: {
|
||||
name: "DevAI.tools",
|
||||
tagline: "AI tools for developers — curated, tested, ranked by the community.",
|
||||
headline: "AI tools for developers",
|
||||
subheadline:
|
||||
"Code assistants, DevOps automation, testing copilots and everything else you ship with.",
|
||||
},
|
||||
navItems: [
|
||||
{ label: "Tools", href: "/tools" },
|
||||
{ label: "Workflows", href: "/best" },
|
||||
{ label: "Use Cases", href: "/ai-tools-for" },
|
||||
{ label: "Rankings", href: "/rankings" },
|
||||
{ label: "Submit", href: "/submit" },
|
||||
],
|
||||
footerGroups: [
|
||||
{
|
||||
title: "Directory",
|
||||
links: [
|
||||
{ label: "All Tools", href: "/tools" },
|
||||
{ label: "Rankings", href: "/rankings" },
|
||||
{ label: "Use Cases", href: "/ai-tools-for" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Build",
|
||||
links: [
|
||||
{ label: "Workflows", href: "/best" },
|
||||
{ label: "Submit a Tool", href: "/submit" },
|
||||
{ label: "Blog", href: "/blog" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Legal",
|
||||
links: [
|
||||
{ label: "Terms", href: "/terms" },
|
||||
{ label: "Privacy", href: "/privacy" },
|
||||
],
|
||||
},
|
||||
],
|
||||
searchPlaceholder: "Search dev tools...",
|
||||
seo: {
|
||||
en: {
|
||||
title: "DevAI.tools — AI Tools for Developers",
|
||||
description:
|
||||
"Code assistants, DevOps automation and testing copilots — the AI tools developers actually upvote, ranked daily.",
|
||||
},
|
||||
zh: {
|
||||
title: "DevAI.tools — 开发者 AI 工具",
|
||||
description: "面向开发者的 AI 工具目录:代码助手、运维自动化、测试副驾驶,由社区每日排名。",
|
||||
},
|
||||
},
|
||||
},
|
||||
findai: {
|
||||
port: 3005,
|
||||
theme: "minimal",
|
||||
brand: {
|
||||
name: "findai",
|
||||
tagline: "The minimal AI tool search engine.",
|
||||
headline: "What are you trying to do?",
|
||||
subheadline: "",
|
||||
},
|
||||
navItems: [
|
||||
{ label: "Search", href: "/tools" },
|
||||
{ label: "Use Cases", href: "/ai-tools-for" },
|
||||
{ label: "Rankings", href: "/rankings" },
|
||||
{ label: "Submit", href: "/submit" },
|
||||
],
|
||||
footerGroups: [
|
||||
{
|
||||
title: "Search",
|
||||
links: [
|
||||
{ label: "All Tools", href: "/tools" },
|
||||
{ label: "Use Cases", href: "/ai-tools-for" },
|
||||
{ label: "Rankings", href: "/rankings" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "About",
|
||||
links: [
|
||||
{ label: "Submit a Tool", href: "/submit" },
|
||||
{ label: "Privacy", href: "/privacy" },
|
||||
{ label: "Terms", href: "/terms" },
|
||||
],
|
||||
},
|
||||
],
|
||||
searchPlaceholder: "What do you need?",
|
||||
seo: {
|
||||
en: {
|
||||
title: "findai — The Minimal AI Tool Search Engine",
|
||||
description:
|
||||
"One search box, every AI tool. Describe what you're trying to do and find the right AI tool, ranked by the community.",
|
||||
},
|
||||
zh: {
|
||||
title: "findai — 极简 AI 工具搜索引擎",
|
||||
description: "一个搜索框,找遍全部 AI 工具。说出你想做的事,社区排名替你选。",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// ====================================================================
|
||||
// 生成逻辑
|
||||
// ====================================================================
|
||||
|
||||
/** 遍历 core/src/pages 下所有 page.tsx / route.ts,返回相对路由信息 */
|
||||
function collectRoutes() {
|
||||
const pagesRoot = path.join(CORE_ROOT, "src", "pages");
|
||||
const routes = [];
|
||||
|
||||
function walk(dir) {
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
walk(full);
|
||||
} else if (
|
||||
entry.name === "page.tsx" ||
|
||||
entry.name === "route.ts" ||
|
||||
entry.name === "layout.tsx" ||
|
||||
entry.name === "not-found.tsx" ||
|
||||
entry.name === "manifest.ts" ||
|
||||
entry.name === "sitemap.ts"
|
||||
) {
|
||||
const rel = path.relative(pagesRoot, full);
|
||||
const kind =
|
||||
entry.name === "route.ts"
|
||||
? "route"
|
||||
: entry.name === "layout.tsx"
|
||||
? "layout"
|
||||
: "page";
|
||||
routes.push({ rel, kind, full });
|
||||
}
|
||||
}
|
||||
}
|
||||
walk(pagesRoot);
|
||||
return routes;
|
||||
}
|
||||
|
||||
/** 从 core 页面模块源码中提取可 re-export 的导出名 */
|
||||
/**
|
||||
* 从 core 页面模块源码中提取导出:
|
||||
* - 函数导出(default/generateMetadata/generateStaticParams...)→ re-export
|
||||
* - 路由段配置常量(revalidate/dynamic...)→ 内联字面量(Next 要求静态可解析,不能 re-export)
|
||||
*/
|
||||
function extractExports(file, kind) {
|
||||
const srcText = fs.readFileSync(file, "utf8");
|
||||
const names = new Set();
|
||||
const inlineConfigs = [];
|
||||
|
||||
if (kind !== "route" && /export\s+default/.test(srcText)) {
|
||||
names.add("default");
|
||||
}
|
||||
|
||||
const fnRe = /export\s+(?:async\s+)?function\s+([A-Za-z0-9_]+)/g;
|
||||
for (const m of srcText.matchAll(fnRe)) names.add(m[1]);
|
||||
|
||||
const configNames = ["revalidate", "dynamic", "runtime", "fetchCache", "maxDuration", "dynamicParams"];
|
||||
const constRe = /export\s+const\s+([A-Za-z0-9_]+)\s*=\s*([^;\n]+);/g;
|
||||
for (const m of srcText.matchAll(constRe)) {
|
||||
const full = m[0];
|
||||
const name = m[1];
|
||||
if (configNames.includes(name)) {
|
||||
inlineConfigs.push(full);
|
||||
}
|
||||
}
|
||||
|
||||
// route 的解构导出(如 export const { POST, GET } = ...)→ 整句 re-export
|
||||
const destructureRe = /export\s+const\s*\{([^}]+)\}\s*=[^;\n]+;/g;
|
||||
for (const m of srcText.matchAll(destructureRe)) {
|
||||
for (const raw of m[1].split(",")) {
|
||||
const name = raw.trim().split(":")[0].trim();
|
||||
if (/^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)$/.test(name)) names.add(name);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
names: [...names].filter((n) => !n.startsWith("type")),
|
||||
inlineConfigs,
|
||||
};
|
||||
}
|
||||
|
||||
function writeFileSafe(file, content) {
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
fs.writeFileSync(file, content);
|
||||
}
|
||||
|
||||
function generateSite(siteKey) {
|
||||
const site = SITES[siteKey];
|
||||
if (!site) throw new Error(`Unknown site: ${siteKey}. 可选: ${Object.keys(SITES).join(", ")}`);
|
||||
const siteRoot = path.join(SITES_ROOT, siteKey);
|
||||
|
||||
console.log(`\n=== 生成站点 ${siteKey} (port ${site.port}, theme ${site.theme}) ===`);
|
||||
fs.mkdirSync(siteRoot, { recursive: true });
|
||||
// 清空生成物,但保留本地环境与依赖
|
||||
for (const entry of fs.readdirSync(siteRoot)) {
|
||||
if (entry === ".env.local" || entry === "node_modules") continue;
|
||||
fs.rmSync(path.join(siteRoot, entry), { recursive: true, force: true });
|
||||
}
|
||||
|
||||
const corePkg = "@aidir/core";
|
||||
|
||||
// ---------- 1) 薄路由 ----------
|
||||
const skipRoutes = new Set([
|
||||
// 站点组合器文件由下方单独生成(营销布局 + 首页)
|
||||
"[locale]/(marketing)/layout.tsx",
|
||||
"[locale]/(marketing)/page.tsx",
|
||||
// 根布局单独生成(需引入站点样式)
|
||||
"layout.tsx",
|
||||
]);
|
||||
let routeCount = 0;
|
||||
for (const route of collectRoutes()) {
|
||||
if (skipRoutes.has(route.rel)) continue;
|
||||
const rel = route.rel.replace(/\\/g, "/");
|
||||
const target = path.join(siteRoot, "src", "app", rel);
|
||||
const importPath = `${corePkg}/pages/${rel.replace(/\.tsx?$/, "")}`;
|
||||
const { names, inlineConfigs } = extractExports(route.full, route.kind);
|
||||
if (names.length === 0 && inlineConfigs.length === 0) continue;
|
||||
|
||||
let content = `// 自动生成:${rel} 的薄路由\n`;
|
||||
for (const cfg of inlineConfigs) content += `${cfg}\n`;
|
||||
if (names.length > 0) {
|
||||
content += `export { ${names.join(", ")} } from "${importPath}";\n`;
|
||||
}
|
||||
writeFileSafe(target, content);
|
||||
routeCount++;
|
||||
}
|
||||
|
||||
// ---------- 2) 根布局(css + 透传) ----------
|
||||
writeFileSafe(
|
||||
path.join(siteRoot, "src/app/layout.tsx"),
|
||||
`import "${corePkg}/styles/globals.css";\n\nexport { default } from "${corePkg}/pages/app-layout";\n`
|
||||
);
|
||||
|
||||
// ---------- 3) 营销布局 + 首页(站点组合器) ----------
|
||||
writeFileSafe(
|
||||
path.join(siteRoot, "src/app/[locale]/(marketing)/layout.tsx"),
|
||||
`import { createMarketingLayout } from "${corePkg}/kit/create-marketing-layout";\nimport { siteConfig } from "@/site/brand";\n\nexport default createMarketingLayout(siteConfig);\n`
|
||||
);
|
||||
|
||||
writeFileSafe(
|
||||
path.join(siteRoot, "src/app/[locale]/(marketing)/page.tsx"),
|
||||
`import { createHomePage, createHomeMetadata } from "${corePkg}/kit/create-home";\nimport { siteConfig } from "@/site/brand";\nimport { SiteHome } from "@/site/home";\n\n// 首页内容变化不频繁,ISR 每小时再生\nexport const revalidate = 3600;\n\nexport const generateMetadata = createHomeMetadata(siteConfig);\nexport default createHomePage(siteConfig, SiteHome);\n`
|
||||
);
|
||||
|
||||
// ---------- 4) 站点品牌 + 首页组件 ----------
|
||||
const themeSrc = fs.readFileSync(
|
||||
path.join(CORE_ROOT, "src/kit/site-themes", site.theme, "index.tsx"),
|
||||
"utf8"
|
||||
);
|
||||
// 改写 import:kit 路径 + 站点品牌
|
||||
let homeSrc = themeSrc
|
||||
.replace(/from "[.\/]*shared\/(section|home-data)"/g, 'from "@/kit/$1"')
|
||||
.replace(/from "[.\/]*shared\/theme-header"/g, 'from "@/kit/theme-header"')
|
||||
.replace(/from "[.\/]*shared\/theme-footer"/g, 'from "@/kit/theme-footer"')
|
||||
.replace(/import type \{ ThemeModule \} from "[.\/]*types";\n/g, "");
|
||||
// 去掉主题文件里的 brand 常量与尾部主题注册导出(品牌来自 brand.tsx)
|
||||
homeSrc = homeSrc.replace(/const brand = \{[\s\S]*?\};/, "");
|
||||
const tailIdx = homeSrc.indexOf("\nexport const ");
|
||||
if (tailIdx !== -1) homeSrc = homeSrc.slice(0, tailIdx);
|
||||
// 统一导出名为 SiteHome(原函数如 ClassicHome / ProductHome...)
|
||||
homeSrc = homeSrc.replace(
|
||||
/(async\s+function\s+)(\w+Home)(\s*\()/,
|
||||
"$1SiteHome$3"
|
||||
);
|
||||
homeSrc += "\nexport { SiteHome };\n";
|
||||
// 未使用的 brand 引用替换
|
||||
homeSrc = homeSrc.replace(/\bbrand\.name\b/g, "siteConfig.brand.name")
|
||||
.replace(/\bbrand\.headline\b/g, "siteConfig.brand.headline")
|
||||
.replace(/\bbrand\.subheadline\b/g, "siteConfig.brand.subheadline")
|
||||
.replace(/\bbrand\.tagline\b/g, "siteConfig.brand.tagline");
|
||||
// 追加 import siteConfig
|
||||
const firstImport = homeSrc.indexOf('import ');
|
||||
homeSrc =
|
||||
homeSrc.slice(0, firstImport) +
|
||||
'import { siteConfig } from "@/site/brand";\n' +
|
||||
homeSrc.slice(firstImport);
|
||||
homeSrc =
|
||||
`// 自动生成:站点首页组件(源自底座 ${site.theme} 形态,品牌来自 @/site/brand)\n` +
|
||||
homeSrc;
|
||||
writeFileSafe(path.join(siteRoot, "src/site/home.tsx"), homeSrc);
|
||||
|
||||
// brand.tsx(SiteConfig,不含 theme 生成元数据)
|
||||
const { theme: _theme, ...siteConfigData } = site;
|
||||
siteConfigData.id = siteKey;
|
||||
writeFileSafe(
|
||||
path.join(siteRoot, "src/site/brand.tsx"),
|
||||
`import type { SiteConfig } from "@/kit/types";\n\n// ${site.brand.name} —— 独立站点的全部品牌/导航/页脚配置\nexport const siteConfig: SiteConfig = ${JSON.stringify(siteConfigData, null, 2)};\n`
|
||||
);
|
||||
|
||||
// ---------- 5) i18n request(站点文案覆盖) ----------
|
||||
writeFileSafe(
|
||||
path.join(siteRoot, "src/i18n/request.ts"),
|
||||
`import { getRequestConfig } from "next-intl/server";
|
||||
import { notFound } from "next/navigation";
|
||||
import { locales, defaultLocale, type Locale } from "${corePkg}/i18n.config";
|
||||
|
||||
/** 站点品牌 SEO 文案覆盖(每个网站不同) */
|
||||
const brandMessages = {
|
||||
en: {
|
||||
seo: { home: ${JSON.stringify({ title: site.seo.en.title, description: site.seo.en.description, ogImage: "/og-image.png" })} },
|
||||
hero: { title: ${JSON.stringify(site.seo.en.title)}, description: ${JSON.stringify(site.seo.en.description)} },
|
||||
common: { brand: { name: ${JSON.stringify(site.brand.name)}, tagline: ${JSON.stringify(site.brand.tagline)} } },
|
||||
},
|
||||
zh: {
|
||||
seo: { home: ${JSON.stringify({ title: site.seo.zh.title, description: site.seo.zh.description, ogImage: "/og-image.png" })} },
|
||||
hero: { title: ${JSON.stringify(site.seo.zh.title)}, description: ${JSON.stringify(site.seo.zh.description)} },
|
||||
common: { brand: { name: ${JSON.stringify(site.brand.name)}, tagline: ${JSON.stringify(site.brand.tagline)} } },
|
||||
},
|
||||
} as const;
|
||||
|
||||
export default getRequestConfig(async ({ requestLocale }) => {
|
||||
let locale = await requestLocale;
|
||||
if (!locale || !locales.includes(locale as Locale)) {
|
||||
locale = defaultLocale;
|
||||
}
|
||||
try {
|
||||
// 底座基础文案 + 站点品牌覆盖(浅合并到顶层命名空间)
|
||||
const base = (await import(\`${corePkg}/messages/\${locale}.json\`)).default;
|
||||
const override = brandMessages[locale as keyof typeof brandMessages];
|
||||
const messages = { ...base };
|
||||
for (const [ns, value] of Object.entries(override)) {
|
||||
messages[ns] = { ...(messages[ns] ?? {}), ...value };
|
||||
}
|
||||
return { locale, messages };
|
||||
} catch (error) {
|
||||
console.error("Error loading messages for locale:", locale, error);
|
||||
notFound();
|
||||
}
|
||||
});
|
||||
`
|
||||
);
|
||||
|
||||
// ---------- 6) proxy / mdx-components ----------
|
||||
writeFileSafe(
|
||||
path.join(siteRoot, "src/proxy.ts"),
|
||||
`import { proxy } from "${corePkg}/proxy";\n\nexport default proxy;\n\nexport const config = {\n matcher: [\n "/",\n "/((?!api|_next|_vercel|.*\\\\..*).*)"\n ]\n};\n`
|
||||
);
|
||||
writeFileSafe(
|
||||
path.join(siteRoot, "mdx-components.tsx"),
|
||||
`export { useMDXComponents } from "${corePkg}/src/mdx-components";\n`
|
||||
);
|
||||
|
||||
// ---------- 7) 配置文件 ----------
|
||||
writeFileSafe(
|
||||
path.join(siteRoot, "package.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
name: `site-${siteKey}`,
|
||||
version: "1.0.0",
|
||||
description: `${site.brand.name} — AI directory site powered by @aidir/core`,
|
||||
private: true,
|
||||
scripts: {
|
||||
dev: `next dev -p ${site.port}`,
|
||||
build: "next build",
|
||||
start: `next start -p ${site.port}`,
|
||||
typecheck: "tsc --noEmit",
|
||||
},
|
||||
dependencies: {
|
||||
"@aidir/core": "workspace:*",
|
||||
next: "16.1.1",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"next-intl": "^4.7.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"next-view-transitions": "^0.3.5",
|
||||
"sonner": "^2.0.7",
|
||||
"geist": "^1.3.0",
|
||||
"lucide-react": "^0.562.0",
|
||||
"framer-motion": "^12.43.0",
|
||||
},
|
||||
devDependencies: {
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"typescript": "^5",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"postcss": "^8",
|
||||
},
|
||||
},
|
||||
null,
|
||||
2
|
||||
) + "\n"
|
||||
);
|
||||
|
||||
writeFileSafe(
|
||||
path.join(siteRoot, "next.config.mjs"),
|
||||
`import createSiteConfig from "@aidir/core/next-config";\n\nexport default createSiteConfig({\n requestPath: "./src/i18n/request.ts",\n});\n`
|
||||
);
|
||||
|
||||
writeFileSafe(
|
||||
path.join(siteRoot, "tsconfig.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
compilerOptions: {
|
||||
target: "ES2017",
|
||||
lib: ["dom", "dom.iterable", "esnext"],
|
||||
allowJs: true,
|
||||
skipLibCheck: true,
|
||||
strict: true,
|
||||
noEmit: true,
|
||||
esModuleInterop: true,
|
||||
module: "esnext",
|
||||
moduleResolution: "bundler",
|
||||
resolveJsonModule: true,
|
||||
isolatedModules: true,
|
||||
jsx: "preserve",
|
||||
incremental: true,
|
||||
plugins: [{ name: "next" }],
|
||||
paths: {
|
||||
"@/*": ["./src/*", "../../packages/core/src/*"],
|
||||
"@aidir/core/pages/*": ["../../packages/core/src/pages/*"],
|
||||
"@aidir/core/kit/*": ["../../packages/core/src/kit/*"],
|
||||
"@aidir/core/src/*": ["../../packages/core/src/*"],
|
||||
"@aidir/core/proxy": ["../../packages/core/src/proxy.ts"],
|
||||
"@aidir/core/i18n.config": ["../../packages/core/src/i18n.config.ts"],
|
||||
"@aidir/core/styles/globals.css": ["../../packages/core/src/styles/globals.css"],
|
||||
"@aidir/core/*": ["../../packages/core/src/*", "../../packages/core/*"],
|
||||
},
|
||||
},
|
||||
include: ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
exclude: ["node_modules"],
|
||||
},
|
||||
null,
|
||||
2
|
||||
) + "\n"
|
||||
);
|
||||
|
||||
writeFileSafe(
|
||||
path.join(siteRoot, "postcss.config.mjs"),
|
||||
`const config = {\n plugins: {\n tailwindcss: {},\n },\n};\n\nexport default config;\n`
|
||||
);
|
||||
|
||||
// tailwind:content 扫描站点 + 底座;主题/插件来自底座 preset
|
||||
writeFileSafe(
|
||||
path.join(siteRoot, "tailwind.config.ts"),
|
||||
`import type { Config } from "tailwindcss";\n\nconst config: Config = {\n content: [\n "./src/**/*.{js,ts,jsx,tsx,mdx}",\n "../../packages/core/src/**/*.{js,ts,jsx,tsx,mdx}",\n ],\n presets: [require("../../packages/core/tailwind.preset.cjs")],\n};\n\nexport default config;\n`
|
||||
);
|
||||
|
||||
// ---------- 8) messages(base 由 i18n request 运行时合并;站点仍保留一份覆盖说明) ----------
|
||||
// 静态拷贝 core messages 便于独立部署检查
|
||||
for (const locale of ["en", "zh"]) {
|
||||
const base = JSON.parse(fs.readFileSync(path.join(CORE_ROOT, "messages", `${locale}.json`), "utf8"));
|
||||
// 品牌覆盖(静态合并进 messages,运行时 request.ts 仍会覆盖以双保险)
|
||||
base.common = base.common ?? {};
|
||||
base.common.brand = { ...(base.common.brand ?? {}), name: site.brand.name, tagline: site.brand.tagline };
|
||||
base.seo = base.seo ?? {};
|
||||
base.seo.home = { ...(base.seo.home ?? {}), ...site.seo[locale] };
|
||||
base.hero = { ...(base.hero ?? {}), title: site.seo[locale].title, description: site.seo[locale].description };
|
||||
fs.mkdirSync(path.join(siteRoot, "messages"), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(siteRoot, "messages", `${locale}.json`),
|
||||
JSON.stringify(base, null, 2) + "\n"
|
||||
);
|
||||
}
|
||||
|
||||
// 但 request.ts 目前从 core 读基础文案 —— 改为读本站 messages(保证站点内容独立可改)
|
||||
let requestSrc = fs.readFileSync(path.join(siteRoot, "src/i18n/request.ts"), "utf8");
|
||||
requestSrc = requestSrc.replace(
|
||||
'const base = (await import(`@aidir/core/messages/${locale}.json`)).default;',
|
||||
'const base = (await import(`../../messages/${locale}.json`)).default;'
|
||||
);
|
||||
writeFileSafe(path.join(siteRoot, "src/i18n/request.ts"), requestSrc);
|
||||
|
||||
// ---------- 9) public + env ----------
|
||||
fs.cpSync(path.join(CORE_ROOT, "public"), path.join(siteRoot, "public"), { recursive: true });
|
||||
// 品牌首字母 favicon(SVG,无 Emoji)
|
||||
const initial = site.brand.name.slice(0, 1).toUpperCase();
|
||||
writeFileSafe(
|
||||
path.join(siteRoot, "public", "icon.svg"),
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="14" fill="currentColor"/><text x="32" y="44" font-family="system-ui,sans-serif" font-size="36" font-weight="700" text-anchor="middle" fill="#fff">${initial}</text></svg>\n`
|
||||
);
|
||||
writeFileSafe(
|
||||
path.join(siteRoot, ".env.example"),
|
||||
`# ${site.brand.name} 环境变量(与底座共享同一套 PostgreSQL / Better Auth / Admin)
|
||||
DATABASE_URL="postgresql://postgres:postgres@127.0.0.1:15433/ai_directory"
|
||||
BETTER_AUTH_SECRET="dev-secret-ai-directory-platform-0123456789abcdef"
|
||||
BETTER_AUTH_URL="http://localhost:${site.port}"
|
||||
AUTH_GOOGLE_ID=""
|
||||
AUTH_GOOGLE_SECRET=""
|
||||
NEXT_PUBLIC_AUTH_GOOGLE_ENABLED="false"
|
||||
AUTH_GITHUB_ID=""
|
||||
AUTH_GITHUB_SECRET=""
|
||||
NEXT_PUBLIC_AUTH_GITHUB_ENABLED="false"
|
||||
RESEND_API_KEY=""
|
||||
RESEND_FROM_EMAIL="${site.brand.name} <noreply@localhost>"
|
||||
NEXT_PUBLIC_APP_URL="http://localhost:${site.port}"
|
||||
NEXT_PUBLIC_APP_NAME="${site.brand.name}"
|
||||
`
|
||||
);
|
||||
|
||||
// ---------- 10) .gitignore ----------
|
||||
writeFileSafe(
|
||||
path.join(siteRoot, ".gitignore"),
|
||||
"/node_modules\n/.next\n/.env*.local\nnext-env.d.ts\n*.tsbuildinfo\n"
|
||||
);
|
||||
|
||||
console.log(`路由薄文件: ${routeCount + 2}(含根布局/营销布局/首页组合器)`);
|
||||
console.log(`输出: sites/${siteKey}`);
|
||||
}
|
||||
|
||||
// main
|
||||
const key = process.argv[2];
|
||||
if (!key || key === "all") {
|
||||
for (const k of Object.keys(SITES)) generateSite(k);
|
||||
} else {
|
||||
generateSite(key);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { platform } from "node:os";
|
||||
|
||||
const cleanEnv = { ...process.env };
|
||||
|
||||
for (const key of [
|
||||
"npm_config_verify_deps_before_run",
|
||||
"NPM_CONFIG_VERIFY_DEPS_BEFORE_RUN",
|
||||
"npm_config__jsr_registry",
|
||||
"NPM_CONFIG__JSR_REGISTRY",
|
||||
]) {
|
||||
if (key in cleanEnv) {
|
||||
delete cleanEnv[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Windows 兼容性修复: Windows 需要 .cmd 扩展名和 shell
|
||||
const isWindows = platform() === "win32";
|
||||
const command = isWindows ? "next.cmd" : "next";
|
||||
|
||||
const child = spawn(command, ["dev"], {
|
||||
stdio: "inherit",
|
||||
env: cleanEnv,
|
||||
shell: isWindows,
|
||||
});
|
||||
|
||||
child.on("exit", (code, signal) => {
|
||||
if (signal) {
|
||||
process.kill(process.pid, signal);
|
||||
} else {
|
||||
process.exit(code ?? 0);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* # 脚本 screenshot-pages
|
||||
* 说明:用 Playwright 对五个主题站点与关键页面截图 + 控制台错误采集。
|
||||
* 用于交付前的视觉验收。
|
||||
*/
|
||||
import { chromium } from "playwright";
|
||||
import { mkdirSync } from "node:fs";
|
||||
|
||||
const OUT = "/tmp/aidir-shots";
|
||||
mkdirSync(OUT, { recursive: true });
|
||||
|
||||
const targets = [
|
||||
{ name: "classic-home", url: "http://localhost:3001/" },
|
||||
{ name: "classic-rankings", url: "http://localhost:3001/rankings" },
|
||||
{ name: "classic-tool-detail", url: "http://localhost:3001/tools/claude" },
|
||||
{ name: "classic-tools-list", url: "http://localhost:3001/tools" },
|
||||
{ name: "classic-best", url: "http://localhost:3001/best/best-ai-writing-tools" },
|
||||
{ name: "classic-usecase", url: "http://localhost:3001/ai-tools-for/blogging" },
|
||||
{ name: "classic-compare", url: "http://localhost:3001/tools/cursor-vs-windsurf" },
|
||||
{ name: "classic-alt", url: "http://localhost:3001/tools/midjourney-alternatives" },
|
||||
{ name: "classic-zh", url: "http://localhost:3001/zh" },
|
||||
{ name: "classic-submit", url: "http://localhost:3001/submit" },
|
||||
{ name: "product-home", url: "http://localhost:3002/" },
|
||||
{ name: "editorial-home", url: "http://localhost:3003/" },
|
||||
{ name: "niche-home", url: "http://localhost:3004/" },
|
||||
{ name: "minimal-home", url: "http://localhost:3005/" },
|
||||
{ name: "minimal-mobile", url: "http://localhost:3005/", mobile: true },
|
||||
{ name: "product-mobile", url: "http://localhost:3002/", mobile: true },
|
||||
];
|
||||
|
||||
const browser = await chromium.launch();
|
||||
const errors: string[] = [];
|
||||
|
||||
for (const target of targets) {
|
||||
const context = await browser.newContext(
|
||||
target.mobile
|
||||
? { viewport: { width: 390, height: 844 }, deviceScaleFactor: 2, isMobile: true, hasTouch: true }
|
||||
: { viewport: { width: 1440, height: 900 }, deviceScaleFactor: 1 }
|
||||
);
|
||||
const page = await context.newPage();
|
||||
page.on("console", (msg) => {
|
||||
if (msg.type() === "error") errors.push(`${target.name}: ${msg.text().slice(0, 200)}`);
|
||||
});
|
||||
page.on("pageerror", (err) => errors.push(`${target.name}: PAGEERROR ${String(err).slice(0, 200)}`));
|
||||
|
||||
try {
|
||||
await page.goto(target.url, { waitUntil: "domcontentloaded", timeout: 30000 });
|
||||
await page.waitForTimeout(1200);
|
||||
await page.screenshot({ path: `${OUT}/${target.name}.png`, fullPage: false });
|
||||
// 顺便记录 H1
|
||||
const h1 = await page.locator("h1").first().textContent().catch(() => "(no h1)");
|
||||
console.log(`OK ${target.name} | h1: ${h1?.trim().slice(0, 60)}`);
|
||||
} catch (error) {
|
||||
console.log(`FAIL ${target.name}: ${String(error).slice(0, 120)}`);
|
||||
}
|
||||
await context.close();
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
console.log("\n=== 控制台错误 ===");
|
||||
console.log(errors.length === 0 ? "(none)" : errors.slice(0, 12).join("\n"));
|
||||
@@ -0,0 +1,520 @@
|
||||
/**
|
||||
* # 脚本 seed-platform
|
||||
* 说明:AI Directory Platform 一体化种子数据。
|
||||
* - 14 分类(无 Emoji 图标)/ 16 用例 / 9 受众
|
||||
* - 40 个真实 AI 工具(含 tagline/定价/分类/用例/受众关联)
|
||||
* - 6 个演示用户(含 1 个管理员,密码 password123)
|
||||
* - 投票 / 评论(含优缺点)/ 收藏 / 合集 / 事件流(近 30 天)
|
||||
* - 同步冗余统计列(voteCount/ratingAvg/viewCount...)
|
||||
*
|
||||
* 用法:bun scripts/seed-platform.ts (可加 --force 清空重灌)
|
||||
*/
|
||||
import { drizzle } from "drizzle-orm/postgres-js";
|
||||
import postgres from "postgres";
|
||||
import dotenv from "dotenv";
|
||||
import { resolve } from "path";
|
||||
import {
|
||||
tool,
|
||||
category,
|
||||
tag,
|
||||
toolCategory,
|
||||
toolTag,
|
||||
useCase,
|
||||
audience,
|
||||
toolUseCase,
|
||||
toolAudience,
|
||||
collection,
|
||||
collectionTool,
|
||||
vote,
|
||||
review,
|
||||
favorite,
|
||||
event,
|
||||
submission,
|
||||
claim,
|
||||
user,
|
||||
} from "../src/lib/db/schema";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { nanoid } from "nanoid";
|
||||
import { hashPassword } from "better-auth/crypto";
|
||||
|
||||
dotenv.config({ path: resolve(process.cwd(), ".env.local") });
|
||||
|
||||
const client = postgres(process.env.DATABASE_URL!, { prepare: false });
|
||||
const db = drizzle(client);
|
||||
|
||||
// ========== 确定性伪随机(可重复灌数) ==========
|
||||
let seedState = 42;
|
||||
function rand(): number {
|
||||
seedState = (seedState * 1103515245 + 12345) % 2147483648;
|
||||
return seedState / 2147483648;
|
||||
}
|
||||
function randInt(min: number, max: number): number {
|
||||
return Math.floor(rand() * (max - min + 1)) + min;
|
||||
}
|
||||
function pick<T>(arr: T[]): T {
|
||||
return arr[randInt(0, arr.length - 1)];
|
||||
}
|
||||
function daysAgo(days: number): Date {
|
||||
return new Date(Date.now() - days * 24 * 3600 * 1000);
|
||||
}
|
||||
|
||||
// ========== 分类(14,设计文档 §11) ==========
|
||||
const categories = [
|
||||
["chatbot", "AI Chatbots", "Conversational AI assistants", "AI 聊天机器人", "对话式 AI 助手"],
|
||||
["writing", "Writing", "AI-powered writing assistants and content tools", "写作与内容", "AI 驱动的写作助手和内容工具"],
|
||||
["image", "Image Generation", "Create and edit images with AI", "图像生成", "用 AI 创作和编辑图像"],
|
||||
["video", "Video", "AI video generation, editing and animation", "视频", "AI 视频生成、编辑与动画"],
|
||||
["audio", "Audio & Music", "Music generation, voice and audio AI", "音频与音乐", "AI 音乐生成、语音与音频"],
|
||||
["coding", "Coding", "AI coding assistants and developer tools", "编程开发", "AI 编程助手与开发者工具"],
|
||||
["marketing", "Marketing", "AI for marketing, SEO and growth", "营销", "面向营销、SEO 与增长的 AI"],
|
||||
["productivity", "Productivity", "Work faster with AI helpers", "效率办公", "用 AI 提升工作效率"],
|
||||
["research", "Research", "AI research and analysis assistants", "研究分析", "AI 研究与数据分析助手"],
|
||||
["business", "Business", "AI for business operations", "商业", "面向商业运营的 AI"],
|
||||
["education", "Education", "AI tutoring and learning tools", "教育", "AI 教学与学习工具"],
|
||||
["finance", "Finance", "AI for finance and investing", "金融", "面向金融与投资的 AI"],
|
||||
["design", "Design", "AI design and creative tools", "设计", "AI 设计与创意工具"],
|
||||
["automation", "Automation", "Workflow and task automation with AI", "自动化", "AI 工作流与任务自动化"],
|
||||
["agent", "AI Agent", "Autonomous agents and copilots", "智能体", "自主智能体与副驾驶"],
|
||||
] as const;
|
||||
|
||||
// ========== 用例(16,设计文档 §11 Use Case) ==========
|
||||
const useCasesData = [
|
||||
["blogging", "Blogging", "Write, outline and edit blog posts faster", "写博客", "更快地撰写、构思和编辑博客"],
|
||||
["presentation", "Making Presentations", "Generate slides and decks from prompts", "制作 PPT", "从提示词生成幻灯片"],
|
||||
["logo-design", "Logo Design", "Create logos and brand marks", "生成 Logo", "创作 Logo 与品牌标识"],
|
||||
["video-editing", "Video Editing", "Cut, caption and polish videos", "视频剪辑", "剪辑、加字幕和精修视频"],
|
||||
["meeting-notes", "Meeting Notes", "Transcribe and summarize meetings", "会议记录", "转写并总结会议内容"],
|
||||
["subtitles", "Subtitles & Captions", "Generate and translate subtitles", "生成字幕", "生成与翻译字幕"],
|
||||
["write-code", "Writing Code", "Autocomplete, refactor and explain code", "写代码", "代码补全、重构与解释"],
|
||||
["seo", "SEO", "Keyword research and content optimization", "搜索引擎优化", "关键词研究与内容优化"],
|
||||
["customer-support", "Customer Support", "Chatbots and helpdesk automation", "客户支持", "客服机器人与工单自动化"],
|
||||
["social-media", "Social Media", "Schedule, write and repurpose posts", "社交媒体", "撰写、排期与复用社媒内容"],
|
||||
["market-analysis", "Market Analysis", "Research markets and competitors", "市场分析", "研究市场与竞品"],
|
||||
["image-upscale", "Image Upscaling", "Upscale and restore images", "图像放大", "放大与修复图像"],
|
||||
["copywriting", "Copywriting", "Ads, landing pages and email copy", "文案写作", "广告、落地页与邮件文案"],
|
||||
["data-extraction", "Data Extraction", "Parse documents and extract structured data", "数据提取", "解析文档并提取结构化数据"],
|
||||
["language-learning", "Language Learning", "Practice and translate languages", "语言学习", "练习与翻译语言"],
|
||||
["music-production", "Music Production", "Compose beats and produce tracks", "音乐制作", "作曲与音乐制作"],
|
||||
] as const;
|
||||
|
||||
// ========== 受众(9,设计文档 §11 Audience) ==========
|
||||
const audiencesData = [
|
||||
["developers", "Developers", "Engineers shipping software", "开发者", "写代码的工程师"],
|
||||
["designers", "Designers", "Product and visual designers", "设计师", "产品与视觉设计师"],
|
||||
["marketers", "Marketers", "Growth, SEO and content marketers", "营销人员", "增长、SEO 与内容营销"],
|
||||
["students", "Students", "Learning and studying", "学生", "学习与备考人群"],
|
||||
["creators", "Creators", "YouTubers, podcasters and streamers", "创作者", "视频博主、播客与主播"],
|
||||
["founders", "Founders", "Startup founders and solopreneurs", "创业者", "创始人与独立开发者"],
|
||||
["researchers", "Researchers", "Academic and market researchers", "研究人员", "学术与市场研究"],
|
||||
["business", "Business Teams", "Ops, sales and finance teams", "商业团队", "运营、销售与财务团队"],
|
||||
["enterprise", "Enterprise", "Large organizations with compliance needs", "大型企业", "有合规需求的大型组织"],
|
||||
] as const;
|
||||
|
||||
// ========== 工具(40 个真实产品) ==========
|
||||
// [name, slug, domain, taglineEn, taglineZh, descEn, pricing, categories, useCases, audiences, featured]
|
||||
const toolsData: [string, string, string, string, string, string, string, string[], string[], string[], boolean][] = [
|
||||
["ChatGPT", "chatgpt", "chatgpt.com", "The AI chatbot that started it all", "开创先河的 AI 聊天机器人", "ChatGPT is OpenAI's conversational AI assistant capable of writing, coding, analysis and more. It popularized large language models for everyday users and now supports multimodal inputs, custom GPTs and web browsing.", "freemium", ["chatbot", "writing", "coding"], ["write-code", "blogging", "copywriting"], ["developers", "marketers", "students", "founders"], true],
|
||||
["Claude", "claude", "claude.ai", "Thoughtful AI assistant by Anthropic", "Anthropic 出品的可靠 AI 助手", "Claude is Anthropic's AI assistant, known for long-context understanding, careful reasoning and strong writing. Ideal for document analysis, coding and nuanced drafting.", "freemium", ["chatbot", "writing", "coding"], ["write-code", "blogging", "data-extraction"], ["developers", "researchers", "business"], true],
|
||||
["Midjourney", "midjourney", "midjourney.com", "Industry-leading AI image generation", "业界领先的 AI 图像生成", "Midjourney creates striking, artistic images from text prompts. It is widely used for concept art, marketing visuals and creative exploration, with a distinctive aesthetic quality.", "paid", ["image", "design"], ["logo-design", "image-upscale"], ["designers", "creators", "marketers"], true],
|
||||
["DALL·E 3", "dall-e-3", "openai.com", "Text-to-image from OpenAI, built into ChatGPT", "OpenAI 的文生图模型,已集成到 ChatGPT", "DALL·E 3 generates detailed images from natural language with excellent prompt adherence, integrated directly into ChatGPT for conversational image creation and iteration.", "freemium", ["image"], ["logo-design", "image-upscale"], ["designers", "marketers"], false],
|
||||
["Stable Diffusion", "stable-diffusion", "stability.ai", "Open-source image generation you can self-host", "可自部署的开源图像生成", "Stable Diffusion is an open-source latent diffusion model for image generation, fine-tuning and local deployment, powering countless creative tools and pipelines.", "open_source", ["image"], ["logo-design", "image-upscale"], ["designers", "developers"], false],
|
||||
["Runway", "runway", "runwayml.com", "Generative video for creators", "面向创作者的生成式视频", "Runway provides AI video generation and editing — text-to-video, motion brush, inpainting and green screen — used across film, advertising and social content.", "freemium", ["video"], ["video-editing"], ["creators", "designers", "marketers"], true],
|
||||
["Pika", "pika", "pika.art", "Playful text-to-video generation", "有趣的文生视频工具", "Pika turns text prompts and images into short animated videos with a friendly creative workflow, popular for social clips and concept visualization.", "freemium", ["video"], ["video-editing", "social-media"], ["creators", "marketers"], false],
|
||||
["ElevenLabs", "elevenlabs", "elevenlabs.io", "The most realistic AI voices", "最逼真的 AI 语音", "ElevenLabs generates hyper-realistic speech in dozens of languages, with voice cloning, dubbing and long-form narration for videos, audiobooks and products.", "freemium", ["audio"], ["subtitles", "video-editing"], ["creators", "developers"], true],
|
||||
["Suno", "suno", "suno.com", "Create songs from text prompts", "从文字生成完整歌曲", "Suno composes full songs — vocals, lyrics and instrumentation — from simple prompts, enabling anyone to produce original music in minutes.", "freemium", ["audio"], ["music-production"], ["creators"], false],
|
||||
["GitHub Copilot", "github-copilot", "github.com", "Your AI pair programmer", "你的 AI 结对程序员", "GitHub Copilot suggests whole lines and functions inside your editor, trained on public code. It accelerates everyday programming across all major languages.", "paid", ["coding"], ["write-code"], ["developers"], true],
|
||||
["Cursor", "cursor", "cursor.com", "The AI-first code editor", "AI 优先的代码编辑器", "Cursor is a fork of VS Code rebuilt around AI: multi-file edits, codebase-aware chat and agent mode that writes and refactors code with full project context.", "freemium", ["coding"], ["write-code"], ["developers"], true],
|
||||
["Windsurf", "windsurf", "codeium.com", "Agentic IDE that keeps you in flow", "保持心流的智能体 IDE", "Windsurf (by Codeium) is an agentic IDE with deep codebase awareness — it plans, writes and runs terminal commands to complete tasks end to end.", "freemium", ["coding"], ["write-code"], ["developers"], false],
|
||||
["Tabnine", "tabnine", "tabnine.com", "Private, secure AI code completion", "私有化部署的 AI 代码补全", "Tabnine offers AI code completion with privacy-first deployment options — self-hosted or air-gapped — for teams with strict compliance requirements.", "freemium", ["coding", "business"], ["write-code"], ["developers", "enterprise"], false],
|
||||
["Jasper", "jasper", "jasper.ai", "AI copywriting for marketing teams", "面向营销团队的 AI 文案", "Jasper helps marketing teams produce on-brand blog posts, ads and emails at scale, with templates, tone control and campaign workflows.", "paid", ["writing", "marketing"], ["copywriting", "blogging", "seo"], ["marketers", "business"], false],
|
||||
["Copy.ai", "copy-ai", "copy.ai", "Sales and marketing copy, automated", "销售与营销文案自动化", "Copy.ai generates marketing and sales copy — from cold emails to product descriptions — with workflow automation for go-to-market teams.", "freemium", ["writing", "marketing"], ["copywriting", "customer-support"], ["marketers", "founders"], false],
|
||||
["Surfer SEO", "surfer-seo", "surferseo.com", "Data-driven on-page SEO optimization", "数据驱动的页面 SEO 优化", "Surfer analyzes SERPs and guides content optimization with keyword density, structure and entity recommendations, helping articles rank faster.", "paid", ["marketing"], ["seo", "blogging"], ["marketers", "business"], false],
|
||||
["Semrush", "semrush", "semrush.com", "The all-in-one SEO and PPC platform", "一站式 SEO 与 PPC 平台", "Semrush provides keyword research, site audits, competitor analysis and rank tracking — the standard toolkit for SEO professionals.", "paid", ["marketing"], ["seo", "market-analysis"], ["marketers", "business"], false],
|
||||
["Notion AI", "notion-ai", "notion.com", "AI built into your workspace", "工作区内置 AI", "Notion AI brings writing, summarization and Q&A into Notion pages and databases, so notes, docs and project data become instantly actionable.", "freemium", ["productivity"], ["meeting-notes", "blogging"], ["business", "founders", "students"], false],
|
||||
["Mem", "mem", "get.mem.ai", "Self-organizing AI notes", "自动整理的 AI 笔记", "Mem is an AI notes app that organizes itself — capture anything and Mem connects, tags and resurfaces it when relevant.", "freemium", ["productivity"], ["meeting-notes"], ["founders", "researchers"], false],
|
||||
["Otter.ai", "otter-ai", "otter.ai", "AI meeting assistant and transcription", "AI 会议助手与转写", "Otter records, transcribes and summarizes meetings in real time, generating action items and searchable transcripts for teams.", "freemium", ["productivity", "audio"], ["meeting-notes", "subtitles"], ["business", "creators"], false],
|
||||
["Perplexity", "perplexity", "perplexity.ai", "The answer engine", "答案引擎", "Perplexity is an AI-powered answer engine that searches the web in real time and responds with cited, sourced summaries — a research shortcut.", "freemium", ["research", "chatbot"], ["market-analysis", "blogging"], ["researchers", "students", "business"], true],
|
||||
["Elicit", "elicit", "elicit.com", "AI research assistant for papers", "面向论文的 AI 研究助手", "Elicit helps researchers find and synthesize academic papers — extracting methods, findings and limitations into structured tables.", "freemium", ["research"], ["market-analysis", "data-extraction"], ["researchers", "students"], false],
|
||||
["Scite", "scite", "scite.ai", "Smart citations for research", "智能引用分析", "Scite shows how papers cite each other — supporting or contrasting — helping researchers evaluate evidence quality quickly.", "paid", ["research", "education"], ["market-analysis"], ["researchers"], false],
|
||||
["Synthesia", "synthesia", "synthesia.io", "AI video avatars for training", "面向培训的 AI 数字人视频", "Synthesia creates studio-quality videos with AI avatars and voiceover from plain text — used for corporate training and product explainers.", "paid", ["video", "business"], ["video-editing"], ["business", "enterprise"], false],
|
||||
["HeyGen", "heygen", "heygen.com", "Spokesperson videos in minutes", "几分钟生成数字人口播视频", "HeyGen generates avatar-led videos with lip-synced translation in 40+ languages, popular for sales outreach and localization.", "freemium", ["video"], ["video-editing", "social-media"], ["marketers", "business"], false],
|
||||
["Canva Magic Studio", "canva", "canva.com", "Design suite with AI everywhere", "全面融入 AI 的设计套件", "Canva's Magic Studio adds AI image generation, magic editing and brand controls to the world's most popular online design suite.", "freemium", ["design", "image"], ["logo-design", "presentation", "social-media"], ["marketers", "students", "business"], false],
|
||||
["Looka", "looka", "looka.com", "AI logo maker for new brands", "面向新品牌的 AI Logo 生成", "Looka generates professional logo sets and brand kits from your preferences — an easy starting point for new businesses.", "freemium", ["design"], ["logo-design"], ["founders"], false],
|
||||
["Figma AI", "figma-ai", "figma.com", "AI in the collaborative design tool", "协作设计工具中的 AI", "Figma integrates AI to generate designs, rename layers, write copy and search files — embedded in the leading collaborative design platform.", "freemium", ["design"], ["presentation"], ["designers"], false],
|
||||
["Zapier AI", "zapier-ai", "zapier.com", "Automate work across 6,000+ apps", "连接 6000+ 应用的自动化", "Zapier connects thousands of apps and adds AI steps — parsing, drafting, routing — so teams automate workflows without code.", "freemium", ["automation"], ["customer-support", "data-extraction"], ["business", "marketers"], false],
|
||||
["Make", "make", "make.com", "Visual automation with AI modules", "可视化自动化平台", "Make (formerly Integromat) offers visual workflow automation with AI modules for content generation, classification and routing.", "freemium", ["automation"], ["data-extraction"], ["business", "developers"], false],
|
||||
["LangChain", "langchain", "langchain.com", "Framework for LLM applications", "大模型应用开发框架", "LangChain is the popular framework for building LLM apps — chains, agents, retrieval and observability for production AI.", "open_source", ["coding", "agent"], ["write-code"], ["developers"], false],
|
||||
["AutoGPT", "autogpt", "agpt.co", "The pioneering autonomous agent", "开创性的自主智能体", "AutoGPT popularized autonomous AI agents that plan, browse and execute multi-step tasks toward a goal.", "open_source", ["agent"], ["write-code", "market-analysis"], ["developers", "founders"], false],
|
||||
["Hugging Face", "hugging-face", "huggingface.co", "The AI models community hub", "AI 模型社区中枢", "Hugging Face hosts hundreds of thousands of models, datasets and demos — the de facto open platform for machine learning.", "freemium", ["agent", "coding", "research"], ["write-code"], ["developers", "researchers"], false],
|
||||
["Grammarly", "grammarly", "grammarly.com", "Writing assistant that follows you everywhere", "随身的写作助手", "Grammarly checks grammar, clarity and tone across apps, with generative rewriting to improve any text you write.", "freemium", ["writing", "productivity"], ["copywriting", "blogging"], ["students", "business", "marketers"], false],
|
||||
["QuillBot", "quillbot", "quillbot.com", "Paraphrasing and summarizing tool", "改写与摘要工具", "QuillBot paraphrases, summarizes and checks grammar, widely used by students and writers to rewrite text quickly.", "freemium", ["writing", "education"], ["blogging", "copywriting"], ["students", "creators"], false],
|
||||
["Duolingo Max", "duolingo-max", "duolingo.com", "Language learning with GPT-4", "GPT-4 加持的语言学习", "Duolingo Max adds AI conversation practice and detailed explanations to the world's most popular language learning app.", "freemium", ["education"], ["language-learning"], ["students"], false],
|
||||
["Khanmigo", "khanmigo", "khanmigo.ai", "Khan Academy's AI tutor", "可汗学院的 AI 导师", "Khanmigo is Khan Academy's AI tutor that guides students through problems step by step instead of giving answers away.", "paid", ["education"], ["language-learning"], ["students"], false],
|
||||
["AlphaSense", "alpha-sense", "alpha-sense.com", "Market intelligence search", "市场情报搜索", "AlphaSense searches earnings calls, filings and expert transcripts with NLP for investment and competitive research.", "contact", ["finance", "research"], ["market-analysis", "data-extraction"], ["business", "enterprise"], false],
|
||||
["Betterment", "betterment", "betterment.com", "Investing with AI-guided portfolios", "AI 引导的投资组合", "Betterment uses automated portfolio management and tax-loss harvesting to make investing hands-off.", "freemium", ["finance"], ["market-analysis"], ["business"], false],
|
||||
["Beautiful.ai", "beautiful-ai", "beautiful.ai", "Presentations that design themselves", "自动设计的演示文稿", "Beautiful.ai applies smart design rules as you add content, keeping slides polished without manual layout work.", "paid", ["productivity", "design"], ["presentation"], ["business", "marketers"], false],
|
||||
] as const;
|
||||
|
||||
// ========== 标签 ==========
|
||||
const tags = [
|
||||
["free", "Free", "免费"],
|
||||
["freemium", "Freemium", "免费增值"],
|
||||
["paid", "Paid", "付费"],
|
||||
["open-source", "Open Source", "开源"],
|
||||
["api", "API Available", "提供 API"],
|
||||
["llm", "LLM", "大语言模型"],
|
||||
["beginner", "Beginner Friendly", "新手友好"],
|
||||
["enterprise", "Enterprise", "企业级"],
|
||||
["real-time", "Real-time", "实时"],
|
||||
["collaboration", "Collaboration", "协作"],
|
||||
] as const;
|
||||
|
||||
// ========== 合集(Best Lists) ==========
|
||||
const collectionsData = [
|
||||
["best-ai-writing-tools", "Best AI Writing Tools in 2026", "Our community-ranked shortlist of AI writing tools for blogs, ads and long-form content — based on votes, reviews and saves.", "2026 年最佳 AI 写作工具:基于投票、评论与收藏的社区榜单,覆盖博客、广告与长文写作。", ["chatgpt", "claude", "jasper", "copy-ai", "grammarly", "quillbot"]],
|
||||
["best-free-ai-tools", "Best Free AI Tools", "Genuinely useful AI tools with free plans — no trials that vanish in 7 days. Ranked by the community.", "真正好用的免费 AI 工具合集——不是 7 天就失效的试用。由社区投票排名。", ["chatgpt", "claude", "stable-diffusion", "grammarly", "canva", "perplexity"]],
|
||||
["best-ai-coding-tools", "Best AI Coding Tools", "From autocomplete to agentic IDEs: the coding AI that developers actually upvote.", "从代码补全到智能体 IDE:开发者真正会投票的编程 AI。", ["cursor", "github-copilot", "claude", "windsurf", "tabnine"]],
|
||||
["best-ai-video-generators", "Best AI Video Generators", "Text-to-video and editing AI compared on quality, speed and price — with community votes breaking ties.", "文生视频与视频剪辑 AI 对比:画质、速度与价格,由社区投票分高下。", ["runway", "pika", "synthesia", "heygen"]],
|
||||
["best-ai-tools-for-marketing", "Best AI Tools for Marketing Teams", "The marketing stack that ranked highest with our community: SEO, copy and social.", "社区评分最高的营销工具栈:SEO、文案与社交媒体。", ["surfer-seo", "semrush", "jasper", "copy-ai", "heygen"]],
|
||||
["best-chatgpt-alternatives", "Best ChatGPT Alternatives", "Looking beyond ChatGPT? These assistants ranked highest with users for writing, coding and research.", "ChatGPT 之外的选择:这些助手在写作、编程和研究上获得最高用户评价。", ["claude", "perplexity", "chatgpt", "notion-ai"]],
|
||||
] as const;
|
||||
|
||||
// ========== 主流程 ==========
|
||||
async function main() {
|
||||
const force = process.argv.includes("--force");
|
||||
const existing = await db.select({ cnt: sql<number>`count(*)` }).from(tool);
|
||||
if (Number(existing[0].cnt) > 0 && !force) {
|
||||
console.log(`数据库已有 ${Number(existing[0].cnt)} 个工具,跳过种子。使用 --force 清空重灌。`);
|
||||
await client.end();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("清空平台数据...");
|
||||
await db.delete(event);
|
||||
await db.delete(vote);
|
||||
await db.delete(review);
|
||||
await db.delete(favorite);
|
||||
await db.delete(collectionTool);
|
||||
await db.delete(collection);
|
||||
await db.delete(toolUseCase);
|
||||
await db.delete(toolAudience);
|
||||
await db.delete(toolCategory);
|
||||
await db.delete(toolTag);
|
||||
await db.delete(tool);
|
||||
await db.delete(useCase);
|
||||
await db.delete(audience);
|
||||
await db.delete(category);
|
||||
await db.delete(tag);
|
||||
await db.delete(submission);
|
||||
await db.delete(claim);
|
||||
await db.delete(user);
|
||||
|
||||
console.log("写入用户...");
|
||||
const passwordHash = await hashPassword("password123");
|
||||
const usersData = [
|
||||
["user_admin", "Platform Admin", "admin@aidirectory.dev", "admin"],
|
||||
["user_alice", "Alice Chen", "alice@demo.dev", "user"],
|
||||
["user_bob", "Bob Martinez", "bob@demo.dev", "user"],
|
||||
["user_carol", "Carol Wang", "carol@demo.dev", "user"],
|
||||
["user_dave", "Dave Kim", "dave@demo.dev", "user"],
|
||||
["user_erin", "Erin Patel", "erin@demo.dev", "user"],
|
||||
] as const;
|
||||
for (const [id, name, email, role] of usersData) {
|
||||
await db.insert(user).values({
|
||||
id,
|
||||
name,
|
||||
email,
|
||||
emailVerified: true,
|
||||
role,
|
||||
createdAt: daysAgo(randInt(30, 120)),
|
||||
updatedAt: daysAgo(randInt(0, 5)),
|
||||
// better-auth 凭据字段(credential 账户,演示密码 password123)
|
||||
...({}),
|
||||
});
|
||||
// 写入 account 表(credential provider)—— 简化:直接操作 SQL
|
||||
await client`
|
||||
INSERT INTO account (id, account_id, provider_id, user_id, password, created_at, updated_at)
|
||||
VALUES (${nanoid()}, ${id}, ${"credential"}, ${id}, ${passwordHash}, NOW(), NOW())
|
||||
ON CONFLICT DO NOTHING`;
|
||||
}
|
||||
|
||||
console.log("写入分类 / 标签 / 用例 / 受众...");
|
||||
for (const [i, [slug, en, descEn, zh, descZh]] of categories.entries()) {
|
||||
await db.insert(category).values({
|
||||
id: `cat_${slug}`,
|
||||
slug,
|
||||
icon: null, // 设计文档 §4.1:禁止 Emoji 图标
|
||||
sortOrder: i + 1,
|
||||
nameEn: en,
|
||||
descriptionEn: descEn,
|
||||
nameZh: zh,
|
||||
descriptionZh: descZh,
|
||||
});
|
||||
}
|
||||
for (const [slug, en, zh] of tags) {
|
||||
await db.insert(tag).values({ id: `tag_${slug}`, slug, nameEn: en, nameZh: zh });
|
||||
}
|
||||
for (const [i, [slug, en, descEn, zh, descZh]] of useCasesData.entries()) {
|
||||
await db.insert(useCase).values({
|
||||
id: `uc_${slug}`,
|
||||
slug,
|
||||
sortOrder: i + 1,
|
||||
nameEn: en,
|
||||
descriptionEn: descEn,
|
||||
nameZh: zh,
|
||||
descriptionZh: descZh,
|
||||
});
|
||||
}
|
||||
for (const [i, [slug, en, descEn, zh, descZh]] of audiencesData.entries()) {
|
||||
await db.insert(audience).values({
|
||||
id: `aud_${slug}`,
|
||||
slug,
|
||||
sortOrder: i + 1,
|
||||
nameEn: en,
|
||||
descriptionEn: descEn,
|
||||
nameZh: zh,
|
||||
descriptionZh: descZh,
|
||||
});
|
||||
}
|
||||
|
||||
console.log("写入工具与关联...");
|
||||
const toolIds: string[] = [];
|
||||
for (const [i, row] of toolsData.entries()) {
|
||||
const [name, slug, domain, taglineEn, taglineZh, descEn, pricing, cats, ucs, auds, featured] = row;
|
||||
const id = `tool_${slug}`;
|
||||
toolIds.push(id);
|
||||
|
||||
await db.insert(tool).values({
|
||||
id,
|
||||
slug,
|
||||
domain,
|
||||
websiteUrl: `https://${domain}`,
|
||||
// favicon 作为演示 logo(next.config 已放开 https 图片域名)
|
||||
logoUrl: `https://www.google.com/s2/favicons?domain=${domain}&sz=128`,
|
||||
nameEn: name,
|
||||
nameZh: name,
|
||||
taglineEn,
|
||||
taglineZh,
|
||||
descriptionEn: descEn,
|
||||
descriptionZh: descEn, // 演示数据中英共用描述
|
||||
pricing,
|
||||
featured,
|
||||
status: "published",
|
||||
// 时间分布:越靠前的工具越早收录(新工具在后面),制造真实的榜单时间差
|
||||
createdAt: daysAgo(300 - i * 6 + randInt(0, 5)),
|
||||
updatedAt: daysAgo(randInt(0, 10)),
|
||||
});
|
||||
|
||||
for (const cat of cats) {
|
||||
await db.insert(toolCategory).values({ toolId: id, categoryId: `cat_${cat}` }).onConflictDoNothing();
|
||||
}
|
||||
const tagPick = [pricing === "free" ? "free" : pricing === "open_source" ? "open-source" : pricing === "freemium" ? "freemium" : "paid", pick(["api", "llm", "beginner"]), pick(["real-time", "collaboration", "enterprise"])];
|
||||
for (const t of tagPick) {
|
||||
await db.insert(toolTag).values({ toolId: id, tagId: `tag_${t}` }).onConflictDoNothing();
|
||||
}
|
||||
for (const uc of ucs) {
|
||||
await db.insert(toolUseCase).values({ toolId: id, useCaseId: `uc_${uc}` }).onConflictDoNothing();
|
||||
}
|
||||
for (const aud of auds) {
|
||||
await db.insert(toolAudience).values({ toolId: id, audienceId: `aud_${aud}` }).onConflictDoNothing();
|
||||
}
|
||||
}
|
||||
|
||||
console.log("写入投票 / 收藏 / 评论...");
|
||||
const reviewTexts = [
|
||||
["Saved me hours every week", "I use this daily and it consistently saves me hours of repetitive work. The output quality keeps improving.", "Fast, reliable output", "Occasionally needs a second pass on nuance"],
|
||||
["Great for our team workflow", "Rolled it out to the whole team. Onboarding took minutes and everyone found it immediately useful.", "Easy onboarding, great docs", "Pricey at team tier"],
|
||||
["Solid but not perfect", "Does the core job well. Some advanced features feel half-finished, but the fundamentals are strong.", "Strong core features", "Advanced features feel unfinished"],
|
||||
["Worth every penny", "Paid plan pays for itself within the first week. Support responded within hours when I hit an issue.", "Excellent ROI and support", "No offline mode"],
|
||||
["Good, with caveats", "Impressive capability for the price, though I hit some rate limits during peak hours.", "Great value for money", "Peak-hour rate limits"],
|
||||
] as const;
|
||||
|
||||
const voteCountByTool = new Map<string, number>();
|
||||
const favCountByTool = new Map<string, number>();
|
||||
for (const [i, toolId] of toolIds.entries()) {
|
||||
const popularity = Math.max(1, 40 - i); // 列表靠前 = 更热门
|
||||
const voters = usersData.filter(([, , , role]) => role === "user" || role === "admin").map(([id]) => id);
|
||||
|
||||
// 投票:热门工具更多近 7 天投票(驱动 Trending)
|
||||
const totalVotes = Math.min(voters.length, Math.round((popularity / 4) * (0.5 + rand())));
|
||||
const recentVotes = Math.min(totalVotes, Math.ceil(totalVotes * (i < 12 ? 0.8 : 0.3)));
|
||||
const votedUsers = new Set<string>();
|
||||
for (let v = 0; v < totalVotes; v++) {
|
||||
const userId = pick(voters);
|
||||
if (votedUsers.has(userId)) continue;
|
||||
votedUsers.add(userId);
|
||||
const isRecent = v < recentVotes;
|
||||
const createdAt = isRecent ? daysAgo(randInt(0, 6)) : daysAgo(randInt(8, 120));
|
||||
await db.insert(vote).values({ id: nanoid(), userId, toolId, createdAt }).onConflictDoNothing();
|
||||
}
|
||||
voteCountByTool.set(toolId, votedUsers.size);
|
||||
|
||||
// 收藏
|
||||
const favUsers = voters.filter(() => rand() < popularity / 60);
|
||||
for (const userId of favUsers) {
|
||||
await db.insert(favorite).values({ userId, toolId, createdAt: daysAgo(randInt(0, 30)) }).onConflictDoNothing();
|
||||
}
|
||||
favCountByTool.set(toolId, favUsers.length);
|
||||
|
||||
// 评论(热门工具更容易被评论)
|
||||
if (rand() < popularity / 14) {
|
||||
const numReviews = randInt(1, 3);
|
||||
const reviewingUsers = new Set<string>();
|
||||
for (let r = 0; r < numReviews; r++) {
|
||||
const userId = pick(voters);
|
||||
if (reviewingUsers.has(userId)) continue;
|
||||
reviewingUsers.add(userId);
|
||||
const [title, content, pros, cons] = pick([...reviewTexts]);
|
||||
const rating = i < 10 ? randInt(4, 5) : randInt(3, 5);
|
||||
await db.insert(review).values({
|
||||
id: nanoid(),
|
||||
userId,
|
||||
toolId,
|
||||
rating,
|
||||
title,
|
||||
content,
|
||||
pros,
|
||||
cons,
|
||||
status: "published",
|
||||
helpfulCount: randInt(0, 12),
|
||||
createdAt: daysAgo(randInt(1, 40)),
|
||||
}).onConflictDoNothing();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("同步冗余统计列...");
|
||||
// 投票 / 收藏数
|
||||
for (const [toolId, cnt] of voteCountByTool) {
|
||||
await db.update(tool).set({ voteCount: cnt, favoriteCount: favCountByTool.get(toolId) ?? 0 }).where(sql`${tool.id} = ${toolId}`);
|
||||
}
|
||||
// 评分(真实聚合)
|
||||
await client`
|
||||
UPDATE tool t SET
|
||||
review_count = COALESCE(agg.cnt, 0),
|
||||
rating_avg = COALESCE(ROUND(agg.avg::numeric, 2), 0)
|
||||
FROM (
|
||||
SELECT tool_id, COUNT(*) AS cnt, AVG(rating) AS avg
|
||||
FROM review WHERE status = 'published'
|
||||
GROUP BY tool_id
|
||||
) agg
|
||||
WHERE t.id = agg.tool_id`;
|
||||
|
||||
console.log("写入合集...");
|
||||
for (const [i, [slug, nameEn, introEn, introZh, toolSlugs]] of collectionsData.entries()) {
|
||||
const id = `col_${slug}`;
|
||||
await db.insert(collection).values({
|
||||
id,
|
||||
slug,
|
||||
sortOrder: i + 1,
|
||||
nameEn,
|
||||
nameZh: nameEn,
|
||||
introEn,
|
||||
introZh,
|
||||
status: "published",
|
||||
});
|
||||
for (const [order, toolSlug] of toolSlugs.entries()) {
|
||||
await db.insert(collectionTool).values({
|
||||
collectionId: id,
|
||||
toolId: `tool_${toolSlug}`,
|
||||
sortOrder: order,
|
||||
}).onConflictDoNothing();
|
||||
}
|
||||
}
|
||||
|
||||
console.log("写入事件流(近 30 天)...");
|
||||
const events: { id: string; event: string; entityType: string | null; entityId: string | null; userId: string | null; sessionId: string | null; metadata: Record<string, unknown> | null; createdAt: Date }[] = [];
|
||||
for (const [i, toolId] of toolIds.entries()) {
|
||||
const popularity = Math.max(1, 40 - i);
|
||||
const views = Math.round(popularity * (4 + rand() * 8));
|
||||
for (let v = 0; v < views; v++) {
|
||||
events.push({
|
||||
id: nanoid(),
|
||||
event: "tool_view",
|
||||
entityType: "tool",
|
||||
entityId: toolId,
|
||||
userId: null,
|
||||
sessionId: nanoid(10),
|
||||
metadata: null,
|
||||
createdAt: daysAgo(randInt(0, 29)),
|
||||
});
|
||||
}
|
||||
const clicks = Math.round(views * (0.15 + rand() * 0.2));
|
||||
for (let v = 0; v < clicks; v++) {
|
||||
events.push({
|
||||
id: nanoid(),
|
||||
event: "tool_click",
|
||||
entityType: "tool",
|
||||
entityId: toolId,
|
||||
userId: null,
|
||||
sessionId: nanoid(10),
|
||||
metadata: null,
|
||||
createdAt: daysAgo(randInt(0, 29)),
|
||||
});
|
||||
}
|
||||
}
|
||||
const searchTerms = ["ai writing", "video generator", "code assistant", "logo maker", "free ai tools", "meeting notes", "seo", "chatbot", "voice cloning", "image upscaler"];
|
||||
for (let v = 0; v < 120; v++) {
|
||||
events.push({
|
||||
id: nanoid(),
|
||||
event: "search",
|
||||
entityType: null,
|
||||
entityId: null,
|
||||
userId: null,
|
||||
sessionId: nanoid(10),
|
||||
metadata: { query: pick(searchTerms) },
|
||||
createdAt: daysAgo(randInt(0, 29)),
|
||||
});
|
||||
}
|
||||
// 分批插入
|
||||
for (let i = 0; i < events.length; i += 500) {
|
||||
await db.insert(event).values(events.slice(i, i + 500));
|
||||
}
|
||||
// 同步 viewCount / clickCount
|
||||
await client`
|
||||
UPDATE tool t SET
|
||||
view_count = COALESCE(v.cnt, 0),
|
||||
click_count = COALESCE(c.cnt, 0)
|
||||
FROM (SELECT entity_id, COUNT(*) cnt FROM event WHERE event = 'tool_view' GROUP BY entity_id) v
|
||||
FULL OUTER JOIN (SELECT entity_id, COUNT(*) cnt FROM event WHERE event = 'tool_click' GROUP BY entity_id) c
|
||||
ON v.entity_id = c.entity_id
|
||||
WHERE t.id = COALESCE(v.entity_id, c.entity_id)`;
|
||||
|
||||
console.log("写入演示提交与认领...");
|
||||
await db.insert(submission).values({
|
||||
id: nanoid(),
|
||||
userId: "user_alice",
|
||||
name: "Demo New Tool",
|
||||
websiteUrl: "https://demonewtool.example.com",
|
||||
domain: "demonewtool.example.com",
|
||||
tagline: "An example submission waiting for review",
|
||||
description: "This is a sample submission in the admin queue so you can try the approve/reject flow immediately. Approving it will create a published tool listing.",
|
||||
pricing: "freemium",
|
||||
founderName: "Alice Chen",
|
||||
contactEmail: "alice@demo.dev",
|
||||
status: "pending",
|
||||
});
|
||||
await db.insert(claim).values({
|
||||
id: nanoid(),
|
||||
toolId: "tool_notion-ai",
|
||||
userId: "user_bob",
|
||||
role: "Product Manager",
|
||||
message: "I work at the company and can verify via corporate email. Happy to provide additional proof if needed.",
|
||||
proofUrl: "https://www.linkedin.com/in/example",
|
||||
status: "pending",
|
||||
});
|
||||
|
||||
const [{ cnt: toolCnt }] = await db.select({ cnt: sql<number>`count(*)` }).from(tool);
|
||||
const [{ cnt: voteCnt }] = await db.select({ cnt: sql<number>`count(*)` }).from(vote);
|
||||
const [{ cnt: reviewCnt }] = await db.select({ cnt: sql<number>`count(*)` }).from(review);
|
||||
const [{ cnt: eventCnt }] = await db.select({ cnt: sql<number>`count(*)` }).from(event);
|
||||
console.log(`\n种子完成:${Number(toolCnt)} 工具 / ${Number(voteCnt)} 投票 / ${Number(reviewCnt)} 评论 / ${Number(eventCnt)} 事件`);
|
||||
console.log("管理员:admin@aidirectory.dev / password123");
|
||||
await client.end();
|
||||
}
|
||||
|
||||
main().catch(async (error) => {
|
||||
console.error("种子失败:", error);
|
||||
await client.end();
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,580 @@
|
||||
#!/usr/bin/env tsx
|
||||
/**
|
||||
* AI 工具种子数据导入脚本
|
||||
*
|
||||
* 用法:
|
||||
* pnpm seed:tools
|
||||
*
|
||||
* 功能:
|
||||
* - 导入 10 个分类
|
||||
* - 导入 30 个标签
|
||||
* - 导入 50 个 AI 工具
|
||||
* - 建立工具-分类、工具-标签关联
|
||||
*/
|
||||
|
||||
import { drizzle } from "drizzle-orm/postgres-js";
|
||||
import postgres from "postgres";
|
||||
import dotenv from "dotenv";
|
||||
import { resolve } from "path";
|
||||
import { tool, category, tag, toolCategory, toolTag } from "../src/lib/db/schema";
|
||||
|
||||
// 加载环境变量
|
||||
dotenv.config({ path: resolve(process.cwd(), ".env.local") });
|
||||
|
||||
// ========== 种子数据 ==========
|
||||
|
||||
const categoriesData = [
|
||||
{ id: 'cat_chatbot', slug: 'chatbot', icon: '💬', sortOrder: 1, nameEn: 'AI Chatbots', descriptionEn: 'Conversational AI assistants and chatbots', nameZh: 'AI 聊天机器人', descriptionZh: '对话式AI助手和聊天机器人' },
|
||||
{ id: 'cat_writing', slug: 'writing', icon: '✍️', sortOrder: 2, nameEn: 'Writing & Content', descriptionEn: 'AI-powered writing assistants and content generation tools', nameZh: 'AI 写作', descriptionZh: 'AI驱动的写作助手和内容生成工具' },
|
||||
{ id: 'cat_image', slug: 'image-generation', icon: '🎨', sortOrder: 3, nameEn: 'Image Generation', descriptionEn: 'AI tools for creating and editing images', nameZh: 'AI 图像生成', descriptionZh: 'AI图像创作和编辑工具' },
|
||||
{ id: 'cat_video', slug: 'video', icon: '🎬', sortOrder: 4, nameEn: 'Video & Animation', descriptionEn: 'AI video generation, editing and animation tools', nameZh: 'AI 视频', descriptionZh: 'AI视频生成、编辑和动画工具' },
|
||||
{ id: 'cat_audio', slug: 'audio-music', icon: '🎵', sortOrder: 5, nameEn: 'Audio & Music', descriptionEn: 'AI music generation, voice synthesis and audio tools', nameZh: 'AI 音频与音乐', descriptionZh: 'AI音乐生成、语音合成和音频工具' },
|
||||
{ id: 'cat_coding', slug: 'coding', icon: '💻', sortOrder: 6, nameEn: 'Coding & Development', descriptionEn: 'AI coding assistants and developer tools', nameZh: 'AI 编程', descriptionZh: 'AI编程助手和开发者工具' },
|
||||
{ id: 'cat_productivity', slug: 'productivity', icon: '⚡', sortOrder: 7, nameEn: 'Productivity', descriptionEn: 'AI tools to boost your work efficiency', nameZh: 'AI 效率工具', descriptionZh: '提升工作效率的AI工具' },
|
||||
{ id: 'cat_research', slug: 'research', icon: '🔬', sortOrder: 8, nameEn: 'Research & Analysis', descriptionEn: 'AI research assistants and data analysis tools', nameZh: 'AI 研究与分析', descriptionZh: 'AI研究助手和数据分析工具' },
|
||||
{ id: 'cat_design', slug: 'design', icon: '🎯', sortOrder: 9, nameEn: 'Design & Creative', descriptionEn: 'AI tools for designers and creative professionals', nameZh: 'AI 设计', descriptionZh: '面向设计师和创意专业人士的AI工具' },
|
||||
{ id: 'cat_business', slug: 'business', icon: '📊', sortOrder: 10, nameEn: 'Business & Marketing', descriptionEn: 'AI tools for business operations and marketing', nameZh: 'AI 商业与营销', descriptionZh: '商业运营和营销AI工具' },
|
||||
];
|
||||
|
||||
const tagsData = [
|
||||
// 功能标签
|
||||
{ id: 'tag_free', slug: 'free', nameEn: 'Free', nameZh: '免费' },
|
||||
{ id: 'tag_freemium', slug: 'freemium', nameEn: 'Freemium', nameZh: '免费增值' },
|
||||
{ id: 'tag_paid', slug: 'paid', nameEn: 'Paid', nameZh: '付费' },
|
||||
{ id: 'tag_open_source', slug: 'open-source', nameEn: 'Open Source', nameZh: '开源' },
|
||||
{ id: 'tag_api', slug: 'api-available', nameEn: 'API Available', nameZh: '提供API' },
|
||||
// 技术标签
|
||||
{ id: 'tag_gpt', slug: 'gpt-powered', nameEn: 'GPT Powered', nameZh: 'GPT驱动' },
|
||||
{ id: 'tag_claude', slug: 'claude-powered', nameEn: 'Claude Powered', nameZh: 'Claude驱动' },
|
||||
{ id: 'tag_diffusion', slug: 'diffusion-model', nameEn: 'Diffusion Model', nameZh: '扩散模型' },
|
||||
{ id: 'tag_llm', slug: 'llm', nameEn: 'Large Language Model', nameZh: '大语言模型' },
|
||||
// 用途标签
|
||||
{ id: 'tag_beginner', slug: 'beginner-friendly', nameEn: 'Beginner Friendly', nameZh: '新手友好' },
|
||||
{ id: 'tag_enterprise', slug: 'enterprise', nameEn: 'Enterprise', nameZh: '企业级' },
|
||||
{ id: 'tag_creator', slug: 'for-creators', nameEn: 'For Creators', nameZh: '创作者适用' },
|
||||
{ id: 'tag_developer', slug: 'for-developers', nameEn: 'For Developers', nameZh: '开发者适用' },
|
||||
{ id: 'tag_marketer', slug: 'for-marketers', nameEn: 'For Marketers', nameZh: '营销人员适用' },
|
||||
{ id: 'tag_student', slug: 'for-students', nameEn: 'For Students', nameZh: '学生适用' },
|
||||
// 特性标签
|
||||
{ id: 'tag_realtime', slug: 'real-time', nameEn: 'Real-time', nameZh: '实时' },
|
||||
{ id: 'tag_collaborative', slug: 'collaborative', nameEn: 'Collaborative', nameZh: '协作' },
|
||||
{ id: 'tag_mobile', slug: 'mobile-app', nameEn: 'Mobile App', nameZh: '移动应用' },
|
||||
{ id: 'tag_browser', slug: 'browser-extension', nameEn: 'Browser Extension', nameZh: '浏览器插件' },
|
||||
{ id: 'tag_desktop', slug: 'desktop-app', nameEn: 'Desktop App', nameZh: '桌面应用' },
|
||||
// 输出类型标签
|
||||
{ id: 'tag_text', slug: 'text-output', nameEn: 'Text Output', nameZh: '文本输出' },
|
||||
{ id: 'tag_image_out', slug: 'image-output', nameEn: 'Image Output', nameZh: '图像输出' },
|
||||
{ id: 'tag_video_out', slug: 'video-output', nameEn: 'Video Output', nameZh: '视频输出' },
|
||||
{ id: 'tag_audio_out', slug: 'audio-output', nameEn: 'Audio Output', nameZh: '音频输出' },
|
||||
{ id: 'tag_code_out', slug: 'code-output', nameEn: 'Code Output', nameZh: '代码输出' },
|
||||
// 热门标签
|
||||
{ id: 'tag_trending', slug: 'trending', nameEn: 'Trending', nameZh: '热门' },
|
||||
{ id: 'tag_new', slug: 'new', nameEn: 'New', nameZh: '新上线' },
|
||||
{ id: 'tag_popular', slug: 'popular', nameEn: 'Popular', nameZh: '受欢迎' },
|
||||
{ id: 'tag_editors_choice', slug: 'editors-choice', nameEn: "Editor's Choice", nameZh: '编辑推荐' },
|
||||
{ id: 'tag_best_2025', slug: 'best-of-2025', nameEn: 'Best of 2025', nameZh: '2025精选' },
|
||||
];
|
||||
|
||||
const toolsData = [
|
||||
// AI 聊天机器人 (8个)
|
||||
{ id: 'tool_chatgpt', slug: 'chatgpt', domain: 'chat.openai.com', websiteUrl: 'https://chat.openai.com', logoUrl: 'https://chat.openai.com/favicon.ico', nameEn: 'ChatGPT', descriptionEn: "OpenAI's advanced conversational AI assistant powered by GPT-4. Capable of natural conversations, code generation, creative writing, and complex reasoning.", nameZh: 'ChatGPT', descriptionZh: 'OpenAI推出的先进对话式AI助手,由GPT-4驱动。能够进行自然对话、代码生成、创意写作和复杂推理。', status: 'published' },
|
||||
{ id: 'tool_claude', slug: 'claude', domain: 'claude.ai', websiteUrl: 'https://claude.ai', logoUrl: 'https://claude.ai/favicon.ico', nameEn: 'Claude', descriptionEn: "Anthropic's helpful, harmless, and honest AI assistant. Known for nuanced understanding, long context windows, and thoughtful responses.", nameZh: 'Claude', descriptionZh: 'Anthropic推出的有用、无害、诚实的AI助手。以细腻的理解、长上下文窗口和深思熟虑的回复著称。', status: 'published' },
|
||||
{ id: 'tool_gemini', slug: 'gemini', domain: 'gemini.google.com', websiteUrl: 'https://gemini.google.com', logoUrl: 'https://gemini.google.com/favicon.ico', nameEn: 'Google Gemini', descriptionEn: "Google's multimodal AI assistant that can understand text, images, audio, and video. Integrated with Google services.", nameZh: 'Google Gemini', descriptionZh: 'Google的多模态AI助手,能够理解文本、图像、音频和视频。与Google服务深度集成。', status: 'published' },
|
||||
{ id: 'tool_perplexity', slug: 'perplexity', domain: 'perplexity.ai', websiteUrl: 'https://perplexity.ai', logoUrl: 'https://perplexity.ai/favicon.ico', nameEn: 'Perplexity AI', descriptionEn: 'AI-powered search engine that provides direct answers with cited sources. Combines web search with language model capabilities.', nameZh: 'Perplexity AI', descriptionZh: 'AI驱动的搜索引擎,提供带引用来源的直接答案。结合网络搜索与语言模型能力。', status: 'published' },
|
||||
{ id: 'tool_poe', slug: 'poe', domain: 'poe.com', websiteUrl: 'https://poe.com', logoUrl: 'https://poe.com/favicon.ico', nameEn: 'Poe', descriptionEn: 'Platform by Quora offering access to multiple AI models including GPT-4, Claude, and custom bots in one interface.', nameZh: 'Poe', descriptionZh: 'Quora推出的平台,在一个界面中提供多个AI模型访问,包括GPT-4、Claude和自定义机器人。', status: 'published' },
|
||||
{ id: 'tool_character', slug: 'character-ai', domain: 'character.ai', websiteUrl: 'https://character.ai', logoUrl: 'https://character.ai/favicon.ico', nameEn: 'Character.AI', descriptionEn: 'Create and chat with AI characters. Popular for roleplay, creative storytelling, and entertainment purposes.', nameZh: 'Character.AI', descriptionZh: '创建AI角色并与之聊天。在角色扮演、创意故事讲述和娱乐方面广受欢迎。', status: 'published' },
|
||||
{ id: 'tool_coze', slug: 'coze', domain: 'coze.com', websiteUrl: 'https://coze.com', logoUrl: 'https://coze.com/favicon.ico', nameEn: 'Coze', descriptionEn: "ByteDance's AI chatbot platform for creating custom AI bots with plugins, workflows, and knowledge bases.", nameZh: 'Coze', descriptionZh: '字节跳动的AI聊天机器人平台,可创建带插件、工作流和知识库的自定义AI机器人。', status: 'published' },
|
||||
{ id: 'tool_pi', slug: 'pi-ai', domain: 'pi.ai', websiteUrl: 'https://pi.ai', logoUrl: 'https://pi.ai/favicon.ico', nameEn: 'Pi', descriptionEn: 'Personal AI assistant by Inflection AI, designed for supportive conversations and emotional intelligence.', nameZh: 'Pi', descriptionZh: 'Inflection AI的个人AI助手,专为支持性对话和情商设计。', status: 'published' },
|
||||
|
||||
// AI 写作 (7个)
|
||||
{ id: 'tool_jasper', slug: 'jasper', domain: 'jasper.ai', websiteUrl: 'https://jasper.ai', logoUrl: 'https://jasper.ai/favicon.ico', nameEn: 'Jasper', descriptionEn: 'AI content creation platform for marketing teams. Generate blog posts, ads, emails, and social media content at scale.', nameZh: 'Jasper', descriptionZh: '面向营销团队的AI内容创作平台。大规模生成博客文章、广告、邮件和社交媒体内容。', status: 'published' },
|
||||
{ id: 'tool_copy_ai', slug: 'copy-ai', domain: 'copy.ai', websiteUrl: 'https://copy.ai', logoUrl: 'https://copy.ai/favicon.ico', nameEn: 'Copy.ai', descriptionEn: 'AI-powered copywriting tool for creating marketing copy, product descriptions, and sales content.', nameZh: 'Copy.ai', descriptionZh: 'AI驱动的文案工具,用于创建营销文案、产品描述和销售内容。', status: 'published' },
|
||||
{ id: 'tool_writesonic', slug: 'writesonic', domain: 'writesonic.com', websiteUrl: 'https://writesonic.com', logoUrl: 'https://writesonic.com/favicon.ico', nameEn: 'Writesonic', descriptionEn: 'AI writer for creating SEO-optimized articles, blog posts, and marketing content with fact-checking capabilities.', nameZh: 'Writesonic', descriptionZh: 'AI写作工具,用于创建SEO优化的文章、博客和营销内容,具有事实核查功能。', status: 'published' },
|
||||
{ id: 'tool_notion_ai', slug: 'notion-ai', domain: 'notion.so', websiteUrl: 'https://notion.so', logoUrl: 'https://notion.so/favicon.ico', nameEn: 'Notion AI', descriptionEn: 'AI writing assistant integrated into Notion. Helps with drafting, summarizing, brainstorming, and improving writing.', nameZh: 'Notion AI', descriptionZh: '集成在Notion中的AI写作助手。帮助起草、总结、头脑风暴和改进写作。', status: 'published' },
|
||||
{ id: 'tool_grammarly', slug: 'grammarly', domain: 'grammarly.com', websiteUrl: 'https://grammarly.com', logoUrl: 'https://grammarly.com/favicon.ico', nameEn: 'Grammarly', descriptionEn: 'AI-powered writing assistant that checks grammar, spelling, punctuation, and offers style improvements.', nameZh: 'Grammarly', descriptionZh: 'AI驱动的写作助手,检查语法、拼写、标点符号,并提供风格改进建议。', status: 'published' },
|
||||
{ id: 'tool_quillbot', slug: 'quillbot', domain: 'quillbot.com', websiteUrl: 'https://quillbot.com', logoUrl: 'https://quillbot.com/favicon.ico', nameEn: 'QuillBot', descriptionEn: 'AI paraphrasing and writing tool. Helps rephrase sentences, summarize text, and improve writing clarity.', nameZh: 'QuillBot', descriptionZh: 'AI改写和写作工具。帮助改述句子、总结文本和提高写作清晰度。', status: 'published' },
|
||||
{ id: 'tool_rytr', slug: 'rytr', domain: 'rytr.me', websiteUrl: 'https://rytr.me', logoUrl: 'https://rytr.me/favicon.ico', nameEn: 'Rytr', descriptionEn: 'AI writing assistant for generating content in 30+ languages and 20+ tones. Affordable alternative for content creators.', nameZh: 'Rytr', descriptionZh: 'AI写作助手,支持30多种语言和20多种语气生成内容。创作者的经济实惠选择。', status: 'published' },
|
||||
|
||||
// AI 图像生成 (8个)
|
||||
{ id: 'tool_midjourney', slug: 'midjourney', domain: 'midjourney.com', websiteUrl: 'https://midjourney.com', logoUrl: 'https://midjourney.com/favicon.ico', nameEn: 'Midjourney', descriptionEn: 'Leading AI art generator known for stunning, artistic image generation. Creates images from text prompts via Discord.', nameZh: 'Midjourney', descriptionZh: '领先的AI艺术生成器,以惊艳的艺术图像生成著称。通过Discord从文本提示创建图像。', status: 'published' },
|
||||
{ id: 'tool_dalle', slug: 'dall-e', domain: 'openai.com/dall-e', websiteUrl: 'https://openai.com/dall-e', logoUrl: 'https://openai.com/favicon.ico', nameEn: 'DALL·E 3', descriptionEn: "OpenAI's latest image generation model with exceptional prompt understanding and photorealistic outputs.", nameZh: 'DALL·E 3', descriptionZh: 'OpenAI最新的图像生成模型,具有出色的提示理解能力和逼真的输出效果。', status: 'published' },
|
||||
{ id: 'tool_stable_diffusion', slug: 'stable-diffusion', domain: 'stability.ai', websiteUrl: 'https://stability.ai', logoUrl: 'https://stability.ai/favicon.ico', nameEn: 'Stable Diffusion', descriptionEn: 'Open-source image generation model by Stability AI. Can be run locally with full customization.', nameZh: 'Stable Diffusion', descriptionZh: 'Stability AI的开源图像生成模型。可在本地运行并完全自定义。', status: 'published' },
|
||||
{ id: 'tool_leonardo', slug: 'leonardo-ai', domain: 'leonardo.ai', websiteUrl: 'https://leonardo.ai', logoUrl: 'https://leonardo.ai/favicon.ico', nameEn: 'Leonardo AI', descriptionEn: 'AI image generator with fine-tuned models for game assets, concept art, and consistent character generation.', nameZh: 'Leonardo AI', descriptionZh: 'AI图像生成器,提供针对游戏资产、概念艺术和一致性角色生成的微调模型。', status: 'published' },
|
||||
{ id: 'tool_ideogram', slug: 'ideogram', domain: 'ideogram.ai', websiteUrl: 'https://ideogram.ai', logoUrl: 'https://ideogram.ai/favicon.ico', nameEn: 'Ideogram', descriptionEn: 'AI image generator excelling at rendering text within images accurately. Great for logos and posters.', nameZh: 'Ideogram', descriptionZh: 'AI图像生成器,擅长准确渲染图像中的文字。适合制作Logo和海报。', status: 'published' },
|
||||
{ id: 'tool_playground', slug: 'playground-ai', domain: 'playground.com', websiteUrl: 'https://playground.com', logoUrl: 'https://playground.com/favicon.ico', nameEn: 'Playground AI', descriptionEn: 'Free AI image generator with powerful editing features. Great for beginners with intuitive interface.', nameZh: 'Playground AI', descriptionZh: '免费AI图像生成器,具有强大的编辑功能。界面直观,适合新手。', status: 'published' },
|
||||
{ id: 'tool_canva_ai', slug: 'canva-ai', domain: 'canva.com', websiteUrl: 'https://canva.com', logoUrl: 'https://canva.com/favicon.ico', nameEn: 'Canva AI', descriptionEn: 'AI-powered design features within Canva including Magic Design, text-to-image, and background removal.', nameZh: 'Canva AI', descriptionZh: 'Canva中的AI设计功能,包括Magic Design、文生图和背景移除。', status: 'published' },
|
||||
{ id: 'tool_adobe_firefly', slug: 'adobe-firefly', domain: 'firefly.adobe.com', websiteUrl: 'https://firefly.adobe.com', logoUrl: 'https://firefly.adobe.com/favicon.ico', nameEn: 'Adobe Firefly', descriptionEn: "Adobe's generative AI for creative professionals. Trained on licensed content for commercial safety.", nameZh: 'Adobe Firefly', descriptionZh: 'Adobe面向创意专业人士的生成式AI。在授权内容上训练,商用安全。', status: 'published' },
|
||||
|
||||
// AI 视频 (6个)
|
||||
{ id: 'tool_runway', slug: 'runway', domain: 'runway.com', websiteUrl: 'https://runway.com', logoUrl: 'https://runway.com/favicon.ico', nameEn: 'Runway', descriptionEn: 'AI video generation and editing platform. Gen-2 creates videos from text or images with cinematic quality.', nameZh: 'Runway', descriptionZh: 'AI视频生成和编辑平台。Gen-2可从文本或图像创建电影级质量的视频。', status: 'published' },
|
||||
{ id: 'tool_pika', slug: 'pika', domain: 'pika.art', websiteUrl: 'https://pika.art', logoUrl: 'https://pika.art/favicon.ico', nameEn: 'Pika', descriptionEn: 'AI video generator that creates and edits videos from text prompts. Known for smooth animations and effects.', nameZh: 'Pika', descriptionZh: 'AI视频生成器,从文本提示创建和编辑视频。以流畅的动画和效果著称。', status: 'published' },
|
||||
{ id: 'tool_heygen', slug: 'heygen', domain: 'heygen.com', websiteUrl: 'https://heygen.com', logoUrl: 'https://heygen.com/favicon.ico', nameEn: 'HeyGen', descriptionEn: 'AI video generator for creating professional videos with AI avatars. Perfect for marketing and training content.', nameZh: 'HeyGen', descriptionZh: 'AI视频生成器,用AI数字人创建专业视频。适合营销和培训内容。', status: 'published' },
|
||||
{ id: 'tool_synthesia', slug: 'synthesia', domain: 'synthesia.io', websiteUrl: 'https://synthesia.io', logoUrl: 'https://synthesia.io/favicon.ico', nameEn: 'Synthesia', descriptionEn: 'Create AI videos with realistic avatars in 120+ languages. Popular for corporate training and marketing.', nameZh: 'Synthesia', descriptionZh: '用逼真数字人创建120多种语言的AI视频。在企业培训和营销领域广受欢迎。', status: 'published' },
|
||||
{ id: 'tool_luma', slug: 'luma-ai', domain: 'lumalabs.ai', websiteUrl: 'https://lumalabs.ai', logoUrl: 'https://lumalabs.ai/favicon.ico', nameEn: 'Luma AI', descriptionEn: 'AI video generation with Dream Machine. Creates cinematic videos from text and images with impressive quality.', nameZh: 'Luma AI', descriptionZh: 'Dream Machine AI视频生成。从文本和图像创建高质量的电影级视频。', status: 'published' },
|
||||
{ id: 'tool_kling', slug: 'kling-ai', domain: 'klingai.com', websiteUrl: 'https://klingai.com', logoUrl: 'https://klingai.com/favicon.ico', nameEn: 'Kling AI', descriptionEn: "Kuaishou's AI video generator with impressive motion and physics understanding. Creates realistic videos.", nameZh: 'Kling AI', descriptionZh: '快手的AI视频生成器,具有出色的运动和物理理解能力。创建逼真视频。', status: 'published' },
|
||||
|
||||
// AI 音频与音乐 (5个)
|
||||
{ id: 'tool_elevenlabs', slug: 'elevenlabs', domain: 'elevenlabs.io', websiteUrl: 'https://elevenlabs.io', logoUrl: 'https://elevenlabs.io/favicon.ico', nameEn: 'ElevenLabs', descriptionEn: 'Leading AI voice synthesis platform. Creates ultra-realistic voiceovers, dubbing, and voice cloning.', nameZh: 'ElevenLabs', descriptionZh: '领先的AI语音合成平台。创建超逼真的配音、翻译配音和声音克隆。', status: 'published' },
|
||||
{ id: 'tool_suno', slug: 'suno', domain: 'suno.com', websiteUrl: 'https://suno.com', logoUrl: 'https://suno.com/favicon.ico', nameEn: 'Suno', descriptionEn: 'AI music generator that creates complete songs with vocals from text prompts. Revolutionary for music creation.', nameZh: 'Suno', descriptionZh: 'AI音乐生成器,从文本提示创建带人声的完整歌曲。音乐创作的革命性工具。', status: 'published' },
|
||||
{ id: 'tool_udio', slug: 'udio', domain: 'udio.com', websiteUrl: 'https://udio.com', logoUrl: 'https://udio.com/favicon.ico', nameEn: 'Udio', descriptionEn: 'AI music generation platform creating studio-quality songs with vocals. Strong competitor to Suno.', nameZh: 'Udio', descriptionZh: 'AI音乐生成平台,创建带人声的录音室品质歌曲。Suno的强劲竞争对手。', status: 'published' },
|
||||
{ id: 'tool_murf', slug: 'murf', domain: 'murf.ai', websiteUrl: 'https://murf.ai', logoUrl: 'https://murf.ai/favicon.ico', nameEn: 'Murf', descriptionEn: 'AI voice generator for creating professional voiceovers for videos, podcasts, and presentations.', nameZh: 'Murf', descriptionZh: 'AI语音生成器,为视频、播客和演示文稿创建专业配音。', status: 'published' },
|
||||
{ id: 'tool_descript', slug: 'descript', domain: 'descript.com', websiteUrl: 'https://descript.com', logoUrl: 'https://descript.com/favicon.ico', nameEn: 'Descript', descriptionEn: 'AI-powered audio and video editor. Edit audio by editing text transcripts. Includes Overdub voice cloning.', nameZh: 'Descript', descriptionZh: 'AI驱动的音视频编辑器。通过编辑文字稿来编辑音频。包含Overdub声音克隆功能。', status: 'published' },
|
||||
|
||||
// AI 编程 (6个)
|
||||
{ id: 'tool_github_copilot', slug: 'github-copilot', domain: 'github.com/features/copilot', websiteUrl: 'https://github.com/features/copilot', logoUrl: 'https://github.com/favicon.ico', nameEn: 'GitHub Copilot', descriptionEn: 'AI pair programmer by GitHub and OpenAI. Suggests code completions, functions, and entire files in your IDE.', nameZh: 'GitHub Copilot', descriptionZh: 'GitHub和OpenAI的AI结对编程助手。在IDE中建议代码补全、函数和完整文件。', status: 'published' },
|
||||
{ id: 'tool_cursor', slug: 'cursor', domain: 'cursor.sh', websiteUrl: 'https://cursor.sh', logoUrl: 'https://cursor.sh/favicon.ico', nameEn: 'Cursor', descriptionEn: 'AI-first code editor built on VS Code. Deep AI integration for code generation, editing, and chat.', nameZh: 'Cursor', descriptionZh: '以AI为核心的代码编辑器,基于VS Code构建。深度AI集成,支持代码生成、编辑和对话。', status: 'published' },
|
||||
{ id: 'tool_replit', slug: 'replit', domain: 'replit.com', websiteUrl: 'https://replit.com', logoUrl: 'https://replit.com/favicon.ico', nameEn: 'Replit AI', descriptionEn: 'Browser-based IDE with AI assistant. Code, deploy, and collaborate with AI-powered code generation.', nameZh: 'Replit AI', descriptionZh: '基于浏览器的IDE,内置AI助手。通过AI驱动的代码生成进行编码、部署和协作。', status: 'published' },
|
||||
{ id: 'tool_codeium', slug: 'codeium', domain: 'codeium.com', websiteUrl: 'https://codeium.com', logoUrl: 'https://codeium.com/favicon.ico', nameEn: 'Codeium', descriptionEn: 'Free AI coding assistant supporting 70+ languages. Fast completions with IDE extensions for major editors.', nameZh: 'Codeium', descriptionZh: '免费AI编程助手,支持70多种语言。快速补全,为主流编辑器提供IDE插件。', status: 'published' },
|
||||
{ id: 'tool_tabnine', slug: 'tabnine', domain: 'tabnine.com', websiteUrl: 'https://tabnine.com', logoUrl: 'https://tabnine.com/favicon.ico', nameEn: 'Tabnine', descriptionEn: 'AI code assistant with enterprise-grade privacy. Train on your codebase for personalized suggestions.', nameZh: 'Tabnine', descriptionZh: '企业级隐私的AI代码助手。在你的代码库上训练,提供个性化建议。', status: 'published' },
|
||||
{ id: 'tool_claude_code', slug: 'claude-code', domain: 'claude.ai/code', websiteUrl: 'https://claude.ai/code', logoUrl: 'https://claude.ai/favicon.ico', nameEn: 'Claude Code', descriptionEn: "Anthropic's agentic coding tool. Natural language coding with full project context understanding.", nameZh: 'Claude Code', descriptionZh: 'Anthropic的代理式编程工具。支持自然语言编程,具有完整的项目上下文理解能力。', status: 'published' },
|
||||
|
||||
// AI 效率工具 (4个)
|
||||
{ id: 'tool_otter', slug: 'otter-ai', domain: 'otter.ai', websiteUrl: 'https://otter.ai', logoUrl: 'https://otter.ai/favicon.ico', nameEn: 'Otter.ai', descriptionEn: 'AI meeting assistant that transcribes, summarizes, and creates action items from meetings automatically.', nameZh: 'Otter.ai', descriptionZh: 'AI会议助手,自动转录、总结会议并创建待办事项。', status: 'published' },
|
||||
{ id: 'tool_fireflies', slug: 'fireflies', domain: 'fireflies.ai', websiteUrl: 'https://fireflies.ai', logoUrl: 'https://fireflies.ai/favicon.ico', nameEn: 'Fireflies.ai', descriptionEn: 'AI notetaker for meetings. Records, transcribes, and summarizes meetings across all major platforms.', nameZh: 'Fireflies.ai', descriptionZh: '会议AI笔记助手。在所有主流平台上录制、转录和总结会议。', status: 'published' },
|
||||
{ id: 'tool_mem', slug: 'mem-ai', domain: 'mem.ai', websiteUrl: 'https://mem.ai', logoUrl: 'https://mem.ai/favicon.ico', nameEn: 'Mem', descriptionEn: 'AI-powered note-taking app that organizes and surfaces relevant information automatically.', nameZh: 'Mem', descriptionZh: 'AI驱动的笔记应用,自动组织和呈现相关信息。', status: 'published' },
|
||||
{ id: 'tool_motion', slug: 'motion', domain: 'usemotion.com', websiteUrl: 'https://usemotion.com', logoUrl: 'https://usemotion.com/favicon.ico', nameEn: 'Motion', descriptionEn: 'AI calendar and project management. Automatically schedules tasks and meetings for optimal productivity.', nameZh: 'Motion', descriptionZh: 'AI日历和项目管理工具。自动安排任务和会议以实现最佳生产力。', status: 'published' },
|
||||
|
||||
// AI 研究与分析 (3个)
|
||||
{ id: 'tool_consensus', slug: 'consensus', domain: 'consensus.app', websiteUrl: 'https://consensus.app', logoUrl: 'https://consensus.app/favicon.ico', nameEn: 'Consensus', descriptionEn: 'AI-powered academic search engine. Find and understand scientific research with AI summaries.', nameZh: 'Consensus', descriptionZh: 'AI驱动的学术搜索引擎。通过AI摘要查找和理解科学研究。', status: 'published' },
|
||||
{ id: 'tool_elicit', slug: 'elicit', domain: 'elicit.com', websiteUrl: 'https://elicit.com', logoUrl: 'https://elicit.com/favicon.ico', nameEn: 'Elicit', descriptionEn: 'AI research assistant that helps find, summarize, and extract data from academic papers.', nameZh: 'Elicit', descriptionZh: 'AI研究助手,帮助查找、总结和提取学术论文中的数据。', status: 'published' },
|
||||
{ id: 'tool_scite', slug: 'scite', domain: 'scite.ai', websiteUrl: 'https://scite.ai', logoUrl: 'https://scite.ai/favicon.ico', nameEn: 'Scite', descriptionEn: 'AI tool for evaluating scientific claims. Shows how papers cite each other with supporting or contrasting evidence.', nameZh: 'Scite', descriptionZh: 'AI科学声明评估工具。显示论文如何相互引用,包含支持或对比证据。', status: 'published' },
|
||||
|
||||
// AI 设计 (3个)
|
||||
{ id: 'tool_figma_ai', slug: 'figma-ai', domain: 'figma.com', websiteUrl: 'https://figma.com', logoUrl: 'https://figma.com/favicon.ico', nameEn: 'Figma AI', descriptionEn: 'AI features in Figma for generating designs, renaming layers, and creating content automatically.', nameZh: 'Figma AI', descriptionZh: 'Figma中的AI功能,用于生成设计、重命名图层和自动创建内容。', status: 'published' },
|
||||
{ id: 'tool_uizard', slug: 'uizard', domain: 'uizard.io', websiteUrl: 'https://uizard.io', logoUrl: 'https://uizard.io/favicon.ico', nameEn: 'Uizard', descriptionEn: 'AI-powered UI design tool. Transform sketches and screenshots into editable designs.', nameZh: 'Uizard', descriptionZh: 'AI驱动的UI设计工具。将草图和截图转换为可编辑的设计。', status: 'published' },
|
||||
{ id: 'tool_magician', slug: 'magician', domain: 'magician.design', websiteUrl: 'https://magician.design', logoUrl: 'https://magician.design/favicon.ico', nameEn: 'Magician', descriptionEn: 'AI design assistant plugin for Figma. Generate icons, images, and copy with AI.', nameZh: 'Magician', descriptionZh: 'Figma的AI设计助手插件。用AI生成图标、图像和文案。', status: 'published' },
|
||||
];
|
||||
|
||||
// 工具-分类关联数据
|
||||
const toolCategoryData = [
|
||||
// 聊天机器人
|
||||
{ toolId: 'tool_chatgpt', categoryId: 'cat_chatbot' },
|
||||
{ toolId: 'tool_claude', categoryId: 'cat_chatbot' },
|
||||
{ toolId: 'tool_gemini', categoryId: 'cat_chatbot' },
|
||||
{ toolId: 'tool_perplexity', categoryId: 'cat_chatbot' },
|
||||
{ toolId: 'tool_perplexity', categoryId: 'cat_research' },
|
||||
{ toolId: 'tool_poe', categoryId: 'cat_chatbot' },
|
||||
{ toolId: 'tool_character', categoryId: 'cat_chatbot' },
|
||||
{ toolId: 'tool_coze', categoryId: 'cat_chatbot' },
|
||||
{ toolId: 'tool_pi', categoryId: 'cat_chatbot' },
|
||||
// 写作
|
||||
{ toolId: 'tool_jasper', categoryId: 'cat_writing' },
|
||||
{ toolId: 'tool_jasper', categoryId: 'cat_business' },
|
||||
{ toolId: 'tool_copy_ai', categoryId: 'cat_writing' },
|
||||
{ toolId: 'tool_copy_ai', categoryId: 'cat_business' },
|
||||
{ toolId: 'tool_writesonic', categoryId: 'cat_writing' },
|
||||
{ toolId: 'tool_notion_ai', categoryId: 'cat_writing' },
|
||||
{ toolId: 'tool_notion_ai', categoryId: 'cat_productivity' },
|
||||
{ toolId: 'tool_grammarly', categoryId: 'cat_writing' },
|
||||
{ toolId: 'tool_quillbot', categoryId: 'cat_writing' },
|
||||
{ toolId: 'tool_rytr', categoryId: 'cat_writing' },
|
||||
// 图像生成
|
||||
{ toolId: 'tool_midjourney', categoryId: 'cat_image' },
|
||||
{ toolId: 'tool_dalle', categoryId: 'cat_image' },
|
||||
{ toolId: 'tool_stable_diffusion', categoryId: 'cat_image' },
|
||||
{ toolId: 'tool_leonardo', categoryId: 'cat_image' },
|
||||
{ toolId: 'tool_ideogram', categoryId: 'cat_image' },
|
||||
{ toolId: 'tool_playground', categoryId: 'cat_image' },
|
||||
{ toolId: 'tool_canva_ai', categoryId: 'cat_image' },
|
||||
{ toolId: 'tool_canva_ai', categoryId: 'cat_design' },
|
||||
{ toolId: 'tool_adobe_firefly', categoryId: 'cat_image' },
|
||||
{ toolId: 'tool_adobe_firefly', categoryId: 'cat_design' },
|
||||
// 视频
|
||||
{ toolId: 'tool_runway', categoryId: 'cat_video' },
|
||||
{ toolId: 'tool_pika', categoryId: 'cat_video' },
|
||||
{ toolId: 'tool_heygen', categoryId: 'cat_video' },
|
||||
{ toolId: 'tool_heygen', categoryId: 'cat_business' },
|
||||
{ toolId: 'tool_synthesia', categoryId: 'cat_video' },
|
||||
{ toolId: 'tool_synthesia', categoryId: 'cat_business' },
|
||||
{ toolId: 'tool_luma', categoryId: 'cat_video' },
|
||||
{ toolId: 'tool_kling', categoryId: 'cat_video' },
|
||||
// 音频
|
||||
{ toolId: 'tool_elevenlabs', categoryId: 'cat_audio' },
|
||||
{ toolId: 'tool_suno', categoryId: 'cat_audio' },
|
||||
{ toolId: 'tool_udio', categoryId: 'cat_audio' },
|
||||
{ toolId: 'tool_murf', categoryId: 'cat_audio' },
|
||||
{ toolId: 'tool_descript', categoryId: 'cat_audio' },
|
||||
{ toolId: 'tool_descript', categoryId: 'cat_video' },
|
||||
// 编程
|
||||
{ toolId: 'tool_github_copilot', categoryId: 'cat_coding' },
|
||||
{ toolId: 'tool_cursor', categoryId: 'cat_coding' },
|
||||
{ toolId: 'tool_replit', categoryId: 'cat_coding' },
|
||||
{ toolId: 'tool_codeium', categoryId: 'cat_coding' },
|
||||
{ toolId: 'tool_tabnine', categoryId: 'cat_coding' },
|
||||
{ toolId: 'tool_claude_code', categoryId: 'cat_coding' },
|
||||
// 效率
|
||||
{ toolId: 'tool_otter', categoryId: 'cat_productivity' },
|
||||
{ toolId: 'tool_fireflies', categoryId: 'cat_productivity' },
|
||||
{ toolId: 'tool_mem', categoryId: 'cat_productivity' },
|
||||
{ toolId: 'tool_motion', categoryId: 'cat_productivity' },
|
||||
// 研究
|
||||
{ toolId: 'tool_consensus', categoryId: 'cat_research' },
|
||||
{ toolId: 'tool_elicit', categoryId: 'cat_research' },
|
||||
{ toolId: 'tool_scite', categoryId: 'cat_research' },
|
||||
// 设计
|
||||
{ toolId: 'tool_figma_ai', categoryId: 'cat_design' },
|
||||
{ toolId: 'tool_uizard', categoryId: 'cat_design' },
|
||||
{ toolId: 'tool_magician', categoryId: 'cat_design' },
|
||||
];
|
||||
|
||||
// 工具-标签关联数据
|
||||
const toolTagData = [
|
||||
// ChatGPT
|
||||
{ toolId: 'tool_chatgpt', tagId: 'tag_freemium' },
|
||||
{ toolId: 'tool_chatgpt', tagId: 'tag_gpt' },
|
||||
{ toolId: 'tool_chatgpt', tagId: 'tag_llm' },
|
||||
{ toolId: 'tool_chatgpt', tagId: 'tag_api' },
|
||||
{ toolId: 'tool_chatgpt', tagId: 'tag_popular' },
|
||||
{ toolId: 'tool_chatgpt', tagId: 'tag_mobile' },
|
||||
{ toolId: 'tool_chatgpt', tagId: 'tag_text' },
|
||||
// Claude
|
||||
{ toolId: 'tool_claude', tagId: 'tag_freemium' },
|
||||
{ toolId: 'tool_claude', tagId: 'tag_claude' },
|
||||
{ toolId: 'tool_claude', tagId: 'tag_llm' },
|
||||
{ toolId: 'tool_claude', tagId: 'tag_api' },
|
||||
{ toolId: 'tool_claude', tagId: 'tag_popular' },
|
||||
{ toolId: 'tool_claude', tagId: 'tag_text' },
|
||||
{ toolId: 'tool_claude', tagId: 'tag_code_out' },
|
||||
// Gemini
|
||||
{ toolId: 'tool_gemini', tagId: 'tag_free' },
|
||||
{ toolId: 'tool_gemini', tagId: 'tag_llm' },
|
||||
{ toolId: 'tool_gemini', tagId: 'tag_api' },
|
||||
{ toolId: 'tool_gemini', tagId: 'tag_mobile' },
|
||||
{ toolId: 'tool_gemini', tagId: 'tag_text' },
|
||||
// Perplexity
|
||||
{ toolId: 'tool_perplexity', tagId: 'tag_freemium' },
|
||||
{ toolId: 'tool_perplexity', tagId: 'tag_llm' },
|
||||
{ toolId: 'tool_perplexity', tagId: 'tag_realtime' },
|
||||
{ toolId: 'tool_perplexity', tagId: 'tag_trending' },
|
||||
{ toolId: 'tool_perplexity', tagId: 'tag_text' },
|
||||
// Poe
|
||||
{ toolId: 'tool_poe', tagId: 'tag_freemium' },
|
||||
{ toolId: 'tool_poe', tagId: 'tag_llm' },
|
||||
{ toolId: 'tool_poe', tagId: 'tag_beginner' },
|
||||
{ toolId: 'tool_poe', tagId: 'tag_mobile' },
|
||||
// Character.AI
|
||||
{ toolId: 'tool_character', tagId: 'tag_free' },
|
||||
{ toolId: 'tool_character', tagId: 'tag_llm' },
|
||||
{ toolId: 'tool_character', tagId: 'tag_creator' },
|
||||
{ toolId: 'tool_character', tagId: 'tag_mobile' },
|
||||
// Coze
|
||||
{ toolId: 'tool_coze', tagId: 'tag_free' },
|
||||
{ toolId: 'tool_coze', tagId: 'tag_llm' },
|
||||
{ toolId: 'tool_coze', tagId: 'tag_api' },
|
||||
{ toolId: 'tool_coze', tagId: 'tag_developer' },
|
||||
// Pi
|
||||
{ toolId: 'tool_pi', tagId: 'tag_free' },
|
||||
{ toolId: 'tool_pi', tagId: 'tag_llm' },
|
||||
{ toolId: 'tool_pi', tagId: 'tag_beginner' },
|
||||
{ toolId: 'tool_pi', tagId: 'tag_mobile' },
|
||||
// Jasper
|
||||
{ toolId: 'tool_jasper', tagId: 'tag_paid' },
|
||||
{ toolId: 'tool_jasper', tagId: 'tag_gpt' },
|
||||
{ toolId: 'tool_jasper', tagId: 'tag_enterprise' },
|
||||
{ toolId: 'tool_jasper', tagId: 'tag_marketer' },
|
||||
{ toolId: 'tool_jasper', tagId: 'tag_text' },
|
||||
// Copy.ai
|
||||
{ toolId: 'tool_copy_ai', tagId: 'tag_freemium' },
|
||||
{ toolId: 'tool_copy_ai', tagId: 'tag_gpt' },
|
||||
{ toolId: 'tool_copy_ai', tagId: 'tag_marketer' },
|
||||
{ toolId: 'tool_copy_ai', tagId: 'tag_text' },
|
||||
// Writesonic
|
||||
{ toolId: 'tool_writesonic', tagId: 'tag_freemium' },
|
||||
{ toolId: 'tool_writesonic', tagId: 'tag_gpt' },
|
||||
{ toolId: 'tool_writesonic', tagId: 'tag_marketer' },
|
||||
{ toolId: 'tool_writesonic', tagId: 'tag_text' },
|
||||
// Notion AI
|
||||
{ toolId: 'tool_notion_ai', tagId: 'tag_paid' },
|
||||
{ toolId: 'tool_notion_ai', tagId: 'tag_collaborative' },
|
||||
{ toolId: 'tool_notion_ai', tagId: 'tag_text' },
|
||||
{ toolId: 'tool_notion_ai', tagId: 'tag_popular' },
|
||||
// Grammarly
|
||||
{ toolId: 'tool_grammarly', tagId: 'tag_freemium' },
|
||||
{ toolId: 'tool_grammarly', tagId: 'tag_browser' },
|
||||
{ toolId: 'tool_grammarly', tagId: 'tag_desktop' },
|
||||
{ toolId: 'tool_grammarly', tagId: 'tag_popular' },
|
||||
{ toolId: 'tool_grammarly', tagId: 'tag_text' },
|
||||
// QuillBot
|
||||
{ toolId: 'tool_quillbot', tagId: 'tag_freemium' },
|
||||
{ toolId: 'tool_quillbot', tagId: 'tag_student' },
|
||||
{ toolId: 'tool_quillbot', tagId: 'tag_browser' },
|
||||
{ toolId: 'tool_quillbot', tagId: 'tag_text' },
|
||||
// Rytr
|
||||
{ toolId: 'tool_rytr', tagId: 'tag_freemium' },
|
||||
{ toolId: 'tool_rytr', tagId: 'tag_beginner' },
|
||||
{ toolId: 'tool_rytr', tagId: 'tag_text' },
|
||||
// Midjourney
|
||||
{ toolId: 'tool_midjourney', tagId: 'tag_paid' },
|
||||
{ toolId: 'tool_midjourney', tagId: 'tag_diffusion' },
|
||||
{ toolId: 'tool_midjourney', tagId: 'tag_creator' },
|
||||
{ toolId: 'tool_midjourney', tagId: 'tag_popular' },
|
||||
{ toolId: 'tool_midjourney', tagId: 'tag_image_out' },
|
||||
{ toolId: 'tool_midjourney', tagId: 'tag_editors_choice' },
|
||||
// DALL-E
|
||||
{ toolId: 'tool_dalle', tagId: 'tag_freemium' },
|
||||
{ toolId: 'tool_dalle', tagId: 'tag_diffusion' },
|
||||
{ toolId: 'tool_dalle', tagId: 'tag_api' },
|
||||
{ toolId: 'tool_dalle', tagId: 'tag_image_out' },
|
||||
{ toolId: 'tool_dalle', tagId: 'tag_popular' },
|
||||
// Stable Diffusion
|
||||
{ toolId: 'tool_stable_diffusion', tagId: 'tag_free' },
|
||||
{ toolId: 'tool_stable_diffusion', tagId: 'tag_open_source' },
|
||||
{ toolId: 'tool_stable_diffusion', tagId: 'tag_diffusion' },
|
||||
{ toolId: 'tool_stable_diffusion', tagId: 'tag_developer' },
|
||||
{ toolId: 'tool_stable_diffusion', tagId: 'tag_image_out' },
|
||||
// Leonardo AI
|
||||
{ toolId: 'tool_leonardo', tagId: 'tag_freemium' },
|
||||
{ toolId: 'tool_leonardo', tagId: 'tag_diffusion' },
|
||||
{ toolId: 'tool_leonardo', tagId: 'tag_creator' },
|
||||
{ toolId: 'tool_leonardo', tagId: 'tag_image_out' },
|
||||
// Ideogram
|
||||
{ toolId: 'tool_ideogram', tagId: 'tag_freemium' },
|
||||
{ toolId: 'tool_ideogram', tagId: 'tag_diffusion' },
|
||||
{ toolId: 'tool_ideogram', tagId: 'tag_image_out' },
|
||||
{ toolId: 'tool_ideogram', tagId: 'tag_trending' },
|
||||
// Playground AI
|
||||
{ toolId: 'tool_playground', tagId: 'tag_free' },
|
||||
{ toolId: 'tool_playground', tagId: 'tag_diffusion' },
|
||||
{ toolId: 'tool_playground', tagId: 'tag_beginner' },
|
||||
{ toolId: 'tool_playground', tagId: 'tag_image_out' },
|
||||
// Canva AI
|
||||
{ toolId: 'tool_canva_ai', tagId: 'tag_freemium' },
|
||||
{ toolId: 'tool_canva_ai', tagId: 'tag_beginner' },
|
||||
{ toolId: 'tool_canva_ai', tagId: 'tag_popular' },
|
||||
{ toolId: 'tool_canva_ai', tagId: 'tag_image_out' },
|
||||
// Adobe Firefly
|
||||
{ toolId: 'tool_adobe_firefly', tagId: 'tag_freemium' },
|
||||
{ toolId: 'tool_adobe_firefly', tagId: 'tag_diffusion' },
|
||||
{ toolId: 'tool_adobe_firefly', tagId: 'tag_enterprise' },
|
||||
{ toolId: 'tool_adobe_firefly', tagId: 'tag_image_out' },
|
||||
// Runway
|
||||
{ toolId: 'tool_runway', tagId: 'tag_freemium' },
|
||||
{ toolId: 'tool_runway', tagId: 'tag_diffusion' },
|
||||
{ toolId: 'tool_runway', tagId: 'tag_creator' },
|
||||
{ toolId: 'tool_runway', tagId: 'tag_video_out' },
|
||||
{ toolId: 'tool_runway', tagId: 'tag_editors_choice' },
|
||||
// Pika
|
||||
{ toolId: 'tool_pika', tagId: 'tag_freemium' },
|
||||
{ toolId: 'tool_pika', tagId: 'tag_diffusion' },
|
||||
{ toolId: 'tool_pika', tagId: 'tag_video_out' },
|
||||
{ toolId: 'tool_pika', tagId: 'tag_trending' },
|
||||
// HeyGen
|
||||
{ toolId: 'tool_heygen', tagId: 'tag_paid' },
|
||||
{ toolId: 'tool_heygen', tagId: 'tag_enterprise' },
|
||||
{ toolId: 'tool_heygen', tagId: 'tag_marketer' },
|
||||
{ toolId: 'tool_heygen', tagId: 'tag_video_out' },
|
||||
// Synthesia
|
||||
{ toolId: 'tool_synthesia', tagId: 'tag_paid' },
|
||||
{ toolId: 'tool_synthesia', tagId: 'tag_enterprise' },
|
||||
{ toolId: 'tool_synthesia', tagId: 'tag_video_out' },
|
||||
{ toolId: 'tool_synthesia', tagId: 'tag_popular' },
|
||||
// Luma AI
|
||||
{ toolId: 'tool_luma', tagId: 'tag_freemium' },
|
||||
{ toolId: 'tool_luma', tagId: 'tag_diffusion' },
|
||||
{ toolId: 'tool_luma', tagId: 'tag_video_out' },
|
||||
{ toolId: 'tool_luma', tagId: 'tag_trending' },
|
||||
// Kling AI
|
||||
{ toolId: 'tool_kling', tagId: 'tag_freemium' },
|
||||
{ toolId: 'tool_kling', tagId: 'tag_diffusion' },
|
||||
{ toolId: 'tool_kling', tagId: 'tag_video_out' },
|
||||
{ toolId: 'tool_kling', tagId: 'tag_new' },
|
||||
// ElevenLabs
|
||||
{ toolId: 'tool_elevenlabs', tagId: 'tag_freemium' },
|
||||
{ toolId: 'tool_elevenlabs', tagId: 'tag_api' },
|
||||
{ toolId: 'tool_elevenlabs', tagId: 'tag_creator' },
|
||||
{ toolId: 'tool_elevenlabs', tagId: 'tag_audio_out' },
|
||||
{ toolId: 'tool_elevenlabs', tagId: 'tag_popular' },
|
||||
// Suno
|
||||
{ toolId: 'tool_suno', tagId: 'tag_freemium' },
|
||||
{ toolId: 'tool_suno', tagId: 'tag_creator' },
|
||||
{ toolId: 'tool_suno', tagId: 'tag_audio_out' },
|
||||
{ toolId: 'tool_suno', tagId: 'tag_trending' },
|
||||
{ toolId: 'tool_suno', tagId: 'tag_editors_choice' },
|
||||
// Udio
|
||||
{ toolId: 'tool_udio', tagId: 'tag_freemium' },
|
||||
{ toolId: 'tool_udio', tagId: 'tag_creator' },
|
||||
{ toolId: 'tool_udio', tagId: 'tag_audio_out' },
|
||||
{ toolId: 'tool_udio', tagId: 'tag_new' },
|
||||
// Murf
|
||||
{ toolId: 'tool_murf', tagId: 'tag_freemium' },
|
||||
{ toolId: 'tool_murf', tagId: 'tag_marketer' },
|
||||
{ toolId: 'tool_murf', tagId: 'tag_audio_out' },
|
||||
// Descript
|
||||
{ toolId: 'tool_descript', tagId: 'tag_freemium' },
|
||||
{ toolId: 'tool_descript', tagId: 'tag_creator' },
|
||||
{ toolId: 'tool_descript', tagId: 'tag_audio_out' },
|
||||
{ toolId: 'tool_descript', tagId: 'tag_video_out' },
|
||||
{ toolId: 'tool_descript', tagId: 'tag_desktop' },
|
||||
// GitHub Copilot
|
||||
{ toolId: 'tool_github_copilot', tagId: 'tag_paid' },
|
||||
{ toolId: 'tool_github_copilot', tagId: 'tag_gpt' },
|
||||
{ toolId: 'tool_github_copilot', tagId: 'tag_developer' },
|
||||
{ toolId: 'tool_github_copilot', tagId: 'tag_code_out' },
|
||||
{ toolId: 'tool_github_copilot', tagId: 'tag_popular' },
|
||||
{ toolId: 'tool_github_copilot', tagId: 'tag_editors_choice' },
|
||||
// Cursor
|
||||
{ toolId: 'tool_cursor', tagId: 'tag_freemium' },
|
||||
{ toolId: 'tool_cursor', tagId: 'tag_claude' },
|
||||
{ toolId: 'tool_cursor', tagId: 'tag_developer' },
|
||||
{ toolId: 'tool_cursor', tagId: 'tag_code_out' },
|
||||
{ toolId: 'tool_cursor', tagId: 'tag_trending' },
|
||||
{ toolId: 'tool_cursor', tagId: 'tag_desktop' },
|
||||
// Replit
|
||||
{ toolId: 'tool_replit', tagId: 'tag_freemium' },
|
||||
{ toolId: 'tool_replit', tagId: 'tag_llm' },
|
||||
{ toolId: 'tool_replit', tagId: 'tag_developer' },
|
||||
{ toolId: 'tool_replit', tagId: 'tag_beginner' },
|
||||
{ toolId: 'tool_replit', tagId: 'tag_code_out' },
|
||||
// Codeium
|
||||
{ toolId: 'tool_codeium', tagId: 'tag_free' },
|
||||
{ toolId: 'tool_codeium', tagId: 'tag_llm' },
|
||||
{ toolId: 'tool_codeium', tagId: 'tag_developer' },
|
||||
{ toolId: 'tool_codeium', tagId: 'tag_code_out' },
|
||||
// Tabnine
|
||||
{ toolId: 'tool_tabnine', tagId: 'tag_freemium' },
|
||||
{ toolId: 'tool_tabnine', tagId: 'tag_enterprise' },
|
||||
{ toolId: 'tool_tabnine', tagId: 'tag_developer' },
|
||||
{ toolId: 'tool_tabnine', tagId: 'tag_code_out' },
|
||||
// Claude Code
|
||||
{ toolId: 'tool_claude_code', tagId: 'tag_paid' },
|
||||
{ toolId: 'tool_claude_code', tagId: 'tag_claude' },
|
||||
{ toolId: 'tool_claude_code', tagId: 'tag_developer' },
|
||||
{ toolId: 'tool_claude_code', tagId: 'tag_code_out' },
|
||||
{ toolId: 'tool_claude_code', tagId: 'tag_new' },
|
||||
// Otter.ai
|
||||
{ toolId: 'tool_otter', tagId: 'tag_freemium' },
|
||||
{ toolId: 'tool_otter', tagId: 'tag_collaborative' },
|
||||
{ toolId: 'tool_otter', tagId: 'tag_realtime' },
|
||||
{ toolId: 'tool_otter', tagId: 'tag_text' },
|
||||
{ toolId: 'tool_otter', tagId: 'tag_mobile' },
|
||||
// Fireflies.ai
|
||||
{ toolId: 'tool_fireflies', tagId: 'tag_freemium' },
|
||||
{ toolId: 'tool_fireflies', tagId: 'tag_collaborative' },
|
||||
{ toolId: 'tool_fireflies', tagId: 'tag_enterprise' },
|
||||
{ toolId: 'tool_fireflies', tagId: 'tag_text' },
|
||||
// Mem
|
||||
{ toolId: 'tool_mem', tagId: 'tag_freemium' },
|
||||
{ toolId: 'tool_mem', tagId: 'tag_llm' },
|
||||
{ toolId: 'tool_mem', tagId: 'tag_text' },
|
||||
// Motion
|
||||
{ toolId: 'tool_motion', tagId: 'tag_paid' },
|
||||
{ toolId: 'tool_motion', tagId: 'tag_collaborative' },
|
||||
// Consensus
|
||||
{ toolId: 'tool_consensus', tagId: 'tag_freemium' },
|
||||
{ toolId: 'tool_consensus', tagId: 'tag_llm' },
|
||||
{ toolId: 'tool_consensus', tagId: 'tag_student' },
|
||||
{ toolId: 'tool_consensus', tagId: 'tag_text' },
|
||||
// Elicit
|
||||
{ toolId: 'tool_elicit', tagId: 'tag_freemium' },
|
||||
{ toolId: 'tool_elicit', tagId: 'tag_llm' },
|
||||
{ toolId: 'tool_elicit', tagId: 'tag_student' },
|
||||
{ toolId: 'tool_elicit', tagId: 'tag_text' },
|
||||
// Scite
|
||||
{ toolId: 'tool_scite', tagId: 'tag_paid' },
|
||||
{ toolId: 'tool_scite', tagId: 'tag_student' },
|
||||
{ toolId: 'tool_scite', tagId: 'tag_text' },
|
||||
// Figma AI
|
||||
{ toolId: 'tool_figma_ai', tagId: 'tag_paid' },
|
||||
{ toolId: 'tool_figma_ai', tagId: 'tag_collaborative' },
|
||||
{ toolId: 'tool_figma_ai', tagId: 'tag_popular' },
|
||||
{ toolId: 'tool_figma_ai', tagId: 'tag_image_out' },
|
||||
// Uizard
|
||||
{ toolId: 'tool_uizard', tagId: 'tag_freemium' },
|
||||
{ toolId: 'tool_uizard', tagId: 'tag_beginner' },
|
||||
{ toolId: 'tool_uizard', tagId: 'tag_image_out' },
|
||||
// Magician
|
||||
{ toolId: 'tool_magician', tagId: 'tag_paid' },
|
||||
{ toolId: 'tool_magician', tagId: 'tag_image_out' },
|
||||
{ toolId: 'tool_magician', tagId: 'tag_text' },
|
||||
];
|
||||
|
||||
// ========== 主函数 ==========
|
||||
async function seedTools() {
|
||||
if (!process.env.DATABASE_URL) {
|
||||
console.error("❌ 未找到 DATABASE_URL 环境变量");
|
||||
console.log("请确保 .env.local 文件中设置了 DATABASE_URL");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const queryClient = postgres(process.env.DATABASE_URL);
|
||||
const db = drizzle(queryClient);
|
||||
|
||||
try {
|
||||
console.log("🚀 开始导入 AI 工具种子数据...\n");
|
||||
|
||||
// 1. 导入分类
|
||||
console.log("📁 导入分类数据 (10 个)...");
|
||||
for (const cat of categoriesData) {
|
||||
await db.insert(category).values(cat).onConflictDoNothing();
|
||||
}
|
||||
console.log("✅ 分类数据导入完成\n");
|
||||
|
||||
// 2. 导入标签
|
||||
console.log("🏷️ 导入标签数据 (30 个)...");
|
||||
for (const t of tagsData) {
|
||||
await db.insert(tag).values(t).onConflictDoNothing();
|
||||
}
|
||||
console.log("✅ 标签数据导入完成\n");
|
||||
|
||||
// 3. 导入工具(pricing 从定价标签推导,featured 从"编辑推荐"标签推导)
|
||||
console.log("🔧 导入工具数据 (50 个)...");
|
||||
const pricingTagMap: Record<string, string> = {
|
||||
tag_free: "free",
|
||||
tag_freemium: "freemium",
|
||||
tag_paid: "paid",
|
||||
};
|
||||
const pricingByTool = new Map<string, string>();
|
||||
const featuredToolIds = new Set<string>();
|
||||
for (const tt of toolTagData) {
|
||||
const pricing = pricingTagMap[tt.tagId];
|
||||
if (pricing) pricingByTool.set(tt.toolId, pricing);
|
||||
if (tt.tagId === "tag_editors_choice") featuredToolIds.add(tt.toolId);
|
||||
}
|
||||
for (const t of toolsData) {
|
||||
await db
|
||||
.insert(tool)
|
||||
.values({
|
||||
...t,
|
||||
pricing: pricingByTool.get(t.id) ?? null,
|
||||
featured: featuredToolIds.has(t.id),
|
||||
})
|
||||
.onConflictDoNothing();
|
||||
}
|
||||
console.log("✅ 工具数据导入完成\n");
|
||||
|
||||
// 4. 导入工具-分类关联
|
||||
console.log("🔗 导入工具-分类关联...");
|
||||
for (const tc of toolCategoryData) {
|
||||
await db.insert(toolCategory).values(tc).onConflictDoNothing();
|
||||
}
|
||||
console.log("✅ 工具-分类关联导入完成\n");
|
||||
|
||||
// 5. 导入工具-标签关联
|
||||
console.log("🏷️ 导入工具-标签关联...");
|
||||
for (const tt of toolTagData) {
|
||||
await db.insert(toolTag).values(tt).onConflictDoNothing();
|
||||
}
|
||||
console.log("✅ 工具-标签关联导入完成\n");
|
||||
|
||||
console.log("====================================");
|
||||
console.log("🎉 所有种子数据导入成功!");
|
||||
console.log("====================================");
|
||||
console.log("\n📊 数据统计:");
|
||||
console.log(` - 分类: ${categoriesData.length} 个`);
|
||||
console.log(` - 标签: ${tagsData.length} 个`);
|
||||
console.log(` - 工具: ${toolsData.length} 个`);
|
||||
console.log(` - 工具-分类关联: ${toolCategoryData.length} 条`);
|
||||
console.log(` - 工具-标签关联: ${toolTagData.length} 条`);
|
||||
|
||||
} catch (error) {
|
||||
console.error("❌ 导入失败:", error);
|
||||
await queryClient.end();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await queryClient.end();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// 运行脚本
|
||||
seedTools();
|
||||
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env tsx
|
||||
/**
|
||||
* 管理员初始化脚本
|
||||
* 用于设置第一个管理员账号
|
||||
*
|
||||
* 使用方法:
|
||||
* 1. 设置环境变量 ADMIN_EMAIL
|
||||
* 2. 运行: pnpm admin:setup
|
||||
* 或直接运行: ADMIN_EMAIL=admin@example.com pnpm admin:setup
|
||||
*/
|
||||
|
||||
import { drizzle } from "drizzle-orm/postgres-js";
|
||||
import postgres from "postgres";
|
||||
import { eq } from "drizzle-orm";
|
||||
import dotenv from "dotenv";
|
||||
import { resolve } from "path";
|
||||
import { pgTable, text, timestamp, boolean, integer } from "drizzle-orm/pg-core";
|
||||
|
||||
// 加载环境变量
|
||||
dotenv.config({ path: resolve(process.cwd(), ".env.local") });
|
||||
|
||||
// 定义 user 表结构(复制自 schema)
|
||||
const user = pgTable("user", {
|
||||
id: text("id").primaryKey(),
|
||||
name: text("name").notNull(),
|
||||
email: text("email").notNull().unique(),
|
||||
emailVerified: boolean("email_verified").default(false).notNull(),
|
||||
image: text("image"),
|
||||
credits: integer("credits").default(0).notNull(),
|
||||
role: text("role").default("user").notNull(),
|
||||
banned: boolean("banned").default(false).notNull(),
|
||||
banReason: text("ban_reason"),
|
||||
banExpires: timestamp("ban_expires"),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at")
|
||||
.defaultNow()
|
||||
.$onUpdate(() => new Date())
|
||||
.notNull(),
|
||||
});
|
||||
|
||||
async function setupAdmin() {
|
||||
const adminEmail = process.env.ADMIN_EMAIL;
|
||||
|
||||
if (!adminEmail) {
|
||||
console.error("❌ 请设置环境变量 ADMIN_EMAIL");
|
||||
console.log("例如: ADMIN_EMAIL=admin@example.com pnpm admin:setup");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!process.env.DATABASE_URL) {
|
||||
console.error("❌ 未找到 DATABASE_URL 环境变量");
|
||||
console.log("请确保 .env.local 文件中设置了 DATABASE_URL");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 创建数据库连接
|
||||
const queryClient = postgres(process.env.DATABASE_URL);
|
||||
const db = drizzle(queryClient);
|
||||
|
||||
try {
|
||||
console.log("🔍 正在查找用户:", adminEmail);
|
||||
|
||||
// 检查用户是否存在
|
||||
const existingUser = await db
|
||||
.select()
|
||||
.from(user)
|
||||
.where(eq(user.email, adminEmail))
|
||||
.limit(1);
|
||||
|
||||
if (existingUser.length === 0) {
|
||||
console.error(`❌ 用户 ${adminEmail} 不存在`);
|
||||
console.log("请先使用此邮箱注册一个账号,然后再运行此脚本");
|
||||
await queryClient.end();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const currentUser = existingUser[0];
|
||||
console.log(`✓ 找到用户: ${currentUser.name} (当前角色: ${currentUser.role})`);
|
||||
|
||||
if (currentUser.role === "admin") {
|
||||
console.log(`✅ 用户 ${adminEmail} 已经是管理员`);
|
||||
await queryClient.end();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// 更新用户为管理员
|
||||
console.log("📝 正在更新用户角色...");
|
||||
await db
|
||||
.update(user)
|
||||
.set({
|
||||
role: "admin",
|
||||
updatedAt: new Date()
|
||||
})
|
||||
.where(eq(user.email, adminEmail));
|
||||
|
||||
console.log(`✅ 成功将用户 ${adminEmail} 设置为管理员`);
|
||||
console.log("🚀 现在你可以访问 /admin 路径来进入管理后台");
|
||||
|
||||
} catch (error) {
|
||||
console.error("❌ 设置管理员失败:", error);
|
||||
await queryClient.end();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 关闭数据库连接
|
||||
await queryClient.end();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// 运行脚本
|
||||
setupAdmin();
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
# 启动五个独立网站(monorepo sites/*,共享同一套数据库/Auth/Admin)
|
||||
# ai-stack:3001 launchai:3002 the-ai-ledger:3003 devai-tools:3004 findai:3005
|
||||
set -e
|
||||
cd "$(dirname "$0")/../../.."
|
||||
|
||||
SITES=(ai-stack:3001 launchai:3002 the-ai-ledger:3003 devai-tools:3004 findai:3005)
|
||||
|
||||
for entry in "${SITES[@]}"; do
|
||||
site="${entry%%:*}"
|
||||
port="${entry##*:}"
|
||||
(cd "sites/$site" && bun run start > "/tmp/site-$site.log" 2>&1) &
|
||||
echo "$site → http://localhost:$port (pid $!)"
|
||||
done
|
||||
|
||||
echo "等待启动..."
|
||||
sleep 5
|
||||
for entry in "${SITES[@]}"; do
|
||||
site="${entry%%:*}"
|
||||
port="${entry##*:}"
|
||||
code=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:$port/" || echo "000")
|
||||
echo "$site [$port] → HTTP $code"
|
||||
done
|
||||
|
||||
wait
|
||||
@@ -0,0 +1,37 @@
|
||||
import { chromium } from "playwright";
|
||||
import postgres from "postgres";
|
||||
|
||||
const sql = postgres(process.env.DATABASE_URL || "postgresql://postgres:postgres@127.0.0.1:15433/ai_directory", { prepare: false });
|
||||
const count = async () => Number((await sql`SELECT COUNT(*) FROM vote WHERE tool_id='tool_claude'`)[0].count);
|
||||
|
||||
const browser = await chromium.launch();
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
|
||||
// 登录
|
||||
await page.goto("http://localhost:3001/login", { waitUntil: "domcontentloaded" });
|
||||
await page.fill('input[type="email"]', "bob@demo.dev");
|
||||
await page.fill('input[type="password"]', "password123");
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForTimeout(2500);
|
||||
|
||||
// 记录 → 点击 → 对比
|
||||
const before = await count();
|
||||
await page.goto("http://localhost:3001/tools/claude", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForTimeout(1500);
|
||||
const btn = page.locator("button", { hasText: /Upvote|Upvoted/ }).first();
|
||||
await btn.click();
|
||||
await page.waitForTimeout(2500);
|
||||
const after = await count();
|
||||
const label = await btn.textContent();
|
||||
console.log(`DB votes before=${before} after=${after} | button="${(label ?? "").trim()}"`);
|
||||
console.log(before !== after ? "TOGGLE PASS" : "TOGGLE FAIL");
|
||||
|
||||
// 再点一次恢复
|
||||
await btn.click();
|
||||
await page.waitForTimeout(2500);
|
||||
const restored = await count();
|
||||
console.log(`再次点击后 DB=${restored} (${restored === before ? "恢复原状 PASS" : "FAIL"})`);
|
||||
|
||||
await browser.close();
|
||||
await sql.end();
|
||||
Reference in New Issue
Block a user