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
2
3
4
5
6
7
8
9
10
<button
type="button"
disabled={
!onDeleteChapter ||
!activeOutlineId ||
!nodes.some((n) => n.id === activeOutlineId && n.kind === "chapter")
}
onClick={() => {
if (!onDeleteChapter || !activeOutlineId) return;
onDeleteChapter(activeOutlineId);

也就是说:卷、节根本没有走这条删除路径,不是「配置删不掉」,而是 UI 根本没提供对它们的删除。


2. 为什么感觉「没法新建卷、节」?

侧栏里只有 「+ 新增章节」,逻辑是:若没有卷,就自动塞一个空卷,再把新章挂到这个卷下面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const next = [...nodes];
let parentVolumeId = next.find((n) => n.kind === "volume")?.id;
if (!parentVolumeId) {
const vId = makeNodeId("plot-volume");
next.push({
id: vId,
kind: "volume",
title: "",
summary: "",
});
parentVolumeId = vId;
}

next.push({
id: makeNodeId("plot-chapter"),
kind: "chapter",
title: targetTitle,
summary: "",
parentId: parentVolumeId,
});

所以没有「显式新建卷 / 新建节」的入口;节要靠数据里自己带 section 节点或别处写入,侧栏没有对应按钮。


3. 为什么不能把章节挪到别的卷 / 节?

拖拽结束后用的是 reorderOutlineFlat,里面写死了:两个节点的父节点必须相同,否则直接放弃重排:

1
2
3
4
5
6
7
8
9
10
11
export function reorderOutlineFlat(
flat: PlotNode[],
activeId: string,
overId: string,
): PlotNode[] | null {
if (activeId === overId) return flat;
const roots = flatToOutlineTree(flat);
const pA = parentOfInTree(roots, activeId);
const pB = parentOfInTree(roots, overId);
if (pA === undefined || pB === undefined) return null;
if (pA !== pB) return null;

因此:跨卷、跨节(换父节点)在代码层面就是不允许的,不是 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 一带的模式)。

六万本会卡在哪里

  1. 列表 / 聚合 / 搜索:没有数据库索引时,任何「遍历全部发布记录」都会随本数线性变慢;六万次读文件在常规磁盘上会很吃力。
  2. 单目录海量文件:六万个文件在同一目录,部分文件系统与备份工具会变慢;运维也不方便。
  3. 并发与一致性:多实例部署时,多进程写同一批 JSON 没有事务,容易冲突或丢更新。
  4. 体量与备份:六万本 × 每本正文体积,全在应用机磁盘上,备份、迁移、容灾都要单独设计。
  5. 图片等资源:若仍走本机或单服务,带宽和存储也要和「本数、章节数」一起算。

什么情况下「能跑」但不算架构达标

单机、内存够、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 策略:沿用现有 novelIdarticleIdchapterId,避免读者链接全断。

阶段 1:引入 DB 与仓储抽象(不改行为)(约 1–2 周)

  • 搭库:建库、迁移工具(Prisma / Drizzle / Kysely 等任选),仅新增表,不接业务
  • 定义 Storage 接口:例如 getPublishRecordlistPublicArticles({ cursor, limit })getStructuresaveStructure……实现两个 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 用 SQL WHERE 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. 给工单系统加图片

第六天开发进度

  1. 有声书上传
  2. 有声书编辑标题/详情
  3. 有声书读者播放

第七天开发进度

  1. 批量txt上传
    https://github.com/whale3070/freeA2B

  2. 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_modelsoutput_langcustom_prompt(可选)

服务端内部复用现有逻辑:buildAuditInstructionBlockrunSingleModelAudit、聚合与 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_BLOCKEND_RUBRIC_SCORES_BLOCK 以及结尾 AI_SCORE:(见 buildAuditInstructionBlock)。SDK 应把 响应里的 reports[].contentavg_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
{ "file_name": "sdk-doc.md", "round_id": "default", "bytes": 1234 }

之后可直接调用现有 POST /api/audittarget_file 填同一 file_nameround_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 / 钱包头等保护。