79 lines
3.5 KiB
TypeScript
79 lines
3.5 KiB
TypeScript
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} 组`)
|