diff --git a/.env.example b/.env.example index 97a17b8..b8554ab 100644 --- a/.env.example +++ b/.env.example @@ -21,6 +21,11 @@ # 按能力选模型(选填,JSON):没配的能力用 BD_AI_MODEL # BD_AI_MODELS={"reasoning": "deepseek-reasoner"} +# OCR(vision-plan §4.1):扫描页、插图交给 OCR 服务(services/ocr)。默认 none(扫描页按没有文字处理),prod 必须接 +# 本地起服务:.venv/bin/bid-doctor-ocr(:8029),或 docker compose ... up -d ocr +# BD_OCR_PROVIDER=http +# BD_OCR_URL=http://127.0.0.1:8029 + # 专业版对公转账的收款信息(决策 M6-6):开通申请里给用户看;没配时显示「(待定)」,prod 下必须配 # BD_PAYEE_NAME= # BD_PAYEE_BANK= diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index db57168..989bde2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,6 +51,8 @@ jobs: run: | ../.venv/bin/ruff check src tests migrations ../.venv/bin/ruff format --check src tests migrations + ../.venv/bin/ruff check ../services/ocr/src --config ../services/ocr/pyproject.toml + ../.venv/bin/ruff format --check ../services/ocr/src ../.venv/bin/pyright ../.venv/bin/lint-imports ../.venv/bin/pytest -q diff --git a/.gitignore b/.gitignore index ca5a3c2..79298e4 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,7 @@ opensource-ref/ # 技术验证的产物(由样本派生,样本不转载) spikes/**/out/ spikes/.venv/ +spikes/**/.venv/ spikes/**/.env # 工程骨架:本地环境与产物 diff --git a/AGENTS.md b/AGENTS.md index e6bbd22..890c1cb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -88,6 +88,7 @@ server/ 后端:api、worker、ops 是同一个包、三种进 app.py worker.py cli.py migrations/ Alembic;sql/0001_baseline/ 是 data-model 的建表脚本,按模块拆分 tests/ 集成测试(真实 PostgreSQL + 行级安全策略)和单元测试;fixtures/ 是虚构的小样例 +services/ocr/ OCR 服务(独立进程,worker 通过 HTTP 调用;RapidOCR,CPU) apps/web/ 用户端单页应用 apps/admin/ 运营后台(单独构建、单独部署,私有化不交付;接口见 docs/architecture/ops-api.md) packages/api-client/ 由 docs/architecture/openapi.yaml 生成的类型 + 客户端(不手写接口类型) @@ -100,7 +101,7 @@ deploy/compose/ Docker Compose;deploy/docker/ 镜像 | 做什么 | 命令 | |---|---| | 装依赖 | `uv sync` 和 `pnpm install --ignore-scripts` | -| 起依赖(PostgreSQL、Valkey、Gotenberg) | `docker compose -f deploy/compose/compose.yaml up -d postgres valkey gotenberg` | +| 起依赖(PostgreSQL、Valkey、Gotenberg、OCR) | `docker compose -f deploy/compose/compose.yaml up -d postgres valkey gotenberg ocr`;OCR 也可以直接 `.venv/bin/bid-doctor-ocr`(:8029)。要识别扫描件,后端进程设 `BD_OCR_PROVIDER=http`(默认 none,扫描页按没有文字处理) | | 建表 / 升级数据库 | `cd server && ../.venv/bin/bid-doctor migrate` | | 起后端 | `cd server && ../.venv/bin/bid-doctor api`(:8009),另开一个终端 `../.venv/bin/bid-doctor worker` | | 起前端 | `pnpm dev`(:5173,`/api` 代理到 :8009)。dev 环境短信验证码固定是 `000000` | @@ -241,4 +242,10 @@ samples/ - 顺手修了取消解析 / 检查时接口报 500 的问题。后端 156 个测试,浏览器从注册走到注销。 - 2026-11-05:运营后台第二版(M6-9、M6-10):左侧菜单(标准 logo、可展开收起的分组、图标、待办数、环境标签),运营人员角色只留结构(`ops_admin.role`、权限点、每个接口的检查点、菜单按权限过滤,现在只有管理员),迁移 0012。 - 2026-11-05:前端走查修改。报告预览改用 pdf.js(浏览器自带的 PDF 查看器是深色的,改不了);读标页改成和查标一样的左右布局(项目头滚出屏幕,清单卡片和原文区贴住顶栏),响应方式收进清单栏的按钮;报告页「导出」统一改称「生成」(生成一份存进项目,下载才是存到电脑),PDF 模板 2026.11-v4;概览检查记录的报告列只写份数和「查看报告」;招标 / 投标文件切换标签改为双箭头、指向展开方向;项目头三项取值对齐。 +- 2026-11-05:扫描件与图片识别计划 v0.2(`docs/architecture/vision-plan.md`)。现状:OCR 和视觉模型都没接,扫描页只做 C10 兜底,招标文件正文里的图片静默忽略,签章有无(FR-M3-04 P0)没做。方案:OCR 给文字和坐标,视觉模型做理解、结论要对上 OCR 行;覆盖读标、查标(附件索引、投标侧证书、签章)、资质库上传识别自动填表。VS1–VS9 已确认:视觉模型用 DeepSeek Flash(实测 V4.1 Flash 收图片),不单独计费,其余按建议(`docs/decisions.md`)。 +- 2026-11-05:扫描件识别初测(`tech-validation.md` §7,代码 `spikes/s5-ocr/`),用国动投标文件的 80 页扫描页、22 页图表。OCR 初定 RapidOCR + PP-OCRv6 small(CPU 0.6 秒 / 页,合成扫描件字符差 ≤ 1.1%,横放页自动转正,坐标准);DeepSeek Flash 页面分类、合同 / 执照字段、图表概述可用,102 页约 0.47 元。补了校验规则(多行引用、字段格式、半截值、手写低可信、红章局部按红色通道重读),招标文件里的图片按重要程度分流(VS10),计划升到 v0.3。顺带发现财务报表和一份合同的单位名是「南京长卿信息技术有限公司」,和投标人名称不一致(多半是曾用名),请用户核对。还差证书扫描件(用户在找)和签章标注。 +- 2026-11-05:证书识别验证(`tech-validation.md` §7.3):用户的 7 张证书(人员证书、软著、一图两证,有打码)各转 4 个角度,200 个字段全对、打码字段 16 次都没给值。补了整页 180° 检测和「所引 OCR 行置信度低于 0.9 标识别不确定」;提示词是对着这 7 张改的,泛化要下一批证书验证。 +- 2026-11-05:**OCR 进文字层**(vision-plan 第 1 步,架构 §13 SK15)。新增 OCR 服务 `services/ocr`(镜像、Compose 服务);扫描页和插图识别后并入文字层(v6),句子标 `source = "ocr"`;出处标「OCR」(迁移 0013);依据识别文字的问题降级,规则不查扫描附件。国动真实投标文件端到端:82 页扫描页、25 张插图读出文字,7 条要求找到了以前读不到的证据;第一次跑附件上的规则噪音 21 个,改后 36 → 16 个提示(`check-eval.md` §16)。后端 167 个测试通过。 +- 2026-11-05:**资质库上传识别**(vision-plan 第 2 步,架构 §13 SK16)。按用户要求简化:「添加资质」对话框最上面传证书照片,识别后填进下面的表单,一张图按一张证书处理(VS11、VS12)。只预填和 OCR 文字对得上的字段并标「识别」,打码、对不上的只给建议;软著取登记号、默认长期有效;「五年」给推算按钮;主体不一致、人员 / 产品证书、编号重复都提醒;识别值留痕、照片挂成附件(迁移 0014)。浏览器走查通过;用户确认国动财务报表上的是曾用名,第 3 步做主体一致时要认曾用名(VS13)。后端 175 个测试通过。 +- 2026-11-05:资质库改为「查看详情」:点行打开只读的键值视图 + 附件缩略图,「更多」菜单只有查看详情、编辑、删除(去掉了和编辑重复的「管理附件」),表单不再显示「不扣额度」。新增大图浏览控件(设计规范 §7.23):深色蒙层、缩放、拖动、翻页、键盘操作,附件不再开新标签页;PDF 附件按页出预览图(新接口 `.../files/{file_id}/previews`)。 - 下一步:可做的有:读标抽取专项优化(`extract-status.md`)、更多真实投标文件、M6 的 P1(协作权限、资产库)。 diff --git a/apps/web/src/components/CitationBlock.vue b/apps/web/src/components/CitationBlock.vue index e207230..69ef575 100644 --- a/apps/web/src/components/CitationBlock.vue +++ b/apps/web/src/components/CitationBlock.vue @@ -3,7 +3,7 @@ * 出处(设计规范 §7.3):左侧 2px 蓝铅笔竖线,上面是「文件 · 第 N 页」,下面照录原句。 * 点出处在原文查看器里翻到那一页、高亮原句;当前打开的出处竖线加粗(.on)。 * 一句话跨页时(tail 是下一页的接续部分)合成一处显示:「第 47–48 页」,原句接起来。 - * TODO:OCR 识别出的原句前加「OCR」小字(等接口给出页的 text_source)。 + * 原句是扫描页或插图上 OCR 识别出来的,前面加「OCR」小字,提醒核对原图(§7.3)。 */ import { computed } from "vue"; import type { Citation } from "@/lib/queries"; @@ -11,6 +11,7 @@ import type { Citation } from "@/lib/queries"; const props = defineProps<{ citation: Citation; tail?: Citation[]; active?: boolean }>(); const lastPage = computed(() => props.tail?.at(-1)?.page_no); const quote = computed(() => [props.citation, ...(props.tail ?? [])].map((c) => c.quote).join("")); +const ocr = computed(() => [props.citation, ...(props.tail ?? [])].some((c) => c.ocr)); const emit = defineEmits<{ open: [] }>(); @@ -19,7 +20,7 @@ const emit = defineEmits<{ open: [] }>(); 第 {{ citation.page_no }} 页 - {{ quote }} + OCR{{ quote }} @@ -32,6 +33,12 @@ const emit = defineEmits<{ open: [] }>(); .cite:hover { background: var(--ai-soft); } +.cite-ocr { + margin-right: var(--sp-1); + font-size: var(--fs-caption); + color: var(--text-secondary); + text-decoration: none; +} .cite:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; diff --git a/apps/web/src/components/ImageViewer.vue b/apps/web/src/components/ImageViewer.vue new file mode 100644 index 0000000..59ecc79 --- /dev/null +++ b/apps/web/src/components/ImageViewer.vue @@ -0,0 +1,298 @@ + + + + + diff --git a/apps/web/src/components/QualificationDetail.vue b/apps/web/src/components/QualificationDetail.vue new file mode 100644 index 0000000..477b916 --- /dev/null +++ b/apps/web/src/components/QualificationDetail.vue @@ -0,0 +1,190 @@ + + + + + diff --git a/apps/web/src/components/QualificationDialog.vue b/apps/web/src/components/QualificationDialog.vue index e93a609..13f60e9 100644 --- a/apps/web/src/components/QualificationDialog.vue +++ b/apps/web/src/components/QualificationDialog.vue @@ -1,19 +1,23 @@