chenchen-plan3
第五天开发进度
1. 工单系统-新建页面
2. “AI 生成后人工微调”
你这个需求我已经改了,发布配置现在支持“AI 生成后人工微调”。
已实现效果
在发布弹窗 发布配置 里:
- 简介从只读展示改为可编辑
textarea- AI 生成后会填进去
- 你可以继续手动改内容
- 标签支持手动管理
- 输入框可添加标签(回车/点击“添加”)
- 支持逗号分隔批量添加
- 点击标签可移除(
#标签 ×)
- 文案也改了,明确是“AI 自动生成,可手动微调”,不再像写死内容
修改文件
apps/web/src/components/publish-novel-modal.tsx
并且已检查 lint,无报错。
如果你愿意,我下一步还可以给标签加“智能推荐 + 手动锁定”,避免你手动改过后点“重新生成”把你自定义标签覆盖。
3. 发布后过长内容撑破页面
已修复“发布后过长内容撑破页面”的问题,主要是给读者页和书库列表补了强制换行策略。
本次修复点
apps/web/src/app/library/[articleId]/page.tsx- 标题、简介支持超长自动换行(
break-words+overflow-wrap:anywhere) - 作者地址改为
break-all,避免长钱包地址撑宽 - 正文渲染区域增强:
- 整体强制断词
- 链接、代码可断行
pre保留横向滚动,避免布局炸裂- 表格单元格支持换行,不会把页面顶开
- 标题、简介支持超长自动换行(
apps/web/src/app/library/page.tsx- 列表卡片标题、简介支持超长自动换行
- 文章 ID 使用
break-all
结果
长文本(尤其无空格长串、长 URL、长英文、连续符号)不会再撑破卡片或正文容器,视觉上会稳定很多。
并且我已经跑了 lint,两个文件都无报错。
4. 英文(或其他翻译语言)阅读时会保留原文图片位置,不再整章丢图。
已修复:英文(或其他翻译语言)阅读时会保留原文图片位置,不再整章丢图。
我做了什么
文件:apps/web/src/app/api/v1/library/articles/route.ts
之前逻辑是:
- 译文存在时直接
plainTextToHtml(translated) - 这一步会把原章节的
<img>全部丢掉
现在改成:
- 从原章节
contentHtml先提取图片占位符(按顺序记录) - 按原文段落位置比例把图片 token 回填到译文段落里
- 再渲染为 HTML,并把 token 还原成原始
<img>标签
也就是说:译文换成目标语言,但图片仍保留在对应阅读位置附近。
结果
?lang=en等翻译语言页面,图片不会再消失- 未翻译章节仍保持原行为
并且我已做 lint 检查,无报错。
5. 英文语言下仍有中文残留
已修复你说的“英文语言下仍有中文残留”的两个关键点(读者详情页):
Language显示不再出现“英文区”这类中文标签- 英文界面会强制显示
English / Japanese / ...等英文语言名(基于article.language)
- 英文界面会强制显示
- 章节目录和章节标题在英文界面下不再显示中文章名
- 若章节标题是中文(如“第六章 …”),会自动显示为
Chapter N
- 若章节标题是中文(如“第六章 …”),会自动显示为
修改文件
apps/web/src/app/library/[articleId]/page.tsx
具体做法
- 新增
localizedLanguageLabel(...):英文 UI 下优先用语言代码映射英文名 - 新增
displayChapterTitle(...):英文 UI 下遇到中文章节名时兜底为Chapter {index+1} - 替换了 4 处显示:
- 语言分区文案
- TOC 按钮标题
- 当前章节标题
- TOC 折叠时“当前章节”文案
并且已经过 lint 检查,无报错。
6. 删不掉卷、节
你的感受和当前实现是对得上的:大纲数据模型里虽然有 volume / chapter / section,但侧边栏只把「章节」当成一等公民来删、来加;排序逻辑也故意锁在「同一父节点」里。
1. 为什么「删除章节」删不掉卷、节?
「删除章节」按钮只有在当前选中节点是 chapter 时才可用:
1 | |
也就是说:卷、节根本没有走这条删除路径,不是「配置删不掉」,而是 UI 根本没提供对它们的删除。
2. 为什么感觉「没法新建卷、节」?
侧栏里只有 「+ 新增章节」,逻辑是:若没有卷,就自动塞一个空卷,再把新章挂到这个卷下面:
1 | |
所以没有「显式新建卷 / 新建节」的入口;节要靠数据里自己带 section 节点或别处写入,侧栏没有对应按钮。
3. 为什么不能把章节挪到别的卷 / 节?
拖拽结束后用的是 reorderOutlineFlat,里面写死了:两个节点的父节点必须相同,否则直接放弃重排:
1 | |
因此:跨卷、跨节(换父节点)在代码层面就是不允许的,不是 dnd-kit 单独的问题,而是这层业务函数的限制。
4. 和「只要章节、不要卷节」的关系
- 若你不需要卷:现在仍会在「第一次加章」时自动加一个
volume,且没有侧栏删除卷,就会留下一个删不掉的空壳卷(除非改数据或以后做「删卷并提升子节点」等功能)。 - 纯扁平章节在类型上是可行的(例如章的
parentId为空、全部在根上),但当前「新增章节」总是挂到第一个volume上,没有「扁平模式」开关。
若要改进,一般会动这几块(供你或 Agent 模式实现时参考)
| 需求 | 方向 |
|---|---|
| 删卷/删节 | 增加删除非章节点;删卷时决定是否把子章节提升到上一级或合并到另一卷 |
| 只要章节 | 「扁平大纲」模式:新章 parentId 不设,或允许删除唯一卷时把子节点提升 |
| 新建卷/节 | 侧栏增加按钮,往 nodes 里 push 对应 kind 并设好 parentId |
| 跨卷调整 | 扩展 reorderOutlineFlat(或单独 reparentOutlineNode):在拖到另一父节点上时改 parentId 并插入顺序;dnd-kit 可能要配合「拖到容器上」的命中逻辑 |
我这边是 Ask 模式,不能直接改仓库。如果你希望把这些行为做进项目,可以切到 Agent 模式 说明优先级(例如:先支持删卷+提升子章节,还是先支持跨卷拖拽)。
7. 假设我有六万本小说,那么这个构架带的动吗?
按你现在这套实现,六万本小说不能指望「架构自然带得动」,它更适合少量作品、单机或小规模部署。不是 Next.js 不行,而是数据层几乎全是本地 JSON 文件,没有按「六万本」做的检索、分片与缓存。
现在大致在干什么
- 作品结构、草稿、发布记录等落在项目下的
.data/里,基本是 「一书一文件」或「一事一文件」(例如structure下按authorId_novelId.json)。 - 读者端书库等逻辑里,会出现
readdir整个publish目录再逐个readFile这类 全量扫盘(见library/articles/route.ts一带的模式)。
六万本会卡在哪里
- 列表 / 聚合 / 搜索:没有数据库索引时,任何「遍历全部发布记录」都会随本数线性变慢;六万次读文件在常规磁盘上会很吃力。
- 单目录海量文件:六万个文件在同一目录,部分文件系统与备份工具会变慢;运维也不方便。
- 并发与一致性:多实例部署时,多进程写同一批 JSON 没有事务,容易冲突或丢更新。
- 体量与备份:六万本 × 每本正文体积,全在应用机磁盘上,备份、迁移、容灾都要单独设计。
- 图片等资源:若仍走本机或单服务,带宽和存储也要和「本数、章节数」一起算。
什么情况下「能跑」但不算架构达标
单机、内存够、SSD、请求量不大、且列表接口不做全量扫盘,有可能勉强撑一段时间,但那是堆机器和运气,不是可预期的产品设计;一旦要做推荐、搜索、分页、多机房,就会立刻撞上瓶颈。
若真要六万本量级,通常要补什么(方向)
- 元数据与列表:PostgreSQL / MySQL / 托管 DB,分页、索引、按作者/标签/时间查询。
- 正文:大字段或对象存储(S3/OSS),或至少与元数据分离。
- 缓存:热门书、书库首页 Redis 等。
- 图片:独立对象存储 + CDN。
- 应用层:多实例时避免多写同一文件,或上队列/锁。
结论:当前「文件 + 扫目录」的构架,不是为六万本准备的;六万本要当成正经内容平台,需要把存储和读路径换成可索引、可分页、可扩展的后端(数据库 + 对象存储是常见组合)。若你只想自用或几百本以内,现有方式可以理解;到「万」级就该规划迁移。
8. 迁移方案
下面是一套可落地、可分阶段的迁移方案,目标是把「扫盘 JSON」换成可索引、可分页、可扩展的后端。不绑定某一云厂商,用 PostgreSQL(或兼容库)+ 对象存储 作为默认组合即可。
阶段 0:定边界与目标(1–3 天)
- 列清单:现有
.data里都有哪些实体(发布记录、作品结构、草稿、人物、翻译、解锁记录、收款码等),各自读写 API 路径。 - 定 SLA:并发、列表页响应时间、是否要多实例、备份 RPO/RTO。
- 选栈
- 元数据 + 可检索列表:PostgreSQL(JSONB 可存半结构化大纲,后期再范式化)。
- 大文本 / 图片:对象存储(S3 兼容)+ CDN;库里只存
key/url/etag。
- 定 ID 策略:沿用现有
novelId、articleId、chapterId,避免读者链接全断。
阶段 1:引入 DB 与仓储抽象(不改行为)(约 1–2 周)
- 搭库:建库、迁移工具(Prisma / Drizzle / Kysely 等任选),仅新增表,不接业务。
- 定义
Storage接口:例如getPublishRecord、listPublicArticles({ cursor, limit })、getStructure、saveStructure……实现两个 adapter:FileStorageAdapter(当前.data逻辑搬进去);DbStorageAdapter(空实现或只写单元测试)。
- API 路由只调接口:
route.ts里不写readFile,只调storage.*,便于切换。
验收:功能与现在一致,部署仍用文件。
阶段 2:双写 + 后台回填(约 2–4 周)
- 建核心表(第一版可粗)
novels(author_id, novel_id, title, updated_at, …)publish_records(article_id, author_id, novel_id, visibility, synopsis, tags, layout_mode, published_at, …)novel_structures(novel_id, author_id, nodes JSONB, updated_at)— 或先整段存 JSONB,以后再拆chapters表。drafts(可选:与结构合并或单独 blob 引用)
- 双写:每次现有
POST成功写盘的同时 写 DB(失败策略要明确:记日志、重试队列,或「DB 失败则整请求失败」——建议至少 异步重试)。 - 回填脚本:一次性把
.data导入 DB(可反复跑、幂等,按updated_at或文件 mtime 比对)。 - 校验:抽样对比文件 vs DB 记录数、关键字段 hash。
验收:DB 里有全量镜像,线上仍以读文件为准(或读 DB 做 shadow 对比)。
阶段 3:读路径切换(分页与索引生效)(约 2–3 周)
- 书库列表 / 搜索:实现
listPublicArticles用 SQLWHERE visibility = 'public' ORDER BY published_at DESC LIMIT ?,cursor 分页(published_at, article_id)。 - 单书详情 / 读者章:按
article_id/novel_id主键或唯一索引读 DB;大正文若已迁对象存储则 读 URL 或拉 blob。 - 开关:环境变量
READ_FROM_DB=true灰度;可先只切「列表」,再切「详情」。 - 监控:慢查询、错误率、对比 shadow 读差异。
验收:核心读路径不再 readdir 全表;列表接口稳定分页。
阶段 4:停写文件、清技术债(约 1–2 周)
- 关闭双写中的文件写入(或只保留导出备份)。
- 删除或归档
.data热路径依赖;保留只读导出任务亦可。 - 大纲/章节:若仍用 JSONB,补 GIN 索引(按标签、作者查询);若章节级运营多,再拆
chapters表。 - 图片:上传接口改为直传对象存储,库里存元数据;读者端用 CDN URL。
验收:部署不依赖本地 .data 目录即可跑通主流程。
阶段 5:扩展能力(持续)
- 搜索:PostgreSQL
tsvector/ Meilisearch / OpenSearch(按产品需求)。 - 缓存:Redis 缓存热门书、首页列表(短 TTL + 失效策略)。
- 多实例:应用无状态;会话/限流用 Redis;DB 用托管高可用。
- 合规与备份:自动快照、跨区域复制、密钥轮换。
风险与顺序建议
| 风险 | 缓解 |
|---|---|
| 双写不一致 | 幂等 key、回填脚本、对账任务 |
| 大 JSON 性能 | 列表只查元数据表;正文/structure 可拆表或走对象存储 |
| 迁移窗口停机 | 先双写再切读,最后停写文件 |
不要第一步就改读者 URL 或删文件;要先抽象存储、双写、回填、再切读。
工作量量级(粗估,视团队与数据量)
- 阶段 1–2:中小团队 3–6 周
- 阶段 3–4:再 3–5 周
- 阶段 5:按产品迭代持续做
若你希望下一步落到「你们仓库里具体要先动哪几个 route.ts、表字段怎么画」,可以切换到 Agent 模式,我可以按你当前 apps/web 的 API 列表拆一版更贴代码的迁移 checklist(仍不直接改生产配置,除非你明确要求)。
9. 我要服务一万个作者,每个作者假设有100个读者。这个网站的构架撑得住吗?然后我还要存六万本免费书籍。
分三块说:能不能撑住取决于你怎么定义「撑住」,以及当前仓库这种以本地文件为主的做法。
1. 规模到底是多少(先对齐数字)
- 1 万作者
- 每位作者 100 个读者:若指 100 个不同的人/钱包,全站约 100 万读者身份(不是「100 次访问」)。
- 6 万本免费书:和「书」相关的存储与检索规模。
读者端还有:打开书、书库列表、解锁/付费记录、统计 等,都会放大读写量。
2. 和当前架构的关系(坦诚结论)
把「能存下」和「能稳定服务」分开看:
| 维度 | 粗判断 |
|---|---|
| 6 万本书「占磁盘」 | 纯 JSON 文件往往装得下,但单目录几万文件、备份、迁移会难受。 |
| 6 万本书「被读者用」 | 若书库/搜索仍靠 扫目录 + 逐文件读,请求一多就会 慢、抖、难扩容。 |
| 1 万作者写稿/发布 | 并发写同一套文件、多机部署时,没有 DB/队列 很容易出一致性和锁的问题。 |
| 约 100 万读者 | 只要有 登录/钱包、书架、解锁记录、访问日志,文件里按 article_wallet 一类存,文件数会爆炸,也不适合查询与分页。 |
所以:「六万本书当冷数据堆在盘上」也许勉强;「一万作者 + 百万读者 + 六万本书当线上产品跑」——以你现在这种架构,不能算撑得住,需要 数据库 +(可选)对象存储 + 缓存/CDN,并改掉「全量扫盘」的读路径。
3. 还要关心的不是「本数」而是「请求量」
同样 6 万本书:
- 若平均每天每本书 10 次阅读,已是 60 万次/天 量级的业务读(还要乘章节、图片)。
- 若再带搜索、推荐、排行榜,没有索引和缓存,单机 API 会很快顶满。
所以评估时要补两句:峰值 QPS、是否多机房、是否必须 99.9% 可用。没有这些数字,只能说量级上必须按「平台级」做后端,而不是「单机 JSON」。
4. 一句话建议
- 目标:1 万作者、约 100 万读者、6 万本免费书 → 按 小型内容平台 规划。
- 最低配:PostgreSQL(元数据 + 索引 + 分页)+ 大正文/图片走 对象存储 + CDN;读者相关关系、解锁、统计进表,不要一人一文件无限长。
- 当前仓库形态:更适合 演示 / 内测 / 小规模;直接扛上述规模 风险大,应执行前面说的分阶段迁库(双写 → 切读 → 停文件)。
如果你愿意,可以补充三句话:是否多实例部署、预期日活/峰值并发、书是否全站公开列表,我可以把「必须上的组件」缩成更短的一张 checklist(仍可在 Ask 模式下给方案,不写代码)。
10. 给工单系统加图片
第六天开发进度
- 有声书上传
- 有声书编辑标题/详情
- 有声书读者播放
第七天开发进度
Aura SDK
根据对 /root/Aura-judgement-system 的阅读,可以这样理解现状和「做成 SDK」的可行路径。
系统实际分工
- 评审与规则解析的核心在 Go:路由和业务集中在
backend/rebuild_backend.go(例如POST /api/audit、规则读写、调用多模型、算分、写结果 JSON)。 - 前端(React/Vite) 主要是管理台与展示;类型与调用方式可参考
src/lib/api.ts里的submitAudit等。
也就是说,「Aura」对外可复用的能力,本质是 跑起来的 Go HTTP 服务 + 一套约定好的请求/响应,而不是前端本身。
当前 API 与「文档 + YAML」的错位
你希望:传入 txt/md 正文 + YAML 规则 → 得到评估结果。
而现有 POST /api/audit 的行为是(见 postAudit):
- 读
target_file,且该文件必须已经存在于对应round_id的 word 目录里(服务端本地路径),不是请求体里直接带全文。 - YAML 规则来自服务端
rules/目录里当前激活的规则版本(loadEffectiveRuleYAMLForRound);请求里没有rules_yaml字段。另:该函数里roundID被忽略,规则实质上是全实例共享的一份 active 规则,不是按轮次隔离(见loadEffectiveRuleYAMLForRound中_ = roundID)。
规则可通过 POST /api/rules/upload(body: { "rawYAML": "..." })上传;实现里会写入 rules/ 并自动设为 active(见 postRulesUpload)。
因此:「只调 HTTP、不传文件到磁盘」 在现状下缺一环——没有「把任意 md/txt 写进 word 目录」的通用上传接口(文档主要来自提交/GitHub ingest 等路径)。
做成 SDK 的三种常见做法
1. 薄封装:各语言 HTTP Client SDK(最少改 Aura)
封装现有端点,例如:
uploadRules(yaml)→POST /api/rules/upload- (可选)
activateRules(versionId)→POST /api/rules/activate audit(targetFile, models, outputLang, roundId)→POST /api/audit
前提:调用方能让 target_file 出现在服务端的 word 目录——例如:
- 与 Aura 同机或共享卷,由集成方写文件;或
- 在后端增加一个例如
POST /api/word/upload/POST /api/audit-inline的接口,接受document+filename(以及理想情况下 per-request 的rules_yaml),这样 SDK 才符合「给 txt/md + yaml 就得结果」的产品形态。
2. 推荐的产品形态:新增「内联评审」API(后端小扩展 + SDK)
增加例如:
POST /api/evaluate(命名随意),body 包含:
document(string 或 base64)、filename(用于落盘或仅审计日志)rules_yaml(或rule_version_id)selected_models、output_lang、custom_prompt(可选)
服务端内部复用现有逻辑:buildAuditInstructionBlock、runSingleModelAudit、聚合与 SavedResult 结构(与 postAudit 类似)。
这样 Python/Node/Go SDK 都只是一层 HTTP + 类型定义,不依赖调用方碰服务器文件系统;若需要 per-request 规则,应避免只靠全局 rules/index.json,而是在该请求内解析传入的 YAML(或临时版本 id)。
3. Go 内嵌库(供其它 Go 程序 import)
把 rebuild_backend.go 里与评审相关的部分抽成可导入的包(例如 internal/audit 或公开模块 github.com/yourorg/aura/sdk),对外暴露:
Evaluate(ctx, document []byte, rulesYAML string, modelIDs []string) (*Result, error)
非 Go 语言则仍可通过 cgo/子进程调用 CLI 或 本地起 HTTP 使用,但维护成本通常高于「HTTP + 内联 API」。
规则与输出格式(SDK 文档里应写清)
后端会用 YAML 里的 dimensions 等拼出提示词,并要求模型输出 RUBRIC_SCORES_BLOCK … END_RUBRIC_SCORES_BLOCK 以及结尾 AI_SCORE:(见 buildAuditInstructionBlock)。SDK 应把 响应里的 reports[].content 和 avg_score / rubric_raw_max / 字母档 等字段当作稳定契约(与前端 AuditResponse / SavedResult 对齐)。
可参考示例规则:examples/aura-five-dim-ladder-rules.yaml。
简要结论:
- 今天要接 Aura,最贴近代码现状的是 「HTTP SDK + 能写入 word 目录或扩展上传 API」。
- 若目标严格是 「任意程序传入 md/txt + YAML 就得结果」,建议在 Go 后端增加 内联文档 +(建议)内联规则 的接口,再为各语言生成薄 SDK。
我当前是 Ask 模式,无法直接改仓库或加接口;若你切换到 Agent 模式,可以指定优先语言(Python / TypeScript / Go)和是否要 per-request 规则,我可以按上述方向具体设计接口字段与调用流程。
Aura上传接口
已接入 POST /api/word/upload,并通过 go build 验证通过。变更摘要如下。
新接口:POST /api/word/upload
请求体(JSON)
| 字段 | 说明 |
|---|---|
file_name |
必填,仅 basename;扩展名只能是 .md、.markdown、.txt;主名需匹配 ^[a-zA-Z0-9][a-zA-Z0-9._-]*$ |
content |
必填,文件正文 |
round_id |
可选,缺省用环境变量 AURA_DEFAULT_ROUND_ID(默认 default) |
encoding |
可选;空 / utf8 / plain / text 表示 UTF-8 明文;base64 表示标准 base64 解码后写入 |
约束
- 解码后长度 1~20 MiB(常量
maxWordUploadBytes) - 文件写入
word/<round_id>/<file_name>,并会ensureRoundDirs(roundID)(含该轮次的 word / judge-result / submissions 目录)
成功响应示例
1 | |
之后可直接调用现有 POST /api/audit,target_file 填同一 file_name,round_id 一致即可。
代码位置
- 文件名校验:
round_paths.go中的sanitizeWordUploadFileName - 处理器:
rebuild_backend.go中的postWordDocumentUpload - 路由:
r.POST("/api/word/upload", postWordDocumentUpload)(在/api/audit之前注册)
附带调整
GET /api/files 现在会列出该轮次 word 目录下的 .md / .markdown / .txt,与上传类型一致,便于前端/管理台看到通过本接口上传的 .txt 文件。
说明:与 POST /api/rules/upload 一样,本接口未加管理员鉴权;若部署在公网,建议用反向代理或后续加 Token / 钱包头等保护。