- notes-extractor 和 category-inference 都改用 MiniMax API - endpoint: https://api.minimaxi.com/v1/chat/completions - model: MiniMax-M2.7 (env 可覆盖 MINIMAX_MODEL) - 提示词明确禁止 <think> 标签和解释文字 - 新增 scripts/backfill_category.js (单条请求 + 严格清洗) - 新增 scripts/clean_category.js (清理 <think>/**加粗/解释行/破折号 等脏值) - 生产 .env 已写入 MINIMAX_API_KEY - 实际回填: 693/761 (91%) 已分配品类, 68 条空品类待后续处理
114 lines
4.0 KiB
JavaScript
114 lines
4.0 KiB
JavaScript
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(/<think>[\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. 不要输出<think>标签
|
||
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())
|