Files
recipe_tool/README.md
T

222 lines
10 KiB
Markdown
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.
# 食谱工具(recipe_tool)
一款面向幼儿园食堂的食谱处理工具集,目前包含两大功能:
1. **食谱翻译工具** —— 基于 AI 的食谱文档翻译:解析 Word 文档、智能翻译菜名、批量导出双语对照 Word
2. **135编辑器转换器** —— 将幼儿园周食谱 `.docx` 一键转换可直接粘贴进 [135编辑器](https://www.135editor.com/) / 微信公众号排版器的 HTML
## 功能特性
### 食谱翻译工具
- **Word 文档解析**:解析 `.docx` 格式的食谱文档,提取菜品信息
- **AI 智能翻译**:支持 OpenAI / 智谱 GLM 等大模型 API,批量翻译菜品名称
- **翻译词库管理**:管理常用词组翻译对照表,命中词库免调 API
- **多格式导出**:导出双语对照的 Word 文档
- **导出设置**:自定义未翻译文本颜色、页面方向、第二页表头等
### 135编辑器转换器(`/editor135`)
- **一键转换**:上传周食谱 `.docx` → 自动解析「星期 × 餐次(早餐/早点/午餐/体弱儿餐/午点)」中英菜名 → 填充进 135 模板生成 HTML
- **三视图展示**:左侧源代码(可复制/下载)与解析结果(按星期折叠面板),右侧 375px 移动端宽度实时预览(与公众号阅读宽度一致,内部滚动、吸顶)
- **缺失星期自动删除**:docx 里缺哪天(含周六/周日、间隔天)就整段移除该天内容,导出的 HTML 不包含;支持「周一/礼拜天」等别名归一化
- **逐条回读校验**:填充后重新解析输出 HTML,与原始数据逐条比对,`mismatch` 必须为空才可信
- **样式零污染**:填充只改文本节点,不改样式与结构;构建前后做样式/图片/section 数快照对比
- **微信图片代理**:预览时微信 CDN(`mmbiz.qpic.cn`)图片走本地代理转发,解决「此图片来自微信公众平台」防盗链占位图;复制/下载的 HTML 仍保持原始 URL,不影响公众号内使用
- **智能容错**:裸文本菜名自动套用模板样式块、条目数与模板不符时自动克隆补足/删除多余
## 技术栈
- **框架**:Next.js 16(App Router, Turbopack)
- **语言**:TypeScript(strict)
- **运行时**:Node.js ≥ 22(jsdom 30 依赖新 Node API)
- **包管理**:pnpm ≥ 11(配置全部位于 `pnpm-workspace.yaml`)
- **样式**:Tailwind CSS 4 + shadcn/ui + radix-ui
- **文档解析**:mammoth(docx → HTML)、cheerio、jsdom(模板填充)
- **文档生成**:docx
- **数据库**:MySQL 8.0(mysql2)
- **AI**:OpenAI SDK / 智谱 GLM
## 项目结构
```
├── app/ # Next.js App Router
│ ├── api/ # API 路由
│ │ ├── ai/ # AI 翻译接口
│ │ ├── config/ # 配置管理接口
│ │ ├── editor135/
│ │ │ ├── render/ # 135转换:解析docx并填充模板
│ │ │ └── img/ # 微信CDN图片代理(仅允许 *.qpic.cn)
│ │ ├── recipe/ # 食谱解析接口
│ │ ├── translate/ # 翻译接口
│ │ ├── update/ # 应用自更新(check/execute)
│ │ └── words/ # 词库管理接口
│ ├── editor135/ # 135转换器页面
│ ├── settings/ # 设置页面
│ │ ├── ai/ # AI 配置
│ │ ├── export/ # 导出设置
│ │ ├── translation/ # 翻译词库
│ │ └── update/ # 应用更新
│ └── page.tsx # 首页(功能导航)
├── components/ # React 组件
├── lib/
│ ├── ai-translate.ts # AI 翻译
│ ├── config.ts # 配置管理
│ ├── database.ts # 数据库连接
│ ├── editor135-core.ts # 135转换核心库(解析/填充/缺失天删除/校验)
│ └── update.ts # 自更新逻辑
├── public/
│ └── template135.html # 135模板(运行时读取,替换此文件即可换模板)
├── scripts/
│ └── test-remove-days.ts # 135转换核心库回归测试(6场景)
├── config.json # 配置文件(首次启动由环境变量初始化)
├── Dockerfile # Docker 构建(node:22-alpine 双阶段)
└── docker-compose.yml # Docker 编排
```
## 快速开始
### 环境要求
- Node.js **22+**(jsdom 30 要求)
- pnpm 11+(构建脚本批准配置在 `pnpm-workspace.yaml`)
- MySQL 8.0(翻译功能需要)
### 本地开发
```bash
# 1. 安装依赖
pnpm install
# 2. 配置环境变量
# 参考下方「环境变量」表格创建 .env,或从已有部署复制
# 3. 启动开发服务器
pnpm dev
```
访问 http://localhost:3000 ,翻译工具在首页,135转换器在 `/editor135` 或首页右上角「135编辑器转换」入口。
### 生产构建
```bash
pnpm build
pnpm start
```
### Docker 部署
```bash
# 1. 配置环境变量
cp .env.docker .env # 或手动创建,见下方表格
# 2. 构建并启动
docker-compose up -d
# 3. 查看日志
docker-compose logs -f app
```
## 配置说明
### 配置文件
所有配置存储在 `config.json` 中,可通过设置页面修改(首次启动由环境变量初始化,之后以 `config.json` 为准):
| 配置项 | 说明 |
|--------|------|
| `db` | 数据库连接配置(host / port / user / password / database) |
| `ai` | AI 翻译 API 配置(apiKey / baseUrl / model) |
| `export` | 导出文档格式配置(untranslatedColor / orientation / secondPageHeader) |
### 环境变量
| 变量名 | 说明 | 默认值 |
|--------|------|--------|
| `DB_HOST` | 数据库地址 | 127.0.0.1 |
| `DB_PORT` | 数据库端口 | 3306 |
| `DB_USER` | 数据库用户名 | root |
| `DB_PASSWORD` | 数据库密码 | - |
| `DB_NAME` | 数据库名 | recipe_tools |
| `AI_API_KEY` | AI API Key | - |
| `AI_BASE_URL` | API 地址 | https://api.openai.com/v1 |
| `AI_MODEL` | 模型名称 | gpt-3.5-turbo |
| `EXPORT_UNTRANSLATED_COLOR` | 未翻译文本颜色 | #FF0000 |
| `EXPORT_ORIENTATION` | 页面方向 | landscape |
| `ONEDEV_ACCESS_TOKEN` | 自更新拉取镜像的令牌 | - |
| `REGISTRY_IMAGE` | 更新镜像仓库地址 | - |
## API 接口
### 翻译工具
| 接口 | 方法 | 说明 |
|------|------|------|
| `/api/config` | GET/POST | 获取/保存配置 |
| `/api/recipe` | POST | 解析食谱文档 |
| `/api/translate` | POST | 批量翻译 |
| `/api/ai/translate` | GET | 单词翻译测试 |
| `/api/words` | GET/POST | 词库列表/添加 |
| `/api/words/[id]` | PATCH/DELETE | 编辑/删除词条 |
### 135编辑器转换器
| 接口 | 方法 | 说明 |
|------|------|------|
| `/api/editor135/render` | POST | 上传 `.docx`(multipart `file` 字段),返回 `{ fileName, summary, recipe, html, report }` |
| `/api/editor135/img` | GET | 微信图片代理:`?u=<URL编码的图片地址>`,仅允许 `*.qpic.cn` 域名 |
`render` 返回的 `report` 结构:
```jsonc
{
"filled": [ // 每个餐次的填充明细
{ "day": "星期一", "meal": "早餐", "replaced": 4, "added": 0, "removed": 0, "oldCount": 4, "newCount": 4 }
],
"warnings": [], // 非致命提示(如模板中无该餐次)
"repaired": [], // 裸文本菜名自动套样式的修复记录
"mismatch": [], // 回读校验差异,正常必须为空数组
"removedDays": ["星期五"], // docx 缺失、已从输出整段删除的星期
"verification": { // 构建前后快照对比
"stylesAdded": 0, "stylesRemoved": 0, // 样式变化数,正常为 0
"imgsUnchanged": true, // 图片是否未被改动
"sections": "570 -> 570", "elements": "1502 -> 1242"
}
}
```
## 135编辑器转换器工作原理
1. **解析**:mammoth 将 docx 转为 HTML,逐叶子节点扫描,识别星期标题(支持 周一/礼拜一 等别名归一化)、餐次标题、中文菜名与紧跟的英文菜名,按「星期 → 餐次 → 条目」收集
2. **删除缺失天**:对比 docx 与模板中出现的星期,缺失的天用「逐节点精确区间删除」从模板 DOM 移除(从该天标题节点删到下一现存天标题之前;最后一天以「三审三校/温馨提示」为尾界)——算法处理了相邻天共用容器、深层嵌套等结构,不会误删保留天
3. **填充**:就地替换文本节点(文本未变不触碰),条目数不符时以模板最后一条为锚点克隆补足或从后往前删除多余
4. **校验**:重新收集输出 DOM 与原始数据逐条比对(`mismatch`),并对比构建前后的样式/图片/元素快照(`verification`)
5. **输出**:序列化 HTML 返回,可直接粘贴进 135编辑器 / 公众号后台
### 更换模板
替换 `public/template135.html` 即可。模板需包含:星期标题(星期一~星期日文本)、餐次标题(早餐/早点/午餐/体弱儿餐/午点)、以及至少一组「中文菜名块 + 紧邻的英文菜名块」作为克隆锚点。
### 运行测试
```bash
# Node 22+ 直接跑 TS(无需编译)
node --experimental-strip-types scripts/test-remove-days.ts
```
测试覆盖 6 个场景:间隔天(一/三/五)、含周末、仅周末、中间连续缺、条数增删路径、完整周回归。
## Docker 构建注意事项
- 基础镜像为 `node:22-alpine`(jsdom 30 需 Node ≥ 22,`builder` 与 `runner` 两个 stage 都要用 22)
- pnpm 11 的构建脚本批准配置在 `pnpm-workspace.yaml` 的 `allowBuilds`,该文件必须在 `pnpm install` 之前 COPY 进镜像
- `pnpm-workspace.yaml` 中已放行 `msw` / `sharp` / `unrs-resolver` 的构建脚本(pnpm 11 在 CI 下未批准会直接报 `ERR_PNPM_IGNORED_BUILDS`)
## 自更新机制
部署后可通过设置页「应用更新」拉取镜像仓库新版本:`/api/update/check` 检查、`/api/update/execute` 执行 `docker pull` 并重启容器(需要挂载 docker.sock 及 `REGISTRY_IMAGE` / `ONEDEV_ACCESS_TOKEN` 配置)。
## License
MIT