diff --git a/README.md b/README.md index d409bba..aa0edea 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ Vite 通常使用 http://localhost:5173。 前端包括问答、知识库管理、来源与更新、实验评估四个页面。实验评估页面没有虚构数据;所有页面明确标记当前功能状态。 -注意:当前已实现本地 Embedding、文本/向量快照、精确向量及 RRF 混合检索;`/api/chat` 仍是占位接口,生成模型实际调用暂缓,网络搜索、后台更新任务和真实领域评测尚未实现。管理接口由服务端认证与角色校验;审核表单的核验人仍是人工输入字段。已完成隔离环境的浏览器 Cookie/CSRF、角色与真实检索核心流程验收,见 [浏览器验收记录](docs/BROWSER_ACCEPTANCE.md);完整生产部署、Docker 数据库与 HTTPS 验收仍待完成。`.env` 不要提交到 Git。 +注意:当前已实现本地 Embedding、文本/向量快照、精确向量及 RRF 混合检索;`/api/chat` 仍是占位接口,生成模型实际调用暂缓,网络搜索、后台更新任务和真实领域评测尚未实现。管理接口由服务端认证与角色校验;审核表单的核验人仍是人工输入字段。已完成隔离环境的浏览器 Cookie/CSRF、角色与真实检索核心流程验收,见 [浏览器验收记录](docs/BROWSER_ACCEPTANCE.md) 与 [导出/采集时间复核](docs/EXPORT_BROWSER_ACCEPTANCE.md);完整生产部署、Docker 数据库与 HTTPS 验收仍待完成。`.env` 不要提交到 Git。 ### 后端测试和迁移回滚 diff --git a/docs/EXPORT_BROWSER_ACCEPTANCE.md b/docs/EXPORT_BROWSER_ACCEPTANCE.md new file mode 100644 index 0000000..47d3e6a --- /dev/null +++ b/docs/EXPORT_BROWSER_ACCEPTANCE.md @@ -0,0 +1,59 @@ +# 2026-10-17 导出与采集时间浏览器验收 + +本轮基于已合入的检索导出、导出工作台和资料生命周期页面,在隔离 PostgreSQL/pgvector 与默认生命周期 FastAPI、Vite、真实浏览器之间联调。全部账号、资料和查询均为合成测试;没有加载 Embedding、调用生成模型或产生领域实验结论。 + +## 基线与准备 + +- 旧 AGit [PR #33](https://gitea.kronecker.cc/saint/border-red-post-nalati/pulls/33) 关闭未合并,其普通分支通过真正 merged 的 [PR #34](https://gitea.kronecker.cc/saint/border-red-post-nalati/pulls/34) 合入 dev,保留原实现并增加下载响应的显式 `Cache-Control: no-store`。原 head、替代 head 和 merge 均已核对为最新 dev 祖先。 +- [PR #36](https://gitea.kronecker.cc/saint/border-red-post-nalati/pulls/36) 的导出页面和 [PR #38](https://gitea.kronecker.cc/saint/border-red-post-nalati/pulls/38) 的来源生命周期页面也已合入。从最新 dev 创建本阶段分支,没有叠加待审功能或合入 main。 +- 独立 HTTP 初始化合成来源、带微秒采集时间的关联文档、已审核知识和文本发布,并实际执行 22 条条目/chunk 关键词检索,准备其中一条的兼容请求上下文。这些是测试准备,不能称为浏览器录入或向量推理。 + +## 浏览器已实际验证的路径 + +| 路径 | 结果 | +|---|---| +| 匿名访问导出页 | 跳转登录并保留回跳路径;管理员登录后回到导出页 | +| 历史分页 | 显示 22 条,第一页 20 条、第二页 2 条;两端翻页按钮正确禁用 | +| 跨页选择 | 第一页选择一条 chunk 记录,第二页选择一条条目记录,仍保留两条选择 | +| 可选上下文 | 勾选关联 RAG 准备上下文后触发真实浏览器文件下载 | +| 下载文件核对 | 文件实际落盘;manifest 为 2 条检索和 1 条上下文;记录区 SHA-256 与页面显示一致,每个 payload 与原保存快照一致 | +| 文档时区缺陷复现 | 原采集时间为 `2026-10-16T12:34:56.789123+08:00`,仅修改备注却变成 `2026-10-16T04:34:00+08:00` | +| 修正后时间回填 | 编辑页显示当地时钟的 `12:34:56.789`,而非 UTC 时钟的 `04:34`;新建表单采集时间留空,要求明确填写 | +| 修正后仅修改备注 | 页面保存成功;独立 API 读取确认原带微秒的时间字符串完全保留,文档版本递增 | +| 文档历史 | 页面展开最新版本快照,可看到版本 4 及原 `12:34:56.789123+08:00` 采集时间 | + +浏览器实际下载: + +![跨页选择并下载](assets/export-acceptance/export-download.jpg) + +备注修改后的历史快照: + +![采集时间保留原微秒值](assets/export-acceptance/document-collected-at.jpg) + +## 修正内容 + +原表单把 `toISOString()` 的 UTC 时钟直接填入 `datetime-local`,提交时又按本地时区解析,造成时区偏移;截取到分钟还会丢失秒和更高精度。 + +新增日期转换函数:回填使用本地时钟,支持秒/毫秒输入;若管理员没有修改时间,直接保留服务端原时间字符串,避免丢失微秒或在夏令时重叠时刻改变实际瞬间。明确修改时间时才转换为带时区的 ISO 时间。新建文档不再预填当前时间作为采集事实,仍由必填校验要求输入。 最新 dev 新合入的知识编辑页使用了相同的 UTC 截取方式,本轮同步复用此转换函数,保留其无采集时间时的空值语义;该知识页面的修改通过共享函数回归和构建验证,没有冒充新的浏览器操作。 + +为复核修正效果,缺陷复现后只在隔离合成数据上通过独立 HTTP 恢复原采集时间,再重新加载页面执行仅改备注的浏览器验证。该恢复属于测试基线准备,没有冒充浏览器操作;不自动修复或猜测既有业务资料的真实采集时间。 + +## 独立 HTTP 交叉核对 + +以下和浏览器操作分开记录: + +- 文档修改后关联知识回到 pending,旧文本发布的检索返回 409;没有继续使用旧审核依据。 +- 被活动知识/文档引用的文档、来源删除返回 409;过期版本修改返回 409。 +- 单独可丢弃来源/文档经过修改和软删除后,正文详情隐藏,删除历史快照保留。 +- 来源与文档修订后再次通过 HTTP 导出原选择,记录区原始字节与首次浏览器下载完全一致;manifest 的导出时间/编号允许变化,生成用量/费用仍未采集。 +- 下载响应 `Cache-Control: no-store`;普通用户直接请求导出返回 403。 + +## 验证命令和边界 + +- `npm run test:datetime`:**6 passed**,覆盖中国时区、UTC、负偏移跨日、夏令时重叠、显式修改、微秒与空值;这些是 Node 时区单元测试,不等同于多浏览器验收。 +- `npm run test:session`:**3 passed**;`npm run build`(含类型检查):通过。 +- `pytest -q backend/tests/test_exports.py backend/tests/test_catalog.py`:**49 passed**;`ruff check backend`、`ruff format --check backend`:通过。本轮未把此前全量 254 项结果当作本阶段重新执行的结果。 +- 原生删除确认框使本次浏览器控制无法继续完成确认;该浏览器删除路径未验收。相关删除保护、软删除与历史保留仅由上述独立 HTTP 和后端专项覆盖,仍需人工复核确认交互。没有据此放宽删除保护或修改确认机制。 +- Docker、生产 HTTPS、多浏览器、浏览器改密、领域资料/问题集及生成模型实际调用仍不在本轮已完成范围。 + +复现时使用单独数据库或隔离 Schema、合成账号和明确的采集时间,按上述步骤比较下载原始字节及修改前后时间;环境、密码、原始 JSONL、数据库、日志和本机路径不入库。验证结束后关闭测试服务并删除本轮隔离 Schema。截图只保留合成数据,导出下载文件和原始检查记录保存在 Git 元数据目录。 diff --git a/docs/RETRIEVAL_EXPORT_API.md b/docs/RETRIEVAL_EXPORT_API.md index 16a42c1..8c36da8 100644 --- a/docs/RETRIEVAL_EXPORT_API.md +++ b/docs/RETRIEVAL_EXPORT_API.md @@ -88,7 +88,7 @@ rows = [json.loads(line) for line in records.rstrip(b"\n").split(b"\n")] assert len(rows) == sum(manifest["counts"].values()) ``` -前端调用层 `services/exports.ts` 的 `exportRetrievalHistory` 返回 Blob、受限文件名和记录摘要,沿用同一 Cookie/CSRF、401 撤销及错误处理,不放入 localStorage。此阶段没有新增下载页面,页面入口由后续前端集成;调用层测试是模拟网络的契约测试,不冒充浏览器下载验收。 +前端调用层 `services/exports.ts` 的 `exportRetrievalHistory` 返回 Blob、受限文件名和记录摘要,沿用同一 Cookie/CSRF、401 撤销及错误处理,不放入 localStorage。下载页面已通过 PR #36 合入,可从管理员工作台进入。真实浏览器跨页选择、可选上下文、落盘文件与摘要核对见 [导出浏览器验收](EXPORT_BROWSER_ACCEPTANCE.md);调用层模拟网络测试与真实浏览器路径分别记录。 ## 验证与撤回 diff --git a/docs/TASKS.md b/docs/TASKS.md index c2b7b32..4de04b0 100644 --- a/docs/TASKS.md +++ b/docs/TASKS.md @@ -109,10 +109,20 @@ - 专项测试 **30 passed**,包括真实 PostgreSQL 并发快照和 HTTP 重启导出;完整回归 **254 passed, 1 skipped**(skip 为须显式启用的真实 Embedding 专项),`ruff check` 与 `ruff format --check` 通过。前端会话下载契约 **3 passed**,类型与生产构建通过。测试使用合成资料与坐标 fixture,未重跑模型推理或调用生成模型。 +## 2026-10-17 T05/T10:导出与采集时间浏览器复核 + +- 导出原实现及显式禁用缓存修正已通过替代 PR #34 真正合入 dev,原 #33 关闭未合并;已核对原 head、当前普通分支 head 与 merge 均包含在最新 dev。导出工作台 #36 与来源生命周期 #38 已合入,再创建唯一阶段分支 `test/backend-export-acceptance`。 +- 真实浏览器完成匿名回跳/管理员登录、22条历史两页跨页选择、关联准备上下文与实际 JSONL 下载;落盘文件为2条检索/1条上下文,记录区摘要与页面一致,payload 与保存的原始快照一致。本轮没有模型推理。 +- 发现并修正仅改文档备注时采集时间偏移8小时且截断到分钟的问题;回填使用本地时钟,未改时间保留服务端原微秒值,新建不猜测采集时间。最新合入的知识编辑页复用同一修正。文档浏览器保存与历史快照、独立API均复核通过;知识页仅共享函数测试和构建验证。 +- 前端时区6项、会话3项测试与构建通过;后端导出/目录专项49项和ruff检查/格式检查通过。来源/文档版本、引用删除保护、软删除历史及修订后原导出字节不变均经独立HTTP核对。详见 [EXPORT_BROWSER_ACCEPTANCE.md](EXPORT_BROWSER_ACCEPTANCE.md)。 +- 浏览器原生删除确认交互受工具限制仍待人工复核,不把独立HTTP删除验证当作浏览器结果;Docker/HTTPS/多浏览器/浏览器改密、领域资料与生成调用继续保留待办。 + ## 2026-10-17 T08:Responses 协议调整 -- 从最新 dev 创建独立 `feat/backend-responses-protocol`,不依赖待审的采集时间/浏览器 PR #44。新准备默认使用 Responses:固定 instructions 与 user input_text、max_output_tokens、非流式 output 解析;模型和真实服务接入继续延期。 +- 从最新 dev 创建独立 `feat/backend-responses-protocol`,协议开发不依赖 PR #44;随后同步了已合入的采集时间/浏览器修正。新准备默认使用 Responses:固定 instructions 与 user input_text、max_output_tokens、非流式 output 解析;模型和真实服务接入继续延期。 - 新配置明确 protocol/provider_profile;提供 compatible、openai、deepseek、bailian 档案。DeepSeek 省略不受支持的 store,其他档案显式 store=false;百炼限制 temperature 小于2。旧 Chat Completions 参数拒绝,不自动探测、换协议或重试。档案通过离线契约验证,不代表已对真实供应商调用或通过全部模型能力验证。 - 解析区分 completed/incomplete/refused/failed,reasoning 不充当答案;拒绝工具、异常状态和错误引用。保存实际模型标识及可选输入/输出、缓存/推理 token 用量,不估算费用。字符预算仍不是模型 token 预算。 - 保留旧准备快照的完整配置、请求体、哈希与 JSONL 字节;两种协议可混合读取/导出。API路径与迁移不变,仍为0007;新请求使用 Responses,旧协议仅用于历史校验。字段、厂商差异、范围和回滚限制见 [RAG_CONTEXT_API.md](RAG_CONTEXT_API.md)。 - 本轮 `pytest -q backend/tests/test_rag.py backend/tests/test_exports.py` **113 passed**;完整 `pytest -q backend/tests` **294 passed, 1 skipped**。skip 为显式启用的真实 Embedding 专项,本轮不运行模型。ruff检查/格式检查及前端类型/生产构建通过;未修改页面交互,不宣称新增浏览器验收。 + +- PR #44 合入后同步最新 dev,保留两份阶段记录并解决本文件追加冲突。此次同步后重新执行前端时区6项、会话3项及类型/生产构建,均通过;backend树与此前294项回归时一致,本次未重复运行全量后端测试,也未新增模型调用或浏览器验收。 diff --git a/docs/assets/export-acceptance/document-collected-at.jpg b/docs/assets/export-acceptance/document-collected-at.jpg new file mode 100644 index 0000000..132a10e Binary files /dev/null and b/docs/assets/export-acceptance/document-collected-at.jpg differ diff --git a/docs/assets/export-acceptance/export-download.jpg b/docs/assets/export-acceptance/export-download.jpg new file mode 100644 index 0000000..11f6d80 Binary files /dev/null and b/docs/assets/export-acceptance/export-download.jpg differ diff --git a/frontend/package.json b/frontend/package.json index cb3a272..cd4064b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,7 +7,8 @@ "dev": "vite", "build": "vue-tsc -b && vite build", "preview": "vite preview", - "test:session": "node --test tests/session.test.mjs" + "test:session": "node --test tests/session.test.mjs", + "test:datetime": "node --test tests/datetime.test.mjs" }, "dependencies": { "lucide-vue-next": "^0.468.0", diff --git a/frontend/src/utils/datetime.ts b/frontend/src/utils/datetime.ts new file mode 100644 index 0000000..495328a --- /dev/null +++ b/frontend/src/utils/datetime.ts @@ -0,0 +1,16 @@ +// datetime-local contains wall-clock time; it must not be filled with UTC clock digits. +export function localDateTimeInput(value: string): string { + const date = new Date(value) + const shifted = new Date(date.getTime() - date.getTimezoneOffset() * 60_000) + const [seconds, fraction] = shifted.toISOString().slice(0, -1).split('.') + const significantFraction = fraction.replace(/0+$/, '') + if (significantFraction) return `${seconds}.${significantFraction}` + return seconds.endsWith(':00') ? seconds.slice(0, -3) : seconds +} + +export function collectedAtFromInput(value: string, original?: string): string { + // Preserve server precision (including microseconds) and ambiguous DST instants + // when the administrator only changes other document fields. + if (original && (value === localDateTimeInput(original) || new Date(value).getTime() === new Date(original).getTime())) return original + return new Date(value).toISOString() +} diff --git a/frontend/src/views/DocumentManagementView.vue b/frontend/src/views/DocumentManagementView.vue index f8094a7..8ddaea5 100644 --- a/frontend/src/views/DocumentManagementView.vue +++ b/frontend/src/views/DocumentManagementView.vue @@ -1,20 +1,21 @@ -