platform-assistant/scripts/backfill_category.js
Sisyphus ab764bbc6c refactor: DeepSeek → MiniMax (M2.7 推理模型) 迁移
- 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 条空品类待后续处理
2026-07-01 11:31:17 +08:00

114 lines
4.0 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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())