Files
what-to-eat/scripts/pull-feedback.ts
T

79 lines
3.5 KiB
TypeScript
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.
import type { FeedbackRow, SignalRow } from './lib/feedback'
/**
* 拉取 cook-feedback 云函数里的用户反馈和筛选信号,汇总给 recipe-data-review skill 使用
*
* 需要在 .env 中配置:
* COOK_FEEDBACK_URL cook-feedback 云函数的 HTTP 访问地址
* COOK_FEEDBACK_TOKEN 与云函数环境变量 COOK_FEEDBACK_EXPORT_TOKEN 相同
*
* pnpm feedback:pull
* → .cook/feedback/summary.md 给人和 AI 看的汇总
* → .cook/feedback/export.json 原始数据(不提交)
* → test/fixtures/search-eval.json 筛选评测样例(匿名聚合,可以提交)
*/
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import { recipeCsvFile, rootDir } from '../packages/cook/src/utils/config'
import { parseRecipeCsv } from '../packages/cook/src/utils/csv'
import { buildEvalCases, summarizeReports } from './lib/feedback'
try {
process.loadEnvFile(path.join(rootDir, '.env'))
}
catch {}
const url = process.env.COOK_FEEDBACK_URL
const token = process.env.COOK_FEEDBACK_TOKEN
if (!url || !token) {
console.error('请先在 .env 中配置 COOK_FEEDBACK_URL 和 COOK_FEEDBACK_TOKEN,见 docs/dev/data.md')
process.exit(1)
}
const res = await fetch(url, { headers: { 'x-cook-token': token }, signal: AbortSignal.timeout(30_000) })
const data = await res.json() as { ok?: boolean, code?: string, feedback?: FeedbackRow[], signals?: SignalRow[] }
if (!res.ok || !data.ok) {
console.error(`拉取失败:${res.status} ${data.code ?? ''}`)
process.exit(1)
}
const outDir = path.join(rootDir, '.cook/feedback')
fs.mkdirSync(outDir, { recursive: true })
fs.writeFileSync(path.join(outDir, 'export.json'), JSON.stringify(data, null, 2))
const recipes = new Map(parseRecipeCsv(fs.readFileSync(recipeCsvFile, 'utf-8')).map(r => [r.id, r]))
const reports = summarizeReports(data.feedback ?? [])
const signals = data.signals ?? []
const empties = signals.filter(s => s.type === 'empty').sort((a, b) => b.count - a.count).slice(0, 30)
const cases = buildEvalCases(signals)
fs.writeFileSync(path.join(rootDir, 'test/fixtures/search-eval.json'), `${JSON.stringify(cases, null, 2)}\n`)
const kindLabel: Record<string, string> = { video: '视频打不开', stuff: '食材或厨具不对', info: '难度或做法不对', other: '其他' }
const lines = [
`# 用户反馈汇总(${new Date().toISOString().slice(0, 10)})`,
'',
`反馈 ${data.feedback?.length ?? 0} 条,涉及 ${reports.length} 道菜;筛选信号 ${signals.length} 组。`,
'',
'## 菜谱反馈(按分数排序,登录用户的反馈算 2 分)',
'',
'| id | 菜名 | 分数 | 人数 | 问题 | 备注 |',
'| --- | --- | --- | --- | --- | --- |',
...reports.map((r) => {
const current = recipes.get(r.recipeId)
const name = current ? current.name : `${r.recipeName}(已不在数据中)`
const kinds = Object.entries(r.kinds).map(([k, n]) => `${kindLabel[k] ?? k}×${n}`).join('、')
return `| ${r.recipeId} | ${name} | ${r.score} | ${r.reporters} | ${kinds} | ${r.notes.slice(0, 3).join(';').replace(/\|/g, '/')} |`
}),
'',
'## 没有结果的食材组合(缺哪些菜)',
'',
'| 食材 | 厨具 | 模式 | 次数 |',
'| --- | --- | --- | --- |',
...empties.map(s => `| ${s.stuff.join('、')} | ${s.tool || '-'} | ${s.mode} | ${s.count} |`),
'',
'筛选排序的评测见 `pnpm eval:search`。',
'',
]
fs.writeFileSync(path.join(outDir, 'summary.md'), lines.join('\n'))
console.log(`已写入 .cook/feedback/summary.md,评测样例 ${cases.length} 组`)