# Proposa AI AI 驱动的投标书生成服务:解析招标文件、采购需求、澄清公告、公司资料和历史参考投书, 经 Step1-6 工作流生成最终 DOCX 投标书。HTTP API 是主要对接入口,后台直接执行与人工 验证相同的 `scripts/test_step1.py` 至 `scripts/test_step6.py`。 ## 处理流程 1. Step 1:文档解析及表/函/表格提取。 2. Step 2:招标需求、评分项、废标项及响应项分析。 3. Step 3:结合模板、评分项和参考投书生成大纲。 4. Step 4:生成章节内容及章节 DOCX 中间产物。 5. Step 5:覆盖性、废标风险和内容完整性审核与修复。 6. Step 6:按模板装配 DOCX,生成最终投标文件。 ## 项目结构 ```text proposa-ai/ ├── src/ # 正式功能代码 │ ├── config.py # .env 与全局配置 │ ├── models.py # 数据模型 │ ├── chapter_policy.py # 章节结构策略 │ ├── doc_reader/ # PDF/DOCX/Markdown 读取 │ ├── llm_client/ # LLM 调用与缓存 │ ├── pdf_table_to_docx/ # PDF 表格提取 │ ├── step1_parsing/ ... step6_exporting/ │ └── templates/ # 正式 DOCX 模板资源 ├── scripts/ # 阶段脚本与快速回归 │ ├── test_step1.py ... test_step6.py │ └── tests/ ├── data/ # 项目输入数据 ├── test_data/ # 手工测试数据 ├── output/ # 生成产物与缓存 ├── models/ # 可选本地 NLP 模型资源 ├── deploy.sh # Linux 部署脚本 ├── .env # 本地配置,Git 忽略 ├── .env.example # 环境变量示例 └── pyproject.toml # uv 依赖与安装配置 ``` ## 环境配置 ```powershell uv sync ``` 项目已在 `pyproject.toml` 的 `[tool.uv]` 中配置清华 PyPI 镜像 (`https://pypi.tuna.tsinghua.edu.cn/simple`)加速依赖下载;需要更换镜像时, 设置环境变量 `UV_INDEX_URL` 覆盖即可。 复制 `.env.example` 为 `.env`: ```powershell Copy-Item .env.example .env ``` 至少填写: ```dotenv DEEPSEEK_API_KEY=你的密钥 ``` 操作系统环境变量的优先级高于 `.env`。 ### 环境变量说明 | 参数 | 必填 | 默认值 | 说明 | | --- | --- | --- | --- | | `DEEPSEEK_API_KEY` | 是 | 无 | DeepSeek API 密钥。仅保存在本地 `.env`,不得提交到 Git。 | | `DEEPSEEK_BASE_URL` | 否 | `https://api.deepseek.com` | DeepSeek 兼容 API 的基础地址。 | | `DEEPSEEK_MODEL` | 否 | `deepseek-v4-flash` | 分析、生成和审核使用的模型名称。 | | `BID_OUTPUT_DIR` | 否 | `output` | 工作流默认输出目录;相对路径以仓库根目录为基准。 | | `BID_WRITER_CONCURRENCY` | 否 | `5` | Step4 并发撰写章节的最大线程数。 | | `BID_ANALYZER_CONCURRENCY` | 否 | `5` | Step2 并发分析文档分块的最大线程数。 | | `BID_HEADING_CONCURRENCY` | 否 | `5` | Step6 标题层级分析的最大 LLM 并发数。 | | `PROPOSA_API_HOST` | 否 | `0.0.0.0` | HTTP API 监听地址;默认监听所有网卡。 | | `PROPOSA_API_PORT` | 否 | `8000` | HTTP API 监听端口。 | | `PROPOSA_WORK_DIR` | 否 | `output/api_jobs` | HTTP API 和 test_step1-6 默认工作目录;每个请求的内容放在 `PROPOSA_WORK_DIR//` 下,中间文件在 `work/`,最终 DOCX 在请求目录根。 | | `REFERENCE_DIR` | 否 | `/data/shenqin/` | JSON 模式下参考投标文件相对路径的首选目录。 | | `REFERENCE_DIR1` | 否 | `/data/dms/dms_upload` | JSON 模式下参考投标文件相对路径的第二候选目录。 | | `TPC_DIR` | 否 | 无 | JSON 模式下招标文件、采购需求和澄清公告相对路径的拼接前缀。 | | `PROPOSA_API_MAX_UPLOAD_MB` | 否 | `512` | 单个上传文件的大小上限,单位 MB。 | | `PROPOSA_API_TIMEOUT_SECONDS` | 否 | `28800` | Step1-6 后台子进程超时秒数,默认 8 小时。 | | `PROPOSA_API_CONCURRENCY` | 否 | `1` | 同时执行的后台生成任务数;大型 DOCX 场景建议保持 1。 | | `PROPOSA_CALLBACK_TIMEOUT_SECONDS` | 否 | `30` | 单次回调 HTTP 请求的超时秒数。 | | `PROPOSA_CALLBACK_MAX_RETRIES` | 否 | `3` | 回调失败时的最大尝试次数,包含首次请求。 | ## 部署(Linux 服务器) 仓库提供一键部署脚本 `deploy.sh`,按顺序完成: 0. 检测到服务器未安装 `uv` 时自动安装(默认开启,`AUTO_INSTALL_UV=0` 可关闭); 1. 使用 ModelScope 将 `gpt2-chinese-cluecorpussmall-onnx` 下载到 `models/` 目录(模型文件已存在时跳过); 2. 使用 `uv sync --frozen` 安装锁定依赖; 3. 后台启动 `proposa-api`,写入 PID/日志文件并做 `/health` 健康检查。 ### 部署前准备 ```bash cp .env.example .env ``` 编辑 `.env` 填写真实配置,至少设置 `DEEPSEEK_API_KEY`;`PROPOSA_API_PORT`、 `PROPOSA_WORK_DIR`、`REFERENCE_DIR`、`REFERENCE_DIR1` 等可按需调整。 ### 执行部署 ```bash sh deploy.sh # 完整部署并后台启动(可重复执行) sh deploy.sh status # 查看运行状态与健康检查 sh deploy.sh stop # 停止后台服务 sh deploy.sh restart # 停止后重新后台启动 ``` 日志位于 `output/api.log`,PID 位于 `output/api.pid`。 可用环境变量:`MODEL_REPO`(默认 `Maiteka/gpt2-chinese-cluecorpussmall-onnx`)、`MODEL_DIR`(默认 `models/gpt2-chinese-cluecorpussmall-onnx`)、`UV_CACHE_DIR`(默认 `<仓库根>/.uv-cache`)、`UV_INDEX_URL`(默认清华 PyPI 镜像 `https://pypi.tuna.tsinghua.edu.cn/simple`)、`PROPOSA_API_PORT`(默认读取 `.env`,缺省 8000)、 `RESTART=1`(服务已运行时强制重启)、`AUTO_INSTALL_UV=0`(uv 缺失时只提示、 不自动安装)。 ## HTTP API(对接主文档) ### 启动服务 ```powershell uv run proposa-api ``` ### 接口总览 | 接口 | 方法 | 说明 | | --- | --- | --- | | `/health` | GET | 健康检查。 | | `/api/v1/final-review` | POST | 提交生成任务,返回 HTTP 202。 | | `/api/v1/jobs/{request_id}` | GET | 查询任务状态。 | | `/api/v1/jobs/{request_id}/progress` | GET | 查询 Step1-6 进度。 | | `/api/v1/jobs/{request_id}/file` | GET | 下载最终 DOCX。 | ### 请求格式 接口同时支持 `multipart/form-data` 文件上传和 `application/json` 服务端本地路径。 #### multipart/form-data ```powershell curl.exe -X POST "http://127.0.0.1:8000/api/v1/final-review" ` -F "CALLBACK_URL=http://121.43.55.7:10026/shenqin/tender/callback" ` -F "txbId=投续表的DMS-ID" ` -F "TENDER_FILE=@招标文件.pdf" ` -F "PROCUREMENT_FILE=@采购需求.docx" ` -F "REFERENCE_BID=@参考投标文件.docx" ` -F "CLA_FILE=@澄清公告.docx" ` -F "CLEAN_INTERMEDIATE=false" ` -F "REFERENCE_IS_ABSOLUTE=false" ``` #### application/json ```powershell curl.exe -X POST "http://127.0.0.1:8000/api/v1/final-review" ` -H "Content-Type: application/json" ` --data '{ "CALLBACK_URL": "http://121.43.55.7:10026/shenqin/tender/callback", "txbId": "投续表的DMS-ID", "TENDER_FILE": "E:/CODE/proposa-ai/test_data/171-上海群众艺术馆/上海市群众艺术馆物业管理服务采购项目招标文件.pdf", "PROCUREMENT_FILE": "E:/CODE/proposa-ai/test_data/171-上海群众艺术馆/采购需求.docx", "TPC_IS_ABSOLUTE": true, "REFERENCE_BID": "E:/CODE/proposa-ai/test_data/171-上海群众艺术馆/参考投标文件/物业管理费项目投标文件.docx", "REFERENCE_IS_ABSOLUTE": true, "CLA_FILE": "", "CLEAN_INTERMEDIATE": false }' ``` ### 请求参数 | 参数 | 必填 | 说明 | | --- | --- | --- | | `CALLBACK_URL` | 是 | 处理成功或失败后接收 JSON 的回调地址。 | | `txbId` | 是 | 投续表的 DMS ID,回调时原样返回。 | | `TENDER_FILE` | 是 | 招标文件;multipart 上传 PDF,JSON 传服务端本地路径。 | | `PROCUREMENT_FILE` | 是 | 采购需求;multipart 上传 DOCX,JSON 传服务端本地路径。 | | `REFERENCE_BID` | 是 | 参考投标文件;multipart 上传 DOCX,JSON 传服务端本地路径。 | | `CLA_FILE` | 否 | 澄清公告文件;当前仅接收并记录,不参与生成。 | | `TPC_IS_ABSOLUTE` | JSON 是 | 仅 JSON 路径模式生效,必须传布尔值;`false` 时 TENDER_FILE、PROCUREMENT_FILE、非空 CLA_FILE 与 `TPC_DIR` 拼接,`true` 时原样读取。接口不会根据路径文本自行判断。 | | `CLEAN_INTERMEDIATE` | 否 | 默认 `true`;设为 `false` 时保留中间文件并返回 `intermediateDir`。 | | `REFERENCE_IS_ABSOLUTE` | JSON 是 | 仅 JSON 路径模式生效,必须传布尔值;`false` 时依次在 `REFERENCE_DIR`、`REFERENCE_DIR1` 下查找 `REFERENCE_BID`,`true` 时原样读取。接口不会根据路径文本自行判断。 | ### REFERENCE_BID 路径处理 JSON 模式下: - `REFERENCE_BID` 为绝对路径时直接使用。 - `REFERENCE_BID` 为相对路径时,设置 `"REFERENCE_IS_ABSOLUTE": false`,接口依次按 `REFERENCE_DIR + REFERENCE_BID`、`REFERENCE_DIR1 + REFERENCE_BID` 拼接并检查文件; 首选实际存在的文件,再复制到隔离任务目录。两处都存在时优先使用 `REFERENCE_DIR`。 - 两个候选目录均未配置,或配置后均不存在目标文件时,接口在启动 worker 前返回 400。 - 接口只按 `REFERENCE_IS_ABSOLUTE` 的布尔值处理,不根据路径是否以 `/` 开头自行判断; 相对模式下会先去掉文件值开头的路径分隔符再与配置目录拼接。 multipart 模式下 `REFERENCE_BID` 是上传文件本身,`REFERENCE_IS_ABSOLUTE` 仅兼容接收, 不参与路径拼接。 ### 上传校验规则 - `TENDER_FILE` 必填且不能为空,只接受能正常打开并提取到文字的 PDF;纯扫描或损坏的 PDF 会返回 HTTP 400。 - `PROCUREMENT_FILE` 和 `REFERENCE_BID` 只接受 DOCX;服务验证 ZIP 容器、必需 OOXML 部件和正文 XML。 - `CLA_FILE` 可选;当前仅保存并在回调中记录原始文件信息,不参与生成或校验。 - JSON 模式路径不存在或扩展名不符合要求时,会在启动 worker 前返回 400。 - Windows 路径建议使用正斜杠,或正确转义反斜杠。 ### 处理流程与文件路径 提交成功后,后台按顺序执行 `test_step1.py` 至 `test_step6.py`。 每个请求的内容都放在: ```text PROPOSA_WORK_DIR// ├── work/ # Step1-6 中间文件与缓存 └── <招标文件名>.docx # 最终 DOCX ``` 最终文件名规则:招标 PDF 原文件名去掉 `.pdf` 后追加 `.docx`。 `CLEAN_INTERMEDIATE=true`(默认)时,处理结束后只清理 `work/`,最终 DOCX 保留; `false` 时 `work/` 也保留。 ### 提交响应(HTTP 202) ```json { "request_id": "501c42526b5b4bb6ad68f0d736ef5884", "txbId": "1", "status": "processing", "message": "三份文件校验通过,已开始处理", "resultPath": "E:/CODE/proposa-ai/api_output_v1/501c42526b5b4bb6ad68f0d736ef5884/招标文件.docx", "output_path": "E:/CODE/proposa-ai/api_output_v1/501c42526b5b4bb6ad68f0d736ef5884/招标文件.docx", "progress_url": "http://127.0.0.1:8000/api/v1/jobs/501c42526b5b4bb6ad68f0d736ef5884/progress" } ``` | 字段 | 说明 | | --- | --- | | `request_id` | 任务唯一 ID,用于查询进度、状态和下载最终文件。 | | `txbId` | 请求传入的投续表 DMS ID,原样回传。 | | `status` | 当前任务状态;202 响应中为 `processing`。 | | `message` | 处理提示信息。 | | `resultPath` | 最终 DOCX 的绝对路径,位于 `PROPOSA_WORK_DIR//` 下。 | | `output_path` | 与 `resultPath` 相同的兼容字段。 | | `progress_url` | 轮询步骤进度的 URL。 | | `intermediate_dir` | 可选;仅当 `CLEAN_INTERMEDIATE=false` 时返回,指向 `work/` 目录。 | ### status 任务状态 | 状态值 | 含义 | | --- | --- | | `queued` | 任务已创建,等待后台 worker 开始处理。 | | `processing` | 正在执行 Step1-6,可通过 `progress_url` 查看当前步骤。 | | `completed` | 处理成功,`resultPath` 指向最终 DOCX,可下载。 | | `failed` | 处理失败,`resultPath` 为 `error: 具体错误信息`。 | ### 进度查询 ```powershell curl.exe "http://127.0.0.1:8000/api/v1/jobs/任务ID/progress" ``` ```json { "updated_at": "2026-08-26T14:29:00+00:00", "current_step": 3, "total_steps": 6, "current_step_name": "Step 3 投书大纲生成", "status": "running", "message": "Step 3 投书大纲生成 正在处理", "completed_steps": [1, 2] } ``` 进度接口中的 `status`: | 状态值 | 含义 | | --- | --- | | `running` | 当前步骤正在执行。 | | `completed` | 当前步骤已完成;当 `current_step` 为 6 且状态为 `completed` 时,表示全部步骤完成。 | ### 任务状态查询与下载 ```powershell curl.exe "http://127.0.0.1:8000/api/v1/jobs/任务ID" curl.exe "http://127.0.0.1:8000/api/v1/jobs/任务ID/file" --output 投标文件.docx ``` `GET /api/v1/jobs/{request_id}/file` 仅在任务 `completed` 且文件存在时返回 DOCX。 ### 回调 回调只在任务成功或失败后发送,不在提交时发送。 成功回调: ```json { "request_id": "任务ID", "txbId": "投续表的DMS-ID", "status": "completed", "resultPath": "output/api_jobs//招标文件.docx", "files": { "TENDER_FILE": {"filename": "招标文件.pdf", "size": 123, "sha256": "..."}, "PROCUREMENT_FILE": {"filename": "采购需求.docx", "size": 456, "sha256": "..."}, "REFERENCE_BID": {"filename": "参考投书.docx", "size": 789, "sha256": "..."} }, "final_review_url": "http://API地址/api/v1/jobs/任务ID/file" } ``` 失败回调: ```json { "request_id": "任务ID", "txbId": "投续表的DMS-ID", "status": "failed", "resultPath": "error: Step1-6 处理进程异常退出(退出码 7)", "error": "Step1-6 处理进程异常退出(退出码 7)" } ``` 回调说明: - 使用 POST JSON,不发送 token。 - 默认最多重试 3 次;重试仍失败时,任务状态记录 `callback_delivered=false` 和 `callback_error`。 - 请求中上传了 `CLA_FILE` 时,`files.CLA_FILE` 会包含 filename/size/sha256。 - `CLEAN_INTERMEDIATE=false` 时,成功和失败回调都会额外返回 `intermediateDir`。 ### 常见 HTTP 状态码 | 状态码 | 含义 | | --- | --- | | 202 | 校验通过,任务已异步开始。 | | 400 | 文件、路径、参数或回调地址校验失败。 | | 413 | 上传文件超过大小限制。 | | 415 | 不支持的 Content-Type。 | | 422 | 缺少必填字段。 | | 500 | 后台任务提交失败。 | ## 测试与局部验证 ```powershell # 快速回归 uv run python -m unittest discover -s scripts/tests -v # 按阶段手工测试(会使用 test_data 并可能调用 LLM) uv run python scripts/test_step3.py uv run python scripts/test_step6.py ``` ## 生成结构保护规则 - 一级章节原则上以模板为准;只有评分项大类无法匹配任何模板标题时,才允许在“项目经理” 一级章之前新增一级章,并统一重排编号。 - “需求理解”章完整保留模板原文,只允许增补属于需求理解的具体小评分项作为直接下一级标题。 - 评分项按“评分大类 → 具体小评分项 → 响应正文”组织;复用时必须能识别小评分项核心主题, 禁止重复创建同名/近义标题;新增标题不带分值,不使用 `SC-xx` 等内部编号。 - 废标项统一归入第一章要求承诺函,不散落、不重复。 - 最终真实一级章统一使用 `第{中文序号}章` 和 Heading 1,并映射到最终大纲。 - 模板中显式 Heading 1-4 只用于标题层级语义,不复制其视觉格式。 - Step5 补充正文必须写入 Step3 绑定的最深层评分标题节点。 - 开标一览表服务内容/服务要求/服务期限按表内填写说明统一填“响应”。 - 只有 `%%表名%%` 占位符允许创建或整表替换;Markdown 表格不能绕过授权生成 DOCX 表格。 - 表格、图片、图注、前后说明、签名照片/签署人/日期等原生内容块必须保持完整。