seed-models.mjs 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. /**
  2. * 模型目录种子脚本 - 从 dashscope-models-20260808.json 导入 236 个模型到 DB
  3. * 幂等:使用 upsert,可安全重复执行
  4. * 用法:node scripts/seed-models.mjs
  5. */
  6. import { PrismaClient } from '@prisma/client';
  7. import { readFileSync } from 'node:fs';
  8. import { join, dirname } from 'node:path';
  9. import { fileURLToPath } from 'node:url';
  10. const __dirname = dirname(fileURLToPath(import.meta.url));
  11. const DATA_PATH = join(__dirname, '..', '..', '..', 'docs', 'models', 'dashscope-models-20260808.json');
  12. // ---- 分类逻辑:按优先级 speech > image > vision > chat ----
  13. function classifyCategory(publicId) {
  14. const lower = publicId.toLowerCase();
  15. // speech: TTS / ASR / audio / realtime / livetranslate / s2s
  16. if (/tts|asr|audio|speech|realtime|livetranslate|s2s/.test(lower)) return 'speech';
  17. // image
  18. if (/image|wan2/i.test(lower)) return 'image';
  19. // vision: vl / omni / ocr
  20. if (/vl|omni|ocr/.test(lower)) return 'vision';
  21. return 'chat';
  22. }
  23. function classifyProvider(publicId) {
  24. const lower = publicId.toLowerCase();
  25. if (lower.startsWith('glm') || lower.includes('zhipu/')) return 'Zhipu';
  26. if (lower.includes('deepseek')) return 'DeepSeek';
  27. if (lower.includes('minimax')) return 'MiniMax';
  28. if (lower.includes('kimi')) return 'Kimi';
  29. if (lower.includes('siliconflow')) return 'SiliconFlow';
  30. if (lower.includes('vanchin')) return 'Vanchin';
  31. if (lower.includes('xiaomi')) return 'Xiaomi';
  32. // Alibaba: qwen, qvq, qwq, tongyi, codeqwen, fun-, sre, test, wan, z-image, gui
  33. if (
  34. lower.startsWith('qwen') || lower.startsWith('qvq') || lower.startsWith('qwq') ||
  35. lower.startsWith('tongyi') || lower.startsWith('codeqwen') ||
  36. lower.startsWith('fun-') || lower.startsWith('sre-') || lower.startsWith('test-') ||
  37. lower.startsWith('wan') || lower.startsWith('z-image') || lower.startsWith('gui')
  38. ) {
  39. return 'Alibaba';
  40. }
  41. return 'Other';
  42. }
  43. function makeName(publicId) {
  44. // 尝试提取一个适合展示的名字
  45. const parts = publicId.split('/');
  46. return parts[parts.length - 1] ?? publicId;
  47. }
  48. function makeDescription(category, provider, publicId) {
  49. switch (category) {
  50. case 'speech': return `${provider} 语音模型: ${publicId}`;
  51. case 'image': return `${provider} 图像模型: ${publicId}`;
  52. case 'vision': return `${provider} 视觉模型: ${publicId}`;
  53. default: return `${provider} 对话模型: ${publicId}`;
  54. }
  55. }
  56. const RECOMMENDED = new Set([
  57. 'glm-5.2',
  58. 'glm-5.2-fast-preview',
  59. 'deepseek-v4-flash',
  60. 'deepseek-v4-pro',
  61. 'qwen-max',
  62. 'qwen-plus',
  63. 'qwen-turbo',
  64. 'qwen-flash',
  65. 'qwen3-max',
  66. 'qwen3-coder-plus',
  67. 'kimi-k2.6',
  68. 'MiniMax-M3',
  69. ]);
  70. async function main() {
  71. const prisma = new PrismaClient();
  72. try {
  73. const raw = readFileSync(DATA_PATH, 'utf-8');
  74. const data = JSON.parse(raw);
  75. const models = /** @type {string[]} */ (data.models);
  76. if (!models || models.length === 0) {
  77. console.error('No models found in JSON file');
  78. process.exit(1);
  79. }
  80. console.log(`Seeding ${models.length} models...`);
  81. let upserted = 0;
  82. for (let i = 0; i < models.length; i++) {
  83. const publicId = models[i];
  84. const category = classifyCategory(publicId);
  85. const provider = classifyProvider(publicId);
  86. const name = makeName(publicId);
  87. const description = makeDescription(category, provider, publicId);
  88. const isRecommended = RECOMMENDED.has(publicId);
  89. try {
  90. await prisma.modelCatalog.upsert({
  91. where: { publicId },
  92. update: {
  93. upstreamId: publicId,
  94. name,
  95. provider,
  96. category,
  97. description,
  98. isRecommended,
  99. status: 'active',
  100. sortOrder: i,
  101. },
  102. create: {
  103. publicId,
  104. upstreamId: publicId,
  105. name,
  106. provider,
  107. category,
  108. description,
  109. // TODO: 价格待定,后续通过管理后台设置
  110. // 定价规则:DashScope 价格 × 0.7
  111. inputPriceCents: 0,
  112. outputPriceCents: 0,
  113. isRecommended,
  114. status: 'active',
  115. sortOrder: i,
  116. },
  117. });
  118. upserted++;
  119. } catch (err) {
  120. console.error(`Failed to upsert ${publicId}: ${err.message}`);
  121. }
  122. }
  123. console.log(`Done. Upserted ${upserted}/${models.length} models.`);
  124. // 打印分类统计
  125. const counts = {};
  126. for (const m of models) {
  127. const cat = classifyCategory(m);
  128. counts[cat] = (counts[cat] || 0) + 1;
  129. }
  130. console.log('Category counts:', counts);
  131. await prisma.$disconnect();
  132. } catch (err) {
  133. console.error('Seed failed:', err);
  134. process.exit(1);
  135. }
  136. }
  137. main();