diff --git a/README.md b/README.md index 741f5be..dc8421d 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ Vite 通常使用 http://localhost:5173。 前端包括问答、知识库管理、来源与更新、实验评估四个页面。实验评估页面没有虚构数据;所有页面明确标记当前功能状态。 -注意:当前已实现本地 Embedding、文本/向量快照、精确向量及 RRF 混合检索;`/api/chat` 仍是占位接口,DeepSeek RAG 调用暂缓,网络搜索、后台更新任务和真实领域评测尚未实现。管理接口由服务端认证与角色校验;审核表单的核验人仍是人工输入字段。服务保持仅本机访问,完整生产部署、Docker 数据库和浏览器端 Cookie 联调尚未验收。`.env` 不要提交到 Git。 +注意:当前已实现本地 Embedding、文本/向量快照、精确向量及 RRF 混合检索;`/api/chat` 仍是占位接口,生成模型实际调用暂缓,网络搜索、后台更新任务和真实领域评测尚未实现。管理接口由服务端认证与角色校验;审核表单的核验人仍是人工输入字段。已完成隔离环境的浏览器 Cookie/CSRF、角色与真实检索核心流程验收,见 [浏览器验收记录](docs/BROWSER_ACCEPTANCE.md);完整生产部署、Docker 数据库与 HTTPS 验收仍待完成。`.env` 不要提交到 Git。 ### 后端测试和迁移回滚 diff --git a/docs/BROWSER_ACCEPTANCE.md b/docs/BROWSER_ACCEPTANCE.md new file mode 100644 index 0000000..89bbdcd --- /dev/null +++ b/docs/BROWSER_ACCEPTANCE.md @@ -0,0 +1,80 @@ +# 2026-10-16 浏览器与后端联调验收 + +本轮在真实浏览器中访问 Vite 页面,经同源 `/api` 代理连接默认生命周期 FastAPI 和隔离 PostgreSQL 16 + pgvector,验证合成资料的录入、审核、发布、检索以及 Cookie/CSRF/角色权限。数据仅用于功能验收,不代表那拉提文旅事实、领域质量或生产性能。 + +## 代码基线与范围 + +- [PR #17](https://gitea.kronecker.cc/saint/border-red-post-nalati/pulls/17) 的上下文阶段经真正 merged 的 [替代 PR #23](https://gitea.kronecker.cc/saint/border-red-post-nalati/pulls/23) 合入 `dev`。重放产生新 SHA,不能只看旧 AGit PR 是否仍打开。本轮核对替代 head/merge 均为最新 dev 的祖先;完整 backend 文件树、RAG 调用类型与专用契约文档和原实现一致,前端依赖 JSON 相同、锁文件对应版本与完整性值相同,通用说明保留集成后的更新。 +- 从最新 dev 创建 `test/backend-browser-acceptance`,没有叠加待审功能;实际生成模型仍延期,没有调用生成模型或配置其凭据。 +- 使用真实固定 BGE small 中文 Embedding(revision `7999e1d3359715c523056ef9478215996d62a620`、512 维)及现有离线缓存;不是用坐标 fixture 生成本轮页面上的检索结果。自动化回归中的坐标 fixture 仍只用于协议/数据库边界测试。 +- 浏览器、SQL 实例、测试账号和原始检查记录属于本机隔离环境;仓库仅保留步骤、结果摘要及明确含有合成资料的截图,不提交运行环境、日志、凭据或个人数据。 + +## 真实浏览器操作与结果 + +| 验收路径 | 观察结果 | +|---|---| +| 匿名打开受保护页面 | 跳转登录页,保留回跳路径 | +| 管理员登录后刷新页面 | Cookie 会话恢复,管理员身份和受保护列表正常读取 | +| 来源登记 | 经浏览器提交来源、网址和合成备注,成功后列表显示 1 条 | +| 知识录入 | 保存为 pending,不能直接进入检索 | +| 缺失采集时间时审核 | 后端拒绝通过,页面如实显示须补全来源与真实采集时间 | +| 完整元数据知识审核 | 填入明确采集时间、来源、合成核验人和说明后审核保存成功 | +| 文本快照构建与发布 | 仅 1 条已核验知识进入 1 个 chunk;另一待审条目被排除;ready 后显式发布、代数递增 | +| 真实向量构建与发布 | 本地固定模型编码后保存向量,ready 后显式发布;页面显示真实向量版本 | +| 管理员 RRF 查询 | 关键词与向量各命中 1 条同一合成依据,页面展示原文与来源链接,说明尚未生成模型答案 | +| 文档登记 | 选择已登记来源并填入采集时间,创建成功;来源关联、正文和第一页列表可读取 | +| 管理员用户列表 | 数据来自服务端;只有一页时前后翻页按钮正确禁用。本轮未验证多页/大规模表现 | +| 退出登录 | 返回登录页;再次访问受保护页面仍跳转登录 | +| 普通用户注册及登录 | 服务端返回 user,界面不显示任何管理菜单 | +| 普通用户直接访问管理路由及登录回跳 | `/admin` 与 `/indexes` 均跳转个人中心的 forbidden 状态,没有获得管理页面 | +| 普通用户查询 | 重新发布当前资料快照后,普通用户仍能收到真实 RRF 原文证据,管理入口保持隐藏 | + +管理员检索画面: + +![管理员真实浏览器检索](assets/browser-acceptance/rrf-browser.jpg) + +普通用户检索与菜单: + +![普通用户检索](assets/browser-acceptance/user-retrieval-browser.jpg) + +普通用户访问管理路由后的角色状态: + +![普通用户角色拦截](assets/browser-acceptance/user-role-browser.jpg) + +## 本轮修正 + +1. 知识录入原表单没有采集时间字段,导致从页面新建的知识无法满足后端审核前置条件。增加可选的采集时间输入,转换为带时区的 ISO 字符串提交;未填写仍可登记为待审,审核通过的后端约束继续有效,不自动猜测采集时间。 +2. 主工作台缺少审核与文档管理入口。增加仅管理员可见的导航按钮,沿用已有路由和服务端授权,普通用户菜单验收通过。 +3. 首页仍显示“尚未接入检索”,来源页仍称文档工作台待接入。按实际状态改为已接入检索证据、生成式回答未启用及已有文档管理入口;README 不再把尚未确定的生成模型供应商写成已选择。 + +## API 与数据库交叉核对 + +以下为独立 HTTP 检查,和上表的浏览器操作分别记录,不把 API 检查冒充浏览器动作: + +- 匿名读取管理资源返回 401;普通用户读取知识、来源、文档、文本/向量索引、用户、检索历史和 RAG 准备历史八类管理资源均返回 403。 +- 管理员写入缺少 CSRF 返回 403;使用正确 CSRF 但 Origin 不在允许列表仍返回 403。浏览器真实写入成功,说明正常代理调用携带了会话和 CSRF。 +- 真实登录响应含 HttpOnly 与 SameSite=Lax;本次仅为隔离 HTTP test 环境关闭 Secure,生产默认配置未改。本轮没有 HTTPS/Secure Cookie 浏览器验收结论。 +- 退出后 `/api/auth/me` 返回 401。浏览器刷新恢复及退出跳转与服务端会话状态一致。 +- 文档等目录写入后,旧向量查询明确返回 409;API 重新构建/发布后恢复查询,仍只命中那条已核验知识。这是现有保守失效策略,不自动使用旧快照。 +- 持久化记录为 1 条来源、2 条知识(其中 1 条未审核)、1 条关联文档。真实模型的空间、版本和归一化契约沿用 [VECTOR_API.md](VECTOR_API.md)。 +- 最新检索记录可准备 RAG 上下文,状态 prepared 且 `model_called=false`;没有以准备结果冒充生成式回答。 + +## 复现路径 + +1. 使用单独测试数据库或隔离 Schema,启用 public 的 vector 扩展,执行 `alembic upgrade head`。不要把合成验收资料写入业务库。 +2. 依 [AUTH_API.md](AUTH_API.md) 初始化本机测试管理员;真实密码、环境文件与日志不入库。前后端 Origin、Cookie 的开发设置按该文档配置。 +3. 依 [VECTOR_API.md](VECTOR_API.md) 配置固定本地模型的离线缓存,启动默认生命周期后端及 Vite。 +4. 从浏览器执行上表步骤,先验证缺失元数据被拒绝,再录入有明确采集时间的合成资料并审核、发布文本和向量、查询证据。 +5. 使用独立普通用户核对菜单、直接路由拦截与退出;同时用 HTTP 核对服务端授权、CSRF 和失效行为。 +6. 保存验收摘要与合成截图后,关闭本次测试服务并清理隔离 Schema;本次运行环境与原始记录仅保存在 Git 元数据目录。 + +## 本轮回归验证 + +- `pytest -q backend/tests`:**224 passed, 1 skipped**;使用隔离 PostgreSQL,skip 为必须显式启用的真实 Embedding 专项。 +- `ruff check backend`、`ruff format --check backend`:通过。 +- 前端 `npm run build`(含 `vue-tsc -b`):通过。 +- 本轮真实固定 BGE small 的运行证据来自上述浏览器/HTTP 验收,和常规自动化的坐标 fixture 分开记录;没有生成模型调用。 + +## 尚未完成的验收 + +本轮不替代 Docker 容器验收、多浏览器/设备回归、生产 HTTPS、浏览器改密和历史知识元数据修订页面。既有后端自动化已覆盖改密、撤销和重启持久化,本轮未将它们称作浏览器操作。生成模型、已核验领域资料/独立问题集和领域质量实验继续保留后续范围。 diff --git a/docs/TASKS.md b/docs/TASKS.md index c5eaab1..269089f 100644 --- a/docs/TASKS.md +++ b/docs/TASKS.md @@ -4,16 +4,16 @@ | 编号 | 模块 | 任务 | 负责人 | 状态 | |---|---|---|---|---| -| T01 | 前端 | 验证 Vue 页面启动、补充截图 | 吉海燕 | 进行中(生产构建已通过;登录、账户设置与角色导航已合入,待浏览器 Cookie 联调) | +| T01 | 前端 | 验证 Vue 页面启动、补充截图 | 吉海燕 | 进行中(生产构建及登录/角色/核心管理流程浏览器联调已通过;账户设置等剩余验收见下文) | | T02 | 后端 | 验证 FastAPI `/docs`、`/api/health` | 范涵宇 | 已完成自动化/真实 HTTP 验证;待目标 Docker 环境复核 | | T03 | 数据库 | 启动 PostgreSQL 并验证知识入库 | 范涵宇 | 已用隔离 PostgreSQL 完成迁移与回归;本机 Docker Compose 验收受阻 | | T04 | 文档 | 明确接口字段、知识来源审核规则 | 杨星萱 | 待办 | | T05 | 知识库 | 设计 sources/documents/chunks/version 表 | 范涵宇 | 已完成(迁移、发布快照和接口已合入;待 Docker 联调) | | T06 | 检索 | 实现关键词检索基线 | 范涵宇 | 已完成(已核验知识 BM25 与检索历史已合入) | | T07 | 向量 | 评估 Embedding 并实现 pgvector | 范涵宇 | 已完成(本地模型、精确向量和 RRF 已合入;待真实领域评测) | -| T08 | RAG | 集成 LLM 与来源展示 | 范涵宇 | 暂缓(已确定 DeepSeek,待明确恢复外部模型调用授权) | +| T08 | RAG | 集成 LLM 与来源展示 | 范涵宇 | 协议已合入(证据与 OpenAI 兼容请求契约已完成;生成模型选型与实际调用按要求延期) | | T09 | 实验 | 构建可核验问题集及评分规范 | 杨星萱 | 待办 | -| T10 | 工程 | 补充测试、Alembic、鉴权与部署 | 范涵宇(主)、全员 | 进行中(迁移、认证、CSRF 与自动化回归已完成;本机 pytest/Docker/部署验收待补) | +| T10 | 工程 | 补充测试、Alembic、鉴权与部署 | 范涵宇(主)、全员 | 进行中(迁移、认证、CSRF、224项回归及浏览器核心流程已通过;Docker/HTTPS/部署待验收) | ## 2026-10-15 首次联调记录 @@ -90,3 +90,12 @@ - 实际验证:`pytest -q` **224 passed, 1 skipped**;较上一阶段新增 43 项上下文/协议/权限/失效/预算/原文/失败回滚/真实 HTTP 重启用例,另新增 1 项上下文迁移降级保留数据验证。skip 为既有须显式启用的真实 Embedding 用例,本轮未重跑真实模型专项,不沿用旧结果冒充生成模型验证。`ruff check`、`ruff format --check` 通过。前端调用层同步,构建在补齐依赖后通过。 - PR #16 已合入的认证路由引用了 `vue-router`,原 package.json/锁文件未声明;本轮补充匹配现有 Vue 3 的 vue-router 4 依赖及锁记录,保留其他依赖原版本和平台元数据。前端 `npm run build` 通过;浏览器实际 Cookie/CSRF/角色及容器验收仍待完成。 - 本轮所有测试为协议/合成资料或坐标 fixture,没有真实生成模型请求、质量分数或费用。生成模型选择和服务凭据保持延期,不重复索要;本阶段提交 PR 后等待实际合入,再继续下一项可独立验收的后端任务。 + + +## 2026-10-16 真实浏览器联调与阶段验收收尾 + +- 上下文阶段已通过真正 merged 的替代 PR #23 合入 dev;完整 backend 树及专用 RAG 契约/API 类型与原 #17 一致,通用文档保留集成更新,已记录重放等价依据。旧 #17 打开不阻塞后续阶段。 +- 从最新 dev 创建 `test/backend-browser-acceptance`,用隔离 PostgreSQL16/pgvector、默认 FastAPI 生命周期、Vite 与真实浏览器走通管理员登录/刷新会话、来源/知识/文档录入、审核、文本与真实 BGE small 向量发布、RRF 原文来源展示,以及普通用户菜单/路由限制和退出。 +- 修正页面联调发现的采集时间缺失、审核/文档导航入口和过期功能说明;后端审核/授权不放宽,不猜测历史资料采集时间。具体路径、HTTP 与浏览器证据分工及合成截图见 [BROWSER_ACCEPTANCE.md](BROWSER_ACCEPTANCE.md)。 +- 独立 HTTP 检查确认匿名401、普通用户八类管理资源403、缺失CSRF/错误Origin403、退出后401;目录写入使旧发布409,重新发布后恢复真实检索;准备上下文仍为 model_called=false。合成数据和本机运行环境、原始记录、凭据不入库。 +- 该轮验收收尾了T02的真实浏览器API连接、T05知识录入/审核与文本/向量页面核心流程、Issue #5登录/Cookie/CSRF/角色的主要浏览器路径。Docker、HTTPS、多浏览器、浏览器改密及历史元数据修订页面仍待验收;生成模型和领域评测继续延期。本轮 `pytest -q backend/tests` **224 passed, 1 skipped**,`ruff check`、`ruff format --check` 和前端 `npm run build` 均通过;常规 skip 为须显式启用的真实模型专项,本轮另外通过浏览器与独立 HTTP 实际运行了固定 BGE small。 diff --git a/docs/assets/browser-acceptance/rrf-browser.jpg b/docs/assets/browser-acceptance/rrf-browser.jpg new file mode 100644 index 0000000..cae38b3 Binary files /dev/null and b/docs/assets/browser-acceptance/rrf-browser.jpg differ diff --git a/docs/assets/browser-acceptance/user-retrieval-browser.jpg b/docs/assets/browser-acceptance/user-retrieval-browser.jpg new file mode 100644 index 0000000..5118bd6 Binary files /dev/null and b/docs/assets/browser-acceptance/user-retrieval-browser.jpg differ diff --git a/docs/assets/browser-acceptance/user-role-browser.jpg b/docs/assets/browser-acceptance/user-role-browser.jpg new file mode 100644 index 0000000..159c0d0 Binary files /dev/null and b/docs/assets/browser-acceptance/user-role-browser.jpg differ diff --git a/frontend/src/App.vue b/frontend/src/App.vue index b9dcfe4..ace22a5 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -8,8 +8,8 @@ import { useRouter } from 'vue-router' import type { SessionUser } from './types/auth' type Page='chat'|'knowledge'|'sources'|'evaluation' const router=useRouter(),currentUser=ref(),page=ref('chat'), apiStatus=ref('检测中'), question=ref(''), busy=ref(false), search=ref(''), showForm=ref(false), error=ref(''), notice=ref('') -const messages=ref([{role:'assistant',content:'你好!我是边疆红驿文旅问答原型。当前后端仅为基础接口,尚未接入知识检索和大语言模型。'}] as {role:string;content:string;sources?:{title:string;source_url?:string|null}[]}[]) -const items=ref([]), form=ref({title:'',category:'旅游资源',content:'',source_url:''}) +const messages=ref([{role:'assistant',content:'你好!我是边疆红驿文旅问答原型。已接入已核验资料的混合检索,可展示原文证据与来源;生成式回答尚未启用。'}] as {role:string;content:string;sources?:{title:string;source_url?:string|null}[]}[]) +const items=ref([]), form=ref({title:'',category:'旅游资源',content:'',source_url:'',collected_at:''}) const sources=ref([]), sourceSearch=ref(''), showSourceForm=ref(false), sourceForm=ref({name:'',source_url:'',notes:''}) const nav=[{id:'chat',label:'智能问答',icon:MessageSquareText},{id:'knowledge',label:'知识库管理',icon:Database,adminOnly:true},{id:'sources',label:'来源与更新',icon:Globe2,adminOnly:true},{id:'evaluation',label:'实验评估',icon:Activity,adminOnly:true}] as const const visibleNav=computed(()=>nav.filter(item=>!('adminOnly' in item)||!item.adminOnly||currentUser.value?.role==='admin')) @@ -22,10 +22,22 @@ watch(sourceSearch,()=>{window.clearTimeout(sourceSearchTimer);sourceSearchTimer onMounted(async()=>{try{currentUser.value=await getCurrentUser()}catch{router.replace('/login');return}try{await getHealth();apiStatus.value='已连接'}catch{apiStatus.value='未连接'}if(currentUser.value.role==='admin')await Promise.all([refresh(),refreshSources()])}) async function send(){const q=question.value.trim();if(!q||busy.value)return;messages.value.push({role:'user',content:q});question.value='';busy.value=true;try{const r=await retrieveHybrid({query:q,top_k:5,candidate_k:50});const content=r.status==='no_evidence'?'未检索到可用的已核验知识依据。请调整问题或补充审核后的资料。':`已通过 RRF 混合检索命中 ${r.total_hits} 条依据(关键词 ${r.lexical_total_hits},向量 ${r.vector_total_hits},${r.elapsed_ms.toFixed(1)} ms)。当前仅展示检索证据,尚未接入大语言模型生成回答。`;messages.value.push({role:'assistant',content,sources:r.results.map(hit=>({title:`${hit.title}:${hit.snippet}`,source_url:hit.source_url}))})}catch(e){messages.value.push({role:'assistant',content:e instanceof Error?e.message:'暂时无法连接混合检索服务。'})}finally{busy.value=false}} function validUrl(value:string){try{if(value)new URL(value);return true}catch{return false}} -async function save(){error.value='';notice.value='';if(!form.value.title.trim()||!form.value.content.trim()){error.value='请填写知识标题和正文。';return}if(!validUrl(form.value.source_url)){error.value='来源网址必须是有效的 http(s) 地址。';return}try{await createKnowledge({...form.value,title:form.value.title.trim(),content:form.value.content.trim(),...(form.value.source_url?{source_url:form.value.source_url}:{})});form.value={title:'',category:'旅游资源',content:'',source_url:''};showForm.value=false;notice.value='已保存为待审核条目,尚未进入检索索引。';await refresh()}catch(e){error.value=e instanceof Error?e.message:'保存失败'}} +async function save(){ + error.value='';notice.value='' + if(!form.value.title.trim()||!form.value.content.trim()){error.value='请填写知识标题和正文。';return} + if(!validUrl(form.value.source_url)){error.value='来源网址必须是有效的 http(s) 地址。';return} + try{ + await createKnowledge({title:form.value.title.trim(),category:form.value.category,content:form.value.content.trim(), + ...(form.value.source_url?{source_url:form.value.source_url}:{}), + ...(form.value.collected_at?{collected_at:new Date(form.value.collected_at).toISOString()}:{}), + }) + form.value={title:'',category:'旅游资源',content:'',source_url:'',collected_at:''} + showForm.value=false;notice.value='已保存为待审核条目,尚未进入检索索引。';await refresh() + }catch(e){error.value=e instanceof Error?e.message:'保存失败'} +} -