chore: 导入清理前的完整工程状态(含 Capacitor 原生壳,作为回滚备份点)
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
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} 组`)
|
||||
Reference in New Issue
Block a user