const { PrismaClient } = require('@prisma/client') const prisma = new PrismaClient() const MINIMAX_ENDPOINT = 'https://api.minimaxi.com/v1/chat/completions' const MINIMAX_MODEL = process.env.MINIMAX_MODEL || 'MiniMax-M2.7' const KNOWN_CATEGORIES = [ '防火门', '配电箱', '电线电缆', '阀门', '钢材', '涂料', '混凝土', '变压器', '水泵', '风机', '管材', '开关', '灯具', '仪表', '电机', ] const EXPLANATION_PREFIX = /^(让我|用户|我需要|好的|现在|首先|分析|根据|对于|这条|这个|考虑|我们|这里|注意|提示|重要|关键|总结|综上|该|其|它|他们|这些|那些|由于|因为|所以|因此|需要|可能|应该|可以|建议|希望|要求|必须)/ function cleanOne(raw) { if (!raw) return '' let s = String(raw).trim() s = s.replace(/[\s\S]*?<\/think>/g, '').trim() s = s.replace(/\*\*/g, '').trim() s = s.replace(/^["「『]+|["」』]+$/g, '').trim() const lines = s.split('\n').map((l) => l.trim()).filter((l) => l) for (const line of lines) { if (EXPLANATION_PREFIX.test(line)) continue if (line.length > 20) continue if (line.includes(',') || line.includes('。') || line.includes(':') || line.includes(':')) continue const cleaned = line.replace(/^[\d]+[\.、::]\s*/, '').trim() if (!cleaned || cleaned.length > 10) continue if (KNOWN_CATEGORIES.includes(cleaned)) return cleaned const m = cleaned.match(/^([\u4e00-\u9fa5]{2,6})$/) if (m && !EXPLANATION_PREFIX.test(m[1])) return m[1] } return '' } async function inferSingle(title, categories) { const apiKey = process.env.MINIMAX_API_KEY if (!apiKey) return '' const systemPrompt = `你是建材工业品分类助手。任务:给一条采购公告分配一个品类名。 严格规则: 1. 只输出 2-6 个中文字的品类名,不要任何其他内容 2. 不要输出标签 3. 不要输出"让我""根据"等解释文字 4. 不要输出标点符号 参考品类:${categories.join('、')}` try { const r = await fetch(MINIMAX_ENDPOINT, { method: 'POST', headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ model: MINIMAX_MODEL, messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: title }, ], temperature: 0, max_tokens: 500, }), }) if (!r.ok) return '' const data = await r.json() const content = data.choices?.[0]?.message?.content || '' return cleanOne(content) } catch { return '' } } async function main() { const categories = await prisma.productCategory.findMany() const catNames = categories.map((c) => c.name) const emptyLeads = await prisma.lead.findMany({ where: { OR: [{ category: null }, { category: '' }] }, select: { id: true, title: true }, }) console.log(`空品类线索: ${emptyLeads.length} 条`) let okCount = 0 let failCount = 0 for (let i = 0; i < emptyLeads.length; i++) { const lead = emptyLeads[i] const catName = await inferSingle(lead.title, catNames) if (catName) { await prisma.lead.update({ where: { id: lead.id }, data: { category: catName } }) okCount++ if (!catNames.includes(catName)) { await prisma.productCategory.upsert({ where: { name: catName }, update: {}, create: { name: catName, keywords: JSON.stringify([lead.title.slice(0, 10)]) }, }) catNames.push(catName) } } else { failCount++ } if ((i + 1) % 20 === 0 || i === emptyLeads.length - 1) { console.log(`进度 ${i + 1}/${emptyLeads.length} 成功 ${okCount} 失败 ${failCount}`) } } const remaining = await prisma.lead.count({ where: { OR: [{ category: null }, { category: '' }] }, }) console.log(`完成: 成功 ${okCount}, 失败 ${failCount}, 剩余空品类: ${remaining}`) } main() .catch((e) => { console.error(e) process.exit(1) }) .finally(() => prisma.$disconnect())