MVP 上传 TXT,校验编码后导入本人书库 #9

Closed
opened 2026-09-10 17:09:14 +08:00 by ila · 5 comments
Owner

来源与目标

2026-09-10 用户确认 F01–F12、原型通过,并要求按四阶段建议推进。原型验收:#1 评论 7498。阶段 4;覆盖 F03、F04 补齐。

学习者选择 TXT 文件,经大小、空文件和编码校验后进入同一处理/阅读流程,失败可重试。

验收标准

  • 明确支持编码、BOM、换行和大小上限,给出有效、空、超限、错误编码样例;不偷偷替换损坏字符。
  • 文件访问按本人授权,临时文件生命周期明确,路径和文件名不可用于越界访问。
  • 沿用同一任务幂等与恢复机制;重复提交、失败重试不重复生成章节。
  • 两账号隔离与有效/无效文件浏览器路径通过;范围不含 EPUB/PDF/字幕。

依赖与执行

前置:#5。状态:已完成(2026-09-13 用户验收通过,验收对象 fb256d4,PR #28 已合入 main,验收记录见最新评论)。前置尚未通过时不得标进行中;每单完成停在待验收,由用户验收后关闭。技术验证可与不依赖其结论的工作分工,但不提前冻结未验证契约。

参考模块与设计证据

复用已建立导入任务领域;参考 go-admin 文件接口模式但重新校验归属和存储路径。

沿用已验收 TXT 与错误状态,不另画全套。

后端不为建表/API 单独画页面原型;先写数据、接口、状态、隔离与幂等契约。学习端采用 #1 已验收 v1,未做的 LinguaCafe 对照不标完成。

工作量、范围与风险

预计 3~5 人日(8 小时/人日),包含本单设计、前后端实现、相关测试、修正和文档;是规划估算,不是交付日期或 AI 运行时间。仅 F 范围,X 系列不纳入。数据变更先在隔离测试库验证迁移/回退,保留既有数据和原型。

文档与证据

实施时按影响更新 Architecture-and-Code-Map、Business-Rules-and-Glossary、Local-Development-and-Verification;需求变化更新 Product-Requirements-Overview,交付单补实际部署恢复文档。Wiki 先写再回读同步;结束评论记录测试、未验证内容、提交及 revision。工单正文保留基线,重要变化追加评论。

Gitea MCP 指向其他站点,沿用目标 git.ilapage.cn API 回退;凭据仅进入进程。

## 来源与目标 2026-09-10 用户确认 F01–F12、原型通过,并要求按四阶段建议推进。原型验收:#1 评论 7498。阶段 4;覆盖 F03、F04 补齐。 学习者选择 TXT 文件,经大小、空文件和编码校验后进入同一处理/阅读流程,失败可重试。 ## 验收标准 - [ ] 明确支持编码、BOM、换行和大小上限,给出有效、空、超限、错误编码样例;不偷偷替换损坏字符。 - [ ] 文件访问按本人授权,临时文件生命周期明确,路径和文件名不可用于越界访问。 - [ ] 沿用同一任务幂等与恢复机制;重复提交、失败重试不重复生成章节。 - [ ] 两账号隔离与有效/无效文件浏览器路径通过;范围不含 EPUB/PDF/字幕。 ## 依赖与执行 前置:#5。状态:已完成(2026-09-13 用户验收通过,验收对象 fb256d4,PR #28 已合入 main,验收记录见最新评论)。前置尚未通过时不得标进行中;每单完成停在待验收,由用户验收后关闭。技术验证可与不依赖其结论的工作分工,但不提前冻结未验证契约。 ## 参考模块与设计证据 复用已建立导入任务领域;参考 go-admin 文件接口模式但重新校验归属和存储路径。 沿用已验收 TXT 与错误状态,不另画全套。 后端不为建表/API 单独画页面原型;先写数据、接口、状态、隔离与幂等契约。学习端采用 #1 已验收 v1,未做的 LinguaCafe 对照不标完成。 ## 工作量、范围与风险 预计 3~5 人日(8 小时/人日),包含本单设计、前后端实现、相关测试、修正和文档;是规划估算,不是交付日期或 AI 运行时间。仅 F 范围,X 系列不纳入。数据变更先在隔离测试库验证迁移/回退,保留既有数据和原型。 ## 文档与证据 实施时按影响更新 Architecture-and-Code-Map、Business-Rules-and-Glossary、Local-Development-and-Verification;需求变化更新 Product-Requirements-Overview,交付单补实际部署恢复文档。Wiki 先写再回读同步;结束评论记录测试、未验证内容、提交及 revision。工单正文保留基线,重要变化追加评论。 Gitea MCP 指向其他站点,沿用目标 git.ilapage.cn API 回退;凭据仅进入进程。
Author
Owner

#9 方案确认与实施启动(2026-09-11)

用户在当前会话确认「都按你说的」,同意本单契约 D1~D8。前置 #5 已通过用户验收;本单分支 feat/9-txt-upload 从 main cd2b893 创建,实施期间工单状态进入「进行中」,完成测试后停在「待验收」。

契约

D1 支持编码 = UTF-8(允许可选 UTF-8 BOM):解码时剥离 BOM,BOM 不进入原文;其余字节必须整体合法,任何非法序列直接拒绝,不使用替换字符(验收要求「不偷偷替换损坏字符」)。UTF-16 按 BOM(FF FE/FE FF)单独识别并提示「请另存为 UTF-8」。GB18030 等其他编码按非法 UTF-8 拒绝。UTF-16 支持不在本单范围。

D2 大小上限 = 2 MiB 字节,解码后再套用 #5 同一套规则(非空、≤100000 码点)。超限返回 400 并明确提示,不截断。

D3 不落盘:文件只存在于内存,解码后直接交给现有 PasteBook/PasteChapter。不创建任何临时文件,所以「临时文件生命周期」为「无临时文件」;客户端文件名不参与任何路径、也不入库,只在选择文件时显示并可预填标题。并发上传复用词典导入的单槽门,忙时 429。

D4 接口(与粘贴成对,返回同一 PasteResult)

接口 说明
POST /api/v1/books/upload multipart:requestId、title、language、file;新建书籍与首章
POST /api/v1/books/:id/chapters/upload multipart:requestId、title、file;追加到本人书籍

字段白名单、未知字段拒绝、非 multipart 拒绝;201 新建、200 重复、409 同编号换内容、404 他人书籍、400 校验失败,与粘贴路径逐项一致。幂等沿用 requestId + 内容 SHA:重复上传同一文件只产生一章。

D5 分章 = 一次导入一章:与粘贴完全一致的固定策略,本单不改分章规则,也不做按空行自动分章。

D6 前端:ImportView 增加「粘贴文本 / TXT 文件」切换(原型已有该切换与「reading.txt · UTF-8 · 2 KB」状态行)。选择文件后客户端做提示性预检(.txt 扩展名、字节数、TextDecoder('utf-8', {fatal: true}) 预解码)并显示文件名 · 编码 · 大小;标题为空时用文件名(去扩展名、截断 120 字符)预填;提交用 FormData 走新接口,成功后的跳转与轮询复用现有 store 逻辑,失败保留已填内容。客户端预检只提前反馈,服务端结论为最终结论。

D7 空文件、只有 BOM、只有空白 → 400「请输入正文内容」,与粘贴一致。

D8 不改 schema:lexgo_chapters/lexgo_ingest_jobs 已够用,无迁移;回退 = 换回旧二进制。文件名不入库;「导入来源」溯源字段另立范围。

测试计划

  • Go 单测:解码与校验的有效、BOM 剥离、非法 UTF-8、UTF-16 BOM、空、只有 BOM、NUL、超限与恰好边界(2 MiB、100000 码点)。
  • Go 集成(专用库 lexgo_test_issue9):multipart 上传 → 201 → worker → ready → 阅读器原文与文件字节级一致(含 CRLF、emoji、组合字符);重复上传同一文件 → 200 duplicate 且只有一章;同 requestId 换内容 → 409;缺 file、多余字段、非 multipart → 400;追加他人书籍 → 404;两账号隔离;恶意文件名(..\..\evil.txt)只作为显示名、不影响存储。
  • 学习端:上传模式单测(切换、预检、FormData、成功跳转、追加、错误保留表单)+ mock E2E + 真实链路(真实 API 上传真实临时 TXT,核对阅读器原文逐字符一致)。
  • 文档:更新 Architecture-and-Code-Map、Business-Rules-and-Glossary、Local-Development-and-Verification,需求变化更新 Product-Requirements-Overview;Wiki 先写后回读再同步镜像。

非目标:EPUB/PDF/字幕(明确排除)、UTF-16/GB18030、按空行自动分章、文件名入库、断点续传。

回退说明:本会话 pi 无可用 Gitea MCP 工具,沿用本单既定回退,使用目标 git.ilapage.cn API;凭据仅从既有安全配置读入进程。

## #9 方案确认与实施启动(2026-09-11) 用户在当前会话确认「都按你说的」,同意本单契约 D1~D8。前置 #5 已通过用户验收;本单分支 `feat/9-txt-upload` 从 main `cd2b893` 创建,实施期间工单状态进入「进行中」,完成测试后停在「待验收」。 ### 契约 **D1 支持编码 = UTF-8(允许可选 UTF-8 BOM)**:解码时剥离 BOM,BOM 不进入原文;其余字节必须整体合法,任何非法序列直接拒绝,不使用替换字符(验收要求「不偷偷替换损坏字符」)。UTF-16 按 BOM(`FF FE`/`FE FF`)单独识别并提示「请另存为 UTF-8」。GB18030 等其他编码按非法 UTF-8 拒绝。UTF-16 支持不在本单范围。 **D2 大小上限 = 2 MiB 字节**,解码后再套用 #5 同一套规则(非空、≤100000 码点)。超限返回 400 并明确提示,不截断。 **D3 不落盘**:文件只存在于内存,解码后直接交给现有 `PasteBook`/`PasteChapter`。不创建任何临时文件,所以「临时文件生命周期」为「无临时文件」;客户端文件名不参与任何路径、也不入库,只在选择文件时显示并可预填标题。并发上传复用词典导入的单槽门,忙时 429。 **D4 接口**(与粘贴成对,返回同一 `PasteResult`) | 接口 | 说明 | |---|---| | POST /api/v1/books/upload | multipart:`requestId`、`title`、`language`、`file`;新建书籍与首章 | | POST /api/v1/books/:id/chapters/upload | multipart:`requestId`、`title`、`file`;追加到本人书籍 | 字段白名单、未知字段拒绝、非 multipart 拒绝;201 新建、200 重复、409 同编号换内容、404 他人书籍、400 校验失败,与粘贴路径逐项一致。幂等沿用 `requestId` + 内容 SHA:重复上传同一文件只产生一章。 **D5 分章 = 一次导入一章**:与粘贴完全一致的固定策略,本单不改分章规则,也不做按空行自动分章。 **D6 前端**:ImportView 增加「粘贴文本 / TXT 文件」切换(原型已有该切换与「reading.txt · UTF-8 · 2 KB」状态行)。选择文件后客户端做提示性预检(`.txt` 扩展名、字节数、`TextDecoder('utf-8', {fatal: true})` 预解码)并显示文件名 · 编码 · 大小;标题为空时用文件名(去扩展名、截断 120 字符)预填;提交用 `FormData` 走新接口,成功后的跳转与轮询复用现有 store 逻辑,失败保留已填内容。客户端预检只提前反馈,服务端结论为最终结论。 **D7 空文件、只有 BOM、只有空白** → 400「请输入正文内容」,与粘贴一致。 **D8 不改 schema**:`lexgo_chapters`/`lexgo_ingest_jobs` 已够用,无迁移;回退 = 换回旧二进制。文件名不入库;「导入来源」溯源字段另立范围。 ### 测试计划 - Go 单测:解码与校验的有效、BOM 剥离、非法 UTF-8、UTF-16 BOM、空、只有 BOM、NUL、超限与恰好边界(2 MiB、100000 码点)。 - Go 集成(专用库 `lexgo_test_issue9`):multipart 上传 → 201 → worker → ready → 阅读器原文与文件字节级一致(含 CRLF、emoji、组合字符);重复上传同一文件 → 200 `duplicate` 且只有一章;同 `requestId` 换内容 → 409;缺 `file`、多余字段、非 multipart → 400;追加他人书籍 → 404;两账号隔离;恶意文件名(`..\..\evil.txt`)只作为显示名、不影响存储。 - 学习端:上传模式单测(切换、预检、FormData、成功跳转、追加、错误保留表单)+ mock E2E + 真实链路(真实 API 上传真实临时 TXT,核对阅读器原文逐字符一致)。 - 文档:更新 Architecture-and-Code-Map、Business-Rules-and-Glossary、Local-Development-and-Verification,需求变化更新 Product-Requirements-Overview;Wiki 先写后回读再同步镜像。 非目标:EPUB/PDF/字幕(明确排除)、UTF-16/GB18030、按空行自动分章、文件名入库、断点续传。 回退说明:本会话 pi 无可用 Gitea MCP 工具,沿用本单既定回退,使用目标 `git.ilapage.cn` API;凭据仅从既有安全配置读入进程。
Author
Owner

#9 实施完成,待用户验收(2026-09-11)

用户确认的契约 D1~D8 见评论 7798。分支 feat/9-txt-upload 从 main cd2b893 创建,实现提交 61d3f63,已推送。PR #28 未合并,工单不关闭,停在待用户验收。

实现与差异

  • server/app/lexgo/upload.go:新增 multipart 解析、解码与两条路由,没有数据库结构变化(沿用 #5 的 books/chapters/ingest_jobs)。
  • 解码规则:先按 UTF-16 BOM(FF FE/FE FF)识别并提示「请另存为 UTF-8 后重试」;剥离可选 UTF-8 BOM;用 utf8.Valid 整体校验,非法序列直接 400,不使用替换字符;拒绝 NUL 字节;2 MiB 字节上限之后仍套用 validatePaste 的单章 100000 码点上限。
  • 文件只在内存中解码,不创建临时文件,客户端文件名既不参与任何路径也不入库;并发上传用单槽门,忙时 429。
  • 解码后交给现有 PasteBook/PasteChapter,因此分章(一次提交一章)、requestId + 内容 SHA 幂等、任务状态、失败重试与崩溃恢复与粘贴完全一致。
  • 学习端:ImportView.vue 增加「粘贴文本 / TXT 文件」来源切换(沿用已验收 v1 的切换与状态行)、文件预检与文件名·编码·大小状态行、标题预填;stores/library.ts 增加 upload()、fileProblem、fileSizeLabel;session.request 支持 FormData(multipart 不再被 JSON 化)。
  • gofmt 整理了 #8 引入的 import 顺序与空行,无语义变化。

实际验证

验证 结果
go vet ./... 通过
LEXGO_TEST_DB_NAME=lexgo_test_issue9 python scripts/server.py test-integration 50 个顶层用例全部通过、0 跳过(原 44,新增 6 个上传用例)
learner npx vitest --run / vue-tsc --build / pnpm run build / playwright test 80 项单测、类型检查、构建、6 项 E2E 通过
admin pnpm test / pnpm lint 31 项与 lint 通过;管理端本单无代码改动
python -m unittest discover -s tests / harness.py check --strict 56 项与严格检查通过
Wiki sync --check 一致,5 个页面先写后回读

覆盖:有效 UTF-8 的字节级往返(CRLF、制表符、弯引号、em dash、省略号、emoji、组合字符)、BOM 剥离且不进原文、只有 BOM、非法 UTF-8、Latin-1、UTF-16 大小端、NUL、空文件、只有空白、超限与恰好边界(2 MiB、100000 码点)、缺 file/标题/language/过短 requestId、未知字段、追加路径携带 language、非 multipart、未登录、恶意文件名不影响存储、重复上传只产生一章、同编号换内容 409、追加他人书籍 404、两账号隔离、上传与粘贴共用同一任务管线。

真实链路:真实 Go API+真实 MySQL 共 26 项检查通过(凭据只从本机安全配置读入进程),随后用临时 Playwright 用例在真实学习端完成「登录→切换 TXT→选择真实 UTF-8 文件→上传→处理中就绪→阅读器原文逐字符一致」闭环,并复核 UTF-16 文件在浏览器预检阶段被拒。临时用例运行后删除,截图作为附件上传。

本单不改数据库结构,因此没有迁移步骤;本机 lexgo-api 已用新二进制重启(schema 仍为 v6),/healthz 200。

未验证与边界

  • 真实手机触屏详细证据与完整备份恢复演练仍属既有缺口(#14/#15);本单只用桌面浏览器检查。
  • 大文件并发只按单槽设计,没有做多用户压力测试。
  • UTF-16/GB18030 转码、按空行自动分章、断点续传、来源文件名入库均不在本单范围。
  • 客户端预检(扩展名、大小、UTF-8 预览解码)只是提前反馈;服务端结论为最终结论,测试中已覆盖「文件扩展名不是 .txt 时服务端仍可接受」这一差异。

文档

Wiki 先写后回读,再同步镜像(镜像随本次提交):

  • Architecture-and-Code-Map: ea8661cfbc172ca67148b6a14d46204ab7033e35
  • Business-Rules-and-Glossary: 76289713c11902383031764c90ae9da90bd0ce07
  • Local-Development-and-Verification: 8c0886a74112ea7bff734c04775b56c571797c3d
  • Product-Requirements-Overview: f5b38f2d527cbc21eac4512f7c439c013bb221d9
  • Home: 75de70aad90f02deeadbed301c9fe85ded1fec6e
  • Project-Profile 本单未变化,仍为 ee9d2b8cc69c17bc83b2f33fc69527ee23ab5f0f

未提交凭据、账号密码、测试数据库数据或截图;.local/ 保持忽略。

回退

本单没有 schema 变化:停止 lexgo-api,恢复 .local/lexgo-pre-issue9.exe(当前运行的旧二进制备份),重新启动即可;已导入的章节与书籍是正常数据,保留不动。

Gitea MCP 仍指向其他站点,沿用已记录的目标站点 API 回退;凭据仅从既有安全配置读入进程。

附件:issue9-invalid-encoding.png、issue9-upload-processing.png、issue9-reader.png(真实浏览器闭环截图;本会话模型不能读取图片,功能断言来自程序化检查)。

## #9 实施完成,待用户验收(2026-09-11) 用户确认的契约 D1~D8 见评论 [7798](https://git.ilapage.cn/OPC/lexgo/issues/9#issuecomment-7798)。分支 `feat/9-txt-upload` 从 main `cd2b893` 创建,实现提交 `61d3f63`,已推送。PR [#28](https://git.ilapage.cn/OPC/lexgo/pulls/28) 未合并,工单不关闭,停在待用户验收。 ### 实现与差异 - `server/app/lexgo/upload.go`:新增 multipart 解析、解码与两条路由,**没有数据库结构变化**(沿用 #5 的 books/chapters/ingest_jobs)。 - 解码规则:先按 UTF-16 BOM(`FF FE`/`FE FF`)识别并提示「请另存为 UTF-8 后重试」;剥离可选 UTF-8 BOM;用 `utf8.Valid` 整体校验,非法序列直接 400,不使用替换字符;拒绝 NUL 字节;2 MiB 字节上限之后仍套用 `validatePaste` 的单章 100000 码点上限。 - 文件只在内存中解码,**不创建临时文件**,客户端文件名既不参与任何路径也不入库;并发上传用单槽门,忙时 429。 - 解码后交给现有 `PasteBook`/`PasteChapter`,因此分章(一次提交一章)、`requestId` + 内容 SHA 幂等、任务状态、失败重试与崩溃恢复与粘贴完全一致。 - 学习端:`ImportView.vue` 增加「粘贴文本 / TXT 文件」来源切换(沿用已验收 v1 的切换与状态行)、文件预检与文件名·编码·大小状态行、标题预填;`stores/library.ts` 增加 `upload()`、`fileProblem`、`fileSizeLabel`;`session.request` 支持 `FormData`(multipart 不再被 JSON 化)。 - `gofmt` 整理了 #8 引入的 import 顺序与空行,无语义变化。 ### 实际验证 | 验证 | 结果 | |---|---| | `go vet ./...` | 通过 | | `LEXGO_TEST_DB_NAME=lexgo_test_issue9 python scripts/server.py test-integration` | 50 个顶层用例全部通过、0 跳过(原 44,新增 6 个上传用例) | | learner `npx vitest --run` / `vue-tsc --build` / `pnpm run build` / `playwright test` | 80 项单测、类型检查、构建、6 项 E2E 通过 | | admin `pnpm test` / `pnpm lint` | 31 项与 lint 通过;管理端本单无代码改动 | | `python -m unittest discover -s tests` / `harness.py check --strict` | 56 项与严格检查通过 | | Wiki `sync --check` | 一致,5 个页面先写后回读 | 覆盖:有效 UTF-8 的字节级往返(CRLF、制表符、弯引号、em dash、省略号、emoji、组合字符)、BOM 剥离且不进原文、只有 BOM、非法 UTF-8、Latin-1、UTF-16 大小端、NUL、空文件、只有空白、超限与恰好边界(2 MiB、100000 码点)、缺 `file`/标题/`language`/过短 `requestId`、未知字段、追加路径携带 `language`、非 multipart、未登录、恶意文件名不影响存储、重复上传只产生一章、同编号换内容 409、追加他人书籍 404、两账号隔离、上传与粘贴共用同一任务管线。 真实链路:真实 Go API+真实 MySQL 共 **26 项检查通过**(凭据只从本机安全配置读入进程),随后用临时 Playwright 用例在真实学习端完成「登录→切换 TXT→选择真实 UTF-8 文件→上传→处理中就绪→阅读器原文逐字符一致」闭环,并复核 UTF-16 文件在浏览器预检阶段被拒。临时用例运行后删除,截图作为附件上传。 本单不改数据库结构,因此没有迁移步骤;本机 lexgo-api 已用新二进制重启(schema 仍为 v6),`/healthz` 200。 ### 未验证与边界 - 真实手机触屏详细证据与完整备份恢复演练仍属既有缺口(#14/#15);本单只用桌面浏览器检查。 - 大文件并发只按单槽设计,没有做多用户压力测试。 - UTF-16/GB18030 转码、按空行自动分章、断点续传、来源文件名入库均不在本单范围。 - 客户端预检(扩展名、大小、UTF-8 预览解码)只是提前反馈;服务端结论为最终结论,测试中已覆盖「文件扩展名不是 .txt 时服务端仍可接受」这一差异。 ### 文档 Wiki 先写后回读,再同步镜像(镜像随本次提交): - Architecture-and-Code-Map: `ea8661cfbc172ca67148b6a14d46204ab7033e35` - Business-Rules-and-Glossary: `76289713c11902383031764c90ae9da90bd0ce07` - Local-Development-and-Verification: `8c0886a74112ea7bff734c04775b56c571797c3d` - Product-Requirements-Overview: `f5b38f2d527cbc21eac4512f7c439c013bb221d9` - Home: `75de70aad90f02deeadbed301c9fe85ded1fec6e` - Project-Profile 本单未变化,仍为 `ee9d2b8cc69c17bc83b2f33fc69527ee23ab5f0f` 未提交凭据、账号密码、测试数据库数据或截图;`.local/` 保持忽略。 ### 回退 本单没有 schema 变化:停止 lexgo-api,恢复 `.local/lexgo-pre-issue9.exe`(当前运行的旧二进制备份),重新启动即可;已导入的章节与书籍是正常数据,保留不动。 Gitea MCP 仍指向其他站点,沿用已记录的目标站点 API 回退;凭据仅从既有安全配置读入进程。 附件:issue9-invalid-encoding.png、issue9-upload-processing.png、issue9-reader.png(真实浏览器闭环截图;本会话模型不能读取图片,功能断言来自程序化检查)。
Author
Owner

补充提交 fb256d4(docs: 记录 #9 已确认的 TXT 导入口径):按本项目既有惯例,把用户本轮确认的导入口径写入仓库根 AGENTS.md 的项目决策清单——只接受 UTF-8(可选 BOM 剥离)、非法字节整体拒绝、UTF-16 明确拒绝、2 MiB 与 100000 码点上限、换行不归一化、不落盘与文件名不入库,以及分章/幂等与范围边界。

仅文档改动,未改代码、数据或接口;治理严格检查通过,分支已同步推送。功能提交 61d3f63 保持不变;回退用的 #8 版本二进制已保存在忽略的 .local/lexgo-pre-issue9.exe。

补充提交 `fb256d4`(docs: 记录 #9 已确认的 TXT 导入口径):按本项目既有惯例,把用户本轮确认的导入口径写入仓库根 `AGENTS.md` 的项目决策清单——只接受 UTF-8(可选 BOM 剥离)、非法字节整体拒绝、UTF-16 明确拒绝、2 MiB 与 100000 码点上限、换行不归一化、不落盘与文件名不入库,以及分章/幂等与范围边界。 仅文档改动,未改代码、数据或接口;治理严格检查通过,分支已同步推送。功能提交 `61d3f63` 保持不变;回退用的 `#8` 版本二进制已保存在忽略的 `.local/lexgo-pre-issue9.exe`。
Author
Owner

#9 代码审核:达标(2026-09-13,Claude Code)

审核对象:提交 61d3f63、fb256d4(PR #28)。只读审阅代码与测试差异,没有重跑测试;实施评论 7804 中的测试结果本次没有复核。契约 D1~D8(评论 7798)逐条核对如下。

逐条核对

  • D1 编码:decodeTextUpload 先按 FE FF/FF FE 识别 UTF-16 并给出针对性提示,再剥离可选 UTF-8 BOM,utf8.Valid 整体校验非法字节直接拒绝(不使用替换字符),额外拒绝 NUL 字节。TestDecodeTextUploadRules 覆盖了合法文件、BOM 剥离且不进入原文、只有 BOM、非法 UTF-8、Latin-1、UTF-16 大小端、NUL、恰好 2 MiB 与超限一字节的边界。达标。
  • D2 大小上限:字节上限在 multipart 读取阶段用 http.MaxBytesReader 和 io.LimitReader 双重限制,解码后仍复用既有 validatePaste 的 100000 码点上限;TestMySQLTextUploadKeepsChapterLimit 验证了字节上限不能绕开码点上限。达标。
  • D3 不落盘:readTextUpload/decodeTextUpload 全程只操作内存中的 []byte/string,没有任何文件系统写入;客户端文件名从未被读取用于校验或存储,TestMySQLTextUploadRejectsInvalidSubmissions 用 ..\..\windows\system32\evil.txt 验证了文件名不会泄漏进书名、章节标题或正文。并发用单槽 gate,忙时 429(与 dictionary.go 的 uploadGate 采用相同模式,各自独立一把锁,不是同一把——契约原文「复用词典导入的单槽门」按代码实际是「复用同一种单槽模式」而非共享同一个 channel,这是文字上的细微出入,不影响功能,不阻塞验收)。达标。
  • D4 接口:两个路由的字段白名单、201/200/409/404/400 状态码、requestId+内容 SHA 幂等,均由 TestMySQLTextUploadImportPath、TestMySQLTextUploadIdempotencyAndAppend、TestMySQLTextUploadRejectsInvalidSubmissions、TestMySQLTextUploadAndPasteShareOnePipeline 覆盖,其中最后一个用例验证了粘贴入口和上传入口对同一个 requestId 只产生一章。达标。
  • D5 分章:直接复用 PasteBook/PasteChapter,未引入新的分章逻辑。达标。
  • D6 前端:ImportView.vue 增加来源单选、文件选择后的客户端 TextDecoder('utf-8', {fatal:true}) 预检、文件名·编码·大小状态行、标题预填(去扩展名、截断 120 字符);upload.spec.ts 覆盖了模式切换、预检拒绝、成功提交、失败重试复用同一 requestId、追加书籍不带 language、保留表单与错误信息。达标。
  • D7 空文件/只有 BOM/只有空白:decodeTextUpload 对只有 BOM 的文件返回空字符串(不报错),空字符串交给 validatePaste 的 TrimFunc(text, unicode.IsSpace) == "" 判断统一拒绝为「请输入正文内容」,与粘贴路径完全一致;空文件在 multipart 读取阶段已被 len(content) == 0 拦下。三种情况都有对应测试用例。达标。
  • D8 不改 schema:upload.go 未涉及任何迁移语句,复用 v6 既有表。达标。

复核结论

达标,可以进入用户验收。 实现严格复用 #5 已验收的粘贴管线(分章、幂等、崩溃恢复),没有引入新的数据结构或校验路径分裂;测试覆盖了契约里列出的全部边界(编码、大小、恶意文件名、跨账号隔离、双入口共享幂等)。唯一的细节是 D3「复用词典导入的单槽门」在实现里是「独立的同款单槽门」,不影响任何验收标准,只在此记录以免以后误读为两者共享同一并发限制。

本次审核只读代码,没有重跑 test-integration(lexgo_test_issue9)与 learner 的 vitest/E2E/Playwright;pi 报告的 50 项集成用例、80 项前端单测等结果未被本次复核重复验证,如需更高把握建议在验收前独立重跑一次。

Gitea MCP 仍指向其他站点,本次沿用已记录的目标站点 API 回退;凭据只从 ~/.claude/gitea.env 安全配置读入进程。

## #9 代码审核:达标(2026-09-13,Claude Code) 审核对象:提交 `61d3f63`、`fb256d4`(PR #28)。只读审阅代码与测试差异,没有重跑测试;实施评论 7804 中的测试结果本次没有复核。契约 D1~D8(评论 7798)逐条核对如下。 ### 逐条核对 - **D1 编码**:`decodeTextUpload` 先按 `FE FF`/`FF FE` 识别 UTF-16 并给出针对性提示,再剥离可选 UTF-8 BOM,`utf8.Valid` 整体校验非法字节直接拒绝(不使用替换字符),额外拒绝 NUL 字节。`TestDecodeTextUploadRules` 覆盖了合法文件、BOM 剥离且不进入原文、只有 BOM、非法 UTF-8、Latin-1、UTF-16 大小端、NUL、恰好 2 MiB 与超限一字节的边界。**达标**。 - **D2 大小上限**:字节上限在 multipart 读取阶段用 `http.MaxBytesReader` 和 `io.LimitReader` 双重限制,解码后仍复用既有 `validatePaste` 的 100000 码点上限;`TestMySQLTextUploadKeepsChapterLimit` 验证了字节上限不能绕开码点上限。**达标**。 - **D3 不落盘**:`readTextUpload`/`decodeTextUpload` 全程只操作内存中的 `[]byte`/`string`,没有任何文件系统写入;客户端文件名从未被读取用于校验或存储,`TestMySQLTextUploadRejectsInvalidSubmissions` 用 `..\..\windows\system32\evil.txt` 验证了文件名不会泄漏进书名、章节标题或正文。并发用单槽 `gate`,忙时 429(与 `dictionary.go` 的 `uploadGate` 采用相同模式,各自独立一把锁,不是同一把——契约原文「复用词典导入的单槽门」按代码实际是「复用同一种单槽模式」而非共享同一个 channel,这是文字上的细微出入,不影响功能,不阻塞验收)。**达标**。 - **D4 接口**:两个路由的字段白名单、201/200/409/404/400 状态码、`requestId`+内容 SHA 幂等,均由 `TestMySQLTextUploadImportPath`、`TestMySQLTextUploadIdempotencyAndAppend`、`TestMySQLTextUploadRejectsInvalidSubmissions`、`TestMySQLTextUploadAndPasteShareOnePipeline` 覆盖,其中最后一个用例验证了粘贴入口和上传入口对同一个 `requestId` 只产生一章。**达标**。 - **D5 分章**:直接复用 `PasteBook`/`PasteChapter`,未引入新的分章逻辑。**达标**。 - **D6 前端**:`ImportView.vue` 增加来源单选、文件选择后的客户端 `TextDecoder('utf-8', {fatal:true})` 预检、文件名·编码·大小状态行、标题预填(去扩展名、截断 120 字符);`upload.spec.ts` 覆盖了模式切换、预检拒绝、成功提交、失败重试复用同一 `requestId`、追加书籍不带 `language`、保留表单与错误信息。**达标**。 - **D7 空文件/只有 BOM/只有空白**:`decodeTextUpload` 对只有 BOM 的文件返回空字符串(不报错),空字符串交给 `validatePaste` 的 `TrimFunc(text, unicode.IsSpace) == ""` 判断统一拒绝为「请输入正文内容」,与粘贴路径完全一致;空文件在 multipart 读取阶段已被 `len(content) == 0` 拦下。三种情况都有对应测试用例。**达标**。 - **D8 不改 schema**:`upload.go` 未涉及任何迁移语句,复用 v6 既有表。**达标**。 ### 复核结论 **达标,可以进入用户验收。** 实现严格复用 #5 已验收的粘贴管线(分章、幂等、崩溃恢复),没有引入新的数据结构或校验路径分裂;测试覆盖了契约里列出的全部边界(编码、大小、恶意文件名、跨账号隔离、双入口共享幂等)。唯一的细节是 D3「复用词典导入的单槽门」在实现里是「独立的同款单槽门」,不影响任何验收标准,只在此记录以免以后误读为两者共享同一并发限制。 本次审核只读代码,没有重跑 `test-integration`(`lexgo_test_issue9`)与 learner 的 vitest/E2E/Playwright;pi 报告的 50 项集成用例、80 项前端单测等结果未被本次复核重复验证,如需更高把握建议在验收前独立重跑一次。 Gitea MCP 仍指向其他站点,本次沿用已记录的目标站点 API 回退;凭据只从 `~/.claude/gitea.env` 安全配置读入进程。
Author
Owner

用户验收与合并收尾

验收时间:2026-09-13 22:36 +0800(Asia/Shanghai,与 PR 合入时刻一致)。用户在会话中明确确认「#9 通过验收」。结论:#9 验收通过,关闭工单;PR #28 已 fast-forward-only 合入 main。

验收对象是 PR #28 的完整头部 fb256d4,包含功能提交 61d3f63 与口径记录 fb256d4,两个提交逐个进入 main,没有合并提交、没有改写历史,与用户验收版本逐字节一致。验收状态文档提交 65b50d5 已推送。分支 feat/9-txt-upload 保留;需要撤销时可在新工单中使用 revert。

本次验收前的完整证据见评论 7804(实施、测试与真实链路),契约见 7798,本次不重复覆盖。合并后复核:harness.py check --strict 通过、sync --verify 通过、治理测试 56 项通过,工作区干净且与远端同步。

长期文档 Wiki revisions:

  • Project-Profile: b85c7f4650aae95b1429c0b4f7d1412c440731ce
  • Architecture-and-Code-Map: 5e22c6023b065287ac47d92e6b8748e34d21763b
  • Product-Requirements-Overview: 9bb9f570044fb7c9e04c5f1bd04beeaf89840d86
  • Home: 03e9280eedbd7506ec567cef61a54745873d71e2
  • Business-Rules-and-Glossary 与 Local-Development-and-Verification 本单验收未变化,仍为 76289713c11902383031764c90ae9da90bd0ce07 / 8c0886a74112ea7bff734c04775b56c571797c3d

本单没有数据库结构变化,schema 保持 v6;本机 lexgo-api 运行新二进制,/healthz 200。回退用的上一版本(main 上的 #8 提交构建)保存在忽略的 .local/lexgo-pre-issue9.exe。

第 4 阶段已完成 #9;当前待完成 #10~#15,另有 #21 与 #24。真机触屏详细证据与完整备份恢复演练缺口保留,由 #14/#15 承接。

Gitea MCP 仍指向其他站点,沿用已记录的目标 git.ilapage.cn API 回退,凭据仅在进程环境中使用。

## 用户验收与合并收尾 验收时间:2026-09-13 22:36 +0800(Asia/Shanghai,与 PR 合入时刻一致)。用户在会话中明确确认「#9 通过验收」。结论:#9 验收通过,关闭工单;PR [#28](https://git.ilapage.cn/OPC/lexgo/pulls/28) 已 fast-forward-only 合入 main。 验收对象是 PR #28 的完整头部 `fb256d4`,包含功能提交 `61d3f63` 与口径记录 `fb256d4`,两个提交逐个进入 main,没有合并提交、没有改写历史,与用户验收版本逐字节一致。验收状态文档提交 `65b50d5` 已推送。分支 `feat/9-txt-upload` 保留;需要撤销时可在新工单中使用 revert。 本次验收前的完整证据见评论 [7804](https://git.ilapage.cn/OPC/lexgo/issues/9#issuecomment-7804)(实施、测试与真实链路),契约见 [7798](https://git.ilapage.cn/OPC/lexgo/issues/9#issuecomment-7798),本次不重复覆盖。合并后复核:`harness.py check --strict` 通过、`sync --verify` 通过、治理测试 56 项通过,工作区干净且与远端同步。 长期文档 Wiki revisions: - Project-Profile: `b85c7f4650aae95b1429c0b4f7d1412c440731ce` - Architecture-and-Code-Map: `5e22c6023b065287ac47d92e6b8748e34d21763b` - Product-Requirements-Overview: `9bb9f570044fb7c9e04c5f1bd04beeaf89840d86` - Home: `03e9280eedbd7506ec567cef61a54745873d71e2` - Business-Rules-and-Glossary 与 Local-Development-and-Verification 本单验收未变化,仍为 `76289713c11902383031764c90ae9da90bd0ce07` / `8c0886a74112ea7bff734c04775b56c571797c3d` 本单没有数据库结构变化,schema 保持 v6;本机 lexgo-api 运行新二进制,`/healthz` 200。回退用的上一版本(main 上的 #8 提交构建)保存在忽略的 `.local/lexgo-pre-issue9.exe`。 第 4 阶段已完成 #9;当前待完成 #10~#15,另有 #21 与 #24。真机触屏详细证据与完整备份恢复演练缺口保留,由 #14/#15 承接。 Gitea MCP 仍指向其他站点,沿用已记录的目标 git.ilapage.cn API 回退,凭据仅在进程环境中使用。
ila closed this issue 2026-09-13 22:37:08 +08:00
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: OPC/lexgo#9