From e3f234ca0b65e0d56f151bb6ec39c47e29be49cd Mon Sep 17 00:00:00 2001 From: YANG JIANKUAN Date: Tue, 3 Nov 2026 21:32:15 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=AF=BB=E6=A0=87=E5=8F=AF=E4=BB=A5?= =?UTF-8?q?=E8=B5=B0=E5=AE=8C=E4=B8=80=E4=BB=BD=E6=96=87=E4=BB=B6=EF=BC=9B?= =?UTF-8?q?=E5=AE=A1=E8=AE=A1=E5=BE=85=E7=A1=AE=E8=AE=A4=E5=8C=BA=EF=BC=8C?= =?UTF-8?q?=E4=BF=AE=E8=A7=84=E5=88=99=E7=BC=BA=E9=99=B7=E3=80=81=E6=8F=90?= =?UTF-8?q?=E7=A4=BA=E8=AF=8D=E5=87=BA=E6=96=B0=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 读标操作(FR-M2-07、08) - 后端:清单项详情(ETag)、修改(If-Match 选填,记 edited_fields 和 AI 原值)、 采纳(补出处或声明「人工确认、无出处」,可带修改)、删除 / 丢弃与恢复、人工新增、 替换出处、人工建出处(选同一页的句子)、修改记录(来自审计)、确认清单(不可变版本, 内容没变复用上一版)、版本列表;正式清单有变化时已确认的清单回到草稿;项目已递交只读 - 重新解析不推翻人工决定:丢弃的不再冒出来,采纳的留在正式清单,人工补过的出处不被覆盖 - 前端:待确认区采纳 / 补上出处后采纳 / 修改后采纳 / 丢弃,正式清单修改、补出处、删除 (可撤销)、修改记录、新增条目、确认清单;pdf.js 原文查看器(点出处跳页、按坐标框高亮、 选句补出处),整屏对照布局,J / K 上下条;跨页的一句话合成一处出处显示 待确认审计(架构 §13 SK11) - 规则只修真正的缺陷:表格框线不算下划线(文字层 v3);句号后的右括号并进同一句 (切句 s3-v3);中文编号小节和冒号编号条目的上级条款,依据沿上级链核对;取值认中文 数字、同一行单元格和评分因素标题;补几种无效写法;比对引文前去掉我们加进输入的标记 - 不放宽:引文最短 8 个字、编号必须对、证据全部通过 - 提示词新版本:substantive v3,qualification / scoring / schedule / tech_param v2; 引文复制整句、并列多项不拆、跨页只引上一行,除实质性条款外输入改成纯文本 - BD_AI_MODELS 按提示词声明的能力选模型;离线评测工具 evals/run_extract.py - 待确认:国动 46 → 4,S02 17 → 13,S06 174 → 57 Co-Authored-By: Claude Opus 5.5 --- .env.example | 2 + .gitignore | 3 + AGENTS.md | 4 +- apps/web/package.json | 1 + apps/web/src/components/AppShell.vue | 2 + apps/web/src/components/ChecklistItemRow.vue | 30 +- apps/web/src/components/CitationBlock.vue | 33 +- apps/web/src/components/HistoryDialog.vue | 86 ++++ apps/web/src/components/ItemDialog.vue | 186 +++++++ apps/web/src/components/PdfViewer.vue | 311 ++++++++++++ apps/web/src/components/Toasts.vue | 16 + apps/web/src/lib/checklistOps.ts | 89 ++++ apps/web/src/lib/citations.ts | 27 + apps/web/src/lib/labels.ts | 35 ++ apps/web/src/lib/toast.ts | 23 + apps/web/src/pages/ChecklistPage.vue | 462 +++++++++++++++--- apps/web/src/styles/app.css | 32 ++ apps/web/vite.config.ts | 2 + docs/architecture/api-design.md | 12 +- docs/architecture/architecture.md | 5 +- evals/README.md | 5 + evals/run_extract.py | 209 ++++++++ pnpm-lock.yaml | 134 +++++ prompts/extract.qualification/v2.md | 67 +++ prompts/extract.schedule/v2.md | 63 +++ prompts/extract.scoring/v2.md | 61 +++ prompts/extract.substantive/v3.md | 62 +++ prompts/extract.tech_param/v2.md | 68 +++ .../bid_doctor/ai/providers/openai_compat.py | 10 +- server/src/bid_doctor/audit/service.py | 11 + server/src/bid_doctor/checklist/api.py | 129 ++++- server/src/bid_doctor/checklist/edit.py | 457 +++++++++++++++++ server/src/bid_doctor/checklist/extract.py | 98 +++- server/src/bid_doctor/checklist/models.py | 31 +- server/src/bid_doctor/checklist/service.py | 129 ++++- server/src/bid_doctor/core/config.py | 2 + server/src/bid_doctor/core/errors.py | 4 + server/src/bid_doctor/document/api.py | 24 +- .../bid_doctor/document/pipeline/runner.py | 2 +- .../bid_doctor/document/pipeline/structure.py | 34 +- .../document/pipeline/text_layer.py | 33 +- server/src/bid_doctor/document/service.py | 32 +- server/src/bid_doctor/identity/service.py | 9 + server/src/bid_doctor/project/service.py | 6 + server/tests/conftest.py | 6 + server/tests/test_checklist_edit.py | 154 ++++++ server/tests/test_units.py | 88 +++- 47 files changed, 3125 insertions(+), 164 deletions(-) create mode 100644 apps/web/src/components/HistoryDialog.vue create mode 100644 apps/web/src/components/ItemDialog.vue create mode 100644 apps/web/src/components/PdfViewer.vue create mode 100644 apps/web/src/components/Toasts.vue create mode 100644 apps/web/src/lib/checklistOps.ts create mode 100644 apps/web/src/lib/citations.ts create mode 100644 apps/web/src/lib/toast.ts create mode 100644 evals/README.md create mode 100644 evals/run_extract.py create mode 100644 prompts/extract.qualification/v2.md create mode 100644 prompts/extract.schedule/v2.md create mode 100644 prompts/extract.scoring/v2.md create mode 100644 prompts/extract.substantive/v3.md create mode 100644 prompts/extract.tech_param/v2.md create mode 100644 server/src/bid_doctor/checklist/edit.py create mode 100644 server/tests/test_checklist_edit.py diff --git a/.env.example b/.env.example index f85d421..6485b33 100644 --- a/.env.example +++ b/.env.example @@ -18,3 +18,5 @@ # BD_AI_BASE_URL=https://api.deepseek.com/v1 # BD_AI_API_KEY= # BD_AI_MODEL=deepseek-chat +# 按能力选模型(选填,JSON):没配的能力用 BD_AI_MODEL +# BD_AI_MODELS={"reasoning": "deepseek-reasoner"} diff --git a/.gitignore b/.gitignore index 2bdea76..ca5a3c2 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,6 @@ dist/ *.tsbuildinfo .pytest_cache/ .ruff_cache/ + +# 评测输出(含真实招标文件内容) +evals/out/ diff --git a/AGENTS.md b/AGENTS.md index c9d30b2..11a5db7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -197,4 +197,6 @@ samples/ - 2026-11-03:接口契约 v0.1(`docs/architecture/api-design.md` + `openapi.yaml`,OpenAPI 3.1,64 个路径、81 个操作),覆盖 MVP 原型 13 页的主流程,运营后台另行定义。API1–API5 全部按建议确认:骨架搭好前以 `openapi.yaml` 为准,之后以 FastAPI 生成的快照为准;事件流用 fetch 读;异地登录确认后挤掉旧设备;扣点说明和实际不一致时拒绝、不扣点;报告后台生成。 - 2026-11-03:工程骨架完成(分支 `feat/engineering-skeleton`)。「上传 → 解析 → 显示清单」最小链路在真实样本 S02、S06 和虚构样例上跑通:分片上传、按文件头识别格式、Gotenberg 转 PDF、文字层与稳定句子编号、实质性条款抽取(假模型 + 三道校验 + 标注类规则兜底)、并入清单;额度预扣 / 结算 / 退回、行级安全策略、对象加密都有测试。后端 30 个测试、pyright 严格模式、import 规则全部通过;前端 5 个页面按设计规范 v0.2 实现,类型检查和构建通过。搭建中的调整记在架构 §13(SK1–SK8),SK1、SK2 待确认。 - 2026-11-03:测试环境接上真实模型(DeepSeek,配置在不进 git 的根目录 `.env`)。读标补齐五类清单和项目基本信息的抽取,修了国动样本暴露的 4 个问题(标注规则碎片、页码混入与跨页拆分、无效写法漏认、上级标题下的无效情形),见架构 §13 SK9、SK10。 -- 下一步:在 S01–S10 上做标注集评测(各类召回率、准确率),再做读标的修改类接口和 pdf.js 原文查看器;之后是查标(M3)。 +- 2026-11-03:读标可以走完一份文件:待确认区采纳 / 补上出处后采纳 / 修改后采纳 / 丢弃,正式清单修改、补出处、删除恢复、人工新增、修改记录,确认清单生成不可变版本;右侧 pdf.js 原文查看器点出处跳页、按坐标框高亮原句,选句补出处。实现约定见 `docs/architecture/api-design.md` §10。 +- 2026-11-03:审计待确认区:多数是好结果被校验误拦,根源在输入(表格框线被认成下划线)、提示词(要求逐项拆开)和模型没守规则。规则只修真正的缺陷,不迁就模型的错误输出;提示词出新版本(引文复制整句、并列不拆、跨页只引上一行,除实质性条款外输入改成纯文本)。国动的待确认 46 → 4,S02 17 → 13,S06 174 → 57。见架构 §13 SK11;离线评测工具 `evals/run_extract.py`。 +- 下一步:在 S01–S10 上做标注集评测(各类召回率、准确率),据此定模型选型;之后是查标(M3)。 diff --git a/apps/web/package.json b/apps/web/package.json index 65accd1..2cf6b70 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -15,6 +15,7 @@ "@fontsource/noto-serif-sc": "5.3.0", "@lucide/vue": "1.50.0", "@tanstack/vue-query": "5.104.1", + "pdfjs-dist": "6.4.299", "reka-ui": "2.10.5", "vue": "3.5.43", "vue-router": "4.6.4" diff --git a/apps/web/src/components/AppShell.vue b/apps/web/src/components/AppShell.vue index 8c6d410..5a22841 100644 --- a/apps/web/src/components/AppShell.vue +++ b/apps/web/src/components/AppShell.vue @@ -8,6 +8,7 @@ import { useQueryClient } from "@tanstack/vue-query"; import { computed, onBeforeUnmount, onMounted, ref } from "vue"; import { RouterLink, RouterView, useRoute, useRouter } from "vue-router"; import logoUrl from "@design/logo/svg/logo-vertical-reverse.svg?url"; +import Toasts from "./Toasts.vue"; import { api } from "@/lib/api"; import { provideCrumbs } from "@/lib/crumbs"; import { startEventStream } from "@/lib/events"; @@ -103,5 +104,6 @@ async function logout() { + diff --git a/apps/web/src/components/ChecklistItemRow.vue b/apps/web/src/components/ChecklistItemRow.vue index 42e1a35..5fb4e1f 100644 --- a/apps/web/src/components/ChecklistItemRow.vue +++ b/apps/web/src/components/ChecklistItemRow.vue @@ -2,17 +2,21 @@ /** * 清单项(设计规范 §7.5、§7.6、§7.16):重要性 + 标题 + 标签(来源、分类、包)+ 出处。 * AI 产出的文字带「AI」标签、正文墨色;待确认区的条目另加左侧 2px 蓝铅笔虚线(.ai-block)。 + * 当前条(.sel)= 浅底 + 3px 墨色竖条(Q3);操作按钮放在 actions 插槽,只在当前条和待确认条目上显示。 */ import { computed } from "vue"; import CitationBlock from "./CitationBlock.vue"; +import { groupCitations, type CitationGroup } from "@/lib/citations"; import { category, IMPORTANCE_MARK, IMPORTANCE_NAME, pendingReason, specFacts } from "@/lib/labels"; import type { ChecklistItem } from "@/lib/queries"; -const props = defineProps<{ item: ChecklistItem; pending?: boolean }>(); +const props = defineProps<{ item: ChecklistItem; pending?: boolean; selected?: boolean; activeCitation?: string | null }>(); +const emit = defineEmits<{ select: []; openCitation: [g: CitationGroup] }>(); -/** 一条结论最多默认展开 2 条出处,其余写「另有 N 处」(§7.3) */ -const shown = computed(() => props.item.citations.slice(0, 2)); -const more = computed(() => props.item.citations.length - shown.value.length); +/** 一条结论最多默认展开 2 处出处,其余写「另有 N 处」(§7.3);跨页的一句话算一处 */ +const groups = computed(() => groupCitations(props.item.citations)); +const shown = computed(() => groups.value.slice(0, 2)); +const more = computed(() => groups.value.length - shown.value.length); const mark = computed(() => IMPORTANCE_MARK[props.item.importance] ?? ""); /** AI 摘要和标题不同时才单独显示,避免重复 */ const summary = computed(() => (props.item.summary && props.item.summary !== props.item.title ? props.item.summary : "")); @@ -21,7 +25,7 @@ const facts = computed(() => specFacts(props.item.spec));