platform-assistant/scripts/reclassify_leads.js
Sisyphus 5a49a37235 docs: 项目交接文档 + 比价功能PRD + 数据维护脚本
- HANDOVER.md: 完整交接文档(架构/数据模型/API/业务流/已知坑/工作区状态/待办)
- PRD-智能比价助手.md: 比价功能产品需求文档(供外部平台参考)
- 数据维护脚本: 清理AI垃圾品类SQL、修复损坏标题SQL、线索重分类/重抓脚本
2026-09-30 09:35:05 +08:00

154 lines
4.8 KiB
JavaScript
Raw 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.

/**
* 重新分类脚本:对 category 为空的线索重新执行分类
* 用法:node scripts/reclassify_leads.js
*
* 需要在项目根目录运行(使用项目内的 Prisma)
*/
const { PrismaClient } = require('@prisma/client')
// 直接复用 inference 逻辑的内联版本
const MINIMAX_ENDPOINT = 'https://api.minimaxi.com/v1/chat/completions'
const MINIMAX_MODEL = process.env.MINIMAX_MODEL || 'MiniMax-M2.7'
const API_KEY = process.env.MINIMAX_API_KEY
function isGarbageKeyword(kw) {
if (/(?:公司|有限|集团|工程局|项目部)$/.test(kw)) return true
if (/^(?:中国电建|新疆)/.test(kw)) return true
const genericTerms = ['公开竞价', '公开招标', '竞争性谈判', '询价采购', '邀请招标']
if (genericTerms.includes(kw)) return true
return false
}
function isValidCategoryName(raw) {
if (!raw) return false
const s = raw.trim()
if (s.length === 0 || s.length > 6) return false
if (/[*\->→]/.test(s)) return false
if (/[,。:;!?、]/.test(s)) return false
if (/[A-Za-z]{2,}/.test(s)) return false
if (!/^[\u4e00-\u9fa5]+$/.test(s)) return false
if (/^(让我|用户|我需要|好的|现在|首先|分析|根据|对于|这条|这个|考虑|我们|这里|注意|提示|重要|关键|总结|综上|该|其|它|他们|这些|那些|由于|因为|所以|因此|需要|可能|应该|可以|建议|希望|要求|必须)/.test(s)) return false
if (/采购|项目|公司|工程|设备|材料|公告|招标|标段|框架|年度/.test(s)) return false
return true
}
function extractCategoryFromLine(line) {
// 剥离数字前缀
let s = line.replace(/^[\d]+[\.、::]\s*/, '').trim()
if (!s) return ''
// 取 → 后面的内容(最可能是品类名)
const arrowIdx = s.lastIndexOf('→')
if (arrowIdx >= 0) {
let afterArrow = s.slice(arrowIdx + 1).trim()
// 提取连续中文字
const m = afterArrow.match(/([\u4e00-\u9fa5]{2,6})/)
if (m && isValidCategoryName(m[1])) return m[1]
}
// 从整句中提取可能的品类名
const parts = s.split(/\s*(?:[-–—]|→)\s*/).map(p => p.trim()).filter(p => p)
// 从最后一部分往前找
for (let i = parts.length - 1; i >= 0; i--) {
const m = parts[i].match(/([\u4e00-\u9fa5]{2,6})/g)
if (m) {
for (let j = m.length - 1; j >= 0; j--) {
if (isValidCategoryName(m[j])) return m[j]
}
}
}
// 从整句提取
const m = s.match(/([\u4e00-\u9fa5]{2,6})/g)
if (m) {
for (let j = m.length - 1; j >= 0; j--) {
if (isValidCategoryName(m[j])) return m[j]
}
}
return ''
}
// 简版: 只对 batch 结果做后处理
function parseBatchResult(content) {
const results = []
// 去掉 think 块
let clean = content.replace(/<think>[\s\S]*?<\/think>/g, '').trim()
const lines = clean.split('\n').map(l => l.trim()).filter(l => l)
for (const line of lines) {
results.push(extractCategoryFromLine(line))
}
return results
}
async function main() {
const prisma = new PrismaClient()
try {
// 1. 找出所有空分类的线索
const leads = await prisma.lead.findMany({
where: { OR: [{ category: null }, { category: '' }] },
select: { id: true, title: true, summary: true },
})
console.log(`找到 ${leads.length} 条待分类线索`)
if (leads.length === 0) {
console.log('无需分类')
return
}
// 2. 获取已有品类
const categories = await prisma.productCategory.findMany()
const catNames = categories.map(c => c.name).join('、')
// 3. 依次尝试关键词匹配,未命中则调 AI
const needsAI = []
const result = new Map()
for (const lead of leads) {
// 关键词匹配(跳过垃圾关键词)
let matched = false
for (const cat of categories) {
const kws = JSON.parse(cat.keywords)
for (const kw of kws) {
if (isGarbageKeyword(kw)) continue
if (lead.title.includes(kw)) {
result.set(lead.id, cat.name)
matched = true
break
}
}
if (matched) break
}
if (!matched) {
needsAI.push(lead)
}
}
console.log(`关键词命中: ${result.size} 条, 需 AI 推断: ${needsAI.length} 条`)
// 4. 未命中的全部归为"其他"
console.log(`关键词未命中的 ${needsAI.length} 条统一归为「其他」品类`)
for (const lead of needsAI) {
result.set(lead.id, '其他')
}
// 5. 更新数据库
let updated = 0
for (const [id, category] of result) {
if (category) {
await prisma.lead.update({
where: { id },
data: { category },
})
updated++
}
}
console.log(`\n完成! 更新了 ${updated}/${leads.length} 条线索的分类`)
console.log(`未分类: ${leads.length - updated} 条(保持为空)`)
} finally {
await prisma.$disconnect()
}
}
main().catch(console.error)