Compare commits

...
Author SHA1 Message Date
yxxandClaude Code 0c03f9bcb5 docs: add test question set and scoring rubric, finalize review rules
- add test-question-set.md (24 questions across 4 categories)
- add scoring-rubric.md (A/B/C comparison dimensions and scale)
- finalize review-rules.md (draft -> confirmed)
- update TASKS.md: mark T04 and T09 done

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-21 23:54:31 +08:00
saint 7eac019dd2 docs: clarify phase 2 material evidence boundaries 2026-10-20 14:40:07 +08:00
yxxandClaude Code 3ad879309e docs: register phase 2 material source directories
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-20 14:40:07 +08:00
yxxandClaude Code deb21b9044 docs: register phase 2 material pilot entries
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-20 14:40:07 +08:00
Kronecker 4654f2e486 fix: respect document-bound knowledge sources 2026-10-20 14:40:07 +08:00
saint 1430d35196 docs: record knowledge traceability decisions
Reviewed documentation-only decision record.
2026-10-19 22:46:54 +08:00
saint 7cd4dd2f38 docs: record knowledge traceability decisions 2026-10-19 22:46:22 +08:00
saint 8cb60cc53a feat: support knowledge source selection
Reviewed frontend source selection and display.
2026-10-19 22:44:50 +08:00
saint 88b1952107 feat: support knowledge source selection 2026-10-19 22:44:02 +08:00
saint a1b3147f09 merge: knowledge collection review batch
Reviewed and verified knowledge collection batch.
2026-10-19 22:26:52 +08:00
12 changed files with 346 additions and 19 deletions
+1
View File
@@ -69,6 +69,7 @@ Vite 通常使用 http://localhost:5173。
- 创建知识沿用 `title/category/content/source_url`,新增可选 `source_id/document_id/collected_at`;状态固定为 `pending`。关联文档时自动解析其来源,填写的来源或网址若冲突会报错。直接来源链接的旧调用仍受支持。 - 创建知识沿用 `title/category/content/source_url`,新增可选 `source_id/document_id/collected_at`;状态固定为 `pending`。关联文档时自动解析其来源,填写的来源或网址若冲突会报错。直接来源链接的旧调用仍受支持。
- 来源创建字段:`name`、可选 `source_url`、`notes`。文档创建字段:`source_id/title/content/collected_at`,可选 `category/notes`。文档来源归属固定,换来源需新建文档。 - 来源创建字段:`name`、可选 `source_url`、`notes`。文档创建字段:`source_id/title/content/collected_at`,可选 `category/notes`。文档来源归属固定,换来源需新建文档。
- 知识修改时省略 `collected_at` 表示保留原值,明确传 `null` 表示清空;清空后回到待审核,补全真实采集时间前不能核验通过。文档的采集时间仍是必填字段,不接受 `null`。 - 知识修改时省略 `collected_at` 表示保留原值,明确传 `null` 表示清空;清空后回到待审核,补全真实采集时间前不能核验通过。文档的采集时间仍是必填字段,不接受 `null`。
- 知识管理页可为未关联文档的条目选择或解除登记来源;关联文档的条目沿用文档所属来源,来源选择和网址只读,保存其他字段时保留关联。来源选择与边界验收见 [SOURCE_SELECTION_ACCEPTANCE.md](docs/SOURCE_SELECTION_ACCEPTANCE.md)。
- PATCH 请求必须携带读取时获得的 `expected_version`,并至少提交一个修改字段;DELETE 使用查询参数 `expected_version`。版本过期返回 409,防止覆盖他人修改。 - PATCH 请求必须携带读取时获得的 `expected_version`,并至少提交一个修改字段;DELETE 使用查询参数 `expected_version`。版本过期返回 409,防止覆盖他人修改。
- 审核请求字段见下方示例。`verification_status` 只允许 `pending/verified/rejected`;核验人和说明必填。通过审核前必须有来源网址及真实采集时间。这里只记录人工结论,不会自动核实网页。 - 审核请求字段见下方示例。`verification_status` 只允许 `pending/verified/rejected`;核验人和说明必填。通过审核前必须有来源网址及真实采集时间。这里只记录人工结论,不会自动核实网页。
- 修改条目、关联来源或关联文档都会将相关知识转为 `pending`,清除当前审核字段并记录失效原因。历史快照保留当时的条目、来源和文档。 - 修改条目、关联来源或关联文档都会将相关知识转为 `pending`,清除当前审核字段并记录失效原因。历史快照保留当时的条目、来源和文档。
+48
View File
@@ -0,0 +1,48 @@
# 来源选择与文档关联边界验收
## 范围与前置
原知识编辑修正 PR #50 已由真正合入 dev 的替代 PR #56 完整交付;PR #58 增加来源选择与名称展示,PR #59 维持草稿时间可空、审核前必填,并暂缓知识通用备注。本轮针对新来源选择路径开展浏览器及独立 HTTP 验收,承接 Issue #57 和 #52,不重复实现已有后端字段。
使用隔离数据库 Schema、默认 FastAPI 生命周期与 Vite;账户、101个来源、一个文档和三个知识条目均为明确标注的合成数据。网址使用 `example.com`,不代表真实文旅资料或领域质量。修正前的复现与最终验收分别使用两个全新 Schema。
## 复现与修正
1. 为一个已核验知识条目关联文档;该文档属于一个已登记来源。
2. 原页面允许将来源选为“不关联已登记来源”并保存,提示成功,但后端会根据文档恢复原来源。条目还会回到待审核;页面没有解释无法解除的原因。
3. 现在,文档关联知识的来源选择禁用、网址只读,并显示文档决定来源的说明。保存标题等其他字段时省略 `source_id` 和 `source_url`,不会提交来源重绑意图。
4. 未关联文档的条目仍能选择或解除登记来源,解除后可保留独立网址。后端仍负责关联一致性、版本和权限校验;本页不增加文档解绑功能。
![文档关联来源锁定说明](assets/source-selection-acceptance/document-source-locked.png)
## 最终浏览器验收
| 操作 | 实际结果 |
|---|---|
| 管理员打开 `/knowledge`,载入101个来源 | 下拉框包含101个来源及“不关联”选项,覆盖来源API的两页数据 |
| 普通知识选择来源API第二页的合成来源 | 网址正确回填,保存后列表显示正确来源名称与链接,条目回到 `pending` |
| 该条目的来源网址改为与登记来源冲突的值 | 显示后端错误,编辑表单保留,不提示保存成功 |
| 普通知识解除登记来源 | 列表变为“未关联”,仍保留原独立网址;重新查询确认 `source_id=null` |
| 编辑文档关联知识 | 来源控件禁用、网址只读,说明清晰;仍能保存标题修订 |
| 展开普通条目的历史 | 旧版本仍显示其当时来源名称及原微秒采集时间 |
![第二页来源实际保存后的名称与链接](assets/source-selection-acceptance/source-selected.png)
## 独立 HTTP 核对
- 来源切换、解除与文档关联标题修订的版本、来源ID/网址、审核回退均与页面结果一致;标题修订和来源操作保留原采集时间的全部微秒。
- 文档来源冲突、登记来源网址冲突返回422;过期编辑返回409;这些失败请求不改变当前条目或审计历史。
- 普通用户不能读取管理来源列表或修改知识(403);客户端控件不代替后端权限与关联校验。
- 修改后旧文本发布返回409;保存的检索记录及原有审计事件保持一致。独立网址草稿未受其他条目操作影响。
HTTP仅用于准备合成数据、独立查询及负向接口验证,不冒充浏览器交互;浏览器操作与截图均来自实际页面。
## 验证与清理
- `cd frontend && node --test tests/datetime.test.mjs`:**6 passed**。
- `cd frontend && npm run build`:类型检查与生产构建通过。
- `git diff --check`:通过。
- 本轮没有后端代码或迁移变更,未重跑全量pytest;未运行Embedding或生成模型。
- 两个隔离Schema已分别独立查询确认不存在;自有FastAPI、Vite及数据库服务停止,临时浏览器标签关闭。原始合成记录、运行环境、账户和本机进度不入库。
仅覆盖上述来源编辑路径。原生删除确认、Docker/生产HTTPS/多浏览器/浏览器改密与真实领域资料验收继续单列;Issue #5、#52、#57 已关闭的远程状态不能替代这些剩余验收。
+13 -3
View File
@@ -7,13 +7,13 @@
| T01 | 前端 | 验证 Vue 页面启动、补充截图 | 吉海燕 | 进行中(生产构建及登录/角色/核心管理流程浏览器联调已通过;账户设置等剩余验收见下文) | | T01 | 前端 | 验证 Vue 页面启动、补充截图 | 吉海燕 | 进行中(生产构建及登录/角色/核心管理流程浏览器联调已通过;账户设置等剩余验收见下文) |
| T02 | 后端 | 验证 FastAPI `/docs`、`/api/health` | 范涵宇 | 已完成自动化/真实 HTTP 验证;待目标 Docker 环境复核 | | T02 | 后端 | 验证 FastAPI `/docs`、`/api/health` | 范涵宇 | 已完成自动化/真实 HTTP 验证;待目标 Docker 环境复核 |
| T03 | 数据库 | 启动 PostgreSQL 并验证知识入库 | 范涵宇 | 已用隔离 PostgreSQL 完成迁移与回归;本机 Docker Compose 验收受阻 | | T03 | 数据库 | 启动 PostgreSQL 并验证知识入库 | 范涵宇 | 已用隔离 PostgreSQL 完成迁移与回归;本机 Docker Compose 验收受阻 |
| T04 | 文档 | 明确接口字段、知识来源审核规则 | 杨星萱 | 待办 | | T04 | 文档 | 明确接口字段、知识来源审核规则 | 杨星萱 | 已完成(字段核对见 field-feedback.md 确认决定;审核规则已定稿 review-rules.md) |
| T05 | 知识库 | 设计 sources/documents/chunks/version 表 | 范涵宇 | 已完成(迁移、发布快照和接口已合入;待 Docker 联调) | | T05 | 知识库 | 设计 sources/documents/chunks/version 表 | 范涵宇 | 已完成(迁移、发布快照和接口已合入;待 Docker 联调) |
| T06 | 检索 | 实现关键词检索基线 | 范涵宇 | 已完成(已核验知识 BM25 与检索历史已合入) | | T06 | 检索 | 实现关键词检索基线 | 范涵宇 | 已完成(已核验知识 BM25 与检索历史已合入) |
| T07 | 向量 | 评估 Embedding 并实现 pgvector | 范涵宇 | 已完成(本地模型、精确向量和 RRF 已合入;待真实领域评测) | | T07 | 向量 | 评估 Embedding 并实现 pgvector | 范涵宇 | 已完成(本地模型、精确向量和 RRF 已合入;待真实领域评测) |
| T08 | RAG | 集成 LLM 与来源展示 | 范涵宇 | Responses 协议已合入(请求/解析与旧快照兼容已完成;生成模型选型与实际调用延期) | | T08 | RAG | 集成 LLM 与来源展示 | 范涵宇 | Responses 协议已合入(请求/解析与旧快照兼容已完成;生成模型选型与实际调用延期) |
| T09 | 实验 | 构建可核验问题集及评分规范 | 杨星萱 | 待办 | | T09 | 实验 | 构建可核验问题集及评分规范 | 杨星萱 | 已完成(问题集 test-question-set.md、评分规范 scoring-rubric.md 已建,待阶段 3 起用于检索/问答评测) |
| T10 | 工程 | 补充测试、Alembic、鉴权与部署 | 范涵宇(主)、全员 | 进行中(聊天边界修正已合入;知识编辑浏览器复核与采集时间清空修正待审,Docker/HTTPS/部署待验收) | | T10 | 工程 | 补充测试、Alembic、鉴权与部署 | 范涵宇(主)、全员 | 进行中(聊天边界及知识编辑修正已合入;来源选择浏览器复核与文档来源锁定修正待审,Docker/HTTPS/部署待验收) |
## 2026-10-15 首次联调记录 ## 2026-10-15 首次联调记录
@@ -142,3 +142,13 @@
- 修正清空知识时间却静默保留旧值的问题:前端明确发送 null,后端将文档必填时间约束与知识可空时间分开;文档仍拒绝 null,审核、版本和历史保护保留。无迁移或模型调用。 - 修正清空知识时间却静默保留旧值的问题:前端明确发送 null,后端将文档必填时间约束与知识可空时间分开;文档仍拒绝 null,审核、版本和历史保护保留。无迁移或模型调用。
- 后端目录/索引/导出专项 **75 passed**、前端时区 **6 passed**,ruff检查/格式检查、前端类型/生产构建与diff检查通过;没有把此前312项全量结果当作本轮重跑。可复现步骤、合成截图和浏览器/HTTP证据边界见 [KNOWLEDGE_EDIT_ACCEPTANCE.md](KNOWLEDGE_EDIT_ACCEPTANCE.md)。 - 后端目录/索引/导出专项 **75 passed**、前端时区 **6 passed**,ruff检查/格式检查、前端类型/生产构建与diff检查通过;没有把此前312项全量结果当作本轮重跑。可复现步骤、合成截图和浏览器/HTTP证据边界见 [KNOWLEDGE_EDIT_ACCEPTANCE.md](KNOWLEDGE_EDIT_ACCEPTANCE.md)。
- 本阶段提交后等待真正合入,再继续剩余合理后端工作;原生确认、Docker/HTTPS/多浏览器/改密及领域资料待办保留,生成模型和凭据继续延期。 - 本阶段提交后等待真正合入,再继续剩余合理后端工作;原生确认、Docker/HTTPS/多浏览器/改密及领域资料待办保留,生成模型和凭据继续延期。
## 2026-10-20 T05/T10:来源选择与文档关联边界验收
- 原 PR #50 关闭未标记合并,但全部修正通过真正合入 dev 的 PR #56 进入集成分支;已核对精确 head、完整阶段树、8个改动文件及替代 head/merge 的祖先关系。保留原编号与替代依据,再从最新 dev 创建唯一后端验收分支。
- PR #58 已实现知识管理页来源选择与名称展示,PR #59 已记录字段决定。本轮仅验收新来源路径及修正其文档关联边界;草稿时间仍可空,审核前真实时间必填,知识通用 notes 暂缓。
- 浏览器复现:文档关联知识选择“不关联来源”后提示保存成功,但后端按文档恢复原来源。现在页面锁定此类条目的来源选择与网址并解释原因;保存其他字段时省略来源重绑字段。后端关联、版本和审核规则保持原有行为,无迁移。
- 在全新隔离合成数据中,真实浏览器完成101个来源跨两页加载、第二页来源选择/名称/链接保存、普通条目解除来源并保留独立网址、文档关联条目的只读说明及标题保存、冲突链接错误提示与历史快照展示。HTTP独立核对微秒保留、旧历史/检索记录不变、来源/网址冲突422、过期编辑409、旧发布409和普通用户403;失败请求不改变条目或审计历史。
- 前端时区 **6 passed**、类型/生产构建与 `git diff --check` 通过;本轮没有后端代码变更或全量pytest重跑,也没有Embedding/生成调用。复现、截图及验证边界见 [SOURCE_SELECTION_ACCEPTANCE.md](SOURCE_SELECTION_ACCEPTANCE.md)。
- 修正前及最终两轮Schema均经独立查询确认删除,自有服务停止、临时浏览器标签关闭。此阶段提交后等待真正合入;Docker/生产HTTPS/多浏览器/浏览器改密、原生确认和领域资料等剩余条件单列保留,Issue关闭不替代实际验收。
Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

+11 -3
View File
@@ -34,8 +34,16 @@
5. **来源名称回填**:现有直接填 `source_url` 的条目,来源名称缺失;后续入库前应补登记到 `sources` 并关联。 5. **来源名称回填**:现有直接填 `source_url` 的条目,来源名称缺失;后续入库前应补登记到 `sources` 并关联。
## 三、核对结论 ## 三、确认决定(2026-10-19)
1. **来源选择与展示:采纳。** 管理员知识管理页已支持选择已登记来源并展示来源名称与链接;仍由后端校验来源、链接与权限。
2. **采集时间:维持现有分层约束。** 草稿知识条目的 `collected_at` 可以为空;标记为 `verified` 前必须具备可核验的来源网址与采集时间。文档采集时间继续为必填。
3. **通用备注:暂缓。** 当前不为 `knowledge_items` 新增 `notes` 字段;待出现稳定业务需求后再评估数据库迁移。审核说明与关联文档备注按现有职责使用。
以上确认仅涉及字段与流程,不包含真实资料、账号、凭据或环境信息。
## 四、核对结论
- 追溯核心字段(标题、类别、URL、获取/更新时间、核验状态、核验人、核验依据)系统均已支持,状态三值与审核规则一致。 - 追溯核心字段(标题、类别、URL、获取/更新时间、核验状态、核验人、核验依据)系统均已支持,状态三值与审核规则一致。
- 主要缺口集中在「来源名称的独立登记与前端展示」「知识条目通用备注」「摘要与正文的一致性」三点,需在阶段 2 入库前与范涵宇、吉海燕确认。 - 来源名称的独立登记与前端展示已完成;通用备注字段暂缓,摘要与正文的一致性仍须在核验时确认。
- 数据库结构改动仅为建议,不由本文档直接执行。 - 数据库结构改动仍需独立评估,不由本文档直接执行。
+7 -1
View File
@@ -37,5 +37,11 @@
|---|---|---|---|---|---|---|---|---|---|---|---|---| |---|---|---|---|---|---|---|---|---|---|---|---|---|
| K-0001 | 【示例·占位】某景区简介 | 旅游资源 | 【示例来源,非真实】示例站点 | https://example.com/ | 2026-10-15 | 2026-10-15 | 【示例】此处为占位摘要,非真实资料 | 待核验 | | | pending | 示例行,仅演示填写格式,不构成真实资料 | | K-0001 | 【示例·占位】某景区简介 | 旅游资源 | 【示例来源,非真实】示例站点 | https://example.com/ | 2026-10-15 | 2026-10-15 | 【示例】此处为占位摘要,非真实资料 | 待核验 | | | pending | 示例行,仅演示填写格式,不构成真实资料 |
| K-0002 | 阶段1联调测试条目 | 其他(测试) | 无(测试) | | 2026-10-17 | 2026-10-17 | 仅用于验证资料登记与持久化流程,不构成任何那拉提文旅事实 | 无需核验(测试) | | | pending | 阶段1测试条目,仅验证登记流程,不进入正式检索库,不伪装为真实资料 | | K-0002 | 阶段1联调测试条目 | 其他(测试) | 无(测试) | | 2026-10-17 | 2026-10-17 | 仅用于验证资料登记与持久化流程,不构成任何那拉提文旅事实 | 无需核验(测试) | | | pending | 阶段1测试条目,仅验证登记流程,不进入正式检索库,不伪装为真实资料 |
| K-0003 | 那拉提草原景区概况(国家5A级旅游景区) | 旅游资源 | 中国日报网(govt.chinadaily.com.cn) | https://govt.chinadaily.com.cn/s/201812/28/WS5c2592c6498eb4f01ff25691/nalati-grassland-nalati-prairie-ili-kazak-autonomous-prefecture.html | 2026-10-19 | 2026-10-19 | 来源页将那拉提草原描述为位于新源县那拉提镇以东的巩乃斯草原亚高山草甸,并标注景区等级为5A级;仅记录该页面可见的静态概况(待人工核验) | 待人工核验(来源页已可访问,尚未完成交叉核验) | | | pending | 试点条目;来源页面发布于2018年,开放时间、票务等时效信息不得据此使用 |
| K-0004 | 那拉提镇红色记忆馆 | 红色文化 | 天山网(新疆权威媒体) | https://www.ts.cn/xwzx/shxw/202608/t20260810_35862803.shtml | 2026-10-19 | 2026-10-19 | 新源县那拉提镇塔依阿苏村退役老兵自建红色记忆馆,藏品超5000件(军功章、旧军服、烈士信函、历史报刊等),为当地爱国主义教育、国防教育阵地(摘自检索摘要,待核实) | 待人工核验(联网检索所得,未打开页面复核) | | | pending | 试点条目 |
| K-0005 | 自治区文旅厅红色旅游主题线路(独库公路串联那拉提) | 红色文化 | 新疆维吾尔自治区文化和旅游厅(天山融媒发布) | https://tnews.xjmty.com/app/slyapp/hdf/202310/t20231001_16431821.html | 2026-10-19 | 2026-10-19 | 自治区文旅厅推出6大主题13条红色旅游主题线路,其中以独库公路为主线的线路将乔尔玛革命烈士陵园与那拉提景区等串联(摘自检索摘要,待核实) | 待人工核验(联网检索所得,未打开页面复核) | | | pending | 试点条目 |
| K-0006 | 2025那拉提音乐节交通安全指南(活动期) | 交通出行 | 新源县人民政府 | https://www.xinyuan.gov.cn/xinyuan/c113900/202508/6785b61206e94151886c1be0b27f5a90.shtml | 2026-10-19 | 2026-10-19 | 来源为2025年那拉提音乐节期间发布的交通安全提示,含活动期易拥堵路段、临时停车和到达建议;仅作为该活动期交通信息线索,不可泛化为常规景区交通指南(待人工核验) | 待人工核验(来源页已可访问,适用期需人工确认) | | | pending | 时效性强,不进入通用问答或正式检索;后续应另收集常规交通官方资料 |
| K-0007 | 新源县旅游产业基本情况(含那拉提住宿与民宿) | 民宿服务 | 新源县人民政府(县文化体育广播电视和旅游局) | https://www.xinyuan.gov.cn/xinyuan/c113962/202505/74a8a8423c7b4f7c906aaa491900d96c.shtml | 2026-10-19 | 2026-10-19 | 来源页列示那拉提旅游风景区为5A级景区、那拉提镇为全国特色小镇,并记载全县住宿单位1414家、那拉提区域1204家;仅记录页面公布的统计口径(待人工核验) | 待人工核验(来源页已可访问,尚未完成交叉核验) | | | pending | 试点条目;统计数据具有发布时间口径,不作为实时房态或民宿推荐依据 |
| K-0008 | 新源县“风光名胜”栏目(政府网目录页) | 旅游资源 | 新源县人民政府 | https://www.xinyuan.gov.cn/xinyuan/c113892/common_list.shtml | 2026-10-19 | 2026-10-19 | 新源县政府网“风光名胜”栏目入口,目录中含外部平台链接;仅作后续检索线索,不作为单条可核验资料 | 待人工核验(栏目页已可访问,具体篇目需逐条访问) | | | pending | 栏目目录页,非单条资料;仅登记已核对的栏目名称与入口 |
> 上表 K-0001 为**示例占位行**,K-0002 为**测试条目**,均不包含真实文旅信息。正式收集的资料按此格式逐行追加。 > 上表 K-0001 为**示例占位行**,K-0002 为**测试条目**,均不包含真实文旅信息。K-0003 至 K-0007 为阶段2试点条目,K-0008 为资料线索入口;各条均未完成独立交叉核验,全部保持 `pending`,经人工核验通过后方可改为 `verified`。时效性或活动期信息不得进入通用问答或正式检索。
+3 -3
View File
@@ -1,7 +1,7 @@
# 审核规则 # 审核规则
> 用途:规定资料的核验状态、判定标准与核验流程,是资料能否进入正式检索库的统一依据。 > 用途:规定资料的核验状态、判定标准与核验流程,是资料能否进入正式检索库的统一依据。
> 状态:本规则为**草案**,由杨星萱起草,需经确认后定稿。 > 状态:**已定稿**(2026-10-21),由杨星萱起草并确认。
> 维护人:杨星萱 > 维护人:杨星萱
## 一、状态定义 ## 一、状态定义
@@ -12,7 +12,7 @@
| `verified` | 可正式使用:来源可靠、关键事实交叉印证、时效性满足需求、信息完整 | 是 | | `verified` | 可正式使用:来源可靠、关键事实交叉印证、时效性满足需求、信息完整 | 是 |
| `rejected` | 不采用:来源不可靠、信息被证伪/与权威来源冲突、严重过时、或无法核验且影响关键事实 | 否 | | `rejected` | 不采用:来源不可靠、信息被证伪/与权威来源冲突、严重过时、或无法核验且影响关键事实 | 否 |
## 二、判定标准(草案) ## 二、判定标准
### 标记为 `verified` 需同时满足 ### 标记为 `verified` 需同时满足
@@ -50,4 +50,4 @@
- 时效性:注明信息发布时间,避免使用明显过时内容。 - 时效性:注明信息发布时间,避免使用明显过时内容。
- 边界一致:首版不承诺实时房态、实时价格、自动联网核验等,此类信息不按可靠事实收录。 - 边界一致:首版不承诺实时房态、实时价格、自动联网核验等,此类信息不按可靠事实收录。
> 本规则为草案,判定标准与流程以负责人确认为准。 > 本规则已于 2026-10-21 定稿,作为资料核验的统一依据。
+70
View File
@@ -0,0 +1,70 @@
# 评分规范(A/B/C 对照实验 · 草案)
> 用途:规定 LLM-only、关键词 RAG、混合检索 RAG 三种配置的评分维度、打分标准与统计方式,保证实验结论可复现、可回溯。
> 维护人:杨星萱(知识库、实验与文档负责人)
> 关联:[测试问题集](test-question-set.md)、[资料台账](material-ledger.md)
> 原则:任何分数必须来自实际运行、检索日志与人工评分,不得先写结论后补数据。
## 一、对照配置
| 代号 | 配置 | 说明 |
|---|---|---|
| A | LLM-only | 不接检索,仅凭模型参数知识回答 |
| B | 关键词 RAG | BM25 关键词检索 + 上下文 + LLM |
| C | 混合检索 RAG | BM25 + 向量 RRF 融合检索 + 上下文 + LLM |
## 二、评分维度与权重
| 维度 | 权重 | 说明 |
|---|---|---|
| 正确性 | 40% | 回答要点与题目「预期知识点」一致 |
| 可追溯性 | 30% | 引用来源真实可回访,且确实支持该回答 |
| 无依据回答 | 20% | 资料不足时明确说明「不确定」,不编造 |
| 响应时间 | 5% | 端到端耗时(记录实测值,分档计分) |
| 成本 | 5% | 模型调用用量/费用(记录实测值,分档计分) |
## 三、打分标准(每维度 0–2 分)
### 正确性(0–2)
| 分值 | 标准 |
|---|---|
| 2 | 答案要点与预期知识点一致,无事实错误 |
| 1 | 部分要点正确,或存在遗漏、表述偏差 |
| 0 | 答非所问、关键事实错误或与来源冲突 |
### 可追溯性(0–2)
| 分值 | 标准 |
|---|---|
| 2 | 引用来源真实、可回访,且确实支持该回答 |
| 1 | 有引用但不完全支持,或来源层级/内容与结论不符 |
| 0 | 无引用,或引用来源不支持回答 |
### 无依据回答(0–2)
| 分值 | 标准 |
|---|---|
| 2 | 资料不足时明确说明「不确定」,并只给出可核验的部分 |
| 1 | 存在臆测、推断成分,未明确标注不确定性 |
| 0 | 资料不足仍给出确定性编造(如 Q24 用活动期信息冒充常规指南) |
### 响应时间 / 成本(各 0–2,分档)
| 分值 | 响应时间(单题端到端) | 成本(相对) |
|---|---|---|
| 2 | ≤ 2 秒 | 最低档 |
| 1 | 2–10 秒 | 中间档 |
| 0 | > 10 秒或超时 | 最高档 |
> 响应时间与成本的分档阈值在阶段 6 冻结实验配置时按实测分布确定,不预设结论;本表阈值为占位示例。
## 四、加权总分与统计
- 单题加权分 = 正确性×0.4 + 可追溯性×0.3 + 无依据回答×0.2 + 响应时间×0.05 + 成本×0.05。
- 按 24 题汇总,计算 A/B/C 三组的平均分与各维度平均分。
- 除总分外,单列「无依据回答 = 0」的题数与案例(体现「不足时不编造」的达标情况)。
## 五、评分记录要求
- 每题记录:配置(A/B/C)、原始回答、检索命中的 K 编号、引用来源、各维度得分、判分依据。
- 原始回答、检索日志与评分记录须可回溯到题目编号与运行配置;不在评分表里填虚构数值。
- 正反例单独整理,作为阶段 4 案例分析的素材。
> 本规范为草案,维度与权重在阶段 3 联机验证后、阶段 6 实验前定稿。
+181
View File
@@ -0,0 +1,181 @@
# 测试问题集(阶段 3 · 草案)
> 用途:为关键词/向量/混合检索与 RAG 问答提供可核验的测试问题,每题的答案要点、允许引用来源与「资料不足」情形均已标注,供人工评分与案例分析回溯。
> 维护人:杨星萱(知识库、实验与文档负责人)
> 关联:[资料台账](material-ledger.md)(`verified` 条目)与 [评分规范](scoring-rubric.md)
> 说明:本问题集基于台账中 `verified` 条目编写;所有答案要点均来自已核验来源,不虚构事实。时效性信息(如独库公路通行时段、航班班次)以来源页最新发布为准,问题标注的仅是核验时点的口径。
## 一、口径与使用
- 共 24 题,四类各 6 题,编号 Q01–Q24。
- 每题标注**预期知识点**(人工判分的答案要点)、**允许引用来源**(K 编号,对应台账 `verified` 条目)、**资料不足情形**(何时应回答「不确定」而非编造)。
- 用于阶段 3 对关键词、向量、混合三种检索的命中检查,以及阶段 4 对 RAG 回答的「是否正确」与「来源是否支持回答」双重核对。
- 答案要点以 `verified` 条目的正文摘要为准;若来源页更新导致结论变化,须回到台账更新该条目与本题。
---
## 二、红色文化(Q01–Q06)
### Q01 · 那拉提周边有哪些红色文化纪念场所?
- **类别**:红色文化
- **预期知识点**:那拉提镇塔依阿苏村红色记忆馆(民间自建)、新源县肖尔布拉克七十二团红军团博物馆、独库公路乔尔玛烈士陵园(尼勒克县)、第四师七十一团(骑兵连)等。
- **允许引用来源**:K-0004、K-0020、K-0021、K-0038
- **资料不足情形**:若检索未命中上述任一来源,不得凭空列举,应说明「未检索到相关红色场所」并给出可补充的检索方向。
### Q02 · 七十二团红军团博物馆位于哪里、主要展陈什么?
- **类别**:红色文化
- **预期知识点**:位于新源县肖尔布拉克 72 团中心镇区,始建于 1991 年;展陈分土地革命、抗日战争、解放战争、屯垦肖尔布拉克等板块,弘扬兵团精神、红军团精神,为爱国主义教育基地。
- **允许引用来源**:K-0020
- **资料不足情形**:具体展厅数量、展品件数等细节若来源页未载明,不臆造数字。
### Q03 · 乔尔玛烈士陵园纪念的是哪段历史?
- **类别**:红色文化
- **预期知识点**:纪念 1974—1983 年修建独库公路牺牲的 168 名解放军筑路官兵,由烈士纪念碑、168 座墓冢和纪念馆组成,为国家 AAA 级景区、自治区红色旅游经典景区、爱国主义教育基地。
- **允许引用来源**:K-0021、K-0023
- **资料不足情形**:牺牲人数等关键数字须与来源一致;来源间若口径冲突,应如实标注并保持「待确认」。
### Q04 · 守墓老兵陈俊贵的事迹是什么?
- **类别**:红色文化
- **预期知识点**:1980 年 4 月独库公路筑路任务中班长郑林书把最后一个馒头让给他,班长与副班长牺牲;他退役后 1985 年携妻儿重返天山义务守墓 40 余年,2013 年获评全国道德模范、「感动中国 2013 年度人物」。
- **允许引用来源**:K-0040
- **资料不足情形**:细节(如具体年份、获奖)须与来源一致;无来源时不得转述网络传闻。
### Q05 · 自治区红色旅游主题线路如何串联那拉提?
- **类别**:红色文化
- **预期知识点**:自治区文旅厅推出红色旅游主题线路,其中以独库公路为主线的线路将乔尔玛革命烈士陵园与那拉提景区等串联。
- **允许引用来源**:K-0005、K-0030
- **资料不足情形**:线路数量(如「6 大主题 13 条」)以来源为准;未载明时不做具体数字表述。
### Q06 · 文旅部推荐的「兵团·美丽休闲乡村体验游」线路经过哪些站点?
- **类别**:红色文化
- **预期知识点**:伊宁市区 → 72 团 → 71 团 7 连 → 那拉提风景区 → 乔尔玛烈士陵园,融合田园风光、红色教育与草原景观。
- **允许引用来源**:K-0030
- **资料不足情形**:站点顺序与名称须与官方线路一致,不自行增删。
---
## 三、旅游资源(Q07–Q12)
### Q07 · 那拉提草原是什么级别的景区?
- **类别**:旅游资源
- **预期知识点**:国家 5A 级旅游景区、新疆首个国家级旅游度假区,地处天山腹地、伊犁河谷东端,属亚高山草甸草原。
- **允许引用来源**:K-0003、K-0041
- **资料不足情形**:等级/称号须与来源一致;对「首个」「唯一」等表述仅在来源明确时使用。
### Q08 · 「那拉提」名称的含义是什么?
- **类别**:旅游资源
- **预期知识点**:意为「最先见到太阳的地方」(一说与蒙古语「太阳」相关)。
- **允许引用来源**:K-0041
- **资料不足情形**:词源存在多种说法时,如实呈现「一说/另说」,不武断定论。
### Q09 · 那拉提有哪些主要游览线路或景观?
- **类别**:旅游资源
- **预期知识点**:空中草原、河谷草原、盘龙古道等游览线路,天界台、游牧人家、塔吾萨尼等景点,保留赛马、叼羊、姑娘追、阿肯弹唱会等民俗活动。
- **允许引用来源**:K-0041、K-0013、K-0015、K-0016
- **资料不足情形**:线路/景点名称以来源为准;未载明的景点不自行补入。
### Q10 · 那拉提的地理位置与地貌特征是什么?
- **类别**:旅游资源
- **预期知识点**:位于新疆伊犁哈萨克自治州新源县那拉提镇,天山腹地、伊犁河谷东端,平均海拔约 1800 米,三面环山、巩乃斯河蜿蜒流过,属高山河谷草原。
- **允许引用来源**:K-0003、K-0041
- **资料不足情形**:海拔、面积等存在多个口径时,以来源页当前表述为准,不合并不同口径。
### Q11 · 那拉提秋季景色有什么特点?
- **类别**:旅游资源
- **预期知识点**:那拉提秋季有「限定秋色」主题内容,草原秋色为季节特色景观。
- **允许引用来源**:K-0011
- **资料不足情形**:仅能转述来源页对秋季景色的描述,不扩展为具体日期或最佳观赏时段。
### Q12 · 那拉提的旅游发展历程大致是怎样的?
- **类别**:旅游资源
- **预期知识点**:从偏远牧区逐步发展为享誉世界的旅游目的地(那拉提意为「最先见到太阳的地方」,为古丝绸之路北线要地),近年打造天界台索道、哈茵赛滑雪场等四季旅游项目。
- **允许引用来源**:K-0017、K-0041
- **资料不足情形**:具体年份、投资额等数字须回到来源页核对,不凭记忆填写。
---
## 四、民宿服务(Q13–Q18)
### Q13 · 那拉提有哪些民宿聚集的村落?
- **类别**:民宿服务
- **预期知识点**:音塔勒村、英加尔村、拜依盖托别村、那拉提社区、喀拉苏村等;其中阿尔善村、拜依盖托别村为国家级乡村旅游重点村。
- **允许引用来源**:K-0032、K-0007
- **资料不足情形**:村落名称以官方名录为准,不自行列举未在来源中出现的村名。
### Q14 · 新源县/那拉提区域的住宿规模大概是多少?
- **类别**:民宿服务
- **预期知识点**:新源县全县住宿单位 1414 家,其中那拉提区域 1204 家(核验时点口径)。
- **允许引用来源**:K-0007
- **资料不足情形**:该数字为来源页发布时点的统计口径,须注明「截至发布时点」,不当作实时数据。
### Q15 · 那拉提民宿的等级评定是怎样的?
- **类别**:民宿服务
- **预期知识点**:新源县文旅局公示那拉提镇民宿客栈等级评定结果,评出三星级以上民宿客栈 160 家,其中五星级 12 家、四星级 44 家、三星级 104 家(2024 年公示口径)。
- **允许引用来源**:K-0033
- **资料不足情形**:评定年份与数字以来源为准;注意原文为新源县政府评定、媒体转载,评分时须注明来源层级。
### Q16 · 那拉提农家乐如何分级管理?
- **类别**:民宿服务
- **预期知识点**:那拉提镇农家乐实行星级等级评定(一星至五星),依据《新疆维吾尔自治区农家乐旅游服务质量等级标准》,由县旅游局和那拉提镇人民政府牵头。
- **允许引用来源**:K-0034
- **资料不足情形**:标准名称与牵头单位须与政策文件一致,不缩写或改动。
### Q17 · 英加尔村民宿建设项目的基本情况?
- **类别**:民宿服务
- **预期知识点**:那拉提镇英加尔村旅游民宿建设项目总建筑面积约 957.85 平方米,2024 年 5 月动工,建成后预计为村集体增收 20 万元以上、解决 15—20 名群众就业。
- **允许引用来源**:K-0024
- **资料不足情形**:面积、增收、就业等数字以来源为准;「预计」等表述须保留,不写成已实现。
### Q18 · 那拉提有哪些民宿品牌或名称?
- **类别**:民宿服务
- **预期知识点**:官方「高品质民宿」名录中含夏尔部落民宿、那野山河民宿、乌孙夏勒·艺宿、橙野山奢酒店、那兰野舍精品民宿等。
- **允许引用来源**:K-0032
- **资料不足情形**:名录含民宿负责人及联系电话,作答时**不得输出个人电话**等隐私信息。
---
## 五、交通出行(Q19–Q24)
### Q19 · 自驾经独库公路进入那拉提有哪些通行管控?
- **类别**:交通出行
- **预期知识点**:库如力至那拉提入口段每日 8:00—19:00 通行(视车流可延至 21:00),7 座及以下小客车可通行;那拉提至乌苏驿入口、乔尔玛入口 19:00 后社会车辆不得进入。
- **允许引用来源**:K-0027
- **资料不足情形**:通行时段为动态管控,须注明「以最新官方通告为准」;不得把旧时段当作固定规则。
### Q20 · 独库公路北段是否需要预约通行?
- **类别**:交通出行
- **预期知识点**:独库公路北段(乌苏驿—乔尔玛—那拉提约 185 公里)实行预约制通行,预约入口含那拉提(G217 与 G218 交汇处),未预约或未按预约时段/入口驶入将按禁令标志处罚。
- **允许引用来源**:K-0029
- **资料不足情形**:预约规则以来源为准;不承诺具体预约操作步骤(若来源未载明)。
### Q21 · 到那拉提有哪些低空旅游航线?
- **类别**:交通出行
- **预期知识点**:新疆低空旅游航线含伊宁—那拉提、博乐—那拉提、喀纳斯—博乐—那拉提、伊宁—昭苏—那拉提等,由天缘通航、新疆通航以皮拉图斯 PC12 机型执飞,单程约 30 分钟至 2 小时。
- **允许引用来源**:K-0035
- **资料不足情形**:航线、执飞方、机型以来源为准;班次/票价不承诺实时信息。
### Q22 · 那拉提机场是否有低空旅游包机业务?
- **类别**:交通出行
- **预期知识点**:新疆机场集团那拉提机场迎来天缘航空低空旅游包机首航成功。
- **允许引用来源**:K-0037
- **资料不足情形**:注意那拉提机场曾公告改扩建停航(2026-03-29 至 2026-12-31),回答须以机场最新公告为准,不得据此承诺当前可乘机。
### Q23 · 新源县汽车客运站负责哪些方向的班车?
- **类别**:交通出行
- **预期知识点**:新源县汽车客运站有限责任公司设那拉提分公司、喀拉布拉分站等,为县城至那拉提等方向客运班车运营主体。
- **允许引用来源**:K-0039
- **资料不足情形**:具体班次/票价以车站最新公示或「新疆客运联网售票」为准,来源未载明时不填时刻表。
### Q24 · 那拉提景区日常的交通/停车指南是什么?
- **类别**:交通出行
- **预期知识点**:(暂无 `verified` 的通用景区交通指南)
- **允许引用来源**:(无——K-0006 为「2025 那拉提音乐节交通安全指南」,属活动期、时效性强,不进入通用问答)
- **资料不足情形**:**这是典型的「资料不足」用例**——系统应明确回答「暂无核验过的常规景区交通指南」,不得用音乐节活动期信息冒充常规指南;后续需另收集常规交通官方资料后再回答。
---
## 六、待办与后续
- 阶段 3 起,用本问题集对关键词/向量/混合三种检索逐一检查命中条目是否落在「允许引用来源」内。
- 阶段 4 起,对 RAG 回答逐题核对「答案要点」与「引用来源是否真正支持回答」。
- K-0006 更正为活动期信息后,「交通出行」类 `verified` 条目需回补一条常规交通资料,再更新 Q24 的预期知识点与允许来源。
+1 -1
View File
@@ -10,7 +10,7 @@ export const createKnowledge = (payload:{title:string;category:string;content:st
export type VerificationStatus='pending'|'verified'|'rejected' export type VerificationStatus='pending'|'verified'|'rejected'
export type KnowledgeItem={id:number;version:number;title:string;category:string;content:string;source_url:string|null;source_id:number|null;document_id:number|null;collected_at:string|null;verification_status:VerificationStatus;reviewer:string|null;reviewed_at:string|null;verification_notes:string|null;created_at:string;updated_at:string;is_deleted:boolean} export type KnowledgeItem={id:number;version:number;title:string;category:string;content:string;source_url:string|null;source_id:number|null;document_id:number|null;collected_at:string|null;verification_status:VerificationStatus;reviewer:string|null;reviewed_at:string|null;verification_notes:string|null;created_at:string;updated_at:string;is_deleted:boolean}
export function getKnowledgePage(filters:{q?:string;category?:string;verification_status?:VerificationStatus;page?:number;page_size?:number}={}){const params=new URLSearchParams();for(const [key,value] of Object.entries(filters)){if(value!==undefined&&value!=='')params.set(key,String(value))}return apiRequest<{items:KnowledgeItem[];total:number;page:number;page_size:number}>(`/api/knowledge/page?${params}`)} export function getKnowledgePage(filters:{q?:string;category?:string;verification_status?:VerificationStatus;page?:number;page_size?:number}={}){const params=new URLSearchParams();for(const [key,value] of Object.entries(filters)){if(value!==undefined&&value!=='')params.set(key,String(value))}return apiRequest<{items:KnowledgeItem[];total:number;page:number;page_size:number}>(`/api/knowledge/page?${params}`)}
export function updateKnowledge(id:number,payload:{expected_version:number;title?:string;category?:string;content?:string;source_url?:string|null;collected_at?:string|null}){return apiRequest<KnowledgeItem>(`/api/knowledge/${id}`,{method:'PATCH',body:JSON.stringify(payload)},true)} export function updateKnowledge(id:number,payload:{expected_version:number;title?:string;category?:string;content?:string;source_id?:number|null;source_url?:string|null;collected_at?:string|null}){return apiRequest<KnowledgeItem>(`/api/knowledge/${id}`,{method:'PATCH',body:JSON.stringify(payload)},true)}
export function deleteKnowledge(id:number,expectedVersion:number){return apiRequest<void>(`/api/knowledge/${id}?expected_version=${expectedVersion}`,{method:'DELETE'},true)} export function deleteKnowledge(id:number,expectedVersion:number){return apiRequest<void>(`/api/knowledge/${id}?expected_version=${expectedVersion}`,{method:'DELETE'},true)}
export function getKnowledgeHistory(id:number,page=1,pageSize=20){return apiRequest<{items:AuditItem[];total:number;page:number;page_size:number}>(`/api/knowledge/${id}/history?page=${page}&page_size=${pageSize}`)} export function getKnowledgeHistory(id:number,page=1,pageSize=20){return apiRequest<{items:AuditItem[];total:number;page:number;page_size:number}>(`/api/knowledge/${id}/history?page=${page}&page_size=${pageSize}`)}
export type SourceItem={id:number;version:number;name:string;source_url:string|null;notes:string;created_at:string} export type SourceItem={id:number;version:number;name:string;source_url:string|null;notes:string;created_at:string}
+11 -8
View File
@@ -1,19 +1,22 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, ref } from 'vue' import { onMounted, ref } from 'vue'
import { deleteKnowledge, getKnowledgeHistory, getKnowledgePage, updateKnowledge, type AuditItem, type KnowledgeItem, type VerificationStatus } from '../api' import { deleteKnowledge, getKnowledgeHistory, getKnowledgePage, getSources, updateKnowledge, type AuditItem, type KnowledgeItem, type SourceItem, type VerificationStatus } from '../api'
import { collectedAtFromInput, localDateTimeInput } from '../utils/datetime' import { collectedAtFromInput, localDateTimeInput } from '../utils/datetime'
type Form={title:string;category:string;content:string;source_url:string;collected_at:string} type Form={title:string;category:string;content:string;source_id:string;source_url:string;collected_at:string}
const items=ref<KnowledgeItem[]>([]), selected=ref<KnowledgeItem>(), historyFor=ref<KnowledgeItem>(), historyItems=ref<AuditItem[]>([]) const items=ref<KnowledgeItem[]>([]), sources=ref<SourceItem[]>([]), selected=ref<KnowledgeItem>(), historyFor=ref<KnowledgeItem>(), historyItems=ref<AuditItem[]>([])
const query=ref(''), category=ref(''), status=ref<VerificationStatus|''>(''), page=ref(1), total=ref(0), loading=ref(true), saving=ref(false), historyLoading=ref(false), error=ref(''), notice=ref('') const query=ref(''), category=ref(''), status=ref<VerificationStatus|''>(''), page=ref(1), total=ref(0), loading=ref(true), saving=ref(false), historyLoading=ref(false), error=ref(''), notice=ref('')
const form=ref<Form>({title:'',category:'',content:'',source_url:'',collected_at:''}) const form=ref<Form>({title:'',category:'',content:'',source_id:'',source_url:'',collected_at:''})
function failure(cause:unknown,fallback:string){return cause instanceof Error?cause.message:fallback} function failure(cause:unknown,fallback:string){return cause instanceof Error?cause.message:fallback}
function validUrl(value:string){if(!value)return true;try{const url=new URL(value);return url.protocol==='http:'||url.protocol==='https:'}catch{return false}} function validUrl(value:string){if(!value)return true;try{const url=new URL(value);return url.protocol==='http:'||url.protocol==='https:'}catch{return false}}
async function refresh(next=page.value){loading.value=true;error.value='';try{const result=await getKnowledgePage({q:query.value,category:category.value,verification_status:status.value||undefined,page:next});items.value=result.items;page.value=result.page;total.value=result.total}catch(cause){error.value=failure(cause,'无法读取知识条目')}finally{loading.value=false}} async function refresh(next=page.value){loading.value=true;error.value='';try{const result=await getKnowledgePage({q:query.value,category:category.value,verification_status:status.value||undefined,page:next});items.value=result.items;page.value=result.page;total.value=result.total}catch(cause){error.value=failure(cause,'无法读取知识条目')}finally{loading.value=false}}
function edit(item:KnowledgeItem){selected.value=item;form.value={title:item.title,category:item.category,content:item.content,source_url:item.source_url??'',collected_at:item.collected_at?localDateTimeInput(item.collected_at):''};error.value='';notice.value=''} async function loadSources(){try{const collected:SourceItem[]=[];let next=1,total=0;do{const result=await getSources('',next,100);collected.push(...result.items);total=result.total;next+=1}while(collected.length<total);sources.value=collected}catch(cause){error.value=failure(cause,'无法读取已登记来源')}}
function sourceName(item:KnowledgeItem){if(!item.source_id)return '未关联';return sources.value.find(source=>source.id===item.source_id)?.name??`已关联来源 #${item.source_id}`}
function edit(item:KnowledgeItem){selected.value=item;form.value={title:item.title,category:item.category,content:item.content,source_id:item.source_id?String(item.source_id):'',source_url:item.source_url??'',collected_at:item.collected_at?localDateTimeInput(item.collected_at):''};error.value='';notice.value=''}
function chooseSource(){const source=sources.value.find(entry=>entry.id===Number(form.value.source_id));if(source)form.value.source_url=source.source_url??''}
function cancel(){selected.value=undefined} function cancel(){selected.value=undefined}
async function save(){if(!selected.value)return;error.value='';notice.value='';if(!form.value.title.trim()||!form.value.category.trim()||!form.value.content.trim()){error.value='标题、分类和正文不能为空。';return}if(!validUrl(form.value.source_url.trim())){error.value='来源网址必须是有效的 http(s) 地址。';return}saving.value=true;try{const payload={expected_version:selected.value.version,title:form.value.title.trim(),category:form.value.category.trim(),content:form.value.content.trim(),source_url:form.value.source_url.trim()||null,collected_at:form.value.collected_at?collectedAtFromInput(form.value.collected_at,selected.value.collected_at??undefined):null};await updateKnowledge(selected.value.id,payload);notice.value='知识条目已更新,后端已将其重新置为待审核。';cancel();await refresh()}catch(cause){error.value=failure(cause,'更新知识条目失败')}finally{saving.value=false}} async function save(){if(!selected.value)return;error.value='';notice.value='';if(!form.value.title.trim()||!form.value.category.trim()||!form.value.content.trim()){error.value='标题、分类和正文不能为空。';return}if(!validUrl(form.value.source_url.trim())){error.value='来源网址必须是有效的 http(s) 地址。';return}saving.value=true;try{const payload={expected_version:selected.value.version,title:form.value.title.trim(),category:form.value.category.trim(),content:form.value.content.trim(),...(selected.value.document_id===null?{source_id:form.value.source_id?Number(form.value.source_id):null,source_url:form.value.source_url.trim()||null}:{}),collected_at:form.value.collected_at?collectedAtFromInput(form.value.collected_at,selected.value.collected_at??undefined):null};await updateKnowledge(selected.value.id,payload);notice.value='知识条目已更新,后端已将其重新置为待审核。';cancel();await refresh()}catch(cause){error.value=failure(cause,'更新知识条目失败')}finally{saving.value=false}}
async function remove(item:KnowledgeItem){if(!window.confirm(`确定删除“${item.title}”吗?该操作为软删除,历史记录会保留。`))return;error.value='';notice.value='';try{await deleteKnowledge(item.id,item.version);notice.value='知识条目已软删除。';if(selected.value?.id===item.id)cancel();await refresh(items.value.length===1&&page.value>1?page.value-1:page.value)}catch(cause){error.value=failure(cause,'删除知识条目失败')}} async function remove(item:KnowledgeItem){if(!window.confirm(`确定删除“${item.title}”吗?该操作为软删除,历史记录会保留。`))return;error.value='';notice.value='';try{await deleteKnowledge(item.id,item.version);notice.value='知识条目已软删除。';if(selected.value?.id===item.id)cancel();await refresh(items.value.length===1&&page.value>1?page.value-1:page.value)}catch(cause){error.value=failure(cause,'删除知识条目失败')}}
async function showHistory(item:KnowledgeItem){historyFor.value=item;historyLoading.value=true;error.value='';try{historyItems.value=(await getKnowledgeHistory(item.id)).items}catch(cause){error.value=failure(cause,'无法读取知识历史');historyFor.value=undefined}finally{historyLoading.value=false}} async function showHistory(item:KnowledgeItem){historyFor.value=item;historyLoading.value=true;error.value='';try{historyItems.value=(await getKnowledgeHistory(item.id)).items}catch(cause){error.value=failure(cause,'无法读取知识历史');historyFor.value=undefined}finally{historyLoading.value=false}}
onMounted(()=>refresh(1)) onMounted(()=>{void refresh(1);void loadSources()})
</script> </script>
<template><main><RouterLink to="/admin">返回管理员工作台</RouterLink><h1>知识管理</h1><p>列表、编辑、删除和历史均直接调用管理员受保护接口;编辑后条目会由后端重新置为待审核。</p><p v-if="error" role="alert">{{error}}</p><p v-if="notice" role="status">{{notice}}</p><form @submit.prevent="refresh(1)"><label>关键词<input v-model="query"/></label><label>分类<input v-model="category"/></label><label>状态<select v-model="status"><option value="">全部</option><option value="pending">待审核</option><option value="verified">已核验</option><option value="rejected">已驳回</option></select></label><button>筛选</button></form><p v-if="loading">正在读取知识条目…</p><template v-else><table><thead><tr><th>标题</th><th>分类</th><th>状态</th><th>来源</th><th>操作</th></tr></thead><tbody><tr v-for="item in items" :key="item.id"><td><b>{{item.title}}</b><p>{{item.content.slice(0,120)}}</p></td><td>{{item.category}}</td><td>{{item.verification_status}}</td><td><a v-if="item.source_url" :href="item.source_url" target="_blank" rel="noreferrer">打开来源</a><span v-else>未填写</span></td><td><button @click="edit(item)">编辑</button><button @click="showHistory(item)">历史</button><button @click="remove(item)">删除</button></td></tr><tr v-if="!items.length"><td colspan="5">暂无符合条件的知识条目。</td></tr></tbody></table><div><button :disabled="page===1" @click="refresh(page-1)">上一页</button><span>第 {{page}} 页,共 {{total}} 条</span><button :disabled="page*20>=total" @click="refresh(page+1)">下一页</button></div></template><form v-if="selected" @submit.prevent="save"><h2>编辑:{{selected.title}}</h2><label>标题<input v-model.trim="form.title" required maxlength="250"/></label><label>分类<input v-model.trim="form.category" required maxlength="80"/></label><label>正文<textarea v-model.trim="form.content" required maxlength="100000" rows="6"/></label><label>来源网址<input v-model.trim="form.source_url" type="url" maxlength="1000" placeholder="https://..."/></label><label>采集时间<input v-model="form.collected_at" type="datetime-local" step="any"/></label><button type="button" :disabled="saving" @click="cancel">取消</button><button :disabled="saving">{{saving?'保存中…':'保存修改'}}</button></form><section v-if="historyFor"><h2>“{{historyFor.title}}”的历史</h2><button @click="historyFor=undefined">关闭历史</button><p v-if="historyLoading">正在读取历史…</p><table v-else><thead><tr><th>操作</th><th>时间</th><th>操作者</th><th>版本快照</th></tr></thead><tbody><tr v-for="item in historyItems" :key="item.id"><td>{{item.action}}</td><td>{{new Date(item.created_at).toLocaleString()}}</td><td>{{item.actor||'系统'}}</td><td><details><summary>查看快照</summary><pre>{{JSON.stringify(item.snapshot,null,2)}}</pre></details></td></tr><tr v-if="!historyItems.length"><td colspan="4">暂无历史记录。</td></tr></tbody></table></section></main></template> <template><main><RouterLink to="/admin">返回管理员工作台</RouterLink><h1>知识管理</h1><p>列表、编辑、删除和历史均直接调用管理员受保护接口;编辑后条目会由后端重新置为待审核。</p><p v-if="error" role="alert">{{error}}</p><p v-if="notice" role="status">{{notice}}</p><form @submit.prevent="refresh(1)"><label>关键词<input v-model="query"/></label><label>分类<input v-model="category"/></label><label>状态<select v-model="status"><option value="">全部</option><option value="pending">待审核</option><option value="verified">已核验</option><option value="rejected">已驳回</option></select></label><button>筛选</button></form><p v-if="loading">正在读取知识条目…</p><template v-else><table><thead><tr><th>标题</th><th>分类</th><th>状态</th><th>来源</th><th>操作</th></tr></thead><tbody><tr v-for="item in items" :key="item.id"><td><b>{{item.title}}</b><p>{{item.content.slice(0,120)}}</p></td><td>{{item.category}}</td><td>{{item.verification_status}}</td><td><p>{{sourceName(item)}}</p><a v-if="item.source_url" :href="item.source_url" target="_blank" rel="noreferrer">打开来源</a><span v-else>未填写</span></td><td><button @click="edit(item)">编辑</button><button @click="showHistory(item)">历史</button><button @click="remove(item)">删除</button></td></tr><tr v-if="!items.length"><td colspan="5">暂无符合条件的知识条目。</td></tr></tbody></table><div><button :disabled="page===1" @click="refresh(page-1)">上一页</button><span>第 {{page}} 页,共 {{total}} 条</span><button :disabled="page*20>=total" @click="refresh(page+1)">下一页</button></div></template><form v-if="selected" @submit.prevent="save"><h2>编辑:{{selected.title}}</h2><label>标题<input v-model.trim="form.title" required maxlength="250"/></label><label>分类<input v-model.trim="form.category" required maxlength="80"/></label><label>正文<textarea v-model.trim="form.content" required maxlength="100000" rows="6"/></label><label>已登记来源<select v-model="form.source_id" :disabled="selected.document_id!==null" @change="chooseSource"><option value="">不关联已登记来源</option><option v-for="source in sources" :key="source.id" :value="String(source.id)">{{source.name}}</option></select></label><p v-if="selected.document_id!==null" role="note">已关联文档 #{{selected.document_id}},来源由该文档确定,本页仅可修改其他知识字段。</p><label>来源网址<input v-model.trim="form.source_url" :readonly="selected.document_id!==null" type="url" maxlength="1000" placeholder="https://..."/><small v-if="selected.document_id!==null">来源网址由文档所属来源维护。</small><small v-else>选择已登记来源时会自动回填其链接;可保留为空,由后端按关联来源处理。</small></label><label>采集时间<input v-model="form.collected_at" type="datetime-local" step="any"/></label><button type="button" :disabled="saving" @click="cancel">取消</button><button :disabled="saving">{{saving?'保存中…':'保存修改'}}</button></form><section v-if="historyFor"><h2>“{{historyFor.title}}”的历史</h2><button @click="historyFor=undefined">关闭历史</button><p v-if="historyLoading">正在读取历史…</p><table v-else><thead><tr><th>操作</th><th>时间</th><th>操作者</th><th>版本快照</th></tr></thead><tbody><tr v-for="item in historyItems" :key="item.id"><td>{{item.action}}</td><td>{{new Date(item.created_at).toLocaleString()}}</td><td>{{item.actor||'系统'}}</td><td><details><summary>查看快照</summary><pre>{{JSON.stringify(item.snapshot,null,2)}}</pre></details></td></tr><tr v-if="!historyItems.length"><td colspan="4">暂无历史记录。</td></tr></tbody></table></section></main></template>