/** * 模型目录种子脚本 - 从 dashscope-models-20260808.json 导入 236 个模型到 DB * 幂等:使用 upsert,可安全重复执行 * 用法:node scripts/seed-models.mjs */ import { PrismaClient } from '@prisma/client'; import { readFileSync } from 'node:fs'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = dirname(fileURLToPath(import.meta.url)); const DATA_PATH = join(__dirname, '..', '..', '..', 'docs', 'models', 'dashscope-models-20260808.json'); // ---- 分类逻辑:按优先级 speech > image > vision > chat ---- function classifyCategory(publicId) { const lower = publicId.toLowerCase(); // speech: TTS / ASR / audio / realtime / livetranslate / s2s if (/tts|asr|audio|speech|realtime|livetranslate|s2s/.test(lower)) return 'speech'; // image if (/image|wan2/i.test(lower)) return 'image'; // vision: vl / omni / ocr if (/vl|omni|ocr/.test(lower)) return 'vision'; return 'chat'; } function classifyProvider(publicId) { const lower = publicId.toLowerCase(); if (lower.startsWith('glm') || lower.includes('zhipu/')) return 'Zhipu'; if (lower.includes('deepseek')) return 'DeepSeek'; if (lower.includes('minimax')) return 'MiniMax'; if (lower.includes('kimi')) return 'Kimi'; if (lower.includes('siliconflow')) return 'SiliconFlow'; if (lower.includes('vanchin')) return 'Vanchin'; if (lower.includes('xiaomi')) return 'Xiaomi'; // Alibaba: qwen, qvq, qwq, tongyi, codeqwen, fun-, sre, test, wan, z-image, gui if ( lower.startsWith('qwen') || lower.startsWith('qvq') || lower.startsWith('qwq') || lower.startsWith('tongyi') || lower.startsWith('codeqwen') || lower.startsWith('fun-') || lower.startsWith('sre-') || lower.startsWith('test-') || lower.startsWith('wan') || lower.startsWith('z-image') || lower.startsWith('gui') ) { return 'Alibaba'; } return 'Other'; } function makeName(publicId) { // 尝试提取一个适合展示的名字 const parts = publicId.split('/'); return parts[parts.length - 1] ?? publicId; } function makeDescription(category, provider, publicId) { switch (category) { case 'speech': return `${provider} 语音模型: ${publicId}`; case 'image': return `${provider} 图像模型: ${publicId}`; case 'vision': return `${provider} 视觉模型: ${publicId}`; default: return `${provider} 对话模型: ${publicId}`; } } const RECOMMENDED = new Set([ 'glm-5.2', 'glm-5.2-fast-preview', 'deepseek-v4-flash', 'deepseek-v4-pro', 'qwen-max', 'qwen-plus', 'qwen-turbo', 'qwen-flash', 'qwen3-max', 'qwen3-coder-plus', 'kimi-k2.6', 'MiniMax-M3', ]); async function main() { const prisma = new PrismaClient(); try { const raw = readFileSync(DATA_PATH, 'utf-8'); const data = JSON.parse(raw); const models = /** @type {string[]} */ (data.models); if (!models || models.length === 0) { console.error('No models found in JSON file'); process.exit(1); } console.log(`Seeding ${models.length} models...`); let upserted = 0; for (let i = 0; i < models.length; i++) { const publicId = models[i]; const category = classifyCategory(publicId); const provider = classifyProvider(publicId); const name = makeName(publicId); const description = makeDescription(category, provider, publicId); const isRecommended = RECOMMENDED.has(publicId); try { await prisma.modelCatalog.upsert({ where: { publicId }, update: { upstreamId: publicId, name, provider, category, description, isRecommended, status: 'active', sortOrder: i, }, create: { publicId, upstreamId: publicId, name, provider, category, description, // TODO: 价格待定,后续通过管理后台设置 // 定价规则:DashScope 价格 × 0.7 inputPriceCents: 0, outputPriceCents: 0, isRecommended, status: 'active', sortOrder: i, }, }); upserted++; } catch (err) { console.error(`Failed to upsert ${publicId}: ${err.message}`); } } console.log(`Done. Upserted ${upserted}/${models.length} models.`); // 打印分类统计 const counts = {}; for (const m of models) { const cat = classifyCategory(m); counts[cat] = (counts[cat] || 0) + 1; } console.log('Category counts:', counts); await prisma.$disconnect(); } catch (err) { console.error('Seed failed:', err); process.exit(1); } } main();