feat(bidding): 产线起点改为上传招标文件+解析产出QC契合度审核门禁
清理(前三步归商机产线): - 删邮箱采集: bid_mail.py/bid_mail_accounts表与页面/首页按钮 - 删招标审批状态机: TD_*/HT_TENDER_APPROVAL/摘要审批工具族 - bid_tenders瘦身为解析抽取的商务要素(去邮件字段) QC契合度审核: - 新表bid_qc_reviews(qc_type/round/fit_score/pass_score/passed/improvement) - bid_qc_capability: start_qc/finish_qc/list_qc/qc_report, 四类产出逐类审核 - 判定: 10分制打分, >9.5放行(参数bid_qc_pass_score); 不通过给改进意见 +清空产出+对账器重派解析任务带意见重做; 超轮次抛qc_escalation人工 - bid_flow对账器A2门禁阶段: 幂等/在办去重/轮次守卫/重做不计首跑上限 - 驾驶舱: qc_report工具+解析QC审核菜单; 参数读appbase表+默认兜底
This commit is contained in:
parent
22fca127cb
commit
44f8c18ac3
253
docs/DESIGN.md
253
docs/DESIGN.md
@ -2,52 +2,62 @@
|
||||
|
||||
> 对照开发产线(`sdlc_general`)设计,复用平台通用内核(项目/会话/任务链/人类任务/交付件/问答冒泡/技能分层),
|
||||
> 只增量定义投标产线自己的 **阶段、角色、工具、技能、任务流转规则、交付件**。
|
||||
|
||||
版本:v1.0.0 创建:2026-08-26
|
||||
> 版本:v1.1.0 创建:2026-08-26 修订:2026-08-31(产线起点改为上传招标文件;解析产出加 QC 契合度审核门禁)
|
||||
|
||||
---
|
||||
|
||||
## 一、业务流程(需求原文映射)
|
||||
## 零、产线边界(v1.1 修订)
|
||||
|
||||
- 招标信息的**采集、摘要、审批归商机产线**。投标产线从「上传招标文件」开始:
|
||||
用户/驾驶舱直接创建投标项目 → 上传招标文件 → 进入解析。
|
||||
- `bid_tenders` 表保留,但只承载**招标文件解析抽取的商务要素**(项目名称/招标人/编号/
|
||||
截止时间/周期/预算/保证金/资质要求,source=tender_doc),不再有邮件采集与审批状态机。
|
||||
- 已删除:邮箱采集(bid_mail.py、bid_mail_accounts 表/页面)、招标审批(tender_approval 人类任务)、
|
||||
摘要/审批/存档工具(fetch_tenders/summarize_tender/submit_tender_approval/approve_tender 等)、
|
||||
招标信息状态机(new→summarized→pending_approval→…)。
|
||||
|
||||
---
|
||||
|
||||
## 一、业务流程
|
||||
|
||||
| # | 业务动作 | 承担者 | 落库 |
|
||||
|---|---|---|---|
|
||||
| 1 | 从 yumoqing@opencomputing.cn(阿里云企业邮)获取招标信息 | agent.tender_scout | bid_tenders |
|
||||
| 2 | 对每条招标信息内容摘要:投标开始时间、项目周期、招标金额等 | agent.tender_scout | bid_tenders.summary/bid_start_at/project_period/budget_amount |
|
||||
| 3 | 走审批流程 | 人(human task + 可选钉钉审批) | pipeline_human_tasks(task_type=tender_approval) |
|
||||
| 4 | 审批不通过 → 招标信息存档作废 | 机制 | bid_tenders.status=archived |
|
||||
| 5 | 审批通过 → 创建项目 | 机制/主 agent | sd_projects(pipeline_id=bidding_general) |
|
||||
| 6 | 设定参与人员 | 主 agent / 人 | bid_members |
|
||||
| 7 | 等待上传招标文件 | 人(阻塞门禁) | pipeline_human_tasks(tender_file_upload) + bid_tender_files |
|
||||
| 8 | 从招标文件抽取评分项及得分规则 | agent.tender_analyst | bid_scoring_items |
|
||||
| 9 | 抽取所需资质 | agent.tender_analyst | bid_qualifications |
|
||||
| 10 | 抽取投标文件要求(章节结构/格式/份数/密封) | agent.tender_analyst | bid_doc_requirements |
|
||||
| 11 | 从公司投标知识库获取所需资质文档 | agent.bid_prep | bid_qualifications.kb_doc_id ← bid_kb_docs |
|
||||
| 12 | 发布人类应提供的文件清单 | agent.bid_prep(阻塞门禁) | pipeline_human_tasks(human_docs_request) |
|
||||
| 13 | 按投标文件要求分章节编写投标文件 | agent.bid_writer | bid_chapters |
|
||||
| 14 | 每章节完成后对照招标/投标文件要求评审,分数不够给改进意见重做 | agent.bid_reviewer | bid_reviews(scope=chapter) → 章节循环 |
|
||||
| 15 | 合成标书 | agent.bid_compositor | bid_documents(docx) |
|
||||
| 16 | 对整个标书评审 + 按评分项和得分规则打分 | agent.bid_scorer | bid_reviews(scope=whole) + bid_scores |
|
||||
| 17 | 不合适 → 改进意见 → 修改问题章节 → 评审 → …… 直到得分通过 | 机制(整书循环) | bid_scores.target_chapter_id → 章节回 rejected |
|
||||
| 1 | 创建投标项目 + 设定参与人员 | 驾驶舱 / 人 | sd_projects(pipeline_id=bidding_general) + bid_members |
|
||||
| 2 | 等待上传招标文件 | 人(阻塞门禁,上传后自动完成) | pipeline_human_tasks(tender_file_upload) + bid_tender_files |
|
||||
| 3 | 抽取评分项及得分规则 | agent.tender_analyst | bid_scoring_items |
|
||||
| 4 | 抽取所需资质 | agent.tender_analyst | bid_qualifications |
|
||||
| 5 | 抽取投标文件要求(章节结构/格式/份数/密封)+ 商务要素 | agent.tender_analyst | bid_doc_requirements + bid_tenders(source=tender_doc) |
|
||||
| 6 | 生成章节骨架(评分项挂载) | agent.tender_analyst | bid_chapters |
|
||||
| 7 | **解析产出 QC 契合度审核(四类逐类,10 分制 >9.5 放行,否则改进意见退回重做)** | agent.qc | bid_qc_reviews |
|
||||
| 8 | 从公司投标知识库获取所需资质文档 | agent.bid_prep | bid_qualifications.kb_doc_id ← bid_kb_docs |
|
||||
| 9 | 发布人类应提供的文件清单 | agent.bid_prep(阻塞门禁) | pipeline_human_tasks(human_docs_request) |
|
||||
| 10 | 按投标文件要求分章节编写投标文件 | agent.bid_writer | bid_chapters |
|
||||
| 11 | 每章节完成后对照招标/投标文件要求评审,分数不够给改进意见重做 | agent.bid_reviewer | bid_reviews(scope=chapter) → 章节循环 |
|
||||
| 12 | 合成标书 | agent.bid_compositor | bid_documents(docx) |
|
||||
| 13 | 对整个标书评审 + 按评分项和得分规则打分 | agent.bid_scorer | bid_reviews(scope=whole) + bid_scores |
|
||||
| 14 | 不合适 → 改进意见 → 修改问题章节 → 评审 → …… 直到得分通过 | 机制(整书循环) | bid_scores.target_chapter_id → 章节回 rejected |
|
||||
| 15 | 交付确认 | 人 | pipeline_human_tasks(bid_delivery_confirm) → 项目 completed |
|
||||
|
||||
---
|
||||
|
||||
## 二、阶段与角色(对照开发产线)
|
||||
## 二、阶段与角色
|
||||
|
||||
| 开发产线 | 投标产线 | 角色规范名 | 职责 | next_role |
|
||||
|---|---|---|---|---|
|
||||
| requirement 需求分析 | 招标情报 | `agent.tender_scout` | 收邮件、去重入库、摘要与要素抽取、提交审批 | `agent.tender_analyst` |
|
||||
| —(新增门禁) | 招标审批 | 人类任务 | 审批通过立项 / 不通过存档作废 | — |
|
||||
| design 设计 | 招标文件解析 | `agent.tender_analyst` | 抽取评分项+得分规则、所需资质、投标文件要求,产出章节骨架 | `agent.bid_prep` |
|
||||
| —(新增) | 资料准备 | `agent.bid_prep` | 知识库资质匹配、缺失项发布人类文件清单 | `agent.bid_writer` |
|
||||
| develop 开发 | 章节编写 | `agent.bid_writer` | 按章节骨架逐章写作(一章一任务) | `agent.bid_reviewer` |
|
||||
| test 测试 | 章节评审 | `agent.bid_reviewer` | 对照招标要求+得分规则逐章打分,不达标退回重写 | `agent.bid_compositor` |
|
||||
| —(新增) | 标书合成 | `agent.bid_compositor` | 合成 docx(封面/目录/页码/偏离表),产出交付件 | `agent.bid_scorer` |
|
||||
| deploy 部署 | 全书评分 | `agent.bid_scorer` | 按评分项+得分规则给整书打分、生成改进意见并映射章节 | ""(终结→人类交付确认) |
|
||||
| agent.pm | 同名复用 | `agent.pm` | 任务审核/派发/验收 | — |
|
||||
| agent.qc | 同名复用 | `agent.qc` | 交付件合规检查 | — |
|
||||
| 阶段 | 角色规范名 | 职责 |
|
||||
|---|---|---|
|
||||
| 立项与上传 | 驾驶舱 + 人 | 建项目、配人、上传招标文件 |
|
||||
| 招标文件解析 | `agent.tender_analyst` | 抽取评分项+得分规则、所需资质、投标文件要求、商务要素,产出章节骨架 |
|
||||
| 解析产出 QC | `agent.qc` | 四类产出对照招标文件原文按 10 分制审核契合度,>9.5 放行,否则改进意见退回 |
|
||||
| 资料准备 | `agent.bid_prep` | 知识库资质匹配、缺失项发布人类文件清单 |
|
||||
| 章节编写 | `agent.bid_writer` | 按章节骨架逐章写作(一章一任务) |
|
||||
| 章节评审 | `agent.bid_reviewer` | 对照招标要求+得分规则逐章打分,不达标退回重写 |
|
||||
| 标书合成 | `agent.bid_compositor` | 合成 docx(封面/目录/页码),产出交付件 |
|
||||
| 全书评分 | `agent.bid_scorer` | 按评分项+得分规则给整书打分、生成改进意见并映射章节 |
|
||||
| 任务验收 | `agent.pm` | 任务审核/验收 |
|
||||
| 合规检查 | `agent.qc`(复用) | 解析产出契合度审核(本产线 QC 的主职责) |
|
||||
|
||||
> 角色命名遵守平台规范:agent 角色 = `agent.{role}`,人类角色 = RBAC `{orgtype}.{role}`。
|
||||
> 同名角色跨产线隔离(按 pipeline_id 查 RoleSpec),`agent.pm`/`agent.qc` 在两产线各有自己的技能与提示词。
|
||||
> **RoleSpec.next_role 全部留空**:投标是「一章一任务 + 多级回退循环」扇出结构,
|
||||
> 流转一律由 `bid_flow` 状态对账器(每 15s)决定,它是角色任务唯一创建者。
|
||||
|
||||
---
|
||||
|
||||
@ -56,68 +66,88 @@
|
||||
### 3.1 主链(机制驱动,非 LLM 驱动)
|
||||
|
||||
```
|
||||
[邮箱] --fetch_tenders--> bid_tenders(new)
|
||||
--summarize_tender--> summarized
|
||||
--submit_tender_approval--> pending_approval ┌── reject ──> archived(作废) ✗终止
|
||||
└── approve ──> approved
|
||||
--create_bid_project--> sd_projects + assign_members
|
||||
--request_tender_file--> [人类任务:上传招标文件] (阻塞)
|
||||
↓ 上传完成
|
||||
agent.tender_analyst → 评分项 / 资质 / 投标文件要求 / 章节骨架
|
||||
↓ approved (PM)
|
||||
agent.bid_prep → 知识库资质匹配 + [人类任务:应提供文件清单](阻塞)
|
||||
↓ approved
|
||||
agent.bid_writer → 逐章写作(每章一个任务,可并行受并发上限约束)
|
||||
↓ 章节 written
|
||||
agent.bid_reviewer → 章节评审打分
|
||||
↓ 全部章节 approved
|
||||
agent.bid_compositor → 合成标书 docx
|
||||
↓
|
||||
agent.bid_scorer → 整书评分
|
||||
↓ 通过
|
||||
[人类任务:交付确认] → 项目 completed
|
||||
[立项] create_bid_project → sd_projects + 「上传招标文件」人类任务(阻塞)
|
||||
↓ 上传完成(对账器自动关闭上传任务)
|
||||
agent.tender_analyst → 评分项 / 资质 / 投标文件要求 / 商务要素 / 章节骨架
|
||||
↓
|
||||
agent.qc(A2 QC 门禁)→ 四类产出逐类契合度审核(10 分制)
|
||||
│ 得分 > 9.5 → 该类放行
|
||||
│ 不通过 → 改进意见 → 清空该类产出 → 解析重做(携带意见)→ 重审
|
||||
│ 轮次超限(默认 3)→ 抛人工(qc_escalation 阻塞,逃逸阀:人工修/强制放行)
|
||||
↓ 全部放行
|
||||
agent.bid_prep → 知识库资质匹配 + [人类任务:应提供文件清单](阻塞)
|
||||
↓
|
||||
agent.bid_writer → 逐章写作(每章一个任务,可并行)
|
||||
↓ 章节 written
|
||||
agent.bid_reviewer → 章节评审打分
|
||||
↓ 全部章节 approved
|
||||
agent.bid_compositor → 合成标书 docx
|
||||
↓
|
||||
agent.bid_scorer → 整书评分
|
||||
↓ 通过
|
||||
[人类任务:交付确认] → 项目 completed
|
||||
```
|
||||
|
||||
### 3.2 循环一:章节级评审回退
|
||||
### 3.2 QC 门禁:解析产出契合度审核(v1.1 新增)
|
||||
|
||||
- **审核对象(四类,逐类审核)**:
|
||||
| qc_type | 落库表 |
|
||||
|---|---|
|
||||
| `scoring_items` 评分项+得分规则 | bid_scoring_items |
|
||||
| `qualifications` 所需资质 | bid_qualifications |
|
||||
| `doc_requirements` 投标文件要求 | bid_doc_requirements |
|
||||
| `chapter_outline` 章节骨架 | bid_chapters |
|
||||
- **判定**:agent.qc 对照招标文件原文逐项核对,按 10 分制打契合度分;
|
||||
**得分高于通过分(`bid_qc_pass_score`,默认 9.5)才放行**(严格大于)。
|
||||
- **不通过**:改进意见必填(缺什么/错在哪/对照原文哪一处)→ 系统清空该类产出物
|
||||
→ 对账器重派解析任务(params.qc_redo=1 + qc_improvements 携带意见)→ 重抽 → 重审。
|
||||
- **轮次上限**:单类审核超 `bid_qc_max_round`(默认 3)→ 抛 `qc_escalation` 阻塞人工任务。
|
||||
逃逸阀:人工在产出物管理页修正后重新提交,或在审核记录页把该记录 passed 改 1 强制放行。
|
||||
- **重做轮次上限**:解析重做任务完成数超 `bid_qc_max_round` 且产出仍缺 → 同样抛人工,防无限循环。
|
||||
- 首跑解析重试上限(MAX_ANALYST_ATTEMPT=2)只计首跑任务,QC 退回的重做不计入。
|
||||
- 记录落 `bid_qc_reviews`(qc_type/round/fit_score/pass_score/passed/improvement)。
|
||||
|
||||
### 3.3 循环一:章节级评审回退
|
||||
|
||||
- 触发:`review_chapter` 判定 `score / max_score < chapter_pass_ratio`(默认 0.8)
|
||||
- 动作:章节 `status=rejected` + `review_comment` 落库 + `revise_count += 1`
|
||||
- 机制:`bid_reviewer` 任务 approved 后,若存在 rejected 章节 → 自动派生 `agent.bid_writer` 重写任务(任务标题带章节号,params 带 `chapter_id` + `improvement`)
|
||||
- 上限:`revise_count > bid_max_revise`(默认 3)→ 抛人类介入(fault_report + 人类任务),不无限重做
|
||||
- 机制:存在 rejected 章节且无在办编写任务 → 对账器自动派生 `agent.bid_writer` 重写任务(params 带 `chapter_id`)
|
||||
- 上限:`revise_count > bid_max_revise`(默认 3)→ 抛人类介入,不无限重做
|
||||
|
||||
### 3.3 循环二:整书评分回退
|
||||
### 3.4 循环二:整书评分回退
|
||||
|
||||
- 触发:`score_bid` 总分率 `< bid_pass_ratio`(默认 0.85),或存在否决项(`is_veto`)未响应
|
||||
- 动作:`bid_scores` 逐评分项落分 + `improvement` + `target_chapter_id`
|
||||
- 机制:把 `target_chapter_id` 命中的章节置 `rejected` → 派生 `bid_writer` 任务 → 重写 → 章节评审 → 全部通过 → 重新合成 → 重新评分
|
||||
- 轮次记录 `bid_reviews.round`,上限 `bid_max_round`(默认 3)→ 抛人类介入
|
||||
- 改进意见未映射到章节 → 抛人工指定(不静默丢失)
|
||||
|
||||
### 3.4 阻塞门禁(人类任务)
|
||||
### 3.5 阻塞门禁(人类任务)
|
||||
|
||||
| task_type | 语义 | 阻塞范围 |
|
||||
|---|---|---|
|
||||
| `tender_approval` | 招标信息审批 | 该 tender 不立项、不推进 |
|
||||
| `tender_file_upload` | 等待上传招标文件 | 项目全部角色任务 |
|
||||
| `tender_file_upload` | 等待上传招标文件(上传后自动完成) | 项目全部角色任务 |
|
||||
| `human_docs_request` | 人类应提供的文件 | 项目全部角色任务 |
|
||||
| `bid_delivery_confirm` | 最终标书交付确认 | 项目收尾 |
|
||||
| `qc_escalation` | 解析产出 QC 连续不达标 | 项目全部角色任务 |
|
||||
| `bid_delivery_confirm` | 最终标书交付确认 | 项目收尾(不阻塞派发) |
|
||||
|
||||
**门禁的守门位置:投标产线的角色任务唯一创建者是 `bid_flow` 对账器**(每 15 秒状态对账)。
|
||||
对账器在创建任务前就检查 `pipeline_human_tasks.project_id = <项目id> AND status='pending'`,
|
||||
有阻塞任务直接不派 —— 门禁在**创建源头**,不在引擎派发层。
|
||||
|
||||
> ⚠️ 概念区分(重要):`tenant_id` 是引擎的通用租户键(宿主可提供为 org_id / user_id 等)。
|
||||
> **只有角色任务(pipeline_id='role_task')这个具体场景约定 `tenant_id = project_id`**
|
||||
> (与开发产线的既有任务创建约定一致,`_create_next_task` 同款)。
|
||||
> **只有角色任务(pipeline_id='role_task')这个具体场景约定 `tenant_id = project_id`**。
|
||||
> 这是角色任务派发机制的约定,不是引擎通用规则 —— 引擎层 SQL 不得引用该等价关系,
|
||||
> 引擎对投标产线零特判;投标的阻塞语义全部收敛在投标产线自己的代码(bid_flow)里。
|
||||
|
||||
### 3.5 硬规则
|
||||
### 3.6 硬规则
|
||||
|
||||
- 审批不通过 = 终态(archived),不得再进入立项;需重新评估须人工 `reopen_tender`。
|
||||
- 章节写作任务与章节一对一(`params.chapter_id`),禁止一个任务写多章(对照开发产线「单任务不要过大」)。
|
||||
- 评审/评分只由 `agent.bid_reviewer` / `agent.bid_scorer` 执行,写作角色不得自评(源头杜绝越权:capability 按角色缩小)。
|
||||
- 得分/改进意见必须落库(`bid_reviews`/`bid_scores`),不接受"文档里声称评审通过"。
|
||||
- 案例与业绩一致性:技术方案引用的案例必须能对应到 `bid_kb_docs(doc_type=case)` 真实合同,禁止虚构客户/金额(写进 bid-doc-spec 技能,评审项硬检查)。
|
||||
- 章节写作任务与章节一对一(`params.chapter_id`),禁止一个任务写多章。
|
||||
- 评审/评分只由 `agent.bid_reviewer` / `agent.bid_scorer` 执行,写作角色不得自评;
|
||||
解析产出契合度审核只由 `agent.qc` 执行(capability 按角色缩小,源头杜绝越权)。
|
||||
- 得分/改进意见必须落库(`bid_reviews`/`bid_scores`/`bid_qc_reviews`),不接受"文档里声称评审通过"。
|
||||
- 案例与业绩一致性:技术方案引用的案例必须能对应到 `bid_kb_docs(doc_type=case)` 真实合同,
|
||||
禁止虚构客户/金额(写进 bid-doc-spec 技能,评审项硬检查)。
|
||||
|
||||
---
|
||||
|
||||
@ -125,21 +155,21 @@
|
||||
|
||||
| 表 | 说明 | 关键字段 |
|
||||
|---|---|---|
|
||||
| `bid_mail_accounts` | 招标邮箱配置(凭据不写死代码) | protocol/host/port/username/password(AES)/folder/filter_keywords/last_uid/enabled |
|
||||
| `bid_tenders` | 招标信息 | mail_uid/subject/received_at/raw_content/title/purchaser/tender_no/summary/bid_start_at/bid_deadline/project_period/budget_amount/deposit/qualification_req/status/project_id |
|
||||
| `bid_tenders` | 招标商务要素(解析抽取) | project_id/source/title/purchaser/tender_no/summary/bid_start_at/bid_deadline/project_period/budget_amount/deposit_amount/qualification_req |
|
||||
| `bid_members` | 项目参与人员 | project_id/user_id/member_role/duty |
|
||||
| `bid_tender_files` | 招标文件及附件 | project_id/tender_id/file_name/file_path/file_type/extract_status |
|
||||
| `bid_scoring_items` | 评分项 + 得分规则 | project_id/section/item_name/max_score/scoring_rule/is_veto/weight/source_ref |
|
||||
| `bid_tender_files` | 招标文件及附件 | project_id/tender_id/file_name/file_path/file_type/extract_status/content_text |
|
||||
| `bid_scoring_items` | 评分项 + 得分规则 | project_id/section/item_name/max_score/scoring_rule/is_veto/target_chapter_no/source_ref |
|
||||
| `bid_qualifications` | 所需资质 | project_id/qual_name/requirement/is_mandatory/kb_doc_id/match_status/owner |
|
||||
| `bid_doc_requirements` | 投标文件要求 | project_id/req_type/chapter_no/chapter_title/requirement/page_limit |
|
||||
| `bid_chapters` | 标书章节 | project_id/chapter_no/title/scoring_item_ids/status/version/content/file_path/review_score/review_comment/revise_count/assignee_role |
|
||||
| `bid_qc_reviews` | **解析产出 QC 审核记录** | project_id/qc_type/round/fit_score/pass_score/passed/improvement/task_id |
|
||||
| `bid_chapters` | 标书章节 | project_id/chapter_no/title/scoring_item_ids/status/version/content/review_score/revise_count/assignee_role |
|
||||
| `bid_reviews` | 评审记录(章节级/整书级) | project_id/scope/chapter_id/round/reviewer/score/max_score/passed/comments |
|
||||
| `bid_scores` | 评分明细(按评分项) | project_id/review_id/scoring_item_id/score/max_score/reason/improvement/target_chapter_id |
|
||||
| `bid_kb_docs` | 公司投标知识库 | org_id/doc_type/doc_name/tags/file_path/content_text/valid_until/status |
|
||||
| `bid_documents` | 合成标书 | project_id/version/file_path/page_count/total_score/status |
|
||||
|
||||
复用平台表:`sd_projects`(项目,pipeline_id=bidding_general)、`pipeline_tasks`(角色任务链)、
|
||||
`pipeline_human_tasks`(审批/人类任务)、`pipeline_deliverables`(交付件)、`pipeline_agent_questions`(问答冒泡)。
|
||||
`pipeline_human_tasks`(人类任务)、`pipeline_deliverables`(交付件)、`pipeline_agent_questions`(问答冒泡)。
|
||||
|
||||
---
|
||||
|
||||
@ -147,18 +177,15 @@
|
||||
|
||||
### 5.1 主 agent(驾驶舱)工具 —— `PipelineAbility.tools`
|
||||
|
||||
**招标情报**:`fetch_tenders` `list_tenders` `tender_detail` `summarize_tender`
|
||||
`submit_tender_approval` `approve_tender` `reject_tender` `archive_tender` `reopen_tender`
|
||||
|
||||
**立项与人员**:`create_bid_project` `assign_members` `list_members` `request_tender_file` `list_tender_files`
|
||||
|
||||
**解析结果查看**:`list_scoring_items` `list_qualifications` `list_doc_requirements`
|
||||
**解析结果与 QC**:`list_scoring_items` `list_doc_requirements` `list_qualifications` `qc_report`
|
||||
|
||||
**知识库**:`search_bid_kb` `list_kb_docs` `add_kb_doc` `request_human_docs`
|
||||
|
||||
**章节与标书**:`create_chapter_outline` `list_chapters` `chapter_detail` `compose_bid` `list_bid_documents`
|
||||
**章节与标书**:`list_chapters` `chapter_detail` `list_bid_documents` `list_reviews` `bid_score_report`
|
||||
|
||||
**评审评分**:`list_reviews` `bid_score_report`
|
||||
**诊断推进**:`diagnose_bid` `advance_bid`
|
||||
|
||||
**通用复用**(引擎级,来自 shared_ability):`create_task` `list_tasks` `task_detail` `approve_task`
|
||||
`complete_task` `cancel_task` `reset_task_retry` `start_agents` `diagnose_project`
|
||||
@ -170,16 +197,16 @@
|
||||
|
||||
| capability | 工具 | 授予角色 |
|
||||
|---|---|---|
|
||||
| `bid_tender_capability` | `fetch_tenders` `summarize_tender` `set_tender_fields` `submit_tender_approval` `archive_tender` `list_tenders` | agent.tender_scout |
|
||||
| `bid_analysis_capability` | `add_scoring_item` `add_qualification` `add_doc_requirement` `create_chapter_outline` `list_scoring_items` `list_doc_requirements` `mark_file_extracted` | agent.tender_analyst |
|
||||
| `bid_kb_capability` | `search_bid_kb` `match_qualification` `mark_qualification_missing` `request_human_docs` `list_qualifications` | agent.bid_prep |
|
||||
| `bid_write_capability` | `list_chapters` `chapter_detail` `write_chapter` `submit_chapter` `list_scoring_items` `search_bid_kb` | agent.bid_writer |
|
||||
| `bid_review_capability` | `review_chapter` `reject_chapter` `approve_chapter` `list_chapters` `list_scoring_items` `list_doc_requirements` | agent.bid_reviewer |
|
||||
| `bid_compose_capability` | `compose_bid` `list_chapters` `list_doc_requirements` | agent.bid_compositor |
|
||||
| `bid_score_capability` | `score_bid` `add_score_item_result` `request_chapter_revise` `list_scoring_items` `list_bid_documents` | agent.bid_scorer |
|
||||
| `bid_analysis_capability` | `read_tender_file` `list_tender_files` `extract_from_file` `add_scoring_item` `add_qualification` `add_doc_requirement` `create_chapter_outline` `list_scoring_items` `list_doc_requirements` `mark_file_extracted` | agent.tender_analyst |
|
||||
| `bid_qc_capability` | `start_qc` `finish_qc` `list_qc` `qc_report`(+ 只读工具) | agent.qc |
|
||||
| `bid_kb_capability` | `search_bid_kb` `kb_doc_detail` `list_qualifications` `match_qualification` `mark_qualification_missing` `auto_match_qualifications` `request_human_docs` | agent.bid_prep |
|
||||
| `bid_write_capability` | `list_chapters` `chapter_detail` `write_chapter` `submit_chapter` `search_bid_kb` `kb_doc_detail` `list_scoring_items` | agent.bid_writer |
|
||||
| `bid_review_capability` | `review_chapter` `approve_chapter` `reject_chapter` `list_reviews` `list_chapters` `list_scoring_items` `list_doc_requirements` | agent.bid_reviewer |
|
||||
| `bid_compose_capability` | `compose_bid` `list_chapters` `list_bid_documents` `read_bid_document` `list_doc_requirements` | agent.bid_compositor |
|
||||
| `bid_score_capability` | `start_bid_score` `add_score_item_result` `finalize_bid_score` `request_chapter_revise` `read_bid_document` `list_scoring_items` `list_bid_documents` `bid_score_report` | agent.bid_scorer |
|
||||
|
||||
> capability 按角色职责缩小 —— 写作角色拿不到 `review_chapter`/`score_bid`,评审角色拿不到 `write_chapter`。
|
||||
> 越权在源头杜绝,不靠事后 RBAC 校验。
|
||||
> capability 按角色职责缩小 —— 写作角色拿不到 `review_chapter`/`score_bid`,评审角色拿不到 `write_chapter`,
|
||||
> 解析角色拿不到 `finish_qc`,QC 拿不到 `extract_from_file`。越权在源头杜绝,不靠事后 RBAC 校验。
|
||||
|
||||
---
|
||||
|
||||
@ -188,8 +215,8 @@
|
||||
### common/(产线通用规范,状态机 + 门禁)
|
||||
| 技能 | 内容 |
|
||||
|---|---|
|
||||
| `bid-workflow` | 产线总纲:阶段链、门禁、两级循环、回退语义、机制 vs LLM 分界 |
|
||||
| `tender` | 招标信息状态机:new→summarized→pending_approval→approved/rejected→archived |
|
||||
| `bid-workflow` | 产线总纲:阶段链、QC 门禁、两级循环、阻塞门禁、机制 vs LLM 分界 |
|
||||
| `bid-qc` | 解析产出 QC 契合度审核规范:四类审核清单、10 分制打分口径、退回重做流程(capability 声明) |
|
||||
| `bid-chapter` | 章节状态机 + 评审循环 + 重做上限 + 一章一任务 |
|
||||
| `bid-review` | 评审与评分规范:对照评分项/得分规则打分、改进意见→章节映射、否决项处理 |
|
||||
| `bid-qualification` | 资质匹配规范:知识库检索 → matched/missing/human_required → 人类文件清单 |
|
||||
@ -197,8 +224,8 @@
|
||||
| `team-communication` | 问答冒泡链(复用开发产线同名规范) |
|
||||
|
||||
### roles/(角色定义,frontmatter 声明 capability)
|
||||
`agent.tender_scout` `agent.tender_analyst` `agent.bid_prep` `agent.bid_writer`
|
||||
`agent.bid_reviewer` `agent.bid_compositor` `agent.bid_scorer` `agent.pm` `agent.qc`
|
||||
`agent.tender_analyst` `agent.bid_prep` `agent.bid_writer` `agent.bid_reviewer`
|
||||
`agent.bid_compositor` `agent.bid_scorer` `agent.qc` `agent.pm`
|
||||
|
||||
---
|
||||
|
||||
@ -206,22 +233,34 @@
|
||||
|
||||
| 内容 | 位置 | 理由 |
|
||||
|---|---|---|
|
||||
| 主 agent 能力包 `bid_ability.py` | pipeline-service | 与 sdlc_ability 同构;ability 注册表在 core,任何模块可注册 |
|
||||
| 角色能力工具 `bid_*_capability.py` | pipeline-service | `exec_capability_tool` 只 import `pipeline_service.*` |
|
||||
| 邮箱采集 `bid_mail_fetcher.py` | pipeline-service | 被能力工具调用 |
|
||||
| 通用任务 handler 汇总 `shared_ability.py` | pipeline-service | 复用而非重写 SDLC 的通用任务/交付件/问答 handler |
|
||||
| 表 / CRUD / UI / dspy / RBAC | pipeline-bidding(新模块仓库) | 产线业务资产独立仓库,宿主 load_pipeline_bidding() 装载 |
|
||||
| 能力模块(bid_*_capability / bid_flow / bid_common) | pipeline-bidding 仓库 | 产线能力包住自己仓库,宿主不装零接触 |
|
||||
| 主 agent 能力包 `bid_ability.py` | pipeline-bidding 仓库 | 与能力模块同仓;ability 注册表在 core,任何模块可注册 |
|
||||
| 表 / CRUD / UI / dspy / RBAC | pipeline-bidding 仓库 | 产线业务资产独立仓库,宿主 load_pipeline_bidding() 装载 |
|
||||
| 技能 | pipeline-core/skills_library/pipelines/bidding_general | 技能库统一目录,部署期同步到机构工作空间 |
|
||||
| 引擎改动 | 仅 `exec_capability_tool` 支持全路径 module(8 行) | 引擎零投标特判 |
|
||||
|
||||
---
|
||||
|
||||
## 八、验收口径
|
||||
## 八、门限参数(appbase params 表 + DEFAULT_PARAMS 兜底)
|
||||
|
||||
1. `bid_*` 12 张表建表成功,CRUD 页面可增删改查。
|
||||
2. `fetch_tenders` 真连阿里云企业邮 IMAP 取到邮件并入库(凭据来自 `bid_mail_accounts`)。
|
||||
3. `summarize_tender` 真调 LLM 产出摘要与 5 要素并落库。
|
||||
4. 审批不通过 → tender 状态 archived;通过 → sd_projects 出现 pipeline_id=bidding_general 的项目。
|
||||
5. 上传招标文件人类任务 pending 时,poller 不派发该项目 agent 任务(项目级门禁生效)。
|
||||
| 参数 | 默认 | 含义 |
|
||||
|---|---|---|
|
||||
| `bid_chapter_pass_ratio` | 0.8 | 章节评审通过门限(得分率) |
|
||||
| `bid_pass_ratio` | 0.85 | 整书评分通过门限(得分率) |
|
||||
| `bid_max_revise` | 3 | 单章节最大重做次数 |
|
||||
| `bid_max_round` | 3 | 整书最大评分轮次 |
|
||||
| `bid_qc_pass_score` | 9.5 | 解析产出 QC 契合度通过分(10 分制,严格大于才放行) |
|
||||
| `bid_qc_max_round` | 3 | 单类产出 QC 最大审核轮次 / 解析重做轮次上限 |
|
||||
|
||||
---
|
||||
|
||||
## 九、验收口径
|
||||
|
||||
1. `bid_*` 12 张表建表成功,CRUD 页面可增删改查(含 bid_qc_reviews)。
|
||||
2. 上传招标文件人类任务 pending 时,对账器不派发该项目任何角色任务(门禁生效);上传后任务自动关闭。
|
||||
3. 解析任务产出评分项/资质/投标文件要求/章节骨架后,对账器自动派 `agent.qc` 审核任务。
|
||||
4. QC 审核得分 >9.5 → 该类放行进入下一阶段;≤9.5 → 该类产出被清空 + 解析重做任务携带改进意见(DB 可查)。
|
||||
5. QC 轮次超限 → `qc_escalation` 阻塞人工任务创建,项目暂停派发。
|
||||
6. 章节评审不达标 → 章节 rejected + 自动派生重写任务(DB 可查)。
|
||||
7. 整书评分不达标 → bid_scores 有 improvement + target_chapter_id,命中章节回 rejected。
|
||||
8. 全流程任务链在 `pipeline_tasks` 中可追溯(role 从 tender_scout 到 bid_scorer)。
|
||||
8. 全流程任务链在 `pipeline_tasks` 中可追溯(role 从 tender_analyst 到 bid_scorer,含 agent.qc)。
|
||||
|
||||
@ -1,23 +1,22 @@
|
||||
招标信息: Tender Notices
|
||||
招标信息台: Tender Inbox
|
||||
邮箱配置: Mail Account Config
|
||||
投标产线: Bidding Pipeline
|
||||
投标驾驶舱: Bidding Cockpit
|
||||
招标商务要素: Tender Business Facts
|
||||
投标知识库: Bid Knowledge Base
|
||||
评分项: Scoring Items
|
||||
标书章节: Bid Chapters
|
||||
合成标书: Assembled Bid Document
|
||||
采集招标信息: Fetch Tender Notices
|
||||
提交审批: Submit for Approval
|
||||
审批通过: Approved
|
||||
审批不通过: Rejected
|
||||
存档作废: Archived / Voided
|
||||
立项: Create Project
|
||||
创建投标项目: Create Bid Project
|
||||
参与人员: Team Members
|
||||
上传招标文件: Upload Tender Document
|
||||
招标文件解析: Tender Document Parsing
|
||||
解析QC审核: Parsing QC Review
|
||||
契合度得分: Fit Score
|
||||
通过分: Pass Score
|
||||
改进意见: Improvement Suggestions
|
||||
资质要求: Qualification Requirements
|
||||
投标文件要求: Bid Document Requirements
|
||||
章节评审: Chapter Review
|
||||
整书评分: Whole-document Scoring
|
||||
改进意见: Improvement Suggestions
|
||||
得分规则: Scoring Rules
|
||||
评审通过: Review Passed
|
||||
退回重写: Returned for Rewrite
|
||||
|
||||
@ -1,23 +1,22 @@
|
||||
招标信息: 招标信息
|
||||
招标信息台: 招标信息台
|
||||
邮箱配置: 邮箱配置
|
||||
投标产线: 投标产线
|
||||
投标驾驶舱: 投标驾驶舱
|
||||
招标商务要素: 招标商务要素
|
||||
投标知识库: 投标知识库
|
||||
评分项: 评分项
|
||||
标书章节: 标书章节
|
||||
合成标书: 合成标书
|
||||
采集招标信息: 采集招标信息
|
||||
提交审批: 提交审批
|
||||
审批通过: 审批通过
|
||||
审批不通过: 审批不通过
|
||||
存档作废: 存档作废
|
||||
立项: 立项
|
||||
创建投标项目: 创建投标项目
|
||||
参与人员: 参与人员
|
||||
上传招标文件: 上传招标文件
|
||||
招标文件解析: 招标文件解析
|
||||
解析QC审核: 解析QC审核
|
||||
契合度得分: 契合度得分
|
||||
通过分: 通过分
|
||||
改进意见: 改进意见
|
||||
资质要求: 资质要求
|
||||
投标文件要求: 投标文件要求
|
||||
章节评审: 章节评审
|
||||
整书评分: 整书评分
|
||||
改进意见: 改进意见
|
||||
得分规则: 得分规则
|
||||
评审通过: 评审通过
|
||||
退回重写: 退回重写
|
||||
|
||||
@ -4,29 +4,27 @@
|
||||
"id": "bidding_general",
|
||||
"name": "通用投标产线",
|
||||
"pipeline_type": "bidding",
|
||||
"description": "招标信息采集→内容摘要→审批→立项与人员→招标文件解析(评分项/得分规则/资质/投标文件要求)→资质准备→分章节编写→章节评审→合成标书→整书评分→交付",
|
||||
"description": "上传招标文件→解析(评分项/得分规则/资质/投标文件要求/章节骨架)→QC契合度审核→资质准备→分章节编写→章节评审→合成标书→整书评分→交付。招标信息的采集/摘要/审批归商机产线。",
|
||||
"status": "published",
|
||||
"org_id": "0"
|
||||
}
|
||||
],
|
||||
"appcodes": [
|
||||
{
|
||||
"parentid": "bid_tender_status",
|
||||
"parentname": "招标信息状态",
|
||||
"parentid": "bid_qc_type",
|
||||
"parentname": "QC审核对象",
|
||||
"items": [
|
||||
{"k": "new", "v": "新采集"},
|
||||
{"k": "summarized", "v": "已摘要"},
|
||||
{"k": "pending_approval", "v": "待审批"},
|
||||
{"k": "approved", "v": "审批通过"},
|
||||
{"k": "rejected", "v": "审批不通过"},
|
||||
{"k": "archived", "v": "存档作废"}
|
||||
{"k": "scoring_items", "v": "评分项+得分规则"},
|
||||
{"k": "qualifications", "v": "所需资质"},
|
||||
{"k": "doc_requirements", "v": "投标文件要求"},
|
||||
{"k": "chapter_outline", "v": "章节骨架"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"parentid": "bid_tender_source",
|
||||
"parentname": "招标信息来源",
|
||||
"items": [
|
||||
{"k": "mail", "v": "邮件采集"},
|
||||
{"k": "tender_doc", "v": "招标文件解析"},
|
||||
{"k": "manual", "v": "手工录入"}
|
||||
]
|
||||
},
|
||||
|
||||
@ -1,42 +0,0 @@
|
||||
{
|
||||
"tblname": "bid_mail_accounts",
|
||||
"title": "招标邮箱配置",
|
||||
"params": {
|
||||
"sortby": [
|
||||
"created_at desc"
|
||||
],
|
||||
"confidential_fields": [],
|
||||
"browserfields": {
|
||||
"exclouded": [
|
||||
"password",
|
||||
"last_uid"
|
||||
]
|
||||
},
|
||||
"editexclouded": [
|
||||
"id",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"last_uid",
|
||||
"last_fetch_at"
|
||||
],
|
||||
"editable": {
|
||||
"new_data_url": "{{entire_url('add_bid_mail_accounts.dspy')}}",
|
||||
"update_data_url": "{{entire_url('update_bid_mail_accounts.dspy')}}",
|
||||
"delete_data_url": "{{entire_url('delete_bid_mail_accounts.dspy')}}"
|
||||
},
|
||||
"data_filter": {
|
||||
"AND": [
|
||||
{
|
||||
"field": "account_name",
|
||||
"op": "LIKE",
|
||||
"var": "name_input"
|
||||
},
|
||||
{
|
||||
"field": "enabled",
|
||||
"op": "=",
|
||||
"var": "enabled_input"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
44
json/bid_qc_reviews.json
Normal file
44
json/bid_qc_reviews.json
Normal file
@ -0,0 +1,44 @@
|
||||
{
|
||||
"tblname": "bid_qc_reviews",
|
||||
"title": "解析产出QC审核",
|
||||
"params": {
|
||||
"sortby": [
|
||||
"qc_type",
|
||||
"round desc"
|
||||
],
|
||||
"confidential_fields": [],
|
||||
"browserfields": {
|
||||
"exclouded": [
|
||||
"comments"
|
||||
]
|
||||
},
|
||||
"editexclouded": [
|
||||
"id",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"task_id",
|
||||
"reviewer",
|
||||
"review_type"
|
||||
],
|
||||
"editable": {
|
||||
"new_data_url": "{{entire_url('add_bid_qc_reviews.dspy')}}",
|
||||
"update_data_url": "{{entire_url('update_bid_qc_reviews.dspy')}}",
|
||||
"delete_data_url": "{{entire_url('delete_bid_qc_reviews.dspy')}}"
|
||||
},
|
||||
"data_filter": {
|
||||
"AND": [
|
||||
{
|
||||
"field": "project_id",
|
||||
"op": "=",
|
||||
"var": "project_id"
|
||||
},
|
||||
{
|
||||
"field": "qc_type",
|
||||
"op": "=",
|
||||
"var": "qc_type_input"
|
||||
}
|
||||
]
|
||||
},
|
||||
"subtables": []
|
||||
}
|
||||
}
|
||||
@ -1,19 +1,15 @@
|
||||
{
|
||||
"tblname": "bid_tenders",
|
||||
"title": "招标信息",
|
||||
"title": "招标商务要素",
|
||||
"params": {
|
||||
"sortby": [
|
||||
"received_at desc"
|
||||
"created_at desc"
|
||||
],
|
||||
"confidential_fields": [],
|
||||
"browserfields": {
|
||||
"exclouded": [
|
||||
"raw_content",
|
||||
"attach_json",
|
||||
"key_dates",
|
||||
"org_id",
|
||||
"account_id",
|
||||
"mail_uid",
|
||||
"risk_note"
|
||||
]
|
||||
},
|
||||
@ -21,7 +17,6 @@
|
||||
"id",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"approval_task_id",
|
||||
"project_id"
|
||||
],
|
||||
"editable": {
|
||||
@ -37,9 +32,9 @@
|
||||
"var": "title_input"
|
||||
},
|
||||
{
|
||||
"field": "status",
|
||||
"field": "project_id",
|
||||
"op": "=",
|
||||
"var": "status_input"
|
||||
"var": "project_id"
|
||||
},
|
||||
{
|
||||
"field": "purchaser",
|
||||
|
||||
@ -1,159 +0,0 @@
|
||||
{
|
||||
"summary": [
|
||||
{
|
||||
"name": "bid_mail_accounts",
|
||||
"title": "招标邮箱配置",
|
||||
"primary": [
|
||||
"id"
|
||||
],
|
||||
"catelog": "entity"
|
||||
}
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"name": "id",
|
||||
"title": "主键ID",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "org_id",
|
||||
"title": "机构ID",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"nullable": "no",
|
||||
"default": "0"
|
||||
},
|
||||
{
|
||||
"name": "account_name",
|
||||
"title": "配置名称",
|
||||
"type": "str",
|
||||
"length": 128,
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "protocol",
|
||||
"title": "协议",
|
||||
"type": "str",
|
||||
"length": 16,
|
||||
"nullable": "no",
|
||||
"default": "imap"
|
||||
},
|
||||
{
|
||||
"name": "host",
|
||||
"title": "服务器地址",
|
||||
"type": "str",
|
||||
"length": 128,
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "port",
|
||||
"title": "端口",
|
||||
"type": "int",
|
||||
"nullable": "no",
|
||||
"default": "993"
|
||||
},
|
||||
{
|
||||
"name": "use_ssl",
|
||||
"title": "使用SSL",
|
||||
"type": "str",
|
||||
"length": 1,
|
||||
"nullable": "no",
|
||||
"default": "1"
|
||||
},
|
||||
{
|
||||
"name": "username",
|
||||
"title": "登录账号",
|
||||
"type": "str",
|
||||
"length": 128,
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "password",
|
||||
"title": "登录密码(AES加密)",
|
||||
"type": "str",
|
||||
"length": 500,
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "folder",
|
||||
"title": "邮件目录",
|
||||
"type": "str",
|
||||
"length": 64,
|
||||
"default": "INBOX"
|
||||
},
|
||||
{
|
||||
"name": "filter_keywords",
|
||||
"title": "招标关键词(逗号分隔)",
|
||||
"type": "str",
|
||||
"length": 500
|
||||
},
|
||||
{
|
||||
"name": "fetch_days",
|
||||
"title": "回溯天数",
|
||||
"type": "int",
|
||||
"default": "7"
|
||||
},
|
||||
{
|
||||
"name": "last_uid",
|
||||
"title": "上次同步UID",
|
||||
"type": "str",
|
||||
"length": 64
|
||||
},
|
||||
{
|
||||
"name": "last_fetch_at",
|
||||
"title": "上次同步时间",
|
||||
"type": "timestamp"
|
||||
},
|
||||
{
|
||||
"name": "enabled",
|
||||
"title": "启用",
|
||||
"type": "str",
|
||||
"length": 1,
|
||||
"nullable": "no",
|
||||
"default": "1"
|
||||
},
|
||||
{
|
||||
"name": "status",
|
||||
"title": "状态",
|
||||
"type": "str",
|
||||
"length": 16,
|
||||
"default": "active"
|
||||
},
|
||||
{
|
||||
"name": "created_by",
|
||||
"title": "创建人",
|
||||
"type": "str",
|
||||
"length": 32
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"title": "创建时间",
|
||||
"type": "timestamp",
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"title": "更新时间",
|
||||
"type": "timestamp"
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"name": "idx_bma_org",
|
||||
"idxtype": "index",
|
||||
"idxfields": [
|
||||
"org_id"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "idx_bma_enabled",
|
||||
"idxtype": "index",
|
||||
"idxfields": [
|
||||
"enabled"
|
||||
]
|
||||
}
|
||||
],
|
||||
"codes": []
|
||||
}
|
||||
129
models/bid_qc_reviews.json
Normal file
129
models/bid_qc_reviews.json
Normal file
@ -0,0 +1,129 @@
|
||||
{
|
||||
"summary": [
|
||||
{
|
||||
"name": "bid_qc_reviews",
|
||||
"title": "解析产出QC审核记录",
|
||||
"primary": [
|
||||
"id"
|
||||
],
|
||||
"catelog": "entity"
|
||||
}
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"name": "id",
|
||||
"title": "主键ID",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "project_id",
|
||||
"title": "项目ID",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "qc_type",
|
||||
"title": "审核对象",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "round",
|
||||
"title": "审核轮次",
|
||||
"type": "int",
|
||||
"nullable": "no",
|
||||
"default": "1"
|
||||
},
|
||||
{
|
||||
"name": "reviewer",
|
||||
"title": "审核方",
|
||||
"type": "str",
|
||||
"length": 64
|
||||
},
|
||||
{
|
||||
"name": "review_type",
|
||||
"title": "审核类型",
|
||||
"type": "str",
|
||||
"length": 16,
|
||||
"default": "agent"
|
||||
},
|
||||
{
|
||||
"name": "fit_score",
|
||||
"title": "契合度得分(10分制)",
|
||||
"type": "float",
|
||||
"length": 4,
|
||||
"dec": 2
|
||||
},
|
||||
{
|
||||
"name": "pass_score",
|
||||
"title": "通过分",
|
||||
"type": "float",
|
||||
"length": 4,
|
||||
"dec": 2
|
||||
},
|
||||
{
|
||||
"name": "passed",
|
||||
"title": "是否通过",
|
||||
"type": "str",
|
||||
"length": 1,
|
||||
"nullable": "no",
|
||||
"default": "0"
|
||||
},
|
||||
{
|
||||
"name": "improvement",
|
||||
"title": "改进意见",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"name": "comments",
|
||||
"title": "审核说明",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"name": "task_id",
|
||||
"title": "关联任务ID",
|
||||
"type": "str",
|
||||
"length": 32
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"title": "创建时间",
|
||||
"type": "timestamp",
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"title": "更新时间",
|
||||
"type": "timestamp"
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"name": "idx_bqr_project",
|
||||
"idxtype": "index",
|
||||
"idxfields": [
|
||||
"project_id"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "idx_bqr_type",
|
||||
"idxtype": "index",
|
||||
"idxfields": [
|
||||
"qc_type"
|
||||
]
|
||||
}
|
||||
],
|
||||
"codes": [
|
||||
{
|
||||
"field": "qc_type",
|
||||
"table": "appcodes_kv",
|
||||
"valuefield": "k",
|
||||
"textfield": "v",
|
||||
"cond": "parentid='bid_qc_type'"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -2,7 +2,7 @@
|
||||
"summary": [
|
||||
{
|
||||
"name": "bid_tenders",
|
||||
"title": "招标信息",
|
||||
"title": "招标商务要素",
|
||||
"primary": [
|
||||
"id"
|
||||
],
|
||||
@ -26,51 +26,18 @@
|
||||
"default": "0"
|
||||
},
|
||||
{
|
||||
"name": "source",
|
||||
"title": "信息来源",
|
||||
"type": "str",
|
||||
"length": 16,
|
||||
"nullable": "no",
|
||||
"default": "mail"
|
||||
},
|
||||
{
|
||||
"name": "account_id",
|
||||
"title": "邮箱配置ID",
|
||||
"name": "project_id",
|
||||
"title": "项目ID",
|
||||
"type": "str",
|
||||
"length": 32
|
||||
},
|
||||
{
|
||||
"name": "mail_uid",
|
||||
"title": "邮件UID",
|
||||
"name": "source",
|
||||
"title": "来源",
|
||||
"type": "str",
|
||||
"length": 64
|
||||
},
|
||||
{
|
||||
"name": "mail_from",
|
||||
"title": "发件人",
|
||||
"type": "str",
|
||||
"length": 255
|
||||
},
|
||||
{
|
||||
"name": "mail_subject",
|
||||
"title": "邮件主题",
|
||||
"type": "str",
|
||||
"length": 500
|
||||
},
|
||||
{
|
||||
"name": "received_at",
|
||||
"title": "收信时间",
|
||||
"type": "timestamp"
|
||||
},
|
||||
{
|
||||
"name": "raw_content",
|
||||
"title": "邮件原文",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"name": "attach_json",
|
||||
"title": "附件清单(JSON)",
|
||||
"type": "text"
|
||||
"length": 16,
|
||||
"nullable": "no",
|
||||
"default": "tender_doc"
|
||||
},
|
||||
{
|
||||
"name": "title",
|
||||
@ -98,18 +65,20 @@
|
||||
},
|
||||
{
|
||||
"name": "summary",
|
||||
"title": "内容摘要",
|
||||
"title": "项目概况摘要",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"name": "bid_start_at",
|
||||
"title": "投标开始时间",
|
||||
"type": "date"
|
||||
"type": "str",
|
||||
"length": 16
|
||||
},
|
||||
{
|
||||
"name": "bid_deadline",
|
||||
"title": "投标截止时间",
|
||||
"type": "date"
|
||||
"type": "str",
|
||||
"length": 16
|
||||
},
|
||||
{
|
||||
"name": "project_period",
|
||||
@ -121,14 +90,14 @@
|
||||
"name": "budget_amount",
|
||||
"title": "招标金额",
|
||||
"type": "float",
|
||||
"length": 18,
|
||||
"length": 14,
|
||||
"dec": 2
|
||||
},
|
||||
{
|
||||
"name": "deposit_amount",
|
||||
"title": "投标保证金",
|
||||
"type": "float",
|
||||
"length": 18,
|
||||
"length": 14,
|
||||
"dec": 2
|
||||
},
|
||||
{
|
||||
@ -138,7 +107,7 @@
|
||||
},
|
||||
{
|
||||
"name": "key_dates",
|
||||
"title": "关键时间节点(JSON)",
|
||||
"title": "关键节点(JSON)",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
@ -151,32 +120,13 @@
|
||||
"title": "状态",
|
||||
"type": "str",
|
||||
"length": 16,
|
||||
"nullable": "no",
|
||||
"default": "new"
|
||||
},
|
||||
{
|
||||
"name": "approval_task_id",
|
||||
"title": "审批任务ID",
|
||||
"type": "str",
|
||||
"length": 32
|
||||
},
|
||||
{
|
||||
"name": "project_id",
|
||||
"title": "立项项目ID",
|
||||
"type": "str",
|
||||
"length": 32
|
||||
},
|
||||
{
|
||||
"name": "archive_reason",
|
||||
"title": "作废/存档原因",
|
||||
"type": "str",
|
||||
"length": 500
|
||||
"default": "summarized"
|
||||
},
|
||||
{
|
||||
"name": "created_by",
|
||||
"title": "创建人",
|
||||
"type": "str",
|
||||
"length": 32
|
||||
"length": 64
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
@ -198,20 +148,6 @@
|
||||
"org_id"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "idx_bt_status",
|
||||
"idxtype": "index",
|
||||
"idxfields": [
|
||||
"status"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "idx_bt_mailuid",
|
||||
"idxtype": "index",
|
||||
"idxfields": [
|
||||
"mail_uid"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "idx_bt_project",
|
||||
"idxtype": "index",
|
||||
@ -228,13 +164,6 @@
|
||||
}
|
||||
],
|
||||
"codes": [
|
||||
{
|
||||
"field": "status",
|
||||
"table": "appcodes_kv",
|
||||
"valuefield": "k",
|
||||
"textfield": "v",
|
||||
"cond": "parentid='bid_tender_status'"
|
||||
},
|
||||
{
|
||||
"field": "source",
|
||||
"table": "appcodes_kv",
|
||||
|
||||
120
mysql.ddl.sql
120
mysql.ddl.sql
@ -1,50 +1,4 @@
|
||||
|
||||
-- /home/ymq/work/pipeline/pipeline-bidding/models/bid_mail_accounts.json
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
-- 建库时请用以下语句,支持emoji字符
|
||||
-- CREATE DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
drop table if exists bid_mail_accounts;
|
||||
CREATE TABLE bid_mail_accounts
|
||||
(
|
||||
|
||||
`id` VARCHAR(32) NOT NULL comment '主键ID',
|
||||
`org_id` VARCHAR(32) NOT NULL DEFAULT '0' comment '机构ID',
|
||||
`account_name` VARCHAR(128) NOT NULL comment '配置名称',
|
||||
`protocol` VARCHAR(16) NOT NULL DEFAULT 'imap' comment '协议',
|
||||
`host` VARCHAR(128) NOT NULL comment '服务器地址',
|
||||
`port` int NOT NULL DEFAULT '993' comment '端口',
|
||||
`use_ssl` VARCHAR(1) NOT NULL DEFAULT '1' comment '使用SSL',
|
||||
`username` VARCHAR(128) NOT NULL comment '登录账号',
|
||||
`password` VARCHAR(500) NOT NULL comment '登录密码(AES加密)',
|
||||
`folder` VARCHAR(64) DEFAULT 'INBOX' comment '邮件目录',
|
||||
`filter_keywords` VARCHAR(500) comment '招标关键词(逗号分隔)',
|
||||
`fetch_days` int DEFAULT '7' comment '回溯天数',
|
||||
`last_uid` VARCHAR(64) comment '上次同步UID',
|
||||
`last_fetch_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP comment '上次同步时间',
|
||||
`enabled` VARCHAR(1) NOT NULL DEFAULT '1' comment '启用',
|
||||
`status` VARCHAR(16) DEFAULT 'active' comment '状态',
|
||||
`created_by` VARCHAR(32) comment '创建人',
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL comment '创建时间',
|
||||
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP comment '更新时间'
|
||||
|
||||
|
||||
,primary key(id)
|
||||
|
||||
|
||||
)
|
||||
CHARACTER SET utf8mb4
|
||||
COLLATE utf8mb4_unicode_ci
|
||||
engine=innodb
|
||||
comment '招标邮箱配置'
|
||||
;
|
||||
|
||||
CREATE INDEX bid_mail_accounts_idx_bma_org ON bid_mail_accounts(org_id);
|
||||
CREATE INDEX bid_mail_accounts_idx_bma_enabled ON bid_mail_accounts(enabled);
|
||||
|
||||
-- /home/ymq/work/pipeline/pipeline-bidding/models/bid_scoring_items.json
|
||||
|
||||
|
||||
@ -302,7 +256,6 @@ CREATE INDEX bid_reviews_idx_br_chapter ON bid_reviews(chapter_id);
|
||||
|
||||
|
||||
|
||||
|
||||
-- 建库时请用以下语句,支持emoji字符
|
||||
-- CREATE DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
drop table if exists bid_tenders;
|
||||
@ -311,32 +264,23 @@ CREATE TABLE bid_tenders
|
||||
|
||||
`id` VARCHAR(32) NOT NULL comment '主键ID',
|
||||
`org_id` VARCHAR(32) NOT NULL DEFAULT '0' comment '机构ID',
|
||||
`source` VARCHAR(16) NOT NULL DEFAULT 'mail' comment '信息来源',
|
||||
`account_id` VARCHAR(32) comment '邮箱配置ID',
|
||||
`mail_uid` VARCHAR(64) comment '邮件UID',
|
||||
`mail_from` VARCHAR(255) comment '发件人',
|
||||
`mail_subject` VARCHAR(500) comment '邮件主题',
|
||||
`received_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP comment '收信时间',
|
||||
`raw_content` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci comment '邮件原文',
|
||||
`attach_json` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci comment '附件清单(JSON)',
|
||||
`project_id` VARCHAR(32) comment '项目ID',
|
||||
`source` VARCHAR(16) NOT NULL DEFAULT 'tender_doc' comment '来源',
|
||||
`title` VARCHAR(255) comment '项目名称',
|
||||
`purchaser` VARCHAR(255) comment '招标人/采购人',
|
||||
`tender_no` VARCHAR(64) comment '招标编号',
|
||||
`industry` VARCHAR(64) comment '行业',
|
||||
`summary` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci comment '内容摘要',
|
||||
`bid_start_at` date comment '投标开始时间',
|
||||
`bid_deadline` date comment '投标截止时间',
|
||||
`summary` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci comment '项目概况摘要',
|
||||
`bid_start_at` VARCHAR(16) comment '投标开始时间',
|
||||
`bid_deadline` VARCHAR(16) comment '投标截止时间',
|
||||
`project_period` VARCHAR(64) comment '项目周期',
|
||||
`budget_amount` double(18,2) comment '招标金额',
|
||||
`deposit_amount` double(18,2) comment '投标保证金',
|
||||
`budget_amount` double(14,2) comment '招标金额',
|
||||
`deposit_amount` double(14,2) comment '投标保证金',
|
||||
`qualification_req` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci comment '资质要求摘要',
|
||||
`key_dates` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci comment '关键时间节点(JSON)',
|
||||
`key_dates` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci comment '关键节点(JSON)',
|
||||
`risk_note` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci comment '风险提示',
|
||||
`status` VARCHAR(16) NOT NULL DEFAULT 'new' comment '状态',
|
||||
`approval_task_id` VARCHAR(32) comment '审批任务ID',
|
||||
`project_id` VARCHAR(32) comment '立项项目ID',
|
||||
`archive_reason` VARCHAR(500) comment '作废/存档原因',
|
||||
`created_by` VARCHAR(32) comment '创建人',
|
||||
`status` VARCHAR(16) DEFAULT 'summarized' comment '状态',
|
||||
`created_by` VARCHAR(64) comment '创建人',
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL comment '创建时间',
|
||||
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP comment '更新时间'
|
||||
|
||||
@ -348,12 +292,10 @@ CREATE TABLE bid_tenders
|
||||
CHARACTER SET utf8mb4
|
||||
COLLATE utf8mb4_unicode_ci
|
||||
engine=innodb
|
||||
comment '招标信息'
|
||||
comment '招标商务要素'
|
||||
;
|
||||
|
||||
CREATE INDEX bid_tenders_idx_bt_org ON bid_tenders(org_id);
|
||||
CREATE INDEX bid_tenders_idx_bt_status ON bid_tenders(status);
|
||||
CREATE INDEX bid_tenders_idx_bt_mailuid ON bid_tenders(mail_uid);
|
||||
CREATE INDEX bid_tenders_idx_bt_project ON bid_tenders(project_id);
|
||||
CREATE INDEX bid_tenders_idx_bt_deadline ON bid_tenders(bid_deadline);
|
||||
|
||||
@ -524,3 +466,43 @@ comment '招标所需资质'
|
||||
|
||||
CREATE INDEX bid_qualifications_idx_bq_project ON bid_qualifications(project_id);
|
||||
CREATE INDEX bid_qualifications_idx_bq_match ON bid_qualifications(match_status);
|
||||
|
||||
-- /home/ymq/work/pipeline/pipeline-bidding/models/bid_qc_reviews.json
|
||||
|
||||
|
||||
|
||||
|
||||
-- 建库时请用以下语句,支持emoji字符
|
||||
-- CREATE DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
drop table if exists bid_qc_reviews;
|
||||
CREATE TABLE bid_qc_reviews
|
||||
(
|
||||
|
||||
`id` VARCHAR(32) NOT NULL comment '主键ID',
|
||||
`project_id` VARCHAR(32) NOT NULL comment '项目ID',
|
||||
`qc_type` VARCHAR(32) NOT NULL comment '审核对象',
|
||||
`round` int NOT NULL DEFAULT '1' comment '审核轮次',
|
||||
`reviewer` VARCHAR(64) comment '审核方',
|
||||
`review_type` VARCHAR(16) DEFAULT 'agent' comment '审核类型',
|
||||
`fit_score` double(4,2) comment '契合度得分(10分制)',
|
||||
`pass_score` double(4,2) comment '通过分',
|
||||
`passed` VARCHAR(1) NOT NULL DEFAULT '0' comment '是否通过',
|
||||
`improvement` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci comment '改进意见',
|
||||
`comments` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci comment '审核说明',
|
||||
`task_id` VARCHAR(32) comment '关联任务ID',
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL comment '创建时间',
|
||||
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP comment '更新时间'
|
||||
|
||||
|
||||
,primary key(id)
|
||||
|
||||
|
||||
)
|
||||
CHARACTER SET utf8mb4
|
||||
COLLATE utf8mb4_unicode_ci
|
||||
engine=innodb
|
||||
comment '解析产出QC审核记录'
|
||||
;
|
||||
|
||||
CREATE INDEX bid_qc_reviews_idx_bqr_project ON bid_qc_reviews(project_id);
|
||||
CREATE INDEX bid_qc_reviews_idx_bqr_type ON bid_qc_reviews(qc_type);
|
||||
|
||||
@ -39,6 +39,8 @@ BID_TOOLS = [
|
||||
parameters={"req_type": "类型(可选 structure/format/seal/copies/deadline)"}, category="analysis"),
|
||||
ToolDefinition(name="list_qualifications", description="查看招标所需资质及匹配状态(matched/missing/human_required)",
|
||||
parameters={"match_status": "匹配状态(可选)"}, category="analysis"),
|
||||
ToolDefinition(name="qc_report", description="查看解析产出 QC 契合度审核总览(评分项/资质/投标文件要求/章节骨架各类的最新得分与放行状态)",
|
||||
parameters={}, category="analysis"),
|
||||
|
||||
# ── 知识库 ──
|
||||
ToolDefinition(name="search_bid_kb", description="检索公司投标知识库(资质证书/业绩案例/模板/财务/人员)",
|
||||
@ -75,10 +77,14 @@ BID_TOOLS = [
|
||||
|
||||
BID_PROMPT = """你是「投标产线」的驾驶舱 agent,负责从上传招标文件到标书交付的全流程管理:
|
||||
创建投标项目 → 上传招标文件 → 解析(评分项/得分规则/资质/投标文件要求/章节骨架)
|
||||
→ 资质文档准备 → 分章节编写 → 章节评审 → 合成标书 → 整书评分 → 交付确认。
|
||||
→ 解析产出 QC 契合度审核 → 资质文档准备 → 分章节编写 → 章节评审 → 合成标书 → 整书评分 → 交付确认。
|
||||
|
||||
招标信息的采集/摘要/审批归商机产线,本产线从「上传招标文件」起步;用户要采集招标信息时说明去商机产线。
|
||||
|
||||
## 流转由机制驱动(重要)
|
||||
角色任务由系统状态对账器自动派发(每 15 秒一轮),你**不需要**手动为每个章节创建任务:
|
||||
- 解析产出须经 QC 契合度审核:评分标准/得分规则/资质/投标文件要求/章节骨架逐类对照招标文件原文
|
||||
按 10 分制打分,得分高于通过分(默认 9.5)才放行;不通过给改进意见 → 清空产出 → 解析重做 → 重审
|
||||
- 章节评审不达标 → 章节自动退回重写
|
||||
- 整书评分不达标 → 改进意见自动落到问题章节并退回重写 → 重新合成 → 重新评分
|
||||
你的职责是:立项与配人、推动上传招标文件、处理卡点与提问、回答评审/评分中的人类决策。
|
||||
@ -88,11 +94,13 @@ BID_PROMPT = """你是「投标产线」的驾驶舱 agent,负责从上传招
|
||||
以下人类任务 pending 时,系统不会给该项目派发任何 agent 任务:
|
||||
- 等待上传招标文件(tender_file_upload,上传后自动完成)
|
||||
- 等待人类提供资质文件(human_docs_request)
|
||||
遇到项目"不动了",先 diagnose_bid 看是不是卡在这两个门禁上,缺什么就催人补什么,不要硬推。
|
||||
- 解析产出 QC 连续不达标(qc_escalation,人工修正产出或在审核记录页强制放行)
|
||||
遇到项目"不动了",先 diagnose_bid 看是不是卡在门禁上,缺什么就催人补什么,不要硬推。
|
||||
|
||||
## 典型场景
|
||||
- 用户说「创建一个投标项目,叫 XXX」→ create_bid_project(name=XXX),随后提示上传招标文件。
|
||||
- 用户问「招标文件上传了吗 / 解析出来了吗」→ list_tender_files 看上传与抽取状态。
|
||||
- 用户问「解析质量怎么样 / QC 过了吗」→ qc_report,讲清哪类产出通过、哪类还在改。
|
||||
- 用户问进展 → diagnose_bid(含章节分布、在办任务、最近评分),不要只报"正在进行"。
|
||||
- 用户问「能得多少分 / 差在哪」→ bid_score_report + list_reviews,讲清失分项与改进意见落到了哪个章节。
|
||||
- 用户要补资质/案例 → add_kb_doc 录入公司投标知识库;知识库缺的用 request_human_docs 让人工提供。
|
||||
@ -298,6 +306,14 @@ async def _h_request_human_docs(sor, p, ctx):
|
||||
return _fmt(ok, msg)
|
||||
|
||||
|
||||
async def _h_qc_report(sor, p, ctx):
|
||||
pid, err = _need_project(ctx)
|
||||
if err:
|
||||
return err
|
||||
from .bid_qc_capability import qc_report
|
||||
return await qc_report(pid)
|
||||
|
||||
|
||||
async def _h_list_chapters(sor, p, ctx):
|
||||
pid, err = _need_project(ctx)
|
||||
if err:
|
||||
@ -364,6 +380,7 @@ BID_HANDLERS = {
|
||||
"list_scoring_items": _h_list_scoring_items,
|
||||
"list_doc_requirements": _h_list_doc_requirements,
|
||||
"list_qualifications": _h_list_qualifications,
|
||||
"qc_report": _h_qc_report,
|
||||
"search_bid_kb": _h_search_bid_kb,
|
||||
"list_kb_docs": _h_list_kb_docs,
|
||||
"add_kb_doc": _h_add_kb_doc,
|
||||
@ -407,6 +424,8 @@ def register_bid_ability():
|
||||
"type": "popup", "width": "88%", "height": "82%", "require_project": True},
|
||||
{"label": "📑 评分项", "icon": "", "url": "/pipeline-bidding/bid_scoring_items/index.ui",
|
||||
"type": "popup", "width": "88%", "height": "82%", "require_project": True},
|
||||
{"label": "🔍 解析QC审核", "icon": "", "url": "/pipeline-bidding/bid_qc_reviews/index.ui",
|
||||
"type": "popup", "width": "88%", "height": "82%", "require_project": True},
|
||||
{"label": "📝 标书章节", "icon": "", "url": "/pipeline-bidding/bid_chapters/index.ui",
|
||||
"type": "popup", "width": "88%", "height": "82%", "require_project": True},
|
||||
{"label": "🎯 资质", "icon": "", "url": "/pipeline-bidding/bid_qualifications/index.ui",
|
||||
|
||||
@ -22,13 +22,8 @@ CH_REVIEWING = "reviewing"
|
||||
CH_APPROVED = "approved"
|
||||
CH_REJECTED = "rejected"
|
||||
|
||||
# ── 招标信息状态 ──
|
||||
TD_NEW = "new"
|
||||
# ── 招标信息状态(bid_tenders 现仅承载解析抽取的商务要素,无审批流)──
|
||||
TD_SUMMARIZED = "summarized"
|
||||
TD_PENDING_APPROVAL = "pending_approval"
|
||||
TD_APPROVED = "approved"
|
||||
TD_REJECTED = "rejected"
|
||||
TD_ARCHIVED = "archived"
|
||||
|
||||
# ── 标书状态 ──
|
||||
DOC_DRAFT = "draft"
|
||||
@ -38,12 +33,27 @@ DOC_REJECTED = "rejected"
|
||||
DOC_DELIVERED = "delivered"
|
||||
|
||||
# ── 人类任务类型(阻塞门禁)──
|
||||
HT_TENDER_APPROVAL = "tender_approval"
|
||||
HT_TENDER_FILE = "tender_file_upload"
|
||||
HT_HUMAN_DOCS = "human_docs_request"
|
||||
HT_QC_ESCALATION = "qc_escalation"
|
||||
HT_DELIVERY_CONFIRM = "bid_delivery_confirm"
|
||||
|
||||
BLOCKING_HT_TYPES = (HT_TENDER_FILE, HT_HUMAN_DOCS)
|
||||
BLOCKING_HT_TYPES = (HT_TENDER_FILE, HT_HUMAN_DOCS, HT_QC_ESCALATION)
|
||||
|
||||
# ── QC 契合度审核类型(解析产出逐类审核)──
|
||||
QC_TYPE_SCORING = "scoring_items" # 评分项 + 得分规则
|
||||
QC_TYPE_QUALS = "qualifications" # 所需资质
|
||||
QC_TYPE_REQS = "doc_requirements" # 投标文件要求
|
||||
QC_TYPE_OUTLINE = "chapter_outline" # 章节骨架
|
||||
QC_TYPES = (QC_TYPE_SCORING, QC_TYPE_QUALS, QC_TYPE_REQS, QC_TYPE_OUTLINE)
|
||||
|
||||
# QC 审核对象 → 产出物落库表(QC 不通过时清空重做的对象;对账器判存在性也用)
|
||||
QC_OUTPUT_TABLE = {
|
||||
QC_TYPE_SCORING: "bid_scoring_items",
|
||||
QC_TYPE_QUALS: "bid_qualifications",
|
||||
QC_TYPE_REQS: "bid_doc_requirements",
|
||||
QC_TYPE_OUTLINE: "bid_chapters",
|
||||
}
|
||||
|
||||
# 任务未终结状态(判断「是否已有在办任务」用)
|
||||
OPEN_TASK_STATES = ("submitted", "running", "review", "qc_review")
|
||||
@ -55,6 +65,8 @@ DEFAULT_PARAMS = {
|
||||
"bid_pass_ratio": "0.85", # 整书评分通过门限
|
||||
"bid_max_revise": "3", # 单章节最大重做次数
|
||||
"bid_max_round": "3", # 整书最大评分轮次
|
||||
"bid_qc_pass_score": "9.5", # 解析产出 QC 契合度通过分(10 分制,高于此分才放行)
|
||||
"bid_qc_max_round": "3", # 单类产出 QC 最大审核轮次(超限抛人工)
|
||||
}
|
||||
|
||||
|
||||
@ -167,12 +179,14 @@ async def resolve_project_id(project_id="", **entity_ids):
|
||||
|
||||
|
||||
async def get_thresholds(sor):
|
||||
"""取四个门限参数(章节门限/整书门限/章节重做上限/整书轮次上限)。"""
|
||||
"""取门限参数(章节/整书/重做上限/轮次上限 + QC 契合度门限)。"""
|
||||
return {
|
||||
"chapter_pass_ratio": to_float(await get_param(sor, "bid_chapter_pass_ratio"), 0.8),
|
||||
"bid_pass_ratio": to_float(await get_param(sor, "bid_pass_ratio"), 0.85),
|
||||
"max_revise": to_int(await get_param(sor, "bid_max_revise"), 3),
|
||||
"max_round": to_int(await get_param(sor, "bid_max_round"), 3),
|
||||
"qc_pass_score": to_float(await get_param(sor, "bid_qc_pass_score"), 9.5),
|
||||
"qc_max_round": to_int(await get_param(sor, "bid_qc_max_round"), 3),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -23,7 +23,9 @@ from .bid_common import (
|
||||
create_human_task, get_project, PIPELINE_ID,
|
||||
CH_PENDING, CH_WRITING, CH_WRITTEN, CH_REJECTED, CH_APPROVED,
|
||||
DOC_DRAFT, DOC_REVIEWING, DOC_PASSED, DOC_REJECTED, DOC_DELIVERED,
|
||||
HT_TENDER_FILE, HT_HUMAN_DOCS, HT_DELIVERY_CONFIRM,
|
||||
HT_TENDER_FILE, HT_HUMAN_DOCS, HT_QC_ESCALATION, HT_DELIVERY_CONFIRM,
|
||||
QC_TYPE_SCORING, QC_TYPE_QUALS, QC_TYPE_REQS, QC_TYPE_OUTLINE, QC_TYPES,
|
||||
QC_OUTPUT_TABLE,
|
||||
)
|
||||
|
||||
logger = logging.getLogger("pipeline.bidding")
|
||||
@ -34,8 +36,9 @@ R_WRITER = "agent.bid_writer"
|
||||
R_REVIEWER = "agent.bid_reviewer"
|
||||
R_COMPOSITOR = "agent.bid_compositor"
|
||||
R_SCORER = "agent.bid_scorer"
|
||||
R_QC = "agent.qc"
|
||||
|
||||
MAX_ANALYST_ATTEMPT = 2 # 解析阶段最多重派次数(防死循环)
|
||||
MAX_ANALYST_ATTEMPT = 2 # 解析阶段首跑最多重派次数(防死循环;QC 退回的重做不计入)
|
||||
MAX_PREP_ATTEMPT = 2
|
||||
POLL_INTERVAL = 15
|
||||
|
||||
@ -75,6 +78,67 @@ async def _auto_close_file_upload_task(sor, project_id):
|
||||
return True
|
||||
|
||||
|
||||
# ══════════════ QC 门禁辅助 ══════════════
|
||||
|
||||
async def _qc_pending_types(sor, project_id):
|
||||
"""返回未放行的产出类型清单(按依赖序),每项 (qc_type, state, improvement, round):
|
||||
|
||||
state=unaudited:产出已存在但从未审核;state=rework:最近一轮审核未通过。
|
||||
已通过(或人工在记录页强制放行)的类型不再出现。
|
||||
"""
|
||||
pending = []
|
||||
for t in QC_TYPES:
|
||||
tbl = QC_OUTPUT_TABLE[t]
|
||||
n_out = await _count(sor, "SELECT COUNT(*) AS c FROM " + tbl +
|
||||
" WHERE project_id=${p}$", {"p": project_id})
|
||||
recs = await sor.sqlExe(
|
||||
"SELECT round, passed, improvement FROM bid_qc_reviews "
|
||||
"WHERE project_id=${p}$ AND qc_type=${t}$ ORDER BY round DESC LIMIT 1",
|
||||
{"p": project_id, "t": t})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
if not recs:
|
||||
if n_out > 0:
|
||||
pending.append((t, "unaudited", "", 0))
|
||||
continue
|
||||
r = rec_to_dict(recs[0])
|
||||
if str(r.get("passed")) == "1":
|
||||
continue
|
||||
rnd = to_int(r.get("round"), 1)
|
||||
if n_out > 0:
|
||||
pending.append((t, "rework", r.get("improvement") or "", rnd))
|
||||
else:
|
||||
# 产出已被清空待重做:round 信息带回,供对账器判断是否已超轮次上限
|
||||
pending.append((t, "awaiting_redo", r.get("improvement") or "", rnd))
|
||||
return pending
|
||||
|
||||
|
||||
async def _qc_collect_improvements(sor, project_id):
|
||||
"""汇总各类产出最新一轮的改进意见(解析重做任务的 params.qc_improvements)。"""
|
||||
items = []
|
||||
for t in QC_TYPES:
|
||||
recs = await sor.sqlExe(
|
||||
"SELECT round, passed, improvement FROM bid_qc_reviews "
|
||||
"WHERE project_id=${p}$ AND qc_type=${t}$ ORDER BY round DESC LIMIT 1",
|
||||
{"p": project_id, "t": t})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
if not recs:
|
||||
continue
|
||||
r = rec_to_dict(recs[0])
|
||||
if str(r.get("passed")) == "1" or not (r.get("improvement") or "").strip():
|
||||
continue
|
||||
items.append("[%s] 第%s轮:%s" % (t, r.get("round"), r["improvement"]))
|
||||
return items
|
||||
|
||||
|
||||
async def _redo_done_count(sor, project_id):
|
||||
"""已完成的 QC 退回重做任务数(解析重做轮次计数,防无限重派)。"""
|
||||
return await _count(
|
||||
sor, "SELECT COUNT(*) AS c FROM pipeline_tasks WHERE tenant_id=${p}$ "
|
||||
"AND pipeline_id='role_task' AND role=${r}$ AND state IN "
|
||||
"('approved','completed') AND params LIKE ${kw}$",
|
||||
{"p": project_id, "r": R_ANALYST, "kw": "%qc_redo%"})
|
||||
|
||||
|
||||
async def reconcile_project(sor, project_id, project_name=""):
|
||||
"""对单个投标项目做一次状态对账。返回动作说明列表(幂等,可反复调用)。"""
|
||||
acts = []
|
||||
@ -122,7 +186,7 @@ async def reconcile_project(sor, project_id, project_name=""):
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
return acts + ["created: 上传招标文件任务 %s" % hid]
|
||||
if await has_pending_blocking_human_task(sor, project_id):
|
||||
return ["blocked: 有待办人类任务(等招标文件/等人类资料),本轮不派发"]
|
||||
return ["blocked: 有待办人类任务(等招标文件/等人类资料/等QC人工介入),本轮不派发"]
|
||||
|
||||
th = await get_thresholds(sor)
|
||||
|
||||
@ -156,8 +220,39 @@ async def reconcile_project(sor, project_id, project_name=""):
|
||||
if need_analysis:
|
||||
if await count_open_tasks(sor, project_id, role=R_ANALYST) > 0:
|
||||
return acts + ["waiting: 招标文件解析任务在办"]
|
||||
# 携带 QC 改进意见的重做任务(qc_redo=1)不计入首跑重试上限
|
||||
n_redo_open = await _count(
|
||||
sor, "SELECT COUNT(*) AS c FROM pipeline_tasks WHERE tenant_id=${p}$ "
|
||||
"AND pipeline_id='role_task' AND role=${r}$ AND state IN "
|
||||
"('submitted','running','review','qc_review') AND params LIKE ${kw}$",
|
||||
{"p": project_id, "r": R_ANALYST, "kw": "%qc_redo%"})
|
||||
attempts = await _done_task_count(sor, project_id, R_ANALYST)
|
||||
if attempts >= MAX_ANALYST_ATTEMPT:
|
||||
n_redo_done = await _redo_done_count(sor, project_id)
|
||||
first_attempts = max(0, attempts - n_redo_done)
|
||||
qc_imp = await _qc_collect_improvements(sor, project_id)
|
||||
if qc_imp or n_redo_open > 0:
|
||||
# QC 退回重做:携带改进意见,不受首跑重试上限约束;但重做轮次有上限(防无限循环)
|
||||
if n_redo_done >= th["qc_max_round"]:
|
||||
ht = await find_human_task(sor, project_id, "general", status="pending")
|
||||
if not ht:
|
||||
await create_human_task(
|
||||
sor, project_id, "general",
|
||||
"解析重做轮次已用尽,需人工介入",
|
||||
("QC 退回后解析已重做 %d 轮,产出仍未通过契合度审核:\n%s\n\n"
|
||||
"请人工核对招标文件并修正产出物(评分项/资质/投标文件要求管理页),"
|
||||
"或在 QC 审核记录页把对应记录 passed 改为 1 强制放行。"
|
||||
% (n_redo_done, "\n".join(qc_imp)[:3000])),
|
||||
assignee_role="owner.superuser")
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
return acts + ["escalated: QC 重做轮次用尽 → 人工介入"]
|
||||
tid = await create_role_task(
|
||||
sor, project_id, R_ANALYST,
|
||||
"%s 招标文件解析重做(QC 退回,按改进意见修正)" % pname,
|
||||
{"stage": "analysis_redo", "task_kind": "bid_analysis", "qc_redo": 1,
|
||||
"qc_improvements": qc_imp})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
return acts + ["created: 解析重做任务 %s(QC 改进意见 %d 条)" % (tid, len(qc_imp))]
|
||||
if first_attempts >= MAX_ANALYST_ATTEMPT:
|
||||
ht = await find_human_task(sor, project_id, "general", status="pending")
|
||||
if not ht:
|
||||
await create_human_task(
|
||||
@ -165,7 +260,7 @@ async def reconcile_project(sor, project_id, project_name=""):
|
||||
("解析任务已完成 %d 次,但仍缺少:%s。\n"
|
||||
"请人工检查招标文件是否可读(bid_tender_files.content_text),"
|
||||
"或人工补录评分项/投标文件要求后完成本任务。"
|
||||
% (attempts,
|
||||
% (first_attempts,
|
||||
"、".join(x for x in [
|
||||
"评分项" if n_items == 0 else "",
|
||||
"投标文件章节结构" if n_struct == 0 else "",
|
||||
@ -181,6 +276,69 @@ async def reconcile_project(sor, project_id, project_name=""):
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
return acts + ["created: 解析任务 %s" % tid]
|
||||
|
||||
# ── A2. 解析产出 QC 门禁(评分标准/得分规则/资质/投标文件要求/章节骨架的契合度审核)──
|
||||
# 每类产出须经 agent.qc 对照招标文件原文按 10 分制打分,得分高于通过分(默认 9.5)才放行。
|
||||
qc_pending = await _qc_pending_types(sor, project_id)
|
||||
if qc_pending:
|
||||
# 轮次用尽守卫:最新审核已达上限仍未通过 → 不再空转派审核任务,
|
||||
# 确保有阻塞人工任务(逃逸阀:人工修产出物或在记录页强制放行后流程自恢复)。
|
||||
over = [(t, s, imp, rnd) for t, s, imp, rnd in qc_pending
|
||||
if s == "rework" and rnd >= th["qc_max_round"]]
|
||||
if over:
|
||||
ht = await find_human_task(sor, project_id, HT_QC_ESCALATION, status="pending")
|
||||
if not ht:
|
||||
await create_human_task(
|
||||
sor, project_id, HT_QC_ESCALATION,
|
||||
"解析产出 QC 连续不达标需人工介入",
|
||||
("以下产出物审核已达 %d 轮上限仍未达标:\n%s\n\n"
|
||||
"请人工核对招标文件并修正产出物,或在 QC 审核记录页(bid_qc_reviews)"
|
||||
"把对应记录 passed 改为 1 强制放行。\n处理完成本任务后流程自动恢复。"
|
||||
% (th["qc_max_round"],
|
||||
"\n".join("- %s(第%s轮):%s" % (t, rnd, (imp or "")[:300])
|
||||
for t, s, imp, rnd in over))),
|
||||
assignee_role="owner.superuser")
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
return acts + ["blocked: QC 审核轮次用尽(%s),等人工介入"
|
||||
% "/".join(t for t, _, _, _ in over)]
|
||||
# awaiting_redo:产出已被 QC 清空 → 先重派解析重做,不能对空产出审核
|
||||
needs_redo = [(t, s, imp, rnd) for t, s, imp, rnd in qc_pending if s == "awaiting_redo"]
|
||||
if needs_redo:
|
||||
if await count_open_tasks(sor, project_id, role=R_ANALYST) > 0:
|
||||
return acts + ["waiting: 解析重做任务在办(QC 退回产出已清空)"]
|
||||
n_redo_done = await _redo_done_count(sor, project_id)
|
||||
if n_redo_done >= th["qc_max_round"]:
|
||||
ht = await find_human_task(sor, project_id, "general", status="pending")
|
||||
if not ht:
|
||||
await create_human_task(
|
||||
sor, project_id, "general",
|
||||
"解析重做轮次已用尽,需人工介入",
|
||||
("QC 退回后解析已重做 %d 轮,产出仍未通过契合度审核。\n"
|
||||
"请人工核对招标文件并修正产出物,或在 QC 审核记录页强制放行。"
|
||||
% n_redo_done),
|
||||
assignee_role="owner.superuser")
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
return acts + ["escalated: QC 重做轮次用尽 → 人工介入"]
|
||||
qc_imp = await _qc_collect_improvements(sor, project_id)
|
||||
tid = await create_role_task(
|
||||
sor, project_id, R_ANALYST,
|
||||
"%s 招标文件解析重做(QC 退回产出已清空,按改进意见重抽)" % pname,
|
||||
{"stage": "analysis_redo", "task_kind": "bid_analysis", "qc_redo": 1,
|
||||
"qc_improvements": qc_imp})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
return acts + ["created: 解析重做任务 %s(产出被 QC 清空:%s)"
|
||||
% (tid, "/".join(t for t, _, _, _ in needs_redo))]
|
||||
needs_qc = [(t, s, imp, rnd) for t, s, imp, rnd in qc_pending]
|
||||
if await count_open_tasks(sor, project_id, role=R_QC) > 0:
|
||||
return acts + ["waiting: 解析产出 QC 审核任务在办"]
|
||||
types_str = "/".join(t for t, _, _, _ in needs_qc)
|
||||
tid = await create_role_task(
|
||||
sor, project_id, R_QC,
|
||||
"%s 解析产出契合度审核(%s)" % (pname, types_str),
|
||||
{"stage": "qc_analysis", "task_kind": "bid_qc",
|
||||
"qc_types": [t for t, _, _, _ in needs_qc]})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
return acts + ["created: QC 审核任务 %s(%s)" % (tid, types_str)]
|
||||
|
||||
# ── B. 资料准备(知识库资质匹配 + 人类文件清单)──
|
||||
prep_done = await has_done_task(sor, project_id, R_PREP)
|
||||
if n_quals > 0 and not prep_done:
|
||||
@ -343,7 +501,7 @@ async def diagnose_bid_project(project_id):
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
counts = {}
|
||||
for tbl in ("bid_tender_files", "bid_scoring_items", "bid_qualifications",
|
||||
"bid_doc_requirements", "bid_chapters"):
|
||||
"bid_doc_requirements", "bid_qc_reviews", "bid_chapters"):
|
||||
counts[tbl] = await _count(
|
||||
sor, "SELECT COUNT(*) AS c FROM " + tbl + " WHERE project_id=${p}$",
|
||||
{"p": project_id})
|
||||
|
||||
@ -1,307 +0,0 @@
|
||||
"""招标邮件采集:IMAP 拉取 → 解析 → 落库 bid_tenders。
|
||||
|
||||
凭据来自 bid_mail_accounts 表(password 用 appPublic.aes AES 加密存储,明文不入库、不写死代码)。
|
||||
阿里云企业邮:imap.qiye.aliyun.com:993 (SSL)。若开启「安全登录」,password 需填客户端专用密码。
|
||||
|
||||
IMAP 是阻塞库 → 统一放线程池执行(run_in_executor),不阻塞事件循环。
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import email
|
||||
import email.header
|
||||
import email.utils
|
||||
import imaplib
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from .bid_common import (get_db, new_id, rec_to_dict, to_int, TD_NEW)
|
||||
|
||||
logger = logging.getLogger("pipeline.bidding.mail")
|
||||
|
||||
# 默认招标关键词(bid_mail_accounts.filter_keywords 未配置时使用)
|
||||
DEFAULT_KEYWORDS = ["招标", "投标", "采购", "询价", "竞争性磋商", "邀标", "标书", "中标", "资格预审"]
|
||||
|
||||
MAX_BODY = 60000 # 单封邮件正文入库上限
|
||||
MAX_MAILS = 200 # 单次最多处理邮件数
|
||||
|
||||
|
||||
def _decode_header(raw):
|
||||
"""MIME 邮件头解码(中文主题/发件人)。"""
|
||||
if not raw:
|
||||
return ""
|
||||
try:
|
||||
parts = email.header.decode_header(raw)
|
||||
out = []
|
||||
for txt, enc in parts:
|
||||
if isinstance(txt, bytes):
|
||||
out.append(txt.decode(enc or "utf-8", errors="ignore"))
|
||||
else:
|
||||
out.append(txt)
|
||||
return "".join(out).strip()
|
||||
except Exception:
|
||||
return str(raw)
|
||||
|
||||
|
||||
def _body_text(msg):
|
||||
"""提取邮件正文纯文本(优先 text/plain,回退 text/html 去标签)。"""
|
||||
plain, html = "", ""
|
||||
if msg.is_multipart():
|
||||
for part in msg.walk():
|
||||
ctype = part.get_content_type()
|
||||
disp = str(part.get("Content-Disposition") or "")
|
||||
if "attachment" in disp:
|
||||
continue
|
||||
if ctype not in ("text/plain", "text/html"):
|
||||
continue
|
||||
try:
|
||||
payload = part.get_payload(decode=True) or b""
|
||||
charset = part.get_content_charset() or "utf-8"
|
||||
txt = payload.decode(charset, errors="ignore")
|
||||
except Exception:
|
||||
continue
|
||||
if ctype == "text/plain":
|
||||
plain += txt + "\n"
|
||||
else:
|
||||
html += txt + "\n"
|
||||
else:
|
||||
try:
|
||||
payload = msg.get_payload(decode=True) or b""
|
||||
charset = msg.get_content_charset() or "utf-8"
|
||||
txt = payload.decode(charset, errors="ignore")
|
||||
except Exception:
|
||||
txt = ""
|
||||
if (msg.get_content_type() or "") == "text/html":
|
||||
html = txt
|
||||
else:
|
||||
plain = txt
|
||||
body = plain.strip()
|
||||
if not body and html:
|
||||
body = re.sub(r"<(script|style)[^>]*>.*?</\1>", " ", html, flags=re.S | re.I)
|
||||
body = re.sub(r"<[^>]+>", " ", body)
|
||||
body = re.sub(r" ?", " ", body)
|
||||
body = re.sub(r"[ \t]+", " ", body)
|
||||
body = re.sub(r"\n{3,}", "\n\n", body)
|
||||
return body.strip()[:MAX_BODY]
|
||||
|
||||
|
||||
def _attachments(msg):
|
||||
"""附件清单 [{name, size, ctype}](只记录元信息,文件由人工上传到 bid_tender_files)。"""
|
||||
out = []
|
||||
if not msg.is_multipart():
|
||||
return out
|
||||
for part in msg.walk():
|
||||
disp = str(part.get("Content-Disposition") or "")
|
||||
fname = part.get_filename()
|
||||
if "attachment" not in disp and not fname:
|
||||
continue
|
||||
name = _decode_header(fname) if fname else ""
|
||||
try:
|
||||
size = len(part.get_payload(decode=True) or b"")
|
||||
except Exception:
|
||||
size = 0
|
||||
out.append({"name": name, "size": size, "ctype": part.get_content_type()})
|
||||
return out
|
||||
|
||||
|
||||
def _match_keywords(subject, body, keywords):
|
||||
text = (subject or "") + "\n" + (body or "")
|
||||
for k in keywords:
|
||||
k = (k or "").strip()
|
||||
if k and k in text:
|
||||
return k
|
||||
return ""
|
||||
|
||||
|
||||
def _fetch_sync(host, port, use_ssl, username, password, folder, days, keywords):
|
||||
"""同步 IMAP 拉取(在线程池中执行)。返回 (mails, error)。"""
|
||||
mails = []
|
||||
m = None
|
||||
try:
|
||||
if use_ssl:
|
||||
m = imaplib.IMAP4_SSL(host, port)
|
||||
else:
|
||||
m = imaplib.IMAP4(host, port)
|
||||
m.login(username, password)
|
||||
m.select(folder or "INBOX")
|
||||
since = (datetime.now() - timedelta(days=max(1, days))).strftime("%d-%b-%Y")
|
||||
typ, data = m.search(None, "(SINCE %s)" % since)
|
||||
if typ != "OK":
|
||||
return [], "IMAP search 失败: %s" % typ
|
||||
ids = (data[0] or b"").split()
|
||||
ids = ids[-MAX_MAILS:]
|
||||
for i in ids:
|
||||
typ, msg_data = m.fetch(i, "(RFC822)")
|
||||
if typ != "OK" or not msg_data:
|
||||
continue
|
||||
raw = None
|
||||
for chunk in msg_data:
|
||||
if isinstance(chunk, tuple) and len(chunk) > 1:
|
||||
raw = chunk[1]
|
||||
break
|
||||
if not raw:
|
||||
continue
|
||||
msg = email.message_from_bytes(raw)
|
||||
subject = _decode_header(msg.get("Subject"))
|
||||
body = _body_text(msg)
|
||||
hit = _match_keywords(subject, body, keywords)
|
||||
if not hit:
|
||||
continue
|
||||
recv = ""
|
||||
try:
|
||||
dt = email.utils.parsedate_to_datetime(msg.get("Date"))
|
||||
if dt:
|
||||
recv = dt.strftime("%Y-%m-%d %H:%M:%S")
|
||||
except Exception:
|
||||
recv = ""
|
||||
mails.append({
|
||||
"uid": (msg.get("Message-ID") or "").strip()[:64] or ("seq-" + i.decode()),
|
||||
"seq": i.decode(),
|
||||
"from": _decode_header(msg.get("From"))[:250],
|
||||
"subject": subject[:480],
|
||||
"received_at": recv,
|
||||
"body": body,
|
||||
"attachments": _attachments(msg),
|
||||
"keyword": hit,
|
||||
})
|
||||
return mails, ""
|
||||
except imaplib.IMAP4.error as e:
|
||||
return [], "IMAP 认证/协议错误: %s(阿里云企业邮开启安全登录后需使用客户端专用密码)" % str(e)[:200]
|
||||
except Exception as e:
|
||||
return [], "%s: %s" % (type(e).__name__, str(e)[:200])
|
||||
finally:
|
||||
if m is not None:
|
||||
try:
|
||||
m.logout()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
async def get_accounts(sor, org_id="", account_id=""):
|
||||
"""取启用的邮箱配置(解密密码)。"""
|
||||
sql = "SELECT * FROM bid_mail_accounts WHERE enabled='1'"
|
||||
p = {}
|
||||
if account_id:
|
||||
sql += " AND id=${aid}$"
|
||||
p["aid"] = account_id
|
||||
if org_id:
|
||||
sql += " AND org_id=${oid}$"
|
||||
p["oid"] = org_id
|
||||
recs = await sor.sqlExe(sql, p)
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
out = []
|
||||
for r in (recs or []):
|
||||
d = rec_to_dict(r)
|
||||
d["password_plain"] = _decrypt(d.get("password", ""))
|
||||
out.append(d)
|
||||
return out
|
||||
|
||||
|
||||
def _decrypt(enc):
|
||||
"""DB 密码解密(AES;明文兼容:解密失败按明文用)。"""
|
||||
if not enc:
|
||||
return ""
|
||||
try:
|
||||
from appPublic.jsonConfig import getConfig
|
||||
from appPublic.aes import aes_decode_b64
|
||||
key = getConfig().password_key
|
||||
if key:
|
||||
v = aes_decode_b64(key, enc)
|
||||
if v:
|
||||
return v
|
||||
except Exception:
|
||||
pass
|
||||
return enc
|
||||
|
||||
|
||||
def encrypt_password(plain):
|
||||
"""页面/脚本写入密码时调用(AES 加密),解密失败场景保持明文兼容。"""
|
||||
if not plain:
|
||||
return ""
|
||||
try:
|
||||
from appPublic.jsonConfig import getConfig
|
||||
from appPublic.aes import aes_encode_b64
|
||||
key = getConfig().password_key
|
||||
if key:
|
||||
return aes_encode_b64(key, plain)
|
||||
except Exception:
|
||||
pass
|
||||
return plain
|
||||
|
||||
|
||||
async def fetch_account_mails(account, days=None):
|
||||
"""按单个邮箱配置拉取邮件(线程池执行 IMAP)。返回 (mails, error)。"""
|
||||
kws = [k.strip() for k in (account.get("filter_keywords") or "").split(",") if k.strip()]
|
||||
if not kws:
|
||||
kws = DEFAULT_KEYWORDS
|
||||
d = to_int(days if days is not None else account.get("fetch_days"), 7)
|
||||
loop = asyncio.get_event_loop()
|
||||
return await loop.run_in_executor(
|
||||
None, _fetch_sync,
|
||||
account.get("host") or "imap.qiye.aliyun.com",
|
||||
to_int(account.get("port"), 993),
|
||||
str(account.get("use_ssl") or "1") == "1",
|
||||
account.get("username") or "",
|
||||
account.get("password_plain") or "",
|
||||
account.get("folder") or "INBOX",
|
||||
d, kws)
|
||||
|
||||
|
||||
async def fetch_tenders_to_db(org_id="", account_id="", days=None):
|
||||
"""采集入口:拉邮件 → 按 mail_uid 去重 → 落库 bid_tenders(status=new)。
|
||||
|
||||
返回 (ok, message)。message 含每个邮箱的新增/跳过数与错误原因(诚实报错,不静默)。
|
||||
"""
|
||||
db, dbname = get_db()
|
||||
lines = []
|
||||
total_new = 0
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
accounts = await get_accounts(sor, org_id=org_id, account_id=account_id)
|
||||
if not accounts:
|
||||
return False, ("未配置启用的招标邮箱(bid_mail_accounts)。"
|
||||
"请在「招标邮箱配置」页面添加:host=imap.qiye.aliyun.com port=993 "
|
||||
"username=邮箱地址 password=客户端专用密码")
|
||||
for acc in accounts:
|
||||
mails, err = await fetch_account_mails(acc, days=days)
|
||||
if err:
|
||||
lines.append("[%s] 采集失败: %s" % (acc.get("account_name") or acc.get("username"), err))
|
||||
continue
|
||||
new_n, skip_n = 0, 0
|
||||
for mail in mails:
|
||||
exist = await sor.sqlExe(
|
||||
"SELECT id FROM bid_tenders WHERE mail_uid=${uid}$ AND account_id=${aid}$ LIMIT 1",
|
||||
{"uid": mail["uid"], "aid": acc.get("id") or ""})
|
||||
if exist:
|
||||
skip_n += 1
|
||||
continue
|
||||
data = {
|
||||
"id": new_id(),
|
||||
"org_id": acc.get("org_id") or "0",
|
||||
"source": "mail",
|
||||
"account_id": acc.get("id") or "",
|
||||
"mail_uid": mail["uid"],
|
||||
"mail_from": mail["from"],
|
||||
"mail_subject": mail["subject"],
|
||||
"raw_content": mail["body"],
|
||||
"attach_json": json.dumps(mail["attachments"], ensure_ascii=False),
|
||||
"title": mail["subject"][:250],
|
||||
"status": TD_NEW,
|
||||
"created_by": "agent.tender_scout",
|
||||
}
|
||||
if mail.get("received_at"):
|
||||
data["received_at"] = mail["received_at"]
|
||||
await sor.C("bid_tenders", data)
|
||||
new_n += 1
|
||||
await sor.sqlExe(
|
||||
"UPDATE bid_mail_accounts SET last_fetch_at=NOW(), updated_at=NOW() "
|
||||
"WHERE id=${aid}$", {"aid": acc.get("id") or ""})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
total_new += new_n
|
||||
lines.append("[%s] 命中招标邮件 %d 封,新增 %d,已存在跳过 %d"
|
||||
% (acc.get("account_name") or acc.get("username"),
|
||||
len(mails), new_n, skip_n))
|
||||
msg = "\n".join(lines)
|
||||
if total_new == 0 and all("采集失败" in x for x in lines):
|
||||
return False, msg
|
||||
return True, msg
|
||||
270
pipeline_bidding/bid_qc_capability.py
Normal file
270
pipeline_bidding/bid_qc_capability.py
Normal file
@ -0,0 +1,270 @@
|
||||
"""解析产出 QC 契合度审核能力(agent.qc)。
|
||||
|
||||
对应业务:招标文件解析的每类产出物(评分项+得分规则 / 资质 / 投标文件要求 / 章节骨架)
|
||||
必须由 QC 对照招标文件原文逐项核对契合度,按 10 分制评分;**得分高于通过分(默认 9.5)才算完成**,
|
||||
否则给出改进意见 → 清空该产出物 → 对账器重派解析任务带意见重做 → 重审。超轮次上限抛人工。
|
||||
|
||||
机制分工(与产线既有约定一致):
|
||||
- 状态流转归机制:bid_flow 对账器派 QC 任务、判门禁、清空产出物触发重做;
|
||||
- 本模块只固化操作原语:开轮(start_qc)/ 落分(finish_qc)/ 查询(qc_report、list_qc)。
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
from .bid_common import (
|
||||
get_db, new_id, rec_to_dict, rows_to_dicts, to_float, to_int,
|
||||
record, create_human_task, find_human_task, get_thresholds, resolve_project_id,
|
||||
QC_TYPE_SCORING, QC_TYPE_QUALS, QC_TYPE_REQS, QC_TYPE_OUTLINE, QC_TYPES,
|
||||
HT_QC_ESCALATION,
|
||||
)
|
||||
|
||||
logger = logging.getLogger("pipeline.bidding.qc")
|
||||
|
||||
# 每类产出的审核清单(start_qc 时随评审记录返回,指导 LLM 对照原文核对)
|
||||
QC_DIMENSIONS = {
|
||||
QC_TYPE_SCORING: (
|
||||
"评分项+得分规则契合度审核(对照招标文件评标办法原文):\n"
|
||||
"1. 完整性:评标办法中的每个评分项是否全部录入,无漏项、无编造项;\n"
|
||||
"2. 满分准确:每项满分与原文一致(商务/技术/价格分合计核对);\n"
|
||||
"3. 得分规则:档位/扣分/加分细则与原文一致,无概括失真;\n"
|
||||
"4. 否决项:废标/否决条款全部标 is_veto=1,无漏标;\n"
|
||||
"5. 归属正确:业务/技术/价格归类与出处(source_ref)可回溯原文。"
|
||||
),
|
||||
QC_TYPE_QUALS: (
|
||||
"所需资质契合度审核(对照招标文件投标人资格要求原文):\n"
|
||||
"1. 完整性:资格要求中的每项资质/证书/业绩门槛全部录入,无漏项;\n"
|
||||
"2. 原文一致:要求描述与原文一致(等级/有效期/联合体口径);\n"
|
||||
"3. 必需性:强制性条款标 is_mandatory=1,加分性资质不得误标必需;\n"
|
||||
"4. 无编造:招标文件未要求的资质不得出现。"
|
||||
),
|
||||
QC_TYPE_REQS: (
|
||||
"投标文件要求契合度审核(对照招标文件投标文件格式/要求原文):\n"
|
||||
"1. 章节结构(structure)完整且顺序与原文一致,章节号/标题准确;\n"
|
||||
"2. 格式/密封/份数/截止时间等要求(format/seal/copies/deadline)无遗漏;\n"
|
||||
"3. 页数限制与原文一致;\n"
|
||||
"4. 出处(source_ref)可回溯。"
|
||||
),
|
||||
QC_TYPE_OUTLINE: (
|
||||
"章节骨架契合度审核(对照投标文件要求与评分项):\n"
|
||||
"1. 章节与 req_type=structure 的要求一一对应,无缺章、无多余章;\n"
|
||||
"2. 章节号/标题/顺序与投标文件要求一致;\n"
|
||||
"3. 评分项挂载正确:每个评分项能落到对应章节(target_chapter_no/名称匹配),无错挂漏挂;\n"
|
||||
"4. 章节要点来自评分标准原文枚举的子维度,不是通用目录。"
|
||||
),
|
||||
}
|
||||
|
||||
# 每类产出对应的落库表(QC 不通过时清空重做的对象)
|
||||
QC_OUTPUT_TABLE = {
|
||||
QC_TYPE_SCORING: "bid_scoring_items",
|
||||
QC_TYPE_QUALS: "bid_qualifications",
|
||||
QC_TYPE_REQS: "bid_doc_requirements",
|
||||
QC_TYPE_OUTLINE: "bid_chapters",
|
||||
}
|
||||
|
||||
|
||||
async def _qc_type_valid(sor, qc_type):
|
||||
if qc_type in QC_TYPES:
|
||||
return qc_type, ""
|
||||
return "", "qc_type 非法(应为 %s)" % "/".join(QC_TYPES)
|
||||
|
||||
|
||||
async def list_qc(project_id, qc_type="", limit=50):
|
||||
"""列出项目 QC 审核记录。"""
|
||||
try:
|
||||
project_id = await resolve_project_id(project_id)
|
||||
except ValueError:
|
||||
return []
|
||||
db, dbname = get_db()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
sql = ("SELECT id, qc_type, round, reviewer, fit_score, pass_score, passed, "
|
||||
"LEFT(improvement, 500) AS improvement, task_id, created_at "
|
||||
"FROM bid_qc_reviews WHERE project_id=${pid}$")
|
||||
p = {"pid": project_id}
|
||||
if qc_type:
|
||||
sql += " AND qc_type=${qt}$"
|
||||
p["qt"] = qc_type
|
||||
sql += " ORDER BY qc_type, round DESC, created_at DESC LIMIT " + str(to_int(limit, 50))
|
||||
recs = await sor.sqlExe(sql, p)
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
return rows_to_dicts(recs)
|
||||
|
||||
|
||||
async def start_qc(qc_type, project_id="", who=None, agent_id=None, task_id=""):
|
||||
"""开启一轮 QC 审核:创建/复用当轮评审记录,返回审核清单 + 该产出物的待核内容。
|
||||
|
||||
LLM 随后读招标文件原文(list_tender_files/read_tender_file)+ 查产出物表,
|
||||
对照审核清单逐项核对,得出 0~10 契合度分,调 finish_qc 落分。
|
||||
"""
|
||||
qc_type, err = await _qc_type_valid(None, qc_type)
|
||||
if err:
|
||||
return False, err
|
||||
try:
|
||||
project_id = await resolve_project_id(project_id)
|
||||
except ValueError as e:
|
||||
return False, str(e)
|
||||
db, dbname = get_db()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
th = await get_thresholds(sor)
|
||||
# 轮次 = 该类型已有审核数 + 1;同轮未落分的记录复用(幂等)
|
||||
rn = await sor.sqlExe(
|
||||
"SELECT COUNT(*) AS c FROM bid_qc_reviews WHERE project_id=${p}$ AND qc_type=${t}$",
|
||||
{"p": project_id, "t": qc_type})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
rnd = to_int(getattr(rn[0], "c", 0) if rn else 0) + 1
|
||||
open_r = await sor.sqlExe(
|
||||
"SELECT id, round FROM bid_qc_reviews WHERE project_id=${p}$ AND qc_type=${t}$ "
|
||||
"AND fit_score IS NULL ORDER BY created_at DESC LIMIT 1",
|
||||
{"p": project_id, "t": qc_type})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
if open_r:
|
||||
rid = getattr(open_r[0], "id", "")
|
||||
rnd = to_int(getattr(open_r[0], "round", 1), 1)
|
||||
else:
|
||||
rid = new_id()
|
||||
await sor.C("bid_qc_reviews", {
|
||||
"id": rid, "project_id": project_id, "qc_type": qc_type, "round": rnd,
|
||||
"reviewer": (who or "agent.qc")[:60], "review_type": "agent",
|
||||
"pass_score": th["qc_pass_score"], "passed": "0",
|
||||
"task_id": task_id or "",
|
||||
})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
|
||||
# 待核产出物(截断防上下文爆炸)
|
||||
out_rows, note = [], ""
|
||||
tbl = QC_OUTPUT_TABLE[qc_type]
|
||||
if qc_type == QC_TYPE_OUTLINE:
|
||||
recs = await sor.sqlExe(
|
||||
"SELECT chapter_no, title, scoring_item_ids, max_score, LEFT(outline, 300) AS outline "
|
||||
"FROM bid_chapters WHERE project_id=${p}$ ORDER BY order_no, chapter_no LIMIT 200",
|
||||
{"p": project_id})
|
||||
elif qc_type == QC_TYPE_SCORING:
|
||||
recs = await sor.sqlExe(
|
||||
"SELECT section, item_no, item_name, max_score, is_veto, target_chapter_no, "
|
||||
"LEFT(scoring_rule, 400) AS scoring_rule, source_ref "
|
||||
"FROM bid_scoring_items WHERE project_id=${p}$ ORDER BY section, order_no LIMIT 300",
|
||||
{"p": project_id})
|
||||
elif qc_type == QC_TYPE_QUALS:
|
||||
recs = await sor.sqlExe(
|
||||
"SELECT qual_name, is_mandatory, LEFT(requirement, 400) AS requirement "
|
||||
"FROM bid_qualifications WHERE project_id=${p}$ ORDER BY is_mandatory DESC LIMIT 300",
|
||||
{"p": project_id})
|
||||
else:
|
||||
recs = await sor.sqlExe(
|
||||
"SELECT req_type, chapter_no, chapter_title, LEFT(requirement, 400) AS requirement, "
|
||||
"page_limit, source_ref FROM bid_doc_requirements WHERE project_id=${p}$ "
|
||||
"ORDER BY order_no LIMIT 300", {"p": project_id})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
out_rows = rows_to_dicts(recs, limit=300)
|
||||
if not out_rows:
|
||||
note = "该类产出物为空,无法审核(应等解析任务产出后再开审)。"
|
||||
return True, json.dumps({
|
||||
"review_id": rid, "qc_type": qc_type, "round": rnd,
|
||||
"pass_score": th["qc_pass_score"], "max_round": th["qc_max_round"],
|
||||
"checklist": QC_DIMENSIONS[qc_type],
|
||||
"outputs_table": tbl, "outputs": out_rows, "note": note,
|
||||
"instruction": ("请读招标文件原文(list_tender_files → read_tender_file 分段)并逐条对照上述清单核对,"
|
||||
"得出 0~10 的契合度分(可带一位小数),然后调 finish_qc 落分。"
|
||||
"不通过(得分未高于通过分)时 improvement 必须写清:漏了什么/错在哪/对照原文哪一处,"
|
||||
"否则工具拒绝提交。"),
|
||||
}, ensure_ascii=False, default=str)
|
||||
|
||||
|
||||
async def finish_qc(review_id, fit_score, improvement="", comments="",
|
||||
who=None, agent_id=None, task_id=""):
|
||||
"""落分并判定:得分 > 通过分 → 该类产出放行;否则清空产出物 + 触发解析重做。
|
||||
|
||||
fit_score:0~10(10 分制契合度分)。
|
||||
"""
|
||||
if not review_id:
|
||||
return False, "缺少 review_id"
|
||||
db, dbname = get_db()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
rv = await sor.sqlExe("SELECT * FROM bid_qc_reviews WHERE id=${r}$", {"r": review_id})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
if not rv:
|
||||
return False, "QC 评审记录不存在: %s" % review_id
|
||||
review = rec_to_dict(rv[0])
|
||||
pid = review.get("project_id")
|
||||
qc_type = review.get("qc_type")
|
||||
th = await get_thresholds(sor)
|
||||
sc = to_float(fit_score, -1.0)
|
||||
if sc < 0 or sc > 10:
|
||||
return False, "fit_score 必须为 0~10 的数字"
|
||||
passed = sc > th["qc_pass_score"] # 严格高于通过分才算完成(9.5 → 需 >9.5)
|
||||
if not passed and not (improvement or "").strip():
|
||||
return False, ("QC 不通过(%.2f 未高于 %.2f)必须给出 improvement:\n"
|
||||
"逐项写清缺了什么、错在哪、对照招标文件原文哪一处,解析角色据此重做。"
|
||||
% (sc, th["qc_pass_score"]))
|
||||
|
||||
summary_lines = ["契合度 %.2f/10(通过线 >%.2f)→ %s"
|
||||
% (sc, th["qc_pass_score"], "通过" if passed else "不通过")]
|
||||
if comments:
|
||||
summary_lines.append("审核说明:" + comments)
|
||||
if improvement:
|
||||
summary_lines.append("改进意见:" + improvement)
|
||||
await sor.sqlExe(
|
||||
"UPDATE bid_qc_reviews SET fit_score=${sc}$, passed=${ps}$, improvement=${imp}$, "
|
||||
"comments=${cm}$, updated_at=NOW() WHERE id=${r}$",
|
||||
{"sc": sc, "ps": "1" if passed else "0", "imp": improvement or "",
|
||||
"cm": "\n".join(summary_lines)[:60000], "r": review_id})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
await record(sor, pid, "bid_qc_reviews", review_id, "qc_" + ("pass" if passed else "reject"),
|
||||
to_state="passed" if passed else "rejected", who=who, agent_id=agent_id,
|
||||
detail="%s round=%s %.2f" % (qc_type, review.get("round"), sc))
|
||||
|
||||
if passed:
|
||||
return True, ("QC 通过:%s 契合度 %.2f > %.2f,该类产出放行。"
|
||||
% (qc_type, sc, th["qc_pass_score"]))
|
||||
|
||||
# ── 不通过:轮次门禁 ──
|
||||
if to_int(review.get("round"), 1) >= th["qc_max_round"]:
|
||||
exist = await find_human_task(sor, pid, HT_QC_ESCALATION, status="pending")
|
||||
if not exist:
|
||||
await create_human_task(
|
||||
sor, pid, HT_QC_ESCALATION,
|
||||
"解析产出 QC 连续不达标需人工介入(%s)" % qc_type,
|
||||
("「%s」已审核 %s 轮,最近契合度 %.2f/10(通过线 >%.2f),仍未达标:\n%s\n\n"
|
||||
"请人工处理:核对招标文件修正产出物(可在对应管理页直接编辑),\n"
|
||||
"或在 QC 审核记录页(bid_qc_reviews)把该记录 passed 改为 1 强制放行。\n"
|
||||
"处理完成本任务后流程自动恢复。"
|
||||
% (qc_type, review.get("round"), sc, th["qc_pass_score"],
|
||||
(improvement or "")[:3000])),
|
||||
assignee_role="owner.superuser")
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
return True, ("QC 不通过且已达审核轮次上限 %d → 已抛人工介入(阻塞门禁)。"
|
||||
% th["qc_max_round"])
|
||||
|
||||
# ── 清空该类产出物(章节骨架退回时章节表一并清空),触发解析重做 ──
|
||||
tbl = QC_OUTPUT_TABLE.get(qc_type or "", "")
|
||||
if not tbl:
|
||||
return False, "QC 记录类型非法: %s" % qc_type
|
||||
await sor.sqlExe("DELETE FROM " + tbl + " WHERE project_id=${p}$", {"p": pid})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
cleared = tbl if qc_type != QC_TYPE_OUTLINE else "bid_chapters(章节骨架)"
|
||||
return True, ("QC 不通过:%s 契合度 %.2f 未高于 %.2f。已清空该类产出物(%s),"
|
||||
"系统将在下一轮对账重派解析任务(携带改进意见)重做后重审。"
|
||||
% (qc_type, sc, th["qc_pass_score"], cleared))
|
||||
|
||||
|
||||
async def qc_report(project_id):
|
||||
"""项目 QC 总览:四类产出的最新审核状态 + 未通过时的改进意见。"""
|
||||
try:
|
||||
project_id = await resolve_project_id(project_id)
|
||||
except ValueError as e:
|
||||
return str(e)
|
||||
db, dbname = get_db()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
th = await get_thresholds(sor)
|
||||
out = {}
|
||||
for t in QC_TYPES:
|
||||
recs = await sor.sqlExe(
|
||||
"SELECT round, fit_score, passed, LEFT(improvement, 600) AS improvement, created_at "
|
||||
"FROM bid_qc_reviews WHERE project_id=${p}$ AND qc_type=${t}$ "
|
||||
"ORDER BY round DESC LIMIT 1", {"p": project_id, "t": t})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
out[t] = rec_to_dict(recs[0]) if recs else {"round": 0, "note": "尚未审核"}
|
||||
return json.dumps({
|
||||
"project_id": project_id, "pass_score": th["qc_pass_score"],
|
||||
"max_round": th["qc_max_round"], "latest": out,
|
||||
}, ensure_ascii=False, default=str)
|
||||
@ -1,9 +1,10 @@
|
||||
"""招标信息能力:采集 / 摘要要素抽取 / 审批 / 存档作废 / 立项 / 人员 / 等待招标文件。
|
||||
"""投标项目能力:立项 / 参与人员 / 等待上传招标文件。
|
||||
|
||||
对应业务:从邮箱获取招标信息 → 内容摘要(投标开始时间/项目周期/招标金额…)→ 走审批流程
|
||||
→ 不通过则存档作废;通过则创建项目、设定参与人员、等待上传招标文件。
|
||||
产线起点是「上传招标文件」:招标信息的采集/摘要/审批归商机产线,本产线不再包含。
|
||||
招标商务要素(项目名称/招标人/截止时间/预算等)由招标文件解析阶段从文件中抽取,
|
||||
落 bid_tenders(source=tender_doc),见 bid_analysis_capability._upsert_tender_facts。
|
||||
|
||||
状态机规范见技能 `tender`;本模块只固化操作原语(CAS 迁移 + 审计),合法性由 LLM 读技能判断。
|
||||
状态机规范见技能 `bid-workflow`;本模块只固化操作原语(CAS 迁移 + 审计),合法性由 LLM 读技能判断。
|
||||
"""
|
||||
|
||||
import json
|
||||
@ -11,333 +12,21 @@ import logging
|
||||
|
||||
from .bid_common import (
|
||||
get_db, new_id, rec_to_dict, rows_to_dicts, json_loads, to_float, to_int,
|
||||
llm_json, record, create_human_task, find_human_task, get_project,
|
||||
PIPELINE_ID, TD_NEW, TD_SUMMARIZED, TD_PENDING_APPROVAL, TD_APPROVED,
|
||||
TD_REJECTED, TD_ARCHIVED, HT_TENDER_APPROVAL, HT_TENDER_FILE,
|
||||
record, create_human_task, find_human_task, get_project,
|
||||
PIPELINE_ID, HT_TENDER_FILE,
|
||||
)
|
||||
|
||||
logger = logging.getLogger("pipeline.bidding.tender")
|
||||
|
||||
TABLE = "bid_tenders"
|
||||
|
||||
SUMMARY_PROMPT = """你是招标信息分析员。下面是一封招标邮件的主题与正文,请抽取结构化要素。
|
||||
|
||||
【邮件主题】
|
||||
__SUBJECT__
|
||||
|
||||
【邮件正文】
|
||||
__BODY__
|
||||
|
||||
严格只输出一个 JSON 对象,字段如下(缺失或正文未提及的填空字符串,不要编造):
|
||||
{
|
||||
"title": "项目名称",
|
||||
"purchaser": "招标人/采购人单位名称",
|
||||
"tender_no": "招标编号",
|
||||
"industry": "所属行业",
|
||||
"summary": "内容摘要,200字以内,说明采购标的、范围、主要要求",
|
||||
"bid_start_at": "投标开始/获取招标文件时间 YYYY-MM-DD",
|
||||
"bid_deadline": "投标截止时间 YYYY-MM-DD",
|
||||
"project_period": "项目周期,如 12个月 / 180日历天",
|
||||
"budget_amount": "招标金额(仅数字,人民币元)",
|
||||
"deposit_amount": "投标保证金(仅数字,人民币元)",
|
||||
"qualification_req": "投标人资格/资质要求原文摘要",
|
||||
"key_dates": [{"name": "节点名称", "date": "YYYY-MM-DD"}],
|
||||
"risk_note": "风险提示:时间紧、资质缺口、否决项等;无则空"
|
||||
}
|
||||
注意:金额只填数字(如 1200000),不带单位和千分位;日期只填 YYYY-MM-DD;无法确定一律留空。"""
|
||||
|
||||
|
||||
async def _get_tender(sor, tender_id):
|
||||
recs = await sor.sqlExe("SELECT * FROM " + TABLE + " WHERE id=${tid}$", {"tid": tender_id})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
return rec_to_dict(recs[0]) if recs else {}
|
||||
|
||||
|
||||
async def _set_status(sor, tender_id, from_status, to_status, action,
|
||||
who=None, agent_id=None, detail=None, extra=None):
|
||||
"""CAS 状态迁移(from_status 为 None 时不校验来源状态)+ 审计。"""
|
||||
data = dict(extra or {})
|
||||
data["status"] = to_status
|
||||
sets = ", ".join("%s=${%s}$" % (k, k) for k in data)
|
||||
p = dict(data)
|
||||
p["tid"] = tender_id
|
||||
sql = "UPDATE " + TABLE + " SET " + sets + ", updated_at=NOW() WHERE id=${tid}$"
|
||||
if from_status:
|
||||
sql += " AND status=${_from}$"
|
||||
p["_from"] = from_status
|
||||
await sor.sqlExe(sql, p)
|
||||
cur = await _get_tender(sor, tender_id)
|
||||
if not cur:
|
||||
return False, "招标信息不存在: %s" % tender_id
|
||||
if cur.get("status") != to_status:
|
||||
return False, ("状态迁移失败(CAS): 期望 from=%s 实际 status=%s"
|
||||
% (from_status, cur.get("status")))
|
||||
await record(sor, cur.get("project_id") or tender_id, TABLE, tender_id, action,
|
||||
from_state=from_status, to_state=to_status, who=who,
|
||||
agent_id=agent_id, detail=detail)
|
||||
return True, to_status
|
||||
|
||||
|
||||
# ══════════════ 采集 ══════════════
|
||||
|
||||
async def fetch_tenders(org_id="", account_id="", days=None, who=None, agent_id=None):
|
||||
"""从配置的招标邮箱采集招标信息入库(去重)。返回 (ok, message)。"""
|
||||
from .bid_mail import fetch_tenders_to_db
|
||||
return await fetch_tenders_to_db(org_id=org_id, account_id=account_id, days=days)
|
||||
|
||||
|
||||
async def import_tender(title, content, purchaser="", tender_no="", org_id="0",
|
||||
who=None, agent_id=None, created_by=""):
|
||||
"""手工导入招标信息(邮箱不可用时的备用入口)。返回 (ok, tender_id)。"""
|
||||
if not title or not str(title).strip():
|
||||
return False, "缺少 title"
|
||||
db, dbname = get_db()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
tid = new_id()
|
||||
await sor.C(TABLE, {
|
||||
"id": tid, "org_id": org_id or "0", "source": "manual",
|
||||
"title": str(title).strip()[:250], "mail_subject": str(title).strip()[:480],
|
||||
"raw_content": content or "", "purchaser": purchaser or "",
|
||||
"tender_no": tender_no or "", "status": TD_NEW,
|
||||
"created_by": created_by or (who or ""),
|
||||
})
|
||||
await record(sor, tid, TABLE, tid, "import", to_state=TD_NEW,
|
||||
who=who, agent_id=agent_id)
|
||||
return True, tid
|
||||
|
||||
|
||||
async def list_tenders(status="", org_id="", limit=50):
|
||||
"""列出招标信息。"""
|
||||
db, dbname = get_db()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
sql = ("SELECT id, title, purchaser, tender_no, status, bid_start_at, bid_deadline, "
|
||||
"project_period, budget_amount, project_id, received_at "
|
||||
"FROM " + TABLE + " WHERE 1=1")
|
||||
p = {}
|
||||
if status:
|
||||
sql += " AND status=${st}$"
|
||||
p["st"] = status
|
||||
if org_id:
|
||||
sql += " AND org_id=${oid}$"
|
||||
p["oid"] = org_id
|
||||
sql += " ORDER BY received_at DESC, created_at DESC LIMIT " + str(to_int(limit, 50))
|
||||
recs = await sor.sqlExe(sql, p)
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
return rows_to_dicts(recs)
|
||||
|
||||
|
||||
async def tender_detail(tender_id):
|
||||
"""招标信息详情(含正文,供分析用)。"""
|
||||
db, dbname = get_db()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
d = await _get_tender(sor, tender_id)
|
||||
if not d:
|
||||
return "招标信息不存在: %s" % tender_id
|
||||
body = d.get("raw_content") or ""
|
||||
d["raw_content"] = body[:8000]
|
||||
return json.dumps(d, ensure_ascii=False, default=str)
|
||||
|
||||
|
||||
# ══════════════ 摘要与要素抽取 ══════════════
|
||||
|
||||
async def summarize_tender(tender_id, model_name="", who=None, agent_id=None):
|
||||
"""LLM 抽取摘要 + 投标开始时间/项目周期/招标金额等要素,落库并置 summarized。"""
|
||||
db, dbname = get_db()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
t = await _get_tender(sor, tender_id)
|
||||
if not t:
|
||||
return False, "招标信息不存在: %s" % tender_id
|
||||
body = (t.get("raw_content") or "")[:20000]
|
||||
if not body.strip():
|
||||
return False, "招标信息正文为空,无法摘要(请先补充 raw_content 或上传招标文件)"
|
||||
prompt = (SUMMARY_PROMPT
|
||||
.replace("__SUBJECT__", (t.get("mail_subject") or t.get("title") or "")[:500])
|
||||
.replace("__BODY__", body))
|
||||
data, err = await llm_json(prompt, model_name=model_name, retries=1)
|
||||
if data is None:
|
||||
return False, "LLM 摘要失败: %s" % err
|
||||
extra = {}
|
||||
for k in ("title", "purchaser", "tender_no", "industry", "summary",
|
||||
"project_period", "qualification_req", "risk_note"):
|
||||
v = data.get(k)
|
||||
if v:
|
||||
extra[k] = str(v)[:2000] if k in ("summary", "qualification_req", "risk_note") else str(v)[:250]
|
||||
for k in ("bid_start_at", "bid_deadline"):
|
||||
v = (data.get(k) or "").strip()
|
||||
if v and len(v) >= 8:
|
||||
extra[k] = v[:10]
|
||||
for k in ("budget_amount", "deposit_amount"):
|
||||
v = data.get(k)
|
||||
if v not in (None, "", "无"):
|
||||
amt = to_float(str(v).replace(",", "").replace("元", "").strip(), 0.0)
|
||||
if amt > 0:
|
||||
extra[k] = amt
|
||||
kd = data.get("key_dates")
|
||||
if kd:
|
||||
extra["key_dates"] = json.dumps(kd, ensure_ascii=False)[:4000]
|
||||
|
||||
db, dbname = get_db()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
ok, msg = await _set_status(sor, tender_id, None, TD_SUMMARIZED, "summarize",
|
||||
who=who, agent_id=agent_id, extra=extra)
|
||||
if not ok:
|
||||
return False, msg
|
||||
return True, ("摘要完成: %s | 投标开始=%s 截止=%s 周期=%s 金额=%s"
|
||||
% (extra.get("title", t.get("title", "")),
|
||||
extra.get("bid_start_at", ""), extra.get("bid_deadline", ""),
|
||||
extra.get("project_period", ""), extra.get("budget_amount", "")))
|
||||
|
||||
|
||||
async def set_tender_fields(tender_id, title="", purchaser="", tender_no="",
|
||||
bid_start_at="", bid_deadline="", project_period="",
|
||||
budget_amount="", deposit_amount="", summary="",
|
||||
qualification_req="", who=None, agent_id=None):
|
||||
"""人工/agent 修正招标要素(LLM 抽错时用)。"""
|
||||
vals = {"title": title, "purchaser": purchaser, "tender_no": tender_no,
|
||||
"bid_start_at": bid_start_at, "bid_deadline": bid_deadline,
|
||||
"project_period": project_period, "summary": summary,
|
||||
"qualification_req": qualification_req}
|
||||
data = {k: v for k, v in vals.items() if v not in (None, "")}
|
||||
for k, v in (("budget_amount", budget_amount), ("deposit_amount", deposit_amount)):
|
||||
if v not in (None, ""):
|
||||
data[k] = to_float(str(v).replace(",", ""), 0.0)
|
||||
if not data:
|
||||
return False, "未提供任何要修改的字段"
|
||||
db, dbname = get_db()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
sets = ", ".join("%s=${%s}$" % (k, k) for k in data)
|
||||
p = dict(data)
|
||||
p["tid"] = tender_id
|
||||
await sor.sqlExe("UPDATE " + TABLE + " SET " + sets + ", updated_at=NOW() WHERE id=${tid}$", p)
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
await record(sor, tender_id, TABLE, tender_id, "set_fields",
|
||||
who=who, agent_id=agent_id, detail=json.dumps(list(data), ensure_ascii=False))
|
||||
return True, "已更新字段: %s" % ", ".join(data)
|
||||
|
||||
|
||||
# ══════════════ 审批流程 ══════════════
|
||||
|
||||
async def submit_tender_approval(tender_id, assignee_role="owner.superuser",
|
||||
assignee_id="", note="", who=None, agent_id=None):
|
||||
"""提交招标信息审批:创建人类审批任务 + 状态置 pending_approval。"""
|
||||
db, dbname = get_db()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
t = await _get_tender(sor, tender_id)
|
||||
if not t:
|
||||
return False, "招标信息不存在: %s" % tender_id
|
||||
if t.get("status") == TD_PENDING_APPROVAL:
|
||||
return True, "已在审批中(human_task=%s)" % (t.get("approval_task_id") or "")
|
||||
if t.get("status") not in (TD_NEW, TD_SUMMARIZED):
|
||||
return False, "当前状态 %s 不可提交审批(需 new/summarized)" % t.get("status")
|
||||
title = "招标信息审批:%s" % (t.get("title") or t.get("mail_subject") or tender_id)
|
||||
desc_lines = [
|
||||
"招标人:%s" % (t.get("purchaser") or "未识别"),
|
||||
"招标编号:%s" % (t.get("tender_no") or "未识别"),
|
||||
"投标开始:%s 截止:%s" % (t.get("bid_start_at") or "-", t.get("bid_deadline") or "-"),
|
||||
"项目周期:%s" % (t.get("project_period") or "-"),
|
||||
"招标金额:%s" % (t.get("budget_amount") or "-"),
|
||||
"保证金:%s" % (t.get("deposit_amount") or "-"),
|
||||
"资质要求:%s" % ((t.get("qualification_req") or "-")[:500]),
|
||||
"摘要:%s" % ((t.get("summary") or "-")[:1000]),
|
||||
"风险提示:%s" % ((t.get("risk_note") or "-")[:500]),
|
||||
]
|
||||
if note:
|
||||
desc_lines.append("提交说明:%s" % note)
|
||||
desc_lines.append("")
|
||||
desc_lines.append("请决策:通过 → 立项投标;不通过 → 该招标信息存档作废。")
|
||||
hid = await create_human_task(
|
||||
sor, "", HT_TENDER_APPROVAL, title, "\n".join(desc_lines),
|
||||
assignee_role=assignee_role, assignee_id=assignee_id,
|
||||
form_schema={"tender_id": tender_id, "decision": ["approve", "reject"]})
|
||||
ok, msg = await _set_status(sor, tender_id, None, TD_PENDING_APPROVAL,
|
||||
"submit_approval", who=who, agent_id=agent_id,
|
||||
extra={"approval_task_id": hid})
|
||||
if not ok:
|
||||
return False, msg
|
||||
return True, "已提交审批(human_task=%s,指派 %s)" % (hid, assignee_role)
|
||||
|
||||
|
||||
async def approve_tender(tender_id, comment="", who=None, agent_id=None,
|
||||
auto_create_project=True):
|
||||
"""审批通过:pending_approval → approved;默认自动立项 + 发上传招标文件任务。"""
|
||||
db, dbname = get_db()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
ok, msg = await _set_status(sor, tender_id, TD_PENDING_APPROVAL, TD_APPROVED,
|
||||
"approve", who=who, agent_id=agent_id, detail=comment)
|
||||
if not ok:
|
||||
ok, msg = await _set_status(sor, tender_id, TD_SUMMARIZED, TD_APPROVED,
|
||||
"approve", who=who, agent_id=agent_id, detail=comment)
|
||||
if not ok:
|
||||
return False, msg
|
||||
await _close_approval_task(sor, tender_id, "done", comment)
|
||||
if not auto_create_project:
|
||||
return True, "审批通过(未自动立项)"
|
||||
ok2, msg2 = await create_bid_project(tender_id, who=who, agent_id=agent_id)
|
||||
if not ok2:
|
||||
return True, "审批通过,但立项失败: %s" % msg2
|
||||
return True, "审批通过并已立项: %s" % msg2
|
||||
|
||||
|
||||
async def reject_tender(tender_id, comment="", who=None, agent_id=None):
|
||||
"""审批不通过:pending_approval → rejected → 立即存档作废(业务要求)。"""
|
||||
db, dbname = get_db()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
ok, msg = await _set_status(sor, tender_id, TD_PENDING_APPROVAL, TD_REJECTED,
|
||||
"reject", who=who, agent_id=agent_id, detail=comment)
|
||||
if not ok:
|
||||
return False, msg
|
||||
await _close_approval_task(sor, tender_id, "rejected", comment)
|
||||
ok2, msg2 = await _set_status(
|
||||
sor, tender_id, TD_REJECTED, TD_ARCHIVED, "archive",
|
||||
who=who, agent_id=agent_id, detail="审批不通过自动存档作废",
|
||||
extra={"archive_reason": ("审批不通过:" + (comment or ""))[:480]})
|
||||
if not ok2:
|
||||
return False, "已驳回但存档失败: %s" % msg2
|
||||
return True, "审批不通过,招标信息已存档作废"
|
||||
|
||||
|
||||
async def archive_tender(tender_id, reason="", who=None, agent_id=None):
|
||||
"""存档作废(任何状态 → archived)。"""
|
||||
db, dbname = get_db()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
return await _set_status(sor, tender_id, None, TD_ARCHIVED, "archive",
|
||||
who=who, agent_id=agent_id, detail=reason,
|
||||
extra={"archive_reason": (reason or "")[:480]})
|
||||
|
||||
|
||||
async def reopen_tender(tender_id, reason="", who=None, agent_id=None):
|
||||
"""存档/驳回后重新评估:archived/rejected → summarized。"""
|
||||
db, dbname = get_db()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
ok, msg = await _set_status(sor, tender_id, TD_ARCHIVED, TD_SUMMARIZED, "reopen",
|
||||
who=who, agent_id=agent_id, detail=reason)
|
||||
if ok:
|
||||
return ok, msg
|
||||
return await _set_status(sor, tender_id, TD_REJECTED, TD_SUMMARIZED, "reopen",
|
||||
who=who, agent_id=agent_id, detail=reason)
|
||||
|
||||
|
||||
async def _close_approval_task(sor, tender_id, status, comment=""):
|
||||
"""关闭招标审批人类任务(同步审批结论)。"""
|
||||
t = await _get_tender(sor, tender_id)
|
||||
hid = t.get("approval_task_id") or ""
|
||||
if not hid:
|
||||
return
|
||||
await sor.sqlExe(
|
||||
"UPDATE pipeline_human_tasks SET status=${st}$, qc_comment=${cm}$, "
|
||||
"submitted_at=NOW() WHERE id=${hid}$ AND status='pending'",
|
||||
{"st": status, "cm": (comment or "")[:480], "hid": hid})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
|
||||
|
||||
# ══════════════ 立项 / 人员 / 招标文件 ══════════════
|
||||
|
||||
async def create_bid_project(name, description="", org_id="", created_by="",
|
||||
who=None, agent_id=None):
|
||||
"""直接创建投标项目(不依赖招标信息前置)+ 发「上传招标文件」任务。
|
||||
"""直接创建投标项目(产线起点)+ 发「上传招标文件」任务。
|
||||
|
||||
产线入口改为「从上传招标文件开始」后,立项不再经过邮件采集/审批,
|
||||
而是用户/agent 直接创建投标项目,随后上传招标文件进入解析。
|
||||
立项不经过邮件采集/审批(那是商机产线的职责):用户/agent 直接创建投标项目,
|
||||
随后上传招标文件进入解析。
|
||||
"""
|
||||
if not name or not str(name).strip():
|
||||
return False, "缺少项目名称(name)"
|
||||
@ -358,13 +47,12 @@ async def create_bid_project(name, description="", org_id="", created_by="",
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
await record(sor, pid, "sd_projects", pid, "create_bid_project",
|
||||
to_state="in_progress", who=who, agent_id=agent_id,
|
||||
detail="direct create (no tender)")
|
||||
detail="direct create")
|
||||
hid = await create_human_task(
|
||||
sor, pid, HT_TENDER_FILE,
|
||||
"上传招标文件:%s" % pname,
|
||||
("项目已创建,请上传招标文件(含答疑/补遗文件)。\n"
|
||||
"上传后系统自动进入招标文件解析(抽取评分项、得分规则、所需资质、投标文件要求、章节骨架)。\n"
|
||||
"上传入口:投标产线 → 招标文件(bid_tender_files)。"),
|
||||
"上传后系统自动进入招标文件解析(抽取评分项、得分规则、所需资质、投标文件要求、章节骨架)。"),
|
||||
assignee_role="owner.superuser",
|
||||
form_schema={"project_id": pid})
|
||||
return True, "project_id=%s(已发上传招标文件任务 %s)" % (pid, hid)
|
||||
@ -406,6 +94,7 @@ async def assign_members(project_id, members="", who=None, agent_id=None):
|
||||
exist = await sor.sqlExe(
|
||||
"SELECT id FROM bid_members WHERE project_id=${pid}$ AND user_name=${nm}$ LIMIT 1",
|
||||
{"pid": project_id, "nm": nm})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
if exist:
|
||||
continue
|
||||
await sor.C("bid_members", {
|
||||
|
||||
@ -192,6 +192,34 @@ BID_ROLE_TOOL_SCHEMAS = {
|
||||
"chapter_id": "章节ID(可选)"},
|
||||
"required": [],
|
||||
},
|
||||
# ── bid_qc_capability:解析产出 QC 契合度审核(agent.qc)──
|
||||
"start_qc": {
|
||||
"module": f"{_M}.bid_qc_capability",
|
||||
"description": "开启一轮解析产出契合度审核:返回审核清单+该类产出物待核内容(对照招标文件原文逐项核对后打分)",
|
||||
"params": {"qc_type": "审核对象 scoring_items|qualifications|doc_requirements|chapter_outline",
|
||||
"project_id": "项目ID(可选,默认当前)"},
|
||||
"required": ["qc_type"],
|
||||
},
|
||||
"finish_qc": {
|
||||
"module": f"{_M}.bid_qc_capability",
|
||||
"description": "落契合度分并判定:得分高于通过分(默认9.5,10分制)放行;不通过必须给改进意见,系统清空该类产出并触发解析重做",
|
||||
"params": {"review_id": "start_qc 返回的评审ID", "fit_score": "契合度得分 0~10(可带一位小数)",
|
||||
"improvement": "改进意见(不通过必填:缺了什么/错在哪/对照原文哪一处)",
|
||||
"comments": "审核说明(可选)"},
|
||||
"required": ["review_id", "fit_score"],
|
||||
},
|
||||
"list_qc": {
|
||||
"module": f"{_M}.bid_qc_capability",
|
||||
"description": "列出项目 QC 审核记录(可按审核对象过滤)",
|
||||
"params": {"project_id": "项目ID(可选,默认当前)", "qc_type": "审核对象(可选)"},
|
||||
"required": [],
|
||||
},
|
||||
"qc_report": {
|
||||
"module": f"{_M}.bid_qc_capability",
|
||||
"description": "项目 QC 总览:四类产出的最新契合度审核状态",
|
||||
"params": {"project_id": "项目ID(可选,默认当前)"},
|
||||
"required": [],
|
||||
},
|
||||
# ── bid_compose_capability:标书合成工程师 ──
|
||||
"compose_bid": {
|
||||
"module": f"{_M}.bid_compose_capability",
|
||||
|
||||
@ -14,8 +14,9 @@ MOD = "pipeline-bidding"
|
||||
APP_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
|
||||
|
||||
TABLES = [
|
||||
"bid_mail_accounts", "bid_tenders", "bid_members", "bid_tender_files",
|
||||
"bid_tenders", "bid_members", "bid_tender_files",
|
||||
"bid_scoring_items", "bid_qualifications", "bid_doc_requirements",
|
||||
"bid_qc_reviews",
|
||||
"bid_chapters", "bid_reviews", "bid_scores", "bid_kb_docs", "bid_documents",
|
||||
]
|
||||
|
||||
|
||||
@ -1,47 +0,0 @@
|
||||
|
||||
ns = params_kw.copy()
|
||||
|
||||
|
||||
debug(f'get_bid_mail_accounts.dspy:{ns=}')
|
||||
if not ns.get('page'):
|
||||
ns['page'] = 1
|
||||
if not ns.get('sort'):
|
||||
|
||||
|
||||
ns['sort'] = json.loads('["created_at desc"]')
|
||||
|
||||
|
||||
|
||||
sql = '''select * from bid_mail_accounts where 1=1 [[filterstr]]'''
|
||||
|
||||
|
||||
filterjson = json.loads('{"AND": [{"field": "account_name", "op": "LIKE", "var": "name_input"}, {"field": "enabled", "op": "=", "var": "enabled_input"}]}')
|
||||
|
||||
|
||||
filterdic = ns.copy()
|
||||
filterdic['filterstr'] = ''
|
||||
filterdic['userorgid'] = '${userorgid}$'
|
||||
filterdic['userid'] = '${userid}$'
|
||||
if filterjson:
|
||||
dbf = DBFilter(filterjson)
|
||||
conds = dbf.gen(ns)
|
||||
if conds:
|
||||
ns.update(dbf.consts)
|
||||
conds = f' and {conds}'
|
||||
filterdic['filterstr'] = conds
|
||||
ac = ArgsConvert('[[', ']]')
|
||||
vars = ac.findAllVariables(sql)
|
||||
NameSpace = {v:'${' + v + '}$' for v in vars if v != 'filterstr' }
|
||||
filterdic.update(NameSpace)
|
||||
sql = ac.convert(sql, filterdic)
|
||||
|
||||
debug(f'{sql=}')
|
||||
db = DBPools()
|
||||
dbname = get_module_dbname('pipeline_bidding')
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
r = await sor.sqlPaging(sql, ns)
|
||||
return r
|
||||
return {
|
||||
"total":0,
|
||||
"rows":[]
|
||||
}
|
||||
@ -1,307 +0,0 @@
|
||||
|
||||
{
|
||||
"widgettype":"VBox",
|
||||
"options":{"height":"100%","width":"100%"},
|
||||
"subwidgets":[
|
||||
{
|
||||
"id":"bid_mail_accounts_tbl",
|
||||
"widgettype":"Tabular",
|
||||
"options":{
|
||||
"width":"100%",
|
||||
"height":"100%",
|
||||
|
||||
|
||||
"title":"招标邮箱配置",
|
||||
|
||||
|
||||
|
||||
|
||||
"toolbar":{
|
||||
"tools": []
|
||||
},
|
||||
|
||||
"css":"card",
|
||||
|
||||
|
||||
"editable":{
|
||||
|
||||
"new_data_url": "{{entire_url('add_bid_mail_accounts.dspy')}}",
|
||||
|
||||
|
||||
"delete_data_url": "{{entire_url('delete_bid_mail_accounts.dspy')}}",
|
||||
|
||||
|
||||
"update_data_url": "{{entire_url('update_bid_mail_accounts.dspy')}}"
|
||||
|
||||
|
||||
},
|
||||
|
||||
|
||||
"data_url":"{{entire_url('./get_bid_mail_accounts.dspy')}}",
|
||||
|
||||
"data_method":"GET",
|
||||
"data_params":{{json.dumps(params_kw, indent=4, ensure_ascii=False)}},
|
||||
"row_options":{
|
||||
|
||||
|
||||
|
||||
"browserfields": {
|
||||
"exclouded": [
|
||||
"password",
|
||||
"last_uid"
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
"editexclouded":[
|
||||
"id",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"last_uid",
|
||||
"last_fetch_at"
|
||||
],
|
||||
|
||||
"fields":[
|
||||
{
|
||||
"name": "id",
|
||||
"title": "主键ID",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"nullable": "no",
|
||||
"cwidth": 18,
|
||||
"uitype": "str",
|
||||
"datatype": "str",
|
||||
"label": "主键ID"
|
||||
},
|
||||
{
|
||||
"name": "org_id",
|
||||
"title": "机构ID",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"nullable": "no",
|
||||
"default": "0",
|
||||
"cwidth": 18,
|
||||
"uitype": "str",
|
||||
"datatype": "str",
|
||||
"label": "机构ID"
|
||||
},
|
||||
{
|
||||
"name": "account_name",
|
||||
"title": "配置名称",
|
||||
"type": "str",
|
||||
"length": 128,
|
||||
"nullable": "no",
|
||||
"cwidth": 18,
|
||||
"uitype": "str",
|
||||
"datatype": "str",
|
||||
"label": "配置名称"
|
||||
},
|
||||
{
|
||||
"name": "protocol",
|
||||
"title": "协议",
|
||||
"type": "str",
|
||||
"length": 16,
|
||||
"nullable": "no",
|
||||
"default": "imap",
|
||||
"cwidth": 16,
|
||||
"uitype": "str",
|
||||
"datatype": "str",
|
||||
"label": "协议"
|
||||
},
|
||||
{
|
||||
"name": "host",
|
||||
"title": "服务器地址",
|
||||
"type": "str",
|
||||
"length": 128,
|
||||
"nullable": "no",
|
||||
"cwidth": 18,
|
||||
"uitype": "str",
|
||||
"datatype": "str",
|
||||
"label": "服务器地址"
|
||||
},
|
||||
{
|
||||
"name": "port",
|
||||
"title": "端口",
|
||||
"type": "int",
|
||||
"nullable": "no",
|
||||
"default": "993",
|
||||
"length": 0,
|
||||
"uitype": "int",
|
||||
"datatype": "int",
|
||||
"label": "端口"
|
||||
},
|
||||
{
|
||||
"name": "use_ssl",
|
||||
"title": "使用SSL",
|
||||
"type": "str",
|
||||
"length": 1,
|
||||
"nullable": "no",
|
||||
"default": "1",
|
||||
"cwidth": 4,
|
||||
"uitype": "str",
|
||||
"datatype": "str",
|
||||
"label": "使用SSL"
|
||||
},
|
||||
{
|
||||
"name": "username",
|
||||
"title": "登录账号",
|
||||
"type": "str",
|
||||
"length": 128,
|
||||
"nullable": "no",
|
||||
"cwidth": 18,
|
||||
"uitype": "str",
|
||||
"datatype": "str",
|
||||
"label": "登录账号"
|
||||
},
|
||||
{
|
||||
"name": "password",
|
||||
"title": "登录密码(AES加密)",
|
||||
"type": "str",
|
||||
"length": 500,
|
||||
"nullable": "no",
|
||||
"cwidth": 18,
|
||||
"uitype": "str",
|
||||
"datatype": "str",
|
||||
"label": "登录密码(AES加密)"
|
||||
},
|
||||
{
|
||||
"name": "folder",
|
||||
"title": "邮件目录",
|
||||
"type": "str",
|
||||
"length": 64,
|
||||
"default": "INBOX",
|
||||
"cwidth": 18,
|
||||
"uitype": "str",
|
||||
"datatype": "str",
|
||||
"label": "邮件目录"
|
||||
},
|
||||
{
|
||||
"name": "filter_keywords",
|
||||
"title": "招标关键词(逗号分隔)",
|
||||
"type": "str",
|
||||
"length": 500,
|
||||
"cwidth": 18,
|
||||
"uitype": "str",
|
||||
"datatype": "str",
|
||||
"label": "招标关键词(逗号分隔)"
|
||||
},
|
||||
{
|
||||
"name": "fetch_days",
|
||||
"title": "回溯天数",
|
||||
"type": "int",
|
||||
"default": "7",
|
||||
"length": 0,
|
||||
"uitype": "int",
|
||||
"datatype": "int",
|
||||
"label": "回溯天数"
|
||||
},
|
||||
{
|
||||
"name": "last_uid",
|
||||
"title": "上次同步UID",
|
||||
"type": "str",
|
||||
"length": 64,
|
||||
"cwidth": 18,
|
||||
"uitype": "str",
|
||||
"datatype": "str",
|
||||
"label": "上次同步UID"
|
||||
},
|
||||
{
|
||||
"name": "last_fetch_at",
|
||||
"title": "上次同步时间",
|
||||
"type": "timestamp",
|
||||
"length": 0,
|
||||
"uitype": "str",
|
||||
"datatype": "timestamp",
|
||||
"label": "上次同步时间"
|
||||
},
|
||||
{
|
||||
"name": "enabled",
|
||||
"title": "启用",
|
||||
"type": "str",
|
||||
"length": 1,
|
||||
"nullable": "no",
|
||||
"default": "1",
|
||||
"cwidth": 4,
|
||||
"uitype": "str",
|
||||
"datatype": "str",
|
||||
"label": "启用"
|
||||
},
|
||||
{
|
||||
"name": "status",
|
||||
"title": "状态",
|
||||
"type": "str",
|
||||
"length": 16,
|
||||
"default": "active",
|
||||
"cwidth": 16,
|
||||
"uitype": "str",
|
||||
"datatype": "str",
|
||||
"label": "状态"
|
||||
},
|
||||
{
|
||||
"name": "created_by",
|
||||
"title": "创建人",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"cwidth": 18,
|
||||
"uitype": "str",
|
||||
"datatype": "str",
|
||||
"label": "创建人"
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"title": "创建时间",
|
||||
"type": "timestamp",
|
||||
"nullable": "no",
|
||||
"length": 0,
|
||||
"uitype": "str",
|
||||
"datatype": "timestamp",
|
||||
"label": "创建时间"
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"title": "更新时间",
|
||||
"type": "timestamp",
|
||||
"length": 0,
|
||||
"uitype": "str",
|
||||
"datatype": "timestamp",
|
||||
"label": "更新时间"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
|
||||
"search_form":{
|
||||
"css":"card",
|
||||
"padding":"8px",
|
||||
"show_label":false,
|
||||
"submit_label":"搜索",
|
||||
"submit_css":"primary",
|
||||
"fields":[
|
||||
{
|
||||
"name": "name_input",
|
||||
"uitype": "str",
|
||||
"placeholder": "配置名称",
|
||||
"cwidth": 15
|
||||
},
|
||||
{
|
||||
"name": "enabled_input",
|
||||
"uitype": "str",
|
||||
"placeholder": "启用",
|
||||
"cwidth": 15
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
"page_rows":160,
|
||||
"cache_limit":5
|
||||
}
|
||||
|
||||
,"binds":[]
|
||||
|
||||
}]
|
||||
}
|
||||
@ -1,44 +0,0 @@
|
||||
|
||||
ns = params_kw.copy()
|
||||
for k,v in list(ns.items()):
|
||||
if v == 'NaN' or v == 'null' or v == '' or v is None:
|
||||
if k != 'id':
|
||||
del ns[k]
|
||||
else:
|
||||
ns[k] = None if v in ('NaN', 'null') else v
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
db = DBPools()
|
||||
dbname = get_module_dbname('pipeline_bidding')
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
|
||||
tblfields = json.loads('[{"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no"}, {"name": "org_id", "title": "机构ID", "type": "str", "length": 32, "nullable": "no", "default": "0"}, {"name": "account_name", "title": "配置名称", "type": "str", "length": 128, "nullable": "no"}, {"name": "protocol", "title": "协议", "type": "str", "length": 16, "nullable": "no", "default": "imap"}, {"name": "host", "title": "服务器地址", "type": "str", "length": 128, "nullable": "no"}, {"name": "port", "title": "端口", "type": "int", "nullable": "no", "default": "993"}, {"name": "use_ssl", "title": "使用SSL", "type": "str", "length": 1, "nullable": "no", "default": "1"}, {"name": "username", "title": "登录账号", "type": "str", "length": 128, "nullable": "no"}, {"name": "password", "title": "登录密码(AES加密)", "type": "str", "length": 500, "nullable": "no"}, {"name": "folder", "title": "邮件目录", "type": "str", "length": 64, "default": "INBOX"}, {"name": "filter_keywords", "title": "招标关键词(逗号分隔)", "type": "str", "length": 500}, {"name": "fetch_days", "title": "回溯天数", "type": "int", "default": "7"}, {"name": "last_uid", "title": "上次同步UID", "type": "str", "length": 64}, {"name": "last_fetch_at", "title": "上次同步时间", "type": "timestamp"}, {"name": "enabled", "title": "启用", "type": "str", "length": 1, "nullable": "no", "default": "1"}, {"name": "status", "title": "状态", "type": "str", "length": 16, "default": "active"}, {"name": "created_by", "title": "创建人", "type": "str", "length": 32}, {"name": "created_at", "title": "创建时间", "type": "timestamp", "nullable": "no"}, {"name": "updated_at", "title": "更新时间", "type": "timestamp"}]')
|
||||
tblfname = [ f['name'] for f in tblfields ]
|
||||
other_keys = [ k for k in ns.keys() if k in tblfname and k != 'id' ]
|
||||
if len(other_keys) > 0:
|
||||
r = await sor.U('bid_mail_accounts', ns)
|
||||
debug('update success');
|
||||
return {
|
||||
"widgettype":"Message",
|
||||
"options":{
|
||||
"title":"Update Success",
|
||||
"cwidth":16,
|
||||
"cheight":9,
|
||||
"timeout":3,
|
||||
"message":"ok"
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
"widgettype":"Error",
|
||||
"options":{
|
||||
"title":"Update Error",
|
||||
"cwidth":16,
|
||||
"cheight":9,
|
||||
"timeout":3,
|
||||
"message":"failed"
|
||||
}
|
||||
}
|
||||
@ -10,13 +10,10 @@ ns['id'] = id
|
||||
|
||||
|
||||
|
||||
|
||||
db = DBPools()
|
||||
dbname = get_module_dbname('pipeline_bidding')
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
r = await sor.C('bid_mail_accounts', ns.copy())
|
||||
_new_rows = await sor.sqlExe("SELECT * FROM bid_mail_accounts WHERE id = ${id}$", {'id': id})
|
||||
_user_data = _new_rows[0] if _new_rows else ns
|
||||
r = await sor.C('bid_qc_reviews', ns.copy())
|
||||
return {
|
||||
"widgettype":"Message",
|
||||
"options":{
|
||||
@ -24,8 +21,7 @@ async with db.sqlorContext(dbname) as sor:
|
||||
"cheight":9,
|
||||
"title":"Add Success",
|
||||
"timeout":3,
|
||||
"message":"ok",
|
||||
"user_data": _user_data
|
||||
"message":"ok"
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ ns = {
|
||||
db = DBPools()
|
||||
dbname = get_module_dbname('pipeline_bidding')
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
r = await sor.D('bid_mail_accounts', ns)
|
||||
r = await sor.D('bid_qc_reviews', ns)
|
||||
debug('delete success');
|
||||
return {
|
||||
"widgettype":"Message",
|
||||
148
wwwroot/bid_qc_reviews/get_bid_qc_reviews.dspy
Normal file
148
wwwroot/bid_qc_reviews/get_bid_qc_reviews.dspy
Normal file
@ -0,0 +1,148 @@
|
||||
|
||||
ns = params_kw.copy()
|
||||
|
||||
|
||||
debug(f'get_bid_qc_reviews.dspy:{ns=}')
|
||||
if not ns.get('page'):
|
||||
ns['page'] = 1
|
||||
if not ns.get('sort'):
|
||||
|
||||
|
||||
ns['sort'] = ["qc_type","round desc"]
|
||||
|
||||
|
||||
|
||||
sql = '''select a.*, b.qc_type_text
|
||||
from (select * from bid_qc_reviews where 1=1 [[filterstr]]) a left join (select k as qc_type,
|
||||
v as qc_type_text from appcodes_kv where parentid='bid_qc_type') b on a.qc_type = b.qc_type'''
|
||||
|
||||
filterjson = params_kw.get('data_filter')
|
||||
if filterjson and isinstance(filterjson, str):
|
||||
try:
|
||||
filterjson = json.loads(filterjson)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
filterjson = None
|
||||
fields_str=r'''[
|
||||
{
|
||||
"name": "id",
|
||||
"title": "主键ID",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "project_id",
|
||||
"title": "项目ID",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "qc_type",
|
||||
"title": "审核对象",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "round",
|
||||
"title": "审核轮次",
|
||||
"type": "int",
|
||||
"nullable": "no",
|
||||
"default": "1"
|
||||
},
|
||||
{
|
||||
"name": "reviewer",
|
||||
"title": "审核方",
|
||||
"type": "str",
|
||||
"length": 64
|
||||
},
|
||||
{
|
||||
"name": "review_type",
|
||||
"title": "审核类型",
|
||||
"type": "str",
|
||||
"length": 16,
|
||||
"default": "agent"
|
||||
},
|
||||
{
|
||||
"name": "fit_score",
|
||||
"title": "契合度得分(10分制)",
|
||||
"type": "float",
|
||||
"length": 4,
|
||||
"dec": 2
|
||||
},
|
||||
{
|
||||
"name": "pass_score",
|
||||
"title": "通过分",
|
||||
"type": "float",
|
||||
"length": 4,
|
||||
"dec": 2
|
||||
},
|
||||
{
|
||||
"name": "passed",
|
||||
"title": "是否通过",
|
||||
"type": "str",
|
||||
"length": 1,
|
||||
"nullable": "no",
|
||||
"default": "0"
|
||||
},
|
||||
{
|
||||
"name": "improvement",
|
||||
"title": "改进意见",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"name": "comments",
|
||||
"title": "审核说明",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"name": "task_id",
|
||||
"title": "关联任务ID",
|
||||
"type": "str",
|
||||
"length": 32
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"title": "创建时间",
|
||||
"type": "timestamp",
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"title": "更新时间",
|
||||
"type": "timestamp"
|
||||
}
|
||||
]'''
|
||||
ori_fields = json.loads(fields_str)
|
||||
if not filterjson:
|
||||
fields = [ f['name'] for f in ori_fields ]
|
||||
filterjson = default_filterjson(fields, ns)
|
||||
|
||||
filterdic = ns.copy()
|
||||
filterdic['filterstr'] = ''
|
||||
filterdic['userorgid'] = '${userorgid}$'
|
||||
filterdic['userid'] = '${userid}$'
|
||||
if filterjson:
|
||||
dbf = DBFilter(filterjson)
|
||||
conds = dbf.gen(ns)
|
||||
if conds:
|
||||
ns.update(dbf.consts)
|
||||
conds = f' and {conds}'
|
||||
filterdic['filterstr'] = conds
|
||||
ac = ArgsConvert('[[', ']]')
|
||||
vars = ac.findAllVariables(sql)
|
||||
NameSpace = {v:'${' + v + '}$' for v in vars if v != 'filterstr' }
|
||||
filterdic.update(NameSpace)
|
||||
sql = ac.convert(sql, filterdic)
|
||||
|
||||
debug(f'{sql=}')
|
||||
db = DBPools()
|
||||
dbname = get_module_dbname('pipeline_bidding')
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
r = await sor.sqlPaging(sql, ns)
|
||||
return r
|
||||
return {
|
||||
"total":0,
|
||||
"rows":[]
|
||||
}
|
||||
250
wwwroot/bid_qc_reviews/index.ui
Normal file
250
wwwroot/bid_qc_reviews/index.ui
Normal file
@ -0,0 +1,250 @@
|
||||
|
||||
{
|
||||
"widgettype":"VBox",
|
||||
"options":{"cheight":40,"width":"100%"},
|
||||
"subwidgets":[{
|
||||
"id":"bid_qc_reviews_tbl",
|
||||
"widgettype":"Tabular",
|
||||
"options":{
|
||||
"width":"100%",
|
||||
"height":"100%",
|
||||
|
||||
|
||||
"title":"解析产出QC审核记录",
|
||||
|
||||
|
||||
|
||||
|
||||
"toolbar":{
|
||||
"tools": []
|
||||
},
|
||||
|
||||
"css":"card",
|
||||
|
||||
|
||||
"editable":{
|
||||
|
||||
"new_data_url":"{{entire_url('add_bid_qc_reviews.dspy')}}",
|
||||
|
||||
|
||||
"delete_data_url":"{{entire_url('delete_bid_qc_reviews.dspy')}}",
|
||||
|
||||
|
||||
"update_data_url":"{{entire_url('update_bid_qc_reviews.dspy')}}"
|
||||
|
||||
},
|
||||
|
||||
|
||||
"data_url":"{{entire_url('./get_bid_qc_reviews.dspy')}}",
|
||||
|
||||
"data_method":"GET",
|
||||
"data_params":{{json.dumps(params_kw, indent=4, ensure_ascii=False)}},
|
||||
"row_options":{
|
||||
|
||||
|
||||
|
||||
"browserfields": {
|
||||
"exclouded": [
|
||||
"comments"
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
"editexclouded":[
|
||||
"id",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"task_id",
|
||||
"reviewer",
|
||||
"review_type"
|
||||
],
|
||||
|
||||
"fields":[
|
||||
{
|
||||
"name": "id",
|
||||
"title": "主键ID",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"nullable": "no",
|
||||
"cwidth": 18,
|
||||
"uitype": "str",
|
||||
"datatype": "str",
|
||||
"label": "主键ID"
|
||||
},
|
||||
{
|
||||
"name": "project_id",
|
||||
"title": "项目ID",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"nullable": "no",
|
||||
"cwidth": 18,
|
||||
"uitype": "str",
|
||||
"datatype": "str",
|
||||
"label": "项目ID"
|
||||
},
|
||||
{
|
||||
"name": "qc_type",
|
||||
"title": "审核对象",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"nullable": "no",
|
||||
"label": "审核对象",
|
||||
"uitype": "code",
|
||||
"valueField": "qc_type",
|
||||
"textField": "qc_type_text",
|
||||
"params": {
|
||||
"dbname": "{{get_module_dbname('pipeline_bidding')}}",
|
||||
"table": "appcodes_kv",
|
||||
"tblvalue": "k",
|
||||
"tbltext": "v",
|
||||
"valueField": "qc_type",
|
||||
"textField": "qc_type_text",
|
||||
"cond": "parentid='bid_qc_type'"
|
||||
},
|
||||
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}"
|
||||
},
|
||||
{
|
||||
"name": "round",
|
||||
"title": "审核轮次",
|
||||
"type": "int",
|
||||
"nullable": "no",
|
||||
"default": "1",
|
||||
"length": 0,
|
||||
"uitype": "int",
|
||||
"datatype": "int",
|
||||
"label": "审核轮次"
|
||||
},
|
||||
{
|
||||
"name": "reviewer",
|
||||
"title": "审核方",
|
||||
"type": "str",
|
||||
"length": 64,
|
||||
"cwidth": 18,
|
||||
"uitype": "str",
|
||||
"datatype": "str",
|
||||
"label": "审核方"
|
||||
},
|
||||
{
|
||||
"name": "review_type",
|
||||
"title": "审核类型",
|
||||
"type": "str",
|
||||
"length": 16,
|
||||
"default": "agent",
|
||||
"cwidth": 16,
|
||||
"uitype": "str",
|
||||
"datatype": "str",
|
||||
"label": "审核类型"
|
||||
},
|
||||
{
|
||||
"name": "fit_score",
|
||||
"title": "契合度得分(10分制)",
|
||||
"type": "float",
|
||||
"length": 4,
|
||||
"dec": 2,
|
||||
"cwidth": 4,
|
||||
"uitype": "float",
|
||||
"datatype": "float",
|
||||
"label": "契合度得分(10分制)"
|
||||
},
|
||||
{
|
||||
"name": "pass_score",
|
||||
"title": "通过分",
|
||||
"type": "float",
|
||||
"length": 4,
|
||||
"dec": 2,
|
||||
"cwidth": 4,
|
||||
"uitype": "float",
|
||||
"datatype": "float",
|
||||
"label": "通过分"
|
||||
},
|
||||
{
|
||||
"name": "passed",
|
||||
"title": "是否通过",
|
||||
"type": "str",
|
||||
"length": 1,
|
||||
"nullable": "no",
|
||||
"default": "0",
|
||||
"cwidth": 4,
|
||||
"uitype": "str",
|
||||
"datatype": "str",
|
||||
"label": "是否通过"
|
||||
},
|
||||
{
|
||||
"name": "improvement",
|
||||
"title": "改进意见",
|
||||
"type": "text",
|
||||
"length": 0,
|
||||
"uitype": "text",
|
||||
"datatype": "text",
|
||||
"label": "改进意见"
|
||||
},
|
||||
{
|
||||
"name": "comments",
|
||||
"title": "审核说明",
|
||||
"type": "text",
|
||||
"length": 0,
|
||||
"uitype": "text",
|
||||
"datatype": "text",
|
||||
"label": "审核说明"
|
||||
},
|
||||
{
|
||||
"name": "task_id",
|
||||
"title": "关联任务ID",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"cwidth": 18,
|
||||
"uitype": "str",
|
||||
"datatype": "str",
|
||||
"label": "关联任务ID"
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"title": "创建时间",
|
||||
"type": "timestamp",
|
||||
"nullable": "no",
|
||||
"length": 0,
|
||||
"uitype": "str",
|
||||
"datatype": "timestamp",
|
||||
"label": "创建时间"
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"title": "更新时间",
|
||||
"type": "timestamp",
|
||||
"length": 0,
|
||||
"uitype": "str",
|
||||
"datatype": "timestamp",
|
||||
"label": "更新时间"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
|
||||
"data_filter":{
|
||||
"AND": [
|
||||
{
|
||||
"field": "project_id",
|
||||
"op": "=",
|
||||
"var": "project_id"
|
||||
},
|
||||
{
|
||||
"field": "qc_type",
|
||||
"op": "=",
|
||||
"var": "qc_type_input"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
"page_rows":160,
|
||||
"cache_limit":5
|
||||
}
|
||||
|
||||
,"binds":[]
|
||||
|
||||
}]
|
||||
}
|
||||
36
wwwroot/bid_qc_reviews/update_bid_qc_reviews.dspy
Normal file
36
wwwroot/bid_qc_reviews/update_bid_qc_reviews.dspy
Normal file
@ -0,0 +1,36 @@
|
||||
|
||||
ns = params_kw.copy()
|
||||
for k,v in ns.items():
|
||||
if v == 'NaN' or v == 'null':
|
||||
ns[k] = None
|
||||
|
||||
|
||||
|
||||
|
||||
db = DBPools()
|
||||
dbname = get_module_dbname('pipeline_bidding')
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
|
||||
r = await sor.U('bid_qc_reviews', ns)
|
||||
debug('update success');
|
||||
return {
|
||||
"widgettype":"Message",
|
||||
"options":{
|
||||
"title":"Update Success",
|
||||
"cwidth":16,
|
||||
"cheight":9,
|
||||
"timeout":3,
|
||||
"message":"ok"
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
"widgettype":"Error",
|
||||
"options":{
|
||||
"title":"Update Error",
|
||||
"cwidth":16,
|
||||
"cheight":9,
|
||||
"timeout":3,
|
||||
"message":"failed"
|
||||
}
|
||||
}
|
||||
@ -10,13 +10,10 @@ ns['id'] = id
|
||||
|
||||
|
||||
|
||||
|
||||
db = DBPools()
|
||||
dbname = get_module_dbname('pipeline_bidding')
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
r = await sor.C('bid_tenders', ns.copy())
|
||||
_new_rows = await sor.sqlExe("SELECT * FROM bid_tenders WHERE id = ${id}$", {'id': id})
|
||||
_user_data = _new_rows[0] if _new_rows else ns
|
||||
return {
|
||||
"widgettype":"Message",
|
||||
"options":{
|
||||
@ -24,8 +21,7 @@ async with db.sqlorContext(dbname) as sor:
|
||||
"cheight":9,
|
||||
"title":"Add Success",
|
||||
"timeout":3,
|
||||
"message":"ok",
|
||||
"user_data": _user_data
|
||||
"message":"ok"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -8,18 +8,155 @@ if not ns.get('page'):
|
||||
if not ns.get('sort'):
|
||||
|
||||
|
||||
ns['sort'] = json.loads('["received_at desc"]')
|
||||
ns['sort'] = ["created_at desc"]
|
||||
|
||||
|
||||
|
||||
sql = '''select a.*, b.status_text, c.source_text
|
||||
from (select * from bid_tenders where 1=1 [[filterstr]]) a left join (select k as status,
|
||||
v as status_text from appcodes_kv where parentid='bid_tender_status') b on a.status = b.status left join (select k as source,
|
||||
v as source_text from appcodes_kv where parentid='bid_tender_source') c on a.source = c.source'''
|
||||
|
||||
|
||||
filterjson = json.loads('{"AND": [{"field": "title", "op": "LIKE", "var": "title_input"}, {"field": "status", "op": "=", "var": "status_input"}, {"field": "purchaser", "op": "LIKE", "var": "purchaser_input"}]}')
|
||||
sql = '''select a.*, b.source_text
|
||||
from (select * from bid_tenders where 1=1 [[filterstr]]) a left join (select k as source,
|
||||
v as source_text from appcodes_kv where parentid='bid_tender_source') b on a.source = b.source'''
|
||||
|
||||
filterjson = params_kw.get('data_filter')
|
||||
if filterjson and isinstance(filterjson, str):
|
||||
try:
|
||||
filterjson = json.loads(filterjson)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
filterjson = None
|
||||
fields_str=r'''[
|
||||
{
|
||||
"name": "id",
|
||||
"title": "主键ID",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "org_id",
|
||||
"title": "机构ID",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"nullable": "no",
|
||||
"default": "0"
|
||||
},
|
||||
{
|
||||
"name": "project_id",
|
||||
"title": "项目ID",
|
||||
"type": "str",
|
||||
"length": 32
|
||||
},
|
||||
{
|
||||
"name": "source",
|
||||
"title": "来源",
|
||||
"type": "str",
|
||||
"length": 16,
|
||||
"nullable": "no",
|
||||
"default": "tender_doc"
|
||||
},
|
||||
{
|
||||
"name": "title",
|
||||
"title": "项目名称",
|
||||
"type": "str",
|
||||
"length": 255
|
||||
},
|
||||
{
|
||||
"name": "purchaser",
|
||||
"title": "招标人/采购人",
|
||||
"type": "str",
|
||||
"length": 255
|
||||
},
|
||||
{
|
||||
"name": "tender_no",
|
||||
"title": "招标编号",
|
||||
"type": "str",
|
||||
"length": 64
|
||||
},
|
||||
{
|
||||
"name": "industry",
|
||||
"title": "行业",
|
||||
"type": "str",
|
||||
"length": 64
|
||||
},
|
||||
{
|
||||
"name": "summary",
|
||||
"title": "项目概况摘要",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"name": "bid_start_at",
|
||||
"title": "投标开始时间",
|
||||
"type": "str",
|
||||
"length": 16
|
||||
},
|
||||
{
|
||||
"name": "bid_deadline",
|
||||
"title": "投标截止时间",
|
||||
"type": "str",
|
||||
"length": 16
|
||||
},
|
||||
{
|
||||
"name": "project_period",
|
||||
"title": "项目周期",
|
||||
"type": "str",
|
||||
"length": 64
|
||||
},
|
||||
{
|
||||
"name": "budget_amount",
|
||||
"title": "招标金额",
|
||||
"type": "float",
|
||||
"length": 14,
|
||||
"dec": 2
|
||||
},
|
||||
{
|
||||
"name": "deposit_amount",
|
||||
"title": "投标保证金",
|
||||
"type": "float",
|
||||
"length": 14,
|
||||
"dec": 2
|
||||
},
|
||||
{
|
||||
"name": "qualification_req",
|
||||
"title": "资质要求摘要",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"name": "key_dates",
|
||||
"title": "关键节点(JSON)",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"name": "risk_note",
|
||||
"title": "风险提示",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"name": "status",
|
||||
"title": "状态",
|
||||
"type": "str",
|
||||
"length": 16,
|
||||
"default": "summarized"
|
||||
},
|
||||
{
|
||||
"name": "created_by",
|
||||
"title": "创建人",
|
||||
"type": "str",
|
||||
"length": 64
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"title": "创建时间",
|
||||
"type": "timestamp",
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"title": "更新时间",
|
||||
"type": "timestamp"
|
||||
}
|
||||
]'''
|
||||
ori_fields = json.loads(fields_str)
|
||||
if not filterjson:
|
||||
fields = [ f['name'] for f in ori_fields ]
|
||||
filterjson = default_filterjson(fields, ns)
|
||||
|
||||
filterdic = ns.copy()
|
||||
filterdic['filterstr'] = ''
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
|
||||
{
|
||||
"widgettype":"VBox",
|
||||
"options":{"height":"100%","width":"100%"},
|
||||
"subwidgets":[
|
||||
{
|
||||
"options":{"cheight":40,"width":"100%"},
|
||||
"subwidgets":[{
|
||||
"id":"bid_tenders_tbl",
|
||||
"widgettype":"Tabular",
|
||||
"options":{
|
||||
@ -11,28 +10,23 @@
|
||||
"height":"100%",
|
||||
|
||||
|
||||
"title":"招标信息",
|
||||
"title":"招标商务要素",
|
||||
|
||||
|
||||
|
||||
|
||||
"toolbar":{
|
||||
"tools": []
|
||||
},
|
||||
|
||||
"css":"card",
|
||||
|
||||
|
||||
"editable":{
|
||||
|
||||
"new_data_url": "{{entire_url('add_bid_tenders.dspy')}}",
|
||||
"new_data_url":"{{entire_url('add_bid_tenders.dspy')}}",
|
||||
|
||||
|
||||
"delete_data_url": "{{entire_url('delete_bid_tenders.dspy')}}",
|
||||
"delete_data_url":"{{entire_url('delete_bid_tenders.dspy')}}",
|
||||
|
||||
|
||||
"update_data_url": "{{entire_url('update_bid_tenders.dspy')}}"
|
||||
|
||||
"update_data_url":"{{entire_url('update_bid_tenders.dspy')}}"
|
||||
|
||||
},
|
||||
|
||||
@ -47,12 +41,8 @@
|
||||
|
||||
"browserfields": {
|
||||
"exclouded": [
|
||||
"raw_content",
|
||||
"attach_json",
|
||||
"key_dates",
|
||||
"org_id",
|
||||
"account_id",
|
||||
"mail_uid",
|
||||
"risk_note"
|
||||
]
|
||||
},
|
||||
@ -62,7 +52,6 @@
|
||||
"id",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"approval_task_id",
|
||||
"project_id"
|
||||
],
|
||||
|
||||
@ -90,14 +79,24 @@
|
||||
"datatype": "str",
|
||||
"label": "机构ID"
|
||||
},
|
||||
{
|
||||
"name": "project_id",
|
||||
"title": "项目ID",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"cwidth": 18,
|
||||
"uitype": "str",
|
||||
"datatype": "str",
|
||||
"label": "项目ID"
|
||||
},
|
||||
{
|
||||
"name": "source",
|
||||
"title": "信息来源",
|
||||
"title": "来源",
|
||||
"type": "str",
|
||||
"length": 16,
|
||||
"nullable": "no",
|
||||
"default": "mail",
|
||||
"label": "信息来源",
|
||||
"default": "tender_doc",
|
||||
"label": "来源",
|
||||
"uitype": "code",
|
||||
"valueField": "source",
|
||||
"textField": "source_text",
|
||||
@ -112,73 +111,6 @@
|
||||
},
|
||||
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}"
|
||||
},
|
||||
{
|
||||
"name": "account_id",
|
||||
"title": "邮箱配置ID",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"cwidth": 18,
|
||||
"uitype": "str",
|
||||
"datatype": "str",
|
||||
"label": "邮箱配置ID"
|
||||
},
|
||||
{
|
||||
"name": "mail_uid",
|
||||
"title": "邮件UID",
|
||||
"type": "str",
|
||||
"length": 64,
|
||||
"cwidth": 18,
|
||||
"uitype": "str",
|
||||
"datatype": "str",
|
||||
"label": "邮件UID"
|
||||
},
|
||||
{
|
||||
"name": "mail_from",
|
||||
"title": "发件人",
|
||||
"type": "str",
|
||||
"length": 255,
|
||||
"cwidth": 18,
|
||||
"uitype": "str",
|
||||
"datatype": "str",
|
||||
"label": "发件人"
|
||||
},
|
||||
{
|
||||
"name": "mail_subject",
|
||||
"title": "邮件主题",
|
||||
"type": "str",
|
||||
"length": 500,
|
||||
"cwidth": 18,
|
||||
"uitype": "str",
|
||||
"datatype": "str",
|
||||
"label": "邮件主题"
|
||||
},
|
||||
{
|
||||
"name": "received_at",
|
||||
"title": "收信时间",
|
||||
"type": "timestamp",
|
||||
"length": 0,
|
||||
"uitype": "str",
|
||||
"datatype": "timestamp",
|
||||
"label": "收信时间"
|
||||
},
|
||||
{
|
||||
"name": "raw_content",
|
||||
"title": "邮件原文",
|
||||
"type": "text",
|
||||
"length": 0,
|
||||
"uitype": "text",
|
||||
"datatype": "text",
|
||||
"label": "邮件原文"
|
||||
},
|
||||
{
|
||||
"name": "attach_json",
|
||||
"title": "附件清单(JSON)",
|
||||
"type": "text",
|
||||
"length": 0,
|
||||
"uitype": "text",
|
||||
"datatype": "text",
|
||||
"label": "附件清单(JSON)"
|
||||
},
|
||||
{
|
||||
"name": "title",
|
||||
"title": "项目名称",
|
||||
@ -221,29 +153,31 @@
|
||||
},
|
||||
{
|
||||
"name": "summary",
|
||||
"title": "内容摘要",
|
||||
"title": "项目概况摘要",
|
||||
"type": "text",
|
||||
"length": 0,
|
||||
"uitype": "text",
|
||||
"datatype": "text",
|
||||
"label": "内容摘要"
|
||||
"label": "项目概况摘要"
|
||||
},
|
||||
{
|
||||
"name": "bid_start_at",
|
||||
"title": "投标开始时间",
|
||||
"type": "date",
|
||||
"length": 0,
|
||||
"uitype": "date",
|
||||
"datatype": "date",
|
||||
"type": "str",
|
||||
"length": 16,
|
||||
"cwidth": 16,
|
||||
"uitype": "str",
|
||||
"datatype": "str",
|
||||
"label": "投标开始时间"
|
||||
},
|
||||
{
|
||||
"name": "bid_deadline",
|
||||
"title": "投标截止时间",
|
||||
"type": "date",
|
||||
"length": 0,
|
||||
"uitype": "date",
|
||||
"datatype": "date",
|
||||
"type": "str",
|
||||
"length": 16,
|
||||
"cwidth": 16,
|
||||
"uitype": "str",
|
||||
"datatype": "str",
|
||||
"label": "投标截止时间"
|
||||
},
|
||||
{
|
||||
@ -260,9 +194,9 @@
|
||||
"name": "budget_amount",
|
||||
"title": "招标金额",
|
||||
"type": "float",
|
||||
"length": 18,
|
||||
"length": 14,
|
||||
"dec": 2,
|
||||
"cwidth": 18,
|
||||
"cwidth": 14,
|
||||
"uitype": "float",
|
||||
"datatype": "float",
|
||||
"label": "招标金额"
|
||||
@ -271,9 +205,9 @@
|
||||
"name": "deposit_amount",
|
||||
"title": "投标保证金",
|
||||
"type": "float",
|
||||
"length": 18,
|
||||
"length": 14,
|
||||
"dec": 2,
|
||||
"cwidth": 18,
|
||||
"cwidth": 14,
|
||||
"uitype": "float",
|
||||
"datatype": "float",
|
||||
"label": "投标保证金"
|
||||
@ -289,12 +223,12 @@
|
||||
},
|
||||
{
|
||||
"name": "key_dates",
|
||||
"title": "关键时间节点(JSON)",
|
||||
"title": "关键节点(JSON)",
|
||||
"type": "text",
|
||||
"length": 0,
|
||||
"uitype": "text",
|
||||
"datatype": "text",
|
||||
"label": "关键时间节点(JSON)"
|
||||
"label": "关键节点(JSON)"
|
||||
},
|
||||
{
|
||||
"name": "risk_note",
|
||||
@ -310,58 +244,17 @@
|
||||
"title": "状态",
|
||||
"type": "str",
|
||||
"length": 16,
|
||||
"nullable": "no",
|
||||
"default": "new",
|
||||
"label": "状态",
|
||||
"uitype": "code",
|
||||
"valueField": "status",
|
||||
"textField": "status_text",
|
||||
"params": {
|
||||
"dbname": "{{get_module_dbname('pipeline_bidding')}}",
|
||||
"table": "appcodes_kv",
|
||||
"tblvalue": "k",
|
||||
"tbltext": "v",
|
||||
"valueField": "status",
|
||||
"textField": "status_text",
|
||||
"cond": "parentid='bid_tender_status'"
|
||||
},
|
||||
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}"
|
||||
},
|
||||
{
|
||||
"name": "approval_task_id",
|
||||
"title": "审批任务ID",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"cwidth": 18,
|
||||
"default": "summarized",
|
||||
"cwidth": 16,
|
||||
"uitype": "str",
|
||||
"datatype": "str",
|
||||
"label": "审批任务ID"
|
||||
},
|
||||
{
|
||||
"name": "project_id",
|
||||
"title": "立项项目ID",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"cwidth": 18,
|
||||
"uitype": "str",
|
||||
"datatype": "str",
|
||||
"label": "立项项目ID"
|
||||
},
|
||||
{
|
||||
"name": "archive_reason",
|
||||
"title": "作废/存档原因",
|
||||
"type": "str",
|
||||
"length": 500,
|
||||
"cwidth": 18,
|
||||
"uitype": "str",
|
||||
"datatype": "str",
|
||||
"label": "作废/存档原因"
|
||||
"label": "状态"
|
||||
},
|
||||
{
|
||||
"name": "created_by",
|
||||
"title": "创建人",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"length": 64,
|
||||
"cwidth": 18,
|
||||
"uitype": "str",
|
||||
"datatype": "str",
|
||||
@ -391,45 +284,25 @@
|
||||
|
||||
|
||||
|
||||
"search_form":{
|
||||
"css":"card",
|
||||
"padding":"8px",
|
||||
"show_label":false,
|
||||
"submit_label":"搜索",
|
||||
"submit_css":"primary",
|
||||
"fields":[
|
||||
{
|
||||
"name": "title_input",
|
||||
"uitype": "str",
|
||||
"placeholder": "项目名称",
|
||||
"cwidth": 15
|
||||
},
|
||||
{
|
||||
"name": "status_input",
|
||||
"uitype": "code",
|
||||
"placeholder": "状态",
|
||||
"cwidth": 15,
|
||||
"valueField": "status_input",
|
||||
"textField": "status_input_text",
|
||||
"params": {
|
||||
"dbname": "{{get_module_dbname('pipeline_bidding')}}",
|
||||
"table": "appcodes_kv",
|
||||
"tblvalue": "k",
|
||||
"tbltext": "v",
|
||||
"valueField": "status_input",
|
||||
"textField": "status_input_text",
|
||||
"cond": "parentid='bid_tender_status'"
|
||||
"data_filter":{
|
||||
"AND": [
|
||||
{
|
||||
"field": "title",
|
||||
"op": "LIKE",
|
||||
"var": "title_input"
|
||||
},
|
||||
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}?prepend_all=1"
|
||||
},
|
||||
{
|
||||
"name": "purchaser_input",
|
||||
"uitype": "str",
|
||||
"placeholder": "招标人/采购人",
|
||||
"cwidth": 15
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"field": "project_id",
|
||||
"op": "=",
|
||||
"var": "project_id"
|
||||
},
|
||||
{
|
||||
"field": "purchaser",
|
||||
"op": "LIKE",
|
||||
"var": "purchaser_input"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
|
||||
|
||||
@ -1,12 +1,8 @@
|
||||
|
||||
ns = params_kw.copy()
|
||||
for k,v in list(ns.items()):
|
||||
if v == 'NaN' or v == 'null' or v == '' or v is None:
|
||||
if k != 'id':
|
||||
del ns[k]
|
||||
else:
|
||||
ns[k] = None if v in ('NaN', 'null') else v
|
||||
|
||||
for k,v in ns.items():
|
||||
if v == 'NaN' or v == 'null':
|
||||
ns[k] = None
|
||||
|
||||
|
||||
|
||||
@ -15,11 +11,7 @@ db = DBPools()
|
||||
dbname = get_module_dbname('pipeline_bidding')
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
|
||||
tblfields = json.loads('[{"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no"}, {"name": "org_id", "title": "机构ID", "type": "str", "length": 32, "nullable": "no", "default": "0"}, {"name": "source", "title": "信息来源", "type": "str", "length": 16, "nullable": "no", "default": "mail"}, {"name": "account_id", "title": "邮箱配置ID", "type": "str", "length": 32}, {"name": "mail_uid", "title": "邮件UID", "type": "str", "length": 64}, {"name": "mail_from", "title": "发件人", "type": "str", "length": 255}, {"name": "mail_subject", "title": "邮件主题", "type": "str", "length": 500}, {"name": "received_at", "title": "收信时间", "type": "timestamp"}, {"name": "raw_content", "title": "邮件原文", "type": "text"}, {"name": "attach_json", "title": "附件清单(JSON)", "type": "text"}, {"name": "title", "title": "项目名称", "type": "str", "length": 255}, {"name": "purchaser", "title": "招标人/采购人", "type": "str", "length": 255}, {"name": "tender_no", "title": "招标编号", "type": "str", "length": 64}, {"name": "industry", "title": "行业", "type": "str", "length": 64}, {"name": "summary", "title": "内容摘要", "type": "text"}, {"name": "bid_start_at", "title": "投标开始时间", "type": "date"}, {"name": "bid_deadline", "title": "投标截止时间", "type": "date"}, {"name": "project_period", "title": "项目周期", "type": "str", "length": 64}, {"name": "budget_amount", "title": "招标金额", "type": "float", "length": 18, "dec": 2}, {"name": "deposit_amount", "title": "投标保证金", "type": "float", "length": 18, "dec": 2}, {"name": "qualification_req", "title": "资质要求摘要", "type": "text"}, {"name": "key_dates", "title": "关键时间节点(JSON)", "type": "text"}, {"name": "risk_note", "title": "风险提示", "type": "text"}, {"name": "status", "title": "状态", "type": "str", "length": 16, "nullable": "no", "default": "new"}, {"name": "approval_task_id", "title": "审批任务ID", "type": "str", "length": 32}, {"name": "project_id", "title": "立项项目ID", "type": "str", "length": 32}, {"name": "archive_reason", "title": "作废/存档原因", "type": "str", "length": 500}, {"name": "created_by", "title": "创建人", "type": "str", "length": 32}, {"name": "created_at", "title": "创建时间", "type": "timestamp", "nullable": "no"}, {"name": "updated_at", "title": "更新时间", "type": "timestamp"}]')
|
||||
tblfname = [ f['name'] for f in tblfields ]
|
||||
other_keys = [ k for k in ns.keys() if k in tblfname and k != 'id' ]
|
||||
if len(other_keys) > 0:
|
||||
r = await sor.U('bid_tenders', ns)
|
||||
r = await sor.U('bid_tenders', ns)
|
||||
debug('update success');
|
||||
return {
|
||||
"widgettype":"Message",
|
||||
|
||||
@ -7,15 +7,15 @@
|
||||
"options": {"width": "100%", "alignItems": "center", "padding": "16px 24px 8px 24px", "cheight": 6, "gap": "12px"},
|
||||
"subwidgets": [
|
||||
{"widgettype": "Title2", "options": {"text": "投标产线"}},
|
||||
{"widgettype": "Text", "options": {"text": "招标采集 → 审批 → 立项 → 解析 → 编写 → 评审 → 评分 → 交付", "cfontsize": 0.9, "color": "#94a3b8"}},
|
||||
{"widgettype": "Text", "options": {"text": "上传招标文件 → 解析 → QC契合度审核 → 编写 → 评审 → 评分 → 交付(招标信息采集/审批在商机产线)", "cfontsize": 0.9, "color": "#94a3b8"}},
|
||||
{"widgettype": "Filler"},
|
||||
{
|
||||
"widgettype": "Button",
|
||||
"options": {"label": "📨 招标信息", "css": "small"},
|
||||
"options": {"label": "📨 招标商务要素", "css": "small"},
|
||||
"binds": [{
|
||||
"wid": "self", "event": "click", "actiontype": "urlwidget",
|
||||
"target": "Popup",
|
||||
"popup_options": {"title": "招标信息", "width": "88%", "height": "82%", "auto_open": true},
|
||||
"popup_options": {"title": "招标商务要素(解析抽取)", "width": "88%", "height": "82%", "auto_open": true},
|
||||
"url": "/pipeline-bidding/bid_tenders/index.ui"
|
||||
}]
|
||||
},
|
||||
@ -28,16 +28,6 @@
|
||||
"popup_options": {"title": "公司投标知识库", "width": "88%", "height": "82%", "auto_open": true},
|
||||
"url": "/pipeline-bidding/bid_kb_docs/index.ui"
|
||||
}]
|
||||
},
|
||||
{
|
||||
"widgettype": "Button",
|
||||
"options": {"label": "✉️ 邮箱配置", "css": "small"},
|
||||
"binds": [{
|
||||
"wid": "self", "event": "click", "actiontype": "urlwidget",
|
||||
"target": "Popup",
|
||||
"popup_options": {"title": "招标邮箱配置", "width": "88%", "height": "82%", "auto_open": true},
|
||||
"url": "/pipeline-bidding/bid_mail_accounts/index.ui"
|
||||
}]
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -55,10 +45,10 @@
|
||||
"options": {"width": "100%", "padding": "16px", "bgcolor": "#ffffff", "border": "1px solid #e2e8f0", "borderRadius": "8px", "spacing": "8px"},
|
||||
"subwidgets": [
|
||||
{"widgettype": "Title3", "options": {"text": "使用流程"}},
|
||||
{"widgettype": "Text", "options": {"text": "1. 邮箱配置:录入阿里云企业邮(imap.qiye.aliyun.com:993,开启安全登录需填客户端专用密码)", "cfontsize": 0.95}},
|
||||
{"widgettype": "Text", "options": {"text": "2. 投标驾驶舱:从「开发产线」同款入口(侧栏 产线管理→驾驶舱)切换到投标项目,对话式推进;或直接说「采集招标信息」", "cfontsize": 0.95}},
|
||||
{"widgettype": "Text", "options": {"text": "3. 审批:招标信息摘要后提交审批,不通过自动存档作废;通过自动立项并发布上传招标文件任务", "cfontsize": 0.95}},
|
||||
{"widgettype": "Text", "options": {"text": "4. 解析与编写:上传招标文件后自动抽取评分项/得分规则/资质/投标文件要求 → 章节骨架 → 一章一任务自动编写与评审循环", "cfontsize": 0.95}},
|
||||
{"widgettype": "Text", "options": {"text": "1. 立项:投标驾驶舱(侧栏 产线管理→投标产线)说「创建投标项目」,设定参与人员", "cfontsize": 0.95}},
|
||||
{"widgettype": "Text", "options": {"text": "2. 上传招标文件:按待办任务上传招标文件(含答疑/补遗),上传后自动进入解析", "cfontsize": 0.95}},
|
||||
{"widgettype": "Text", "options": {"text": "3. 解析与QC:自动抽取评分项/得分规则/资质/投标文件要求/章节骨架,QC 对照原文按10分制审核契合度,高于9.5分放行,否则退回重做", "cfontsize": 0.95}},
|
||||
{"widgettype": "Text", "options": {"text": "4. 编写与评审:资质准备(知识库匹配+人工文件清单)→ 一章一任务自动编写与评审循环", "cfontsize": 0.95}},
|
||||
{"widgettype": "Text", "options": {"text": "5. 评分与交付:合成标书 → 整书按评分规则打分 → 不达标自动退回问题章节 → 通过后人工交付确认", "cfontsize": 0.95}}
|
||||
]
|
||||
},
|
||||
@ -106,6 +96,13 @@
|
||||
"binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "Popup",
|
||||
"popup_options": {"title": "评审记录", "width": "88%", "height": "82%", "auto_open": true},
|
||||
"url": "/pipeline-bidding/bid_reviews/index.ui"}]
|
||||
},
|
||||
{
|
||||
"widgettype": "Button",
|
||||
"options": {"label": "✅ 解析QC审核", "css": "small"},
|
||||
"binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "Popup",
|
||||
"popup_options": {"title": "解析产出QC契合度审核", "width": "88%", "height": "82%", "auto_open": true},
|
||||
"url": "/pipeline-bidding/bid_qc_reviews/index.ui"}]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user