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`)设计,复用平台通用内核(项目/会话/任务链/人类任务/交付件/问答冒泡/技能分层),
|
> 对照开发产线(`sdlc_general`)设计,复用平台通用内核(项目/会话/任务链/人类任务/交付件/问答冒泡/技能分层),
|
||||||
> 只增量定义投标产线自己的 **阶段、角色、工具、技能、任务流转规则、交付件**。
|
> 只增量定义投标产线自己的 **阶段、角色、工具、技能、任务流转规则、交付件**。
|
||||||
|
> 版本:v1.1.0 创建:2026-08-26 修订:2026-08-31(产线起点改为上传招标文件;解析产出加 QC 契合度审核门禁)
|
||||||
版本:v1.0.0 创建:2026-08-26
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 一、业务流程(需求原文映射)
|
## 零、产线边界(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 |
|
| 1 | 创建投标项目 + 设定参与人员 | 驾驶舱 / 人 | sd_projects(pipeline_id=bidding_general) + bid_members |
|
||||||
| 2 | 对每条招标信息内容摘要:投标开始时间、项目周期、招标金额等 | agent.tender_scout | bid_tenders.summary/bid_start_at/project_period/budget_amount |
|
| 2 | 等待上传招标文件 | 人(阻塞门禁,上传后自动完成) | pipeline_human_tasks(tender_file_upload) + bid_tender_files |
|
||||||
| 3 | 走审批流程 | 人(human task + 可选钉钉审批) | pipeline_human_tasks(task_type=tender_approval) |
|
| 3 | 抽取评分项及得分规则 | agent.tender_analyst | bid_scoring_items |
|
||||||
| 4 | 审批不通过 → 招标信息存档作废 | 机制 | bid_tenders.status=archived |
|
| 4 | 抽取所需资质 | agent.tender_analyst | bid_qualifications |
|
||||||
| 5 | 审批通过 → 创建项目 | 机制/主 agent | sd_projects(pipeline_id=bidding_general) |
|
| 5 | 抽取投标文件要求(章节结构/格式/份数/密封)+ 商务要素 | agent.tender_analyst | bid_doc_requirements + bid_tenders(source=tender_doc) |
|
||||||
| 6 | 设定参与人员 | 主 agent / 人 | bid_members |
|
| 6 | 生成章节骨架(评分项挂载) | agent.tender_analyst | bid_chapters |
|
||||||
| 7 | 等待上传招标文件 | 人(阻塞门禁) | pipeline_human_tasks(tender_file_upload) + bid_tender_files |
|
| 7 | **解析产出 QC 契合度审核(四类逐类,10 分制 >9.5 放行,否则改进意见退回重做)** | agent.qc | bid_qc_reviews |
|
||||||
| 8 | 从招标文件抽取评分项及得分规则 | agent.tender_analyst | bid_scoring_items |
|
| 8 | 从公司投标知识库获取所需资质文档 | agent.bid_prep | bid_qualifications.kb_doc_id ← bid_kb_docs |
|
||||||
| 9 | 抽取所需资质 | agent.tender_analyst | bid_qualifications |
|
| 9 | 发布人类应提供的文件清单 | agent.bid_prep(阻塞门禁) | pipeline_human_tasks(human_docs_request) |
|
||||||
| 10 | 抽取投标文件要求(章节结构/格式/份数/密封) | agent.tender_analyst | bid_doc_requirements |
|
| 10 | 按投标文件要求分章节编写投标文件 | agent.bid_writer | bid_chapters |
|
||||||
| 11 | 从公司投标知识库获取所需资质文档 | agent.bid_prep | bid_qualifications.kb_doc_id ← bid_kb_docs |
|
| 11 | 每章节完成后对照招标/投标文件要求评审,分数不够给改进意见重做 | agent.bid_reviewer | bid_reviews(scope=chapter) → 章节循环 |
|
||||||
| 12 | 发布人类应提供的文件清单 | agent.bid_prep(阻塞门禁) | pipeline_human_tasks(human_docs_request) |
|
| 12 | 合成标书 | agent.bid_compositor | bid_documents(docx) |
|
||||||
| 13 | 按投标文件要求分章节编写投标文件 | agent.bid_writer | bid_chapters |
|
| 13 | 对整个标书评审 + 按评分项和得分规则打分 | agent.bid_scorer | bid_reviews(scope=whole) + bid_scores |
|
||||||
| 14 | 每章节完成后对照招标/投标文件要求评审,分数不够给改进意见重做 | agent.bid_reviewer | bid_reviews(scope=chapter) → 章节循环 |
|
| 14 | 不合适 → 改进意见 → 修改问题章节 → 评审 → …… 直到得分通过 | 机制(整书循环) | bid_scores.target_chapter_id → 章节回 rejected |
|
||||||
| 15 | 合成标书 | agent.bid_compositor | bid_documents(docx) |
|
| 15 | 交付确认 | 人 | pipeline_human_tasks(bid_delivery_confirm) → 项目 completed |
|
||||||
| 16 | 对整个标书评审 + 按评分项和得分规则打分 | agent.bid_scorer | bid_reviews(scope=whole) + bid_scores |
|
|
||||||
| 17 | 不合适 → 改进意见 → 修改问题章节 → 评审 → …… 直到得分通过 | 机制(整书循环) | bid_scores.target_chapter_id → 章节回 rejected |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 二、阶段与角色(对照开发产线)
|
## 二、阶段与角色
|
||||||
|
|
||||||
| 开发产线 | 投标产线 | 角色规范名 | 职责 | next_role |
|
| 阶段 | 角色规范名 | 职责 |
|
||||||
|---|---|---|---|---|
|
|---|---|---|
|
||||||
| requirement 需求分析 | 招标情报 | `agent.tender_scout` | 收邮件、去重入库、摘要与要素抽取、提交审批 | `agent.tender_analyst` |
|
| 立项与上传 | 驾驶舱 + 人 | 建项目、配人、上传招标文件 |
|
||||||
| —(新增门禁) | 招标审批 | 人类任务 | 审批通过立项 / 不通过存档作废 | — |
|
| 招标文件解析 | `agent.tender_analyst` | 抽取评分项+得分规则、所需资质、投标文件要求、商务要素,产出章节骨架 |
|
||||||
| design 设计 | 招标文件解析 | `agent.tender_analyst` | 抽取评分项+得分规则、所需资质、投标文件要求,产出章节骨架 | `agent.bid_prep` |
|
| 解析产出 QC | `agent.qc` | 四类产出对照招标文件原文按 10 分制审核契合度,>9.5 放行,否则改进意见退回 |
|
||||||
| —(新增) | 资料准备 | `agent.bid_prep` | 知识库资质匹配、缺失项发布人类文件清单 | `agent.bid_writer` |
|
| 资料准备 | `agent.bid_prep` | 知识库资质匹配、缺失项发布人类文件清单 |
|
||||||
| develop 开发 | 章节编写 | `agent.bid_writer` | 按章节骨架逐章写作(一章一任务) | `agent.bid_reviewer` |
|
| 章节编写 | `agent.bid_writer` | 按章节骨架逐章写作(一章一任务) |
|
||||||
| test 测试 | 章节评审 | `agent.bid_reviewer` | 对照招标要求+得分规则逐章打分,不达标退回重写 | `agent.bid_compositor` |
|
| 章节评审 | `agent.bid_reviewer` | 对照招标要求+得分规则逐章打分,不达标退回重写 |
|
||||||
| —(新增) | 标书合成 | `agent.bid_compositor` | 合成 docx(封面/目录/页码/偏离表),产出交付件 | `agent.bid_scorer` |
|
| 标书合成 | `agent.bid_compositor` | 合成 docx(封面/目录/页码),产出交付件 |
|
||||||
| deploy 部署 | 全书评分 | `agent.bid_scorer` | 按评分项+得分规则给整书打分、生成改进意见并映射章节 | ""(终结→人类交付确认) |
|
| 全书评分 | `agent.bid_scorer` | 按评分项+得分规则给整书打分、生成改进意见并映射章节 |
|
||||||
| agent.pm | 同名复用 | `agent.pm` | 任务审核/派发/验收 | — |
|
| 任务验收 | `agent.pm` | 任务审核/验收 |
|
||||||
| agent.qc | 同名复用 | `agent.qc` | 交付件合规检查 | — |
|
| 合规检查 | `agent.qc`(复用) | 解析产出契合度审核(本产线 QC 的主职责) |
|
||||||
|
|
||||||
> 角色命名遵守平台规范:agent 角色 = `agent.{role}`,人类角色 = RBAC `{orgtype}.{role}`。
|
> 角色命名遵守平台规范: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 驱动)
|
### 3.1 主链(机制驱动,非 LLM 驱动)
|
||||||
|
|
||||||
```
|
```
|
||||||
[邮箱] --fetch_tenders--> bid_tenders(new)
|
[立项] create_bid_project → sd_projects + 「上传招标文件」人类任务(阻塞)
|
||||||
--summarize_tender--> summarized
|
↓ 上传完成(对账器自动关闭上传任务)
|
||||||
--submit_tender_approval--> pending_approval ┌── reject ──> archived(作废) ✗终止
|
agent.tender_analyst → 评分项 / 资质 / 投标文件要求 / 商务要素 / 章节骨架
|
||||||
└── approve ──> approved
|
↓
|
||||||
--create_bid_project--> sd_projects + assign_members
|
agent.qc(A2 QC 门禁)→ 四类产出逐类契合度审核(10 分制)
|
||||||
--request_tender_file--> [人类任务:上传招标文件] (阻塞)
|
│ 得分 > 9.5 → 该类放行
|
||||||
↓ 上传完成
|
│ 不通过 → 改进意见 → 清空该类产出 → 解析重做(携带意见)→ 重审
|
||||||
agent.tender_analyst → 评分项 / 资质 / 投标文件要求 / 章节骨架
|
│ 轮次超限(默认 3)→ 抛人工(qc_escalation 阻塞,逃逸阀:人工修/强制放行)
|
||||||
↓ approved (PM)
|
↓ 全部放行
|
||||||
agent.bid_prep → 知识库资质匹配 + [人类任务:应提供文件清单](阻塞)
|
agent.bid_prep → 知识库资质匹配 + [人类任务:应提供文件清单](阻塞)
|
||||||
↓ approved
|
↓
|
||||||
agent.bid_writer → 逐章写作(每章一个任务,可并行受并发上限约束)
|
agent.bid_writer → 逐章写作(每章一个任务,可并行)
|
||||||
↓ 章节 written
|
↓ 章节 written
|
||||||
agent.bid_reviewer → 章节评审打分
|
agent.bid_reviewer → 章节评审打分
|
||||||
↓ 全部章节 approved
|
↓ 全部章节 approved
|
||||||
agent.bid_compositor → 合成标书 docx
|
agent.bid_compositor → 合成标书 docx
|
||||||
↓
|
↓
|
||||||
agent.bid_scorer → 整书评分
|
agent.bid_scorer → 整书评分
|
||||||
↓ 通过
|
↓ 通过
|
||||||
[人类任务:交付确认] → 项目 completed
|
[人类任务:交付确认] → 项目 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)
|
- 触发:`review_chapter` 判定 `score / max_score < chapter_pass_ratio`(默认 0.8)
|
||||||
- 动作:章节 `status=rejected` + `review_comment` 落库 + `revise_count += 1`
|
- 动作:章节 `status=rejected` + `review_comment` 落库 + `revise_count += 1`
|
||||||
- 机制:`bid_reviewer` 任务 approved 后,若存在 rejected 章节 → 自动派生 `agent.bid_writer` 重写任务(任务标题带章节号,params 带 `chapter_id` + `improvement`)
|
- 机制:存在 rejected 章节且无在办编写任务 → 对账器自动派生 `agent.bid_writer` 重写任务(params 带 `chapter_id`)
|
||||||
- 上限:`revise_count > bid_max_revise`(默认 3)→ 抛人类介入(fault_report + 人类任务),不无限重做
|
- 上限:`revise_count > bid_max_revise`(默认 3)→ 抛人类介入,不无限重做
|
||||||
|
|
||||||
### 3.3 循环二:整书评分回退
|
### 3.4 循环二:整书评分回退
|
||||||
|
|
||||||
- 触发:`score_bid` 总分率 `< bid_pass_ratio`(默认 0.85),或存在否决项(`is_veto`)未响应
|
- 触发:`score_bid` 总分率 `< bid_pass_ratio`(默认 0.85),或存在否决项(`is_veto`)未响应
|
||||||
- 动作:`bid_scores` 逐评分项落分 + `improvement` + `target_chapter_id`
|
- 动作:`bid_scores` 逐评分项落分 + `improvement` + `target_chapter_id`
|
||||||
- 机制:把 `target_chapter_id` 命中的章节置 `rejected` → 派生 `bid_writer` 任务 → 重写 → 章节评审 → 全部通过 → 重新合成 → 重新评分
|
- 机制:把 `target_chapter_id` 命中的章节置 `rejected` → 派生 `bid_writer` 任务 → 重写 → 章节评审 → 全部通过 → 重新合成 → 重新评分
|
||||||
- 轮次记录 `bid_reviews.round`,上限 `bid_max_round`(默认 3)→ 抛人类介入
|
- 轮次记录 `bid_reviews.round`,上限 `bid_max_round`(默认 3)→ 抛人类介入
|
||||||
|
- 改进意见未映射到章节 → 抛人工指定(不静默丢失)
|
||||||
|
|
||||||
### 3.4 阻塞门禁(人类任务)
|
### 3.5 阻塞门禁(人类任务)
|
||||||
|
|
||||||
| task_type | 语义 | 阻塞范围 |
|
| task_type | 语义 | 阻塞范围 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `tender_approval` | 招标信息审批 | 该 tender 不立项、不推进 |
|
| `tender_file_upload` | 等待上传招标文件(上传后自动完成) | 项目全部角色任务 |
|
||||||
| `tender_file_upload` | 等待上传招标文件 | 项目全部角色任务 |
|
|
||||||
| `human_docs_request` | 人类应提供的文件 | 项目全部角色任务 |
|
| `human_docs_request` | 人类应提供的文件 | 项目全部角色任务 |
|
||||||
| `bid_delivery_confirm` | 最终标书交付确认 | 项目收尾 |
|
| `qc_escalation` | 解析产出 QC 连续不达标 | 项目全部角色任务 |
|
||||||
|
| `bid_delivery_confirm` | 最终标书交付确认 | 项目收尾(不阻塞派发) |
|
||||||
|
|
||||||
**门禁的守门位置:投标产线的角色任务唯一创建者是 `bid_flow` 对账器**(每 15 秒状态对账)。
|
**门禁的守门位置:投标产线的角色任务唯一创建者是 `bid_flow` 对账器**(每 15 秒状态对账)。
|
||||||
对账器在创建任务前就检查 `pipeline_human_tasks.project_id = <项目id> AND status='pending'`,
|
对账器在创建任务前就检查 `pipeline_human_tasks.project_id = <项目id> AND status='pending'`,
|
||||||
有阻塞任务直接不派 —— 门禁在**创建源头**,不在引擎派发层。
|
有阻塞任务直接不派 —— 门禁在**创建源头**,不在引擎派发层。
|
||||||
|
|
||||||
> ⚠️ 概念区分(重要):`tenant_id` 是引擎的通用租户键(宿主可提供为 org_id / user_id 等)。
|
> ⚠️ 概念区分(重要):`tenant_id` 是引擎的通用租户键(宿主可提供为 org_id / user_id 等)。
|
||||||
> **只有角色任务(pipeline_id='role_task')这个具体场景约定 `tenant_id = project_id`**
|
> **只有角色任务(pipeline_id='role_task')这个具体场景约定 `tenant_id = project_id`**。
|
||||||
> (与开发产线的既有任务创建约定一致,`_create_next_task` 同款)。
|
|
||||||
> 这是角色任务派发机制的约定,不是引擎通用规则 —— 引擎层 SQL 不得引用该等价关系,
|
> 这是角色任务派发机制的约定,不是引擎通用规则 —— 引擎层 SQL 不得引用该等价关系,
|
||||||
> 引擎对投标产线零特判;投标的阻塞语义全部收敛在投标产线自己的代码(bid_flow)里。
|
> 引擎对投标产线零特判;投标的阻塞语义全部收敛在投标产线自己的代码(bid_flow)里。
|
||||||
|
|
||||||
### 3.5 硬规则
|
### 3.6 硬规则
|
||||||
|
|
||||||
- 审批不通过 = 终态(archived),不得再进入立项;需重新评估须人工 `reopen_tender`。
|
- 章节写作任务与章节一对一(`params.chapter_id`),禁止一个任务写多章。
|
||||||
- 章节写作任务与章节一对一(`params.chapter_id`),禁止一个任务写多章(对照开发产线「单任务不要过大」)。
|
- 评审/评分只由 `agent.bid_reviewer` / `agent.bid_scorer` 执行,写作角色不得自评;
|
||||||
- 评审/评分只由 `agent.bid_reviewer` / `agent.bid_scorer` 执行,写作角色不得自评(源头杜绝越权:capability 按角色缩小)。
|
解析产出契合度审核只由 `agent.qc` 执行(capability 按角色缩小,源头杜绝越权)。
|
||||||
- 得分/改进意见必须落库(`bid_reviews`/`bid_scores`),不接受"文档里声称评审通过"。
|
- 得分/改进意见必须落库(`bid_reviews`/`bid_scores`/`bid_qc_reviews`),不接受"文档里声称评审通过"。
|
||||||
- 案例与业绩一致性:技术方案引用的案例必须能对应到 `bid_kb_docs(doc_type=case)` 真实合同,禁止虚构客户/金额(写进 bid-doc-spec 技能,评审项硬检查)。
|
- 案例与业绩一致性:技术方案引用的案例必须能对应到 `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` | 招标商务要素(解析抽取) | project_id/source/title/purchaser/tender_no/summary/bid_start_at/bid_deadline/project_period/budget_amount/deposit_amount/qualification_req |
|
||||||
| `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_members` | 项目参与人员 | project_id/user_id/member_role/duty |
|
| `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_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/weight/source_ref |
|
| `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_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_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_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_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_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 |
|
| `bid_documents` | 合成标书 | project_id/version/file_path/page_count/total_score/status |
|
||||||
|
|
||||||
复用平台表:`sd_projects`(项目,pipeline_id=bidding_general)、`pipeline_tasks`(角色任务链)、
|
复用平台表:`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`
|
### 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`
|
**立项与人员**:`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`
|
**知识库**:`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`
|
**通用复用**(引擎级,来自 shared_ability):`create_task` `list_tasks` `task_detail` `approve_task`
|
||||||
`complete_task` `cancel_task` `reset_task_retry` `start_agents` `diagnose_project`
|
`complete_task` `cancel_task` `reset_task_retry` `start_agents` `diagnose_project`
|
||||||
@ -170,16 +197,16 @@
|
|||||||
|
|
||||||
| capability | 工具 | 授予角色 |
|
| capability | 工具 | 授予角色 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `bid_tender_capability` | `fetch_tenders` `summarize_tender` `set_tender_fields` `submit_tender_approval` `archive_tender` `list_tenders` | agent.tender_scout |
|
| `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_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_qc_capability` | `start_qc` `finish_qc` `list_qc` `qc_report`(+ 只读工具) | agent.qc |
|
||||||
| `bid_kb_capability` | `search_bid_kb` `match_qualification` `mark_qualification_missing` `request_human_docs` `list_qualifications` | agent.bid_prep |
|
| `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` `list_scoring_items` `search_bid_kb` | agent.bid_writer |
|
| `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` `reject_chapter` `approve_chapter` `list_chapters` `list_scoring_items` `list_doc_requirements` | agent.bid_reviewer |
|
| `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_doc_requirements` | agent.bid_compositor |
|
| `bid_compose_capability` | `compose_bid` `list_chapters` `list_bid_documents` `read_bid_document` `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_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`。
|
> capability 按角色职责缩小 —— 写作角色拿不到 `review_chapter`/`score_bid`,评审角色拿不到 `write_chapter`,
|
||||||
> 越权在源头杜绝,不靠事后 RBAC 校验。
|
> 解析角色拿不到 `finish_qc`,QC 拿不到 `extract_from_file`。越权在源头杜绝,不靠事后 RBAC 校验。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -188,8 +215,8 @@
|
|||||||
### common/(产线通用规范,状态机 + 门禁)
|
### common/(产线通用规范,状态机 + 门禁)
|
||||||
| 技能 | 内容 |
|
| 技能 | 内容 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `bid-workflow` | 产线总纲:阶段链、门禁、两级循环、回退语义、机制 vs LLM 分界 |
|
| `bid-workflow` | 产线总纲:阶段链、QC 门禁、两级循环、阻塞门禁、机制 vs LLM 分界 |
|
||||||
| `tender` | 招标信息状态机:new→summarized→pending_approval→approved/rejected→archived |
|
| `bid-qc` | 解析产出 QC 契合度审核规范:四类审核清单、10 分制打分口径、退回重做流程(capability 声明) |
|
||||||
| `bid-chapter` | 章节状态机 + 评审循环 + 重做上限 + 一章一任务 |
|
| `bid-chapter` | 章节状态机 + 评审循环 + 重做上限 + 一章一任务 |
|
||||||
| `bid-review` | 评审与评分规范:对照评分项/得分规则打分、改进意见→章节映射、否决项处理 |
|
| `bid-review` | 评审与评分规范:对照评分项/得分规则打分、改进意见→章节映射、否决项处理 |
|
||||||
| `bid-qualification` | 资质匹配规范:知识库检索 → matched/missing/human_required → 人类文件清单 |
|
| `bid-qualification` | 资质匹配规范:知识库检索 → matched/missing/human_required → 人类文件清单 |
|
||||||
@ -197,8 +224,8 @@
|
|||||||
| `team-communication` | 问答冒泡链(复用开发产线同名规范) |
|
| `team-communication` | 问答冒泡链(复用开发产线同名规范) |
|
||||||
|
|
||||||
### roles/(角色定义,frontmatter 声明 capability)
|
### roles/(角色定义,frontmatter 声明 capability)
|
||||||
`agent.tender_scout` `agent.tender_analyst` `agent.bid_prep` `agent.bid_writer`
|
`agent.tender_analyst` `agent.bid_prep` `agent.bid_writer` `agent.bid_reviewer`
|
||||||
`agent.bid_reviewer` `agent.bid_compositor` `agent.bid_scorer` `agent.pm` `agent.qc`
|
`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 / bid_flow / bid_common) | pipeline-bidding 仓库 | 产线能力包住自己仓库,宿主不装零接触 |
|
||||||
| 角色能力工具 `bid_*_capability.py` | pipeline-service | `exec_capability_tool` 只 import `pipeline_service.*` |
|
| 主 agent 能力包 `bid_ability.py` | pipeline-bidding 仓库 | 与能力模块同仓;ability 注册表在 core,任何模块可注册 |
|
||||||
| 邮箱采集 `bid_mail_fetcher.py` | pipeline-service | 被能力工具调用 |
|
| 表 / CRUD / UI / dspy / RBAC | pipeline-bidding 仓库 | 产线业务资产独立仓库,宿主 load_pipeline_bidding() 装载 |
|
||||||
| 通用任务 handler 汇总 `shared_ability.py` | pipeline-service | 复用而非重写 SDLC 的通用任务/交付件/问答 handler |
|
|
||||||
| 表 / CRUD / UI / dspy / RBAC | pipeline-bidding(新模块仓库) | 产线业务资产独立仓库,宿主 load_pipeline_bidding() 装载 |
|
|
||||||
| 技能 | pipeline-core/skills_library/pipelines/bidding_general | 技能库统一目录,部署期同步到机构工作空间 |
|
| 技能 | 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 要素并落库。
|
| `bid_chapter_pass_ratio` | 0.8 | 章节评审通过门限(得分率) |
|
||||||
4. 审批不通过 → tender 状态 archived;通过 → sd_projects 出现 pipeline_id=bidding_general 的项目。
|
| `bid_pass_ratio` | 0.85 | 整书评分通过门限(得分率) |
|
||||||
5. 上传招标文件人类任务 pending 时,poller 不派发该项目 agent 任务(项目级门禁生效)。
|
| `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 可查)。
|
6. 章节评审不达标 → 章节 rejected + 自动派生重写任务(DB 可查)。
|
||||||
7. 整书评分不达标 → bid_scores 有 improvement + target_chapter_id,命中章节回 rejected。
|
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
|
投标产线: Bidding Pipeline
|
||||||
招标信息台: Tender Inbox
|
投标驾驶舱: Bidding Cockpit
|
||||||
邮箱配置: Mail Account Config
|
招标商务要素: Tender Business Facts
|
||||||
投标知识库: Bid Knowledge Base
|
投标知识库: Bid Knowledge Base
|
||||||
评分项: Scoring Items
|
评分项: Scoring Items
|
||||||
标书章节: Bid Chapters
|
标书章节: Bid Chapters
|
||||||
合成标书: Assembled Bid Document
|
合成标书: Assembled Bid Document
|
||||||
采集招标信息: Fetch Tender Notices
|
创建投标项目: Create Bid Project
|
||||||
提交审批: Submit for Approval
|
|
||||||
审批通过: Approved
|
|
||||||
审批不通过: Rejected
|
|
||||||
存档作废: Archived / Voided
|
|
||||||
立项: Create Project
|
|
||||||
参与人员: Team Members
|
参与人员: Team Members
|
||||||
上传招标文件: Upload Tender Document
|
上传招标文件: Upload Tender Document
|
||||||
|
招标文件解析: Tender Document Parsing
|
||||||
|
解析QC审核: Parsing QC Review
|
||||||
|
契合度得分: Fit Score
|
||||||
|
通过分: Pass Score
|
||||||
|
改进意见: Improvement Suggestions
|
||||||
资质要求: Qualification Requirements
|
资质要求: Qualification Requirements
|
||||||
投标文件要求: Bid Document Requirements
|
投标文件要求: Bid Document Requirements
|
||||||
章节评审: Chapter Review
|
章节评审: Chapter Review
|
||||||
整书评分: Whole-document Scoring
|
整书评分: Whole-document Scoring
|
||||||
改进意见: Improvement Suggestions
|
|
||||||
得分规则: Scoring Rules
|
得分规则: Scoring Rules
|
||||||
评审通过: Review Passed
|
评审通过: Review Passed
|
||||||
退回重写: Returned for Rewrite
|
退回重写: Returned for Rewrite
|
||||||
|
|||||||
@ -1,23 +1,22 @@
|
|||||||
招标信息: 招标信息
|
投标产线: 投标产线
|
||||||
招标信息台: 招标信息台
|
投标驾驶舱: 投标驾驶舱
|
||||||
邮箱配置: 邮箱配置
|
招标商务要素: 招标商务要素
|
||||||
投标知识库: 投标知识库
|
投标知识库: 投标知识库
|
||||||
评分项: 评分项
|
评分项: 评分项
|
||||||
标书章节: 标书章节
|
标书章节: 标书章节
|
||||||
合成标书: 合成标书
|
合成标书: 合成标书
|
||||||
采集招标信息: 采集招标信息
|
创建投标项目: 创建投标项目
|
||||||
提交审批: 提交审批
|
|
||||||
审批通过: 审批通过
|
|
||||||
审批不通过: 审批不通过
|
|
||||||
存档作废: 存档作废
|
|
||||||
立项: 立项
|
|
||||||
参与人员: 参与人员
|
参与人员: 参与人员
|
||||||
上传招标文件: 上传招标文件
|
上传招标文件: 上传招标文件
|
||||||
|
招标文件解析: 招标文件解析
|
||||||
|
解析QC审核: 解析QC审核
|
||||||
|
契合度得分: 契合度得分
|
||||||
|
通过分: 通过分
|
||||||
|
改进意见: 改进意见
|
||||||
资质要求: 资质要求
|
资质要求: 资质要求
|
||||||
投标文件要求: 投标文件要求
|
投标文件要求: 投标文件要求
|
||||||
章节评审: 章节评审
|
章节评审: 章节评审
|
||||||
整书评分: 整书评分
|
整书评分: 整书评分
|
||||||
改进意见: 改进意见
|
|
||||||
得分规则: 得分规则
|
得分规则: 得分规则
|
||||||
评审通过: 评审通过
|
评审通过: 评审通过
|
||||||
退回重写: 退回重写
|
退回重写: 退回重写
|
||||||
|
|||||||
@ -4,29 +4,27 @@
|
|||||||
"id": "bidding_general",
|
"id": "bidding_general",
|
||||||
"name": "通用投标产线",
|
"name": "通用投标产线",
|
||||||
"pipeline_type": "bidding",
|
"pipeline_type": "bidding",
|
||||||
"description": "招标信息采集→内容摘要→审批→立项与人员→招标文件解析(评分项/得分规则/资质/投标文件要求)→资质准备→分章节编写→章节评审→合成标书→整书评分→交付",
|
"description": "上传招标文件→解析(评分项/得分规则/资质/投标文件要求/章节骨架)→QC契合度审核→资质准备→分章节编写→章节评审→合成标书→整书评分→交付。招标信息的采集/摘要/审批归商机产线。",
|
||||||
"status": "published",
|
"status": "published",
|
||||||
"org_id": "0"
|
"org_id": "0"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"appcodes": [
|
"appcodes": [
|
||||||
{
|
{
|
||||||
"parentid": "bid_tender_status",
|
"parentid": "bid_qc_type",
|
||||||
"parentname": "招标信息状态",
|
"parentname": "QC审核对象",
|
||||||
"items": [
|
"items": [
|
||||||
{"k": "new", "v": "新采集"},
|
{"k": "scoring_items", "v": "评分项+得分规则"},
|
||||||
{"k": "summarized", "v": "已摘要"},
|
{"k": "qualifications", "v": "所需资质"},
|
||||||
{"k": "pending_approval", "v": "待审批"},
|
{"k": "doc_requirements", "v": "投标文件要求"},
|
||||||
{"k": "approved", "v": "审批通过"},
|
{"k": "chapter_outline", "v": "章节骨架"}
|
||||||
{"k": "rejected", "v": "审批不通过"},
|
|
||||||
{"k": "archived", "v": "存档作废"}
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parentid": "bid_tender_source",
|
"parentid": "bid_tender_source",
|
||||||
"parentname": "招标信息来源",
|
"parentname": "招标信息来源",
|
||||||
"items": [
|
"items": [
|
||||||
{"k": "mail", "v": "邮件采集"},
|
{"k": "tender_doc", "v": "招标文件解析"},
|
||||||
{"k": "manual", "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",
|
"tblname": "bid_tenders",
|
||||||
"title": "招标信息",
|
"title": "招标商务要素",
|
||||||
"params": {
|
"params": {
|
||||||
"sortby": [
|
"sortby": [
|
||||||
"received_at desc"
|
"created_at desc"
|
||||||
],
|
],
|
||||||
"confidential_fields": [],
|
"confidential_fields": [],
|
||||||
"browserfields": {
|
"browserfields": {
|
||||||
"exclouded": [
|
"exclouded": [
|
||||||
"raw_content",
|
|
||||||
"attach_json",
|
|
||||||
"key_dates",
|
"key_dates",
|
||||||
"org_id",
|
"org_id",
|
||||||
"account_id",
|
|
||||||
"mail_uid",
|
|
||||||
"risk_note"
|
"risk_note"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -21,7 +17,6 @@
|
|||||||
"id",
|
"id",
|
||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
"approval_task_id",
|
|
||||||
"project_id"
|
"project_id"
|
||||||
],
|
],
|
||||||
"editable": {
|
"editable": {
|
||||||
@ -37,9 +32,9 @@
|
|||||||
"var": "title_input"
|
"var": "title_input"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"field": "status",
|
"field": "project_id",
|
||||||
"op": "=",
|
"op": "=",
|
||||||
"var": "status_input"
|
"var": "project_id"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"field": "purchaser",
|
"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": [
|
"summary": [
|
||||||
{
|
{
|
||||||
"name": "bid_tenders",
|
"name": "bid_tenders",
|
||||||
"title": "招标信息",
|
"title": "招标商务要素",
|
||||||
"primary": [
|
"primary": [
|
||||||
"id"
|
"id"
|
||||||
],
|
],
|
||||||
@ -26,51 +26,18 @@
|
|||||||
"default": "0"
|
"default": "0"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "source",
|
"name": "project_id",
|
||||||
"title": "信息来源",
|
"title": "项目ID",
|
||||||
"type": "str",
|
|
||||||
"length": 16,
|
|
||||||
"nullable": "no",
|
|
||||||
"default": "mail"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "account_id",
|
|
||||||
"title": "邮箱配置ID",
|
|
||||||
"type": "str",
|
"type": "str",
|
||||||
"length": 32
|
"length": 32
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "mail_uid",
|
"name": "source",
|
||||||
"title": "邮件UID",
|
"title": "来源",
|
||||||
"type": "str",
|
"type": "str",
|
||||||
"length": 64
|
"length": 16,
|
||||||
},
|
"nullable": "no",
|
||||||
{
|
"default": "tender_doc"
|
||||||
"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",
|
"name": "title",
|
||||||
@ -98,18 +65,20 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "summary",
|
"name": "summary",
|
||||||
"title": "内容摘要",
|
"title": "项目概况摘要",
|
||||||
"type": "text"
|
"type": "text"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "bid_start_at",
|
"name": "bid_start_at",
|
||||||
"title": "投标开始时间",
|
"title": "投标开始时间",
|
||||||
"type": "date"
|
"type": "str",
|
||||||
|
"length": 16
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "bid_deadline",
|
"name": "bid_deadline",
|
||||||
"title": "投标截止时间",
|
"title": "投标截止时间",
|
||||||
"type": "date"
|
"type": "str",
|
||||||
|
"length": 16
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "project_period",
|
"name": "project_period",
|
||||||
@ -121,14 +90,14 @@
|
|||||||
"name": "budget_amount",
|
"name": "budget_amount",
|
||||||
"title": "招标金额",
|
"title": "招标金额",
|
||||||
"type": "float",
|
"type": "float",
|
||||||
"length": 18,
|
"length": 14,
|
||||||
"dec": 2
|
"dec": 2
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "deposit_amount",
|
"name": "deposit_amount",
|
||||||
"title": "投标保证金",
|
"title": "投标保证金",
|
||||||
"type": "float",
|
"type": "float",
|
||||||
"length": 18,
|
"length": 14,
|
||||||
"dec": 2
|
"dec": 2
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -138,7 +107,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "key_dates",
|
"name": "key_dates",
|
||||||
"title": "关键时间节点(JSON)",
|
"title": "关键节点(JSON)",
|
||||||
"type": "text"
|
"type": "text"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -151,32 +120,13 @@
|
|||||||
"title": "状态",
|
"title": "状态",
|
||||||
"type": "str",
|
"type": "str",
|
||||||
"length": 16,
|
"length": 16,
|
||||||
"nullable": "no",
|
"default": "summarized"
|
||||||
"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",
|
"name": "created_by",
|
||||||
"title": "创建人",
|
"title": "创建人",
|
||||||
"type": "str",
|
"type": "str",
|
||||||
"length": 32
|
"length": 64
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "created_at",
|
"name": "created_at",
|
||||||
@ -198,20 +148,6 @@
|
|||||||
"org_id"
|
"org_id"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "idx_bt_status",
|
|
||||||
"idxtype": "index",
|
|
||||||
"idxfields": [
|
|
||||||
"status"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "idx_bt_mailuid",
|
|
||||||
"idxtype": "index",
|
|
||||||
"idxfields": [
|
|
||||||
"mail_uid"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "idx_bt_project",
|
"name": "idx_bt_project",
|
||||||
"idxtype": "index",
|
"idxtype": "index",
|
||||||
@ -228,13 +164,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"codes": [
|
"codes": [
|
||||||
{
|
|
||||||
"field": "status",
|
|
||||||
"table": "appcodes_kv",
|
|
||||||
"valuefield": "k",
|
|
||||||
"textfield": "v",
|
|
||||||
"cond": "parentid='bid_tender_status'"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"field": "source",
|
"field": "source",
|
||||||
"table": "appcodes_kv",
|
"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
|
-- /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字符
|
-- 建库时请用以下语句,支持emoji字符
|
||||||
-- CREATE DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
-- CREATE DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
drop table if exists bid_tenders;
|
drop table if exists bid_tenders;
|
||||||
@ -311,32 +264,23 @@ CREATE TABLE bid_tenders
|
|||||||
|
|
||||||
`id` VARCHAR(32) NOT NULL comment '主键ID',
|
`id` VARCHAR(32) NOT NULL comment '主键ID',
|
||||||
`org_id` VARCHAR(32) NOT NULL DEFAULT '0' comment '机构ID',
|
`org_id` VARCHAR(32) NOT NULL DEFAULT '0' comment '机构ID',
|
||||||
`source` VARCHAR(16) NOT NULL DEFAULT 'mail' comment '信息来源',
|
`project_id` VARCHAR(32) comment '项目ID',
|
||||||
`account_id` VARCHAR(32) comment '邮箱配置ID',
|
`source` VARCHAR(16) NOT NULL DEFAULT 'tender_doc' comment '来源',
|
||||||
`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)',
|
|
||||||
`title` VARCHAR(255) comment '项目名称',
|
`title` VARCHAR(255) comment '项目名称',
|
||||||
`purchaser` VARCHAR(255) comment '招标人/采购人',
|
`purchaser` VARCHAR(255) comment '招标人/采购人',
|
||||||
`tender_no` VARCHAR(64) comment '招标编号',
|
`tender_no` VARCHAR(64) comment '招标编号',
|
||||||
`industry` VARCHAR(64) comment '行业',
|
`industry` VARCHAR(64) comment '行业',
|
||||||
`summary` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci comment '内容摘要',
|
`summary` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci comment '项目概况摘要',
|
||||||
`bid_start_at` date comment '投标开始时间',
|
`bid_start_at` VARCHAR(16) comment '投标开始时间',
|
||||||
`bid_deadline` date comment '投标截止时间',
|
`bid_deadline` VARCHAR(16) comment '投标截止时间',
|
||||||
`project_period` VARCHAR(64) comment '项目周期',
|
`project_period` VARCHAR(64) comment '项目周期',
|
||||||
`budget_amount` double(18,2) comment '招标金额',
|
`budget_amount` double(14,2) comment '招标金额',
|
||||||
`deposit_amount` double(18,2) comment '投标保证金',
|
`deposit_amount` double(14,2) comment '投标保证金',
|
||||||
`qualification_req` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci 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 '风险提示',
|
`risk_note` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci comment '风险提示',
|
||||||
`status` VARCHAR(16) NOT NULL DEFAULT 'new' comment '状态',
|
`status` VARCHAR(16) DEFAULT 'summarized' comment '状态',
|
||||||
`approval_task_id` VARCHAR(32) comment '审批任务ID',
|
`created_by` VARCHAR(64) comment '创建人',
|
||||||
`project_id` VARCHAR(32) comment '立项项目ID',
|
|
||||||
`archive_reason` VARCHAR(500) comment '作废/存档原因',
|
|
||||||
`created_by` VARCHAR(32) comment '创建人',
|
|
||||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL comment '创建时间',
|
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL comment '创建时间',
|
||||||
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP comment '更新时间'
|
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP comment '更新时间'
|
||||||
|
|
||||||
@ -348,12 +292,10 @@ CREATE TABLE bid_tenders
|
|||||||
CHARACTER SET utf8mb4
|
CHARACTER SET utf8mb4
|
||||||
COLLATE utf8mb4_unicode_ci
|
COLLATE utf8mb4_unicode_ci
|
||||||
engine=innodb
|
engine=innodb
|
||||||
comment '招标信息'
|
comment '招标商务要素'
|
||||||
;
|
;
|
||||||
|
|
||||||
CREATE INDEX bid_tenders_idx_bt_org ON bid_tenders(org_id);
|
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_project ON bid_tenders(project_id);
|
||||||
CREATE INDEX bid_tenders_idx_bt_deadline ON bid_tenders(bid_deadline);
|
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_project ON bid_qualifications(project_id);
|
||||||
CREATE INDEX bid_qualifications_idx_bq_match ON bid_qualifications(match_status);
|
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"),
|
parameters={"req_type": "类型(可选 structure/format/seal/copies/deadline)"}, category="analysis"),
|
||||||
ToolDefinition(name="list_qualifications", description="查看招标所需资质及匹配状态(matched/missing/human_required)",
|
ToolDefinition(name="list_qualifications", description="查看招标所需资质及匹配状态(matched/missing/human_required)",
|
||||||
parameters={"match_status": "匹配状态(可选)"}, category="analysis"),
|
parameters={"match_status": "匹配状态(可选)"}, category="analysis"),
|
||||||
|
ToolDefinition(name="qc_report", description="查看解析产出 QC 契合度审核总览(评分项/资质/投标文件要求/章节骨架各类的最新得分与放行状态)",
|
||||||
|
parameters={}, category="analysis"),
|
||||||
|
|
||||||
# ── 知识库 ──
|
# ── 知识库 ──
|
||||||
ToolDefinition(name="search_bid_kb", description="检索公司投标知识库(资质证书/业绩案例/模板/财务/人员)",
|
ToolDefinition(name="search_bid_kb", description="检索公司投标知识库(资质证书/业绩案例/模板/财务/人员)",
|
||||||
@ -75,10 +77,14 @@ BID_TOOLS = [
|
|||||||
|
|
||||||
BID_PROMPT = """你是「投标产线」的驾驶舱 agent,负责从上传招标文件到标书交付的全流程管理:
|
BID_PROMPT = """你是「投标产线」的驾驶舱 agent,负责从上传招标文件到标书交付的全流程管理:
|
||||||
创建投标项目 → 上传招标文件 → 解析(评分项/得分规则/资质/投标文件要求/章节骨架)
|
创建投标项目 → 上传招标文件 → 解析(评分项/得分规则/资质/投标文件要求/章节骨架)
|
||||||
→ 资质文档准备 → 分章节编写 → 章节评审 → 合成标书 → 整书评分 → 交付确认。
|
→ 解析产出 QC 契合度审核 → 资质文档准备 → 分章节编写 → 章节评审 → 合成标书 → 整书评分 → 交付确认。
|
||||||
|
|
||||||
|
招标信息的采集/摘要/审批归商机产线,本产线从「上传招标文件」起步;用户要采集招标信息时说明去商机产线。
|
||||||
|
|
||||||
## 流转由机制驱动(重要)
|
## 流转由机制驱动(重要)
|
||||||
角色任务由系统状态对账器自动派发(每 15 秒一轮),你**不需要**手动为每个章节创建任务:
|
角色任务由系统状态对账器自动派发(每 15 秒一轮),你**不需要**手动为每个章节创建任务:
|
||||||
|
- 解析产出须经 QC 契合度审核:评分标准/得分规则/资质/投标文件要求/章节骨架逐类对照招标文件原文
|
||||||
|
按 10 分制打分,得分高于通过分(默认 9.5)才放行;不通过给改进意见 → 清空产出 → 解析重做 → 重审
|
||||||
- 章节评审不达标 → 章节自动退回重写
|
- 章节评审不达标 → 章节自动退回重写
|
||||||
- 整书评分不达标 → 改进意见自动落到问题章节并退回重写 → 重新合成 → 重新评分
|
- 整书评分不达标 → 改进意见自动落到问题章节并退回重写 → 重新合成 → 重新评分
|
||||||
你的职责是:立项与配人、推动上传招标文件、处理卡点与提问、回答评审/评分中的人类决策。
|
你的职责是:立项与配人、推动上传招标文件、处理卡点与提问、回答评审/评分中的人类决策。
|
||||||
@ -88,11 +94,13 @@ BID_PROMPT = """你是「投标产线」的驾驶舱 agent,负责从上传招
|
|||||||
以下人类任务 pending 时,系统不会给该项目派发任何 agent 任务:
|
以下人类任务 pending 时,系统不会给该项目派发任何 agent 任务:
|
||||||
- 等待上传招标文件(tender_file_upload,上传后自动完成)
|
- 等待上传招标文件(tender_file_upload,上传后自动完成)
|
||||||
- 等待人类提供资质文件(human_docs_request)
|
- 等待人类提供资质文件(human_docs_request)
|
||||||
遇到项目"不动了",先 diagnose_bid 看是不是卡在这两个门禁上,缺什么就催人补什么,不要硬推。
|
- 解析产出 QC 连续不达标(qc_escalation,人工修正产出或在审核记录页强制放行)
|
||||||
|
遇到项目"不动了",先 diagnose_bid 看是不是卡在门禁上,缺什么就催人补什么,不要硬推。
|
||||||
|
|
||||||
## 典型场景
|
## 典型场景
|
||||||
- 用户说「创建一个投标项目,叫 XXX」→ create_bid_project(name=XXX),随后提示上传招标文件。
|
- 用户说「创建一个投标项目,叫 XXX」→ create_bid_project(name=XXX),随后提示上传招标文件。
|
||||||
- 用户问「招标文件上传了吗 / 解析出来了吗」→ list_tender_files 看上传与抽取状态。
|
- 用户问「招标文件上传了吗 / 解析出来了吗」→ list_tender_files 看上传与抽取状态。
|
||||||
|
- 用户问「解析质量怎么样 / QC 过了吗」→ qc_report,讲清哪类产出通过、哪类还在改。
|
||||||
- 用户问进展 → diagnose_bid(含章节分布、在办任务、最近评分),不要只报"正在进行"。
|
- 用户问进展 → diagnose_bid(含章节分布、在办任务、最近评分),不要只报"正在进行"。
|
||||||
- 用户问「能得多少分 / 差在哪」→ bid_score_report + list_reviews,讲清失分项与改进意见落到了哪个章节。
|
- 用户问「能得多少分 / 差在哪」→ bid_score_report + list_reviews,讲清失分项与改进意见落到了哪个章节。
|
||||||
- 用户要补资质/案例 → add_kb_doc 录入公司投标知识库;知识库缺的用 request_human_docs 让人工提供。
|
- 用户要补资质/案例 → add_kb_doc 录入公司投标知识库;知识库缺的用 request_human_docs 让人工提供。
|
||||||
@ -298,6 +306,14 @@ async def _h_request_human_docs(sor, p, ctx):
|
|||||||
return _fmt(ok, msg)
|
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):
|
async def _h_list_chapters(sor, p, ctx):
|
||||||
pid, err = _need_project(ctx)
|
pid, err = _need_project(ctx)
|
||||||
if err:
|
if err:
|
||||||
@ -364,6 +380,7 @@ BID_HANDLERS = {
|
|||||||
"list_scoring_items": _h_list_scoring_items,
|
"list_scoring_items": _h_list_scoring_items,
|
||||||
"list_doc_requirements": _h_list_doc_requirements,
|
"list_doc_requirements": _h_list_doc_requirements,
|
||||||
"list_qualifications": _h_list_qualifications,
|
"list_qualifications": _h_list_qualifications,
|
||||||
|
"qc_report": _h_qc_report,
|
||||||
"search_bid_kb": _h_search_bid_kb,
|
"search_bid_kb": _h_search_bid_kb,
|
||||||
"list_kb_docs": _h_list_kb_docs,
|
"list_kb_docs": _h_list_kb_docs,
|
||||||
"add_kb_doc": _h_add_kb_doc,
|
"add_kb_doc": _h_add_kb_doc,
|
||||||
@ -407,6 +424,8 @@ def register_bid_ability():
|
|||||||
"type": "popup", "width": "88%", "height": "82%", "require_project": True},
|
"type": "popup", "width": "88%", "height": "82%", "require_project": True},
|
||||||
{"label": "📑 评分项", "icon": "", "url": "/pipeline-bidding/bid_scoring_items/index.ui",
|
{"label": "📑 评分项", "icon": "", "url": "/pipeline-bidding/bid_scoring_items/index.ui",
|
||||||
"type": "popup", "width": "88%", "height": "82%", "require_project": True},
|
"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",
|
{"label": "📝 标书章节", "icon": "", "url": "/pipeline-bidding/bid_chapters/index.ui",
|
||||||
"type": "popup", "width": "88%", "height": "82%", "require_project": True},
|
"type": "popup", "width": "88%", "height": "82%", "require_project": True},
|
||||||
{"label": "🎯 资质", "icon": "", "url": "/pipeline-bidding/bid_qualifications/index.ui",
|
{"label": "🎯 资质", "icon": "", "url": "/pipeline-bidding/bid_qualifications/index.ui",
|
||||||
|
|||||||
@ -22,13 +22,8 @@ CH_REVIEWING = "reviewing"
|
|||||||
CH_APPROVED = "approved"
|
CH_APPROVED = "approved"
|
||||||
CH_REJECTED = "rejected"
|
CH_REJECTED = "rejected"
|
||||||
|
|
||||||
# ── 招标信息状态 ──
|
# ── 招标信息状态(bid_tenders 现仅承载解析抽取的商务要素,无审批流)──
|
||||||
TD_NEW = "new"
|
|
||||||
TD_SUMMARIZED = "summarized"
|
TD_SUMMARIZED = "summarized"
|
||||||
TD_PENDING_APPROVAL = "pending_approval"
|
|
||||||
TD_APPROVED = "approved"
|
|
||||||
TD_REJECTED = "rejected"
|
|
||||||
TD_ARCHIVED = "archived"
|
|
||||||
|
|
||||||
# ── 标书状态 ──
|
# ── 标书状态 ──
|
||||||
DOC_DRAFT = "draft"
|
DOC_DRAFT = "draft"
|
||||||
@ -38,12 +33,27 @@ DOC_REJECTED = "rejected"
|
|||||||
DOC_DELIVERED = "delivered"
|
DOC_DELIVERED = "delivered"
|
||||||
|
|
||||||
# ── 人类任务类型(阻塞门禁)──
|
# ── 人类任务类型(阻塞门禁)──
|
||||||
HT_TENDER_APPROVAL = "tender_approval"
|
|
||||||
HT_TENDER_FILE = "tender_file_upload"
|
HT_TENDER_FILE = "tender_file_upload"
|
||||||
HT_HUMAN_DOCS = "human_docs_request"
|
HT_HUMAN_DOCS = "human_docs_request"
|
||||||
|
HT_QC_ESCALATION = "qc_escalation"
|
||||||
HT_DELIVERY_CONFIRM = "bid_delivery_confirm"
|
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")
|
OPEN_TASK_STATES = ("submitted", "running", "review", "qc_review")
|
||||||
@ -55,6 +65,8 @@ DEFAULT_PARAMS = {
|
|||||||
"bid_pass_ratio": "0.85", # 整书评分通过门限
|
"bid_pass_ratio": "0.85", # 整书评分通过门限
|
||||||
"bid_max_revise": "3", # 单章节最大重做次数
|
"bid_max_revise": "3", # 单章节最大重做次数
|
||||||
"bid_max_round": "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):
|
async def get_thresholds(sor):
|
||||||
"""取四个门限参数(章节门限/整书门限/章节重做上限/整书轮次上限)。"""
|
"""取门限参数(章节/整书/重做上限/轮次上限 + QC 契合度门限)。"""
|
||||||
return {
|
return {
|
||||||
"chapter_pass_ratio": to_float(await get_param(sor, "bid_chapter_pass_ratio"), 0.8),
|
"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),
|
"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_revise": to_int(await get_param(sor, "bid_max_revise"), 3),
|
||||||
"max_round": to_int(await get_param(sor, "bid_max_round"), 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,
|
create_human_task, get_project, PIPELINE_ID,
|
||||||
CH_PENDING, CH_WRITING, CH_WRITTEN, CH_REJECTED, CH_APPROVED,
|
CH_PENDING, CH_WRITING, CH_WRITTEN, CH_REJECTED, CH_APPROVED,
|
||||||
DOC_DRAFT, DOC_REVIEWING, DOC_PASSED, DOC_REJECTED, DOC_DELIVERED,
|
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")
|
logger = logging.getLogger("pipeline.bidding")
|
||||||
@ -34,8 +36,9 @@ R_WRITER = "agent.bid_writer"
|
|||||||
R_REVIEWER = "agent.bid_reviewer"
|
R_REVIEWER = "agent.bid_reviewer"
|
||||||
R_COMPOSITOR = "agent.bid_compositor"
|
R_COMPOSITOR = "agent.bid_compositor"
|
||||||
R_SCORER = "agent.bid_scorer"
|
R_SCORER = "agent.bid_scorer"
|
||||||
|
R_QC = "agent.qc"
|
||||||
|
|
||||||
MAX_ANALYST_ATTEMPT = 2 # 解析阶段最多重派次数(防死循环)
|
MAX_ANALYST_ATTEMPT = 2 # 解析阶段首跑最多重派次数(防死循环;QC 退回的重做不计入)
|
||||||
MAX_PREP_ATTEMPT = 2
|
MAX_PREP_ATTEMPT = 2
|
||||||
POLL_INTERVAL = 15
|
POLL_INTERVAL = 15
|
||||||
|
|
||||||
@ -75,6 +78,67 @@ async def _auto_close_file_upload_task(sor, project_id):
|
|||||||
return True
|
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=""):
|
async def reconcile_project(sor, project_id, project_name=""):
|
||||||
"""对单个投标项目做一次状态对账。返回动作说明列表(幂等,可反复调用)。"""
|
"""对单个投标项目做一次状态对账。返回动作说明列表(幂等,可反复调用)。"""
|
||||||
acts = []
|
acts = []
|
||||||
@ -122,7 +186,7 @@ async def reconcile_project(sor, project_id, project_name=""):
|
|||||||
await sor.sqlExe("COMMIT", {})
|
await sor.sqlExe("COMMIT", {})
|
||||||
return acts + ["created: 上传招标文件任务 %s" % hid]
|
return acts + ["created: 上传招标文件任务 %s" % hid]
|
||||||
if await has_pending_blocking_human_task(sor, project_id):
|
if await has_pending_blocking_human_task(sor, project_id):
|
||||||
return ["blocked: 有待办人类任务(等招标文件/等人类资料),本轮不派发"]
|
return ["blocked: 有待办人类任务(等招标文件/等人类资料/等QC人工介入),本轮不派发"]
|
||||||
|
|
||||||
th = await get_thresholds(sor)
|
th = await get_thresholds(sor)
|
||||||
|
|
||||||
@ -156,8 +220,39 @@ async def reconcile_project(sor, project_id, project_name=""):
|
|||||||
if need_analysis:
|
if need_analysis:
|
||||||
if await count_open_tasks(sor, project_id, role=R_ANALYST) > 0:
|
if await count_open_tasks(sor, project_id, role=R_ANALYST) > 0:
|
||||||
return acts + ["waiting: 招标文件解析任务在办"]
|
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)
|
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")
|
ht = await find_human_task(sor, project_id, "general", status="pending")
|
||||||
if not ht:
|
if not ht:
|
||||||
await create_human_task(
|
await create_human_task(
|
||||||
@ -165,7 +260,7 @@ async def reconcile_project(sor, project_id, project_name=""):
|
|||||||
("解析任务已完成 %d 次,但仍缺少:%s。\n"
|
("解析任务已完成 %d 次,但仍缺少:%s。\n"
|
||||||
"请人工检查招标文件是否可读(bid_tender_files.content_text),"
|
"请人工检查招标文件是否可读(bid_tender_files.content_text),"
|
||||||
"或人工补录评分项/投标文件要求后完成本任务。"
|
"或人工补录评分项/投标文件要求后完成本任务。"
|
||||||
% (attempts,
|
% (first_attempts,
|
||||||
"、".join(x for x in [
|
"、".join(x for x in [
|
||||||
"评分项" if n_items == 0 else "",
|
"评分项" if n_items == 0 else "",
|
||||||
"投标文件章节结构" if n_struct == 0 else "",
|
"投标文件章节结构" if n_struct == 0 else "",
|
||||||
@ -181,6 +276,69 @@ async def reconcile_project(sor, project_id, project_name=""):
|
|||||||
await sor.sqlExe("COMMIT", {})
|
await sor.sqlExe("COMMIT", {})
|
||||||
return acts + ["created: 解析任务 %s" % tid]
|
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. 资料准备(知识库资质匹配 + 人类文件清单)──
|
# ── B. 资料准备(知识库资质匹配 + 人类文件清单)──
|
||||||
prep_done = await has_done_task(sor, project_id, R_PREP)
|
prep_done = await has_done_task(sor, project_id, R_PREP)
|
||||||
if n_quals > 0 and not prep_done:
|
if n_quals > 0 and not prep_done:
|
||||||
@ -343,7 +501,7 @@ async def diagnose_bid_project(project_id):
|
|||||||
await sor.sqlExe("COMMIT", {})
|
await sor.sqlExe("COMMIT", {})
|
||||||
counts = {}
|
counts = {}
|
||||||
for tbl in ("bid_tender_files", "bid_scoring_items", "bid_qualifications",
|
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(
|
counts[tbl] = await _count(
|
||||||
sor, "SELECT COUNT(*) AS c FROM " + tbl + " WHERE project_id=${p}$",
|
sor, "SELECT COUNT(*) AS c FROM " + tbl + " WHERE project_id=${p}$",
|
||||||
{"p": project_id})
|
{"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
|
import json
|
||||||
@ -11,333 +12,21 @@ import logging
|
|||||||
|
|
||||||
from .bid_common import (
|
from .bid_common import (
|
||||||
get_db, new_id, rec_to_dict, rows_to_dicts, json_loads, to_float, to_int,
|
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,
|
record, create_human_task, find_human_task, get_project,
|
||||||
PIPELINE_ID, TD_NEW, TD_SUMMARIZED, TD_PENDING_APPROVAL, TD_APPROVED,
|
PIPELINE_ID, HT_TENDER_FILE,
|
||||||
TD_REJECTED, TD_ARCHIVED, HT_TENDER_APPROVAL, HT_TENDER_FILE,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger("pipeline.bidding.tender")
|
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="",
|
async def create_bid_project(name, description="", org_id="", created_by="",
|
||||||
who=None, agent_id=None):
|
who=None, agent_id=None):
|
||||||
"""直接创建投标项目(不依赖招标信息前置)+ 发「上传招标文件」任务。
|
"""直接创建投标项目(产线起点)+ 发「上传招标文件」任务。
|
||||||
|
|
||||||
产线入口改为「从上传招标文件开始」后,立项不再经过邮件采集/审批,
|
立项不经过邮件采集/审批(那是商机产线的职责):用户/agent 直接创建投标项目,
|
||||||
而是用户/agent 直接创建投标项目,随后上传招标文件进入解析。
|
随后上传招标文件进入解析。
|
||||||
"""
|
"""
|
||||||
if not name or not str(name).strip():
|
if not name or not str(name).strip():
|
||||||
return False, "缺少项目名称(name)"
|
return False, "缺少项目名称(name)"
|
||||||
@ -358,13 +47,12 @@ async def create_bid_project(name, description="", org_id="", created_by="",
|
|||||||
await sor.sqlExe("COMMIT", {})
|
await sor.sqlExe("COMMIT", {})
|
||||||
await record(sor, pid, "sd_projects", pid, "create_bid_project",
|
await record(sor, pid, "sd_projects", pid, "create_bid_project",
|
||||||
to_state="in_progress", who=who, agent_id=agent_id,
|
to_state="in_progress", who=who, agent_id=agent_id,
|
||||||
detail="direct create (no tender)")
|
detail="direct create")
|
||||||
hid = await create_human_task(
|
hid = await create_human_task(
|
||||||
sor, pid, HT_TENDER_FILE,
|
sor, pid, HT_TENDER_FILE,
|
||||||
"上传招标文件:%s" % pname,
|
"上传招标文件:%s" % pname,
|
||||||
("项目已创建,请上传招标文件(含答疑/补遗文件)。\n"
|
("项目已创建,请上传招标文件(含答疑/补遗文件)。\n"
|
||||||
"上传后系统自动进入招标文件解析(抽取评分项、得分规则、所需资质、投标文件要求、章节骨架)。\n"
|
"上传后系统自动进入招标文件解析(抽取评分项、得分规则、所需资质、投标文件要求、章节骨架)。"),
|
||||||
"上传入口:投标产线 → 招标文件(bid_tender_files)。"),
|
|
||||||
assignee_role="owner.superuser",
|
assignee_role="owner.superuser",
|
||||||
form_schema={"project_id": pid})
|
form_schema={"project_id": pid})
|
||||||
return True, "project_id=%s(已发上传招标文件任务 %s)" % (pid, hid)
|
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(
|
exist = await sor.sqlExe(
|
||||||
"SELECT id FROM bid_members WHERE project_id=${pid}$ AND user_name=${nm}$ LIMIT 1",
|
"SELECT id FROM bid_members WHERE project_id=${pid}$ AND user_name=${nm}$ LIMIT 1",
|
||||||
{"pid": project_id, "nm": nm})
|
{"pid": project_id, "nm": nm})
|
||||||
|
await sor.sqlExe("COMMIT", {})
|
||||||
if exist:
|
if exist:
|
||||||
continue
|
continue
|
||||||
await sor.C("bid_members", {
|
await sor.C("bid_members", {
|
||||||
|
|||||||
@ -192,6 +192,34 @@ BID_ROLE_TOOL_SCHEMAS = {
|
|||||||
"chapter_id": "章节ID(可选)"},
|
"chapter_id": "章节ID(可选)"},
|
||||||
"required": [],
|
"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:标书合成工程师 ──
|
# ── bid_compose_capability:标书合成工程师 ──
|
||||||
"compose_bid": {
|
"compose_bid": {
|
||||||
"module": f"{_M}.bid_compose_capability",
|
"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__), "..", "..", ".."))
|
APP_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
|
||||||
|
|
||||||
TABLES = [
|
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_scoring_items", "bid_qualifications", "bid_doc_requirements",
|
||||||
|
"bid_qc_reviews",
|
||||||
"bid_chapters", "bid_reviews", "bid_scores", "bid_kb_docs", "bid_documents",
|
"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()
|
db = DBPools()
|
||||||
dbname = get_module_dbname('pipeline_bidding')
|
dbname = get_module_dbname('pipeline_bidding')
|
||||||
async with db.sqlorContext(dbname) as sor:
|
async with db.sqlorContext(dbname) as sor:
|
||||||
r = await sor.C('bid_mail_accounts', ns.copy())
|
r = await sor.C('bid_qc_reviews', 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
|
|
||||||
return {
|
return {
|
||||||
"widgettype":"Message",
|
"widgettype":"Message",
|
||||||
"options":{
|
"options":{
|
||||||
@ -24,8 +21,7 @@ async with db.sqlorContext(dbname) as sor:
|
|||||||
"cheight":9,
|
"cheight":9,
|
||||||
"title":"Add Success",
|
"title":"Add Success",
|
||||||
"timeout":3,
|
"timeout":3,
|
||||||
"message":"ok",
|
"message":"ok"
|
||||||
"user_data": _user_data
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -7,7 +7,7 @@ ns = {
|
|||||||
db = DBPools()
|
db = DBPools()
|
||||||
dbname = get_module_dbname('pipeline_bidding')
|
dbname = get_module_dbname('pipeline_bidding')
|
||||||
async with db.sqlorContext(dbname) as sor:
|
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');
|
debug('delete success');
|
||||||
return {
|
return {
|
||||||
"widgettype":"Message",
|
"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()
|
db = DBPools()
|
||||||
dbname = get_module_dbname('pipeline_bidding')
|
dbname = get_module_dbname('pipeline_bidding')
|
||||||
async with db.sqlorContext(dbname) as sor:
|
async with db.sqlorContext(dbname) as sor:
|
||||||
r = await sor.C('bid_tenders', ns.copy())
|
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 {
|
return {
|
||||||
"widgettype":"Message",
|
"widgettype":"Message",
|
||||||
"options":{
|
"options":{
|
||||||
@ -24,8 +21,7 @@ async with db.sqlorContext(dbname) as sor:
|
|||||||
"cheight":9,
|
"cheight":9,
|
||||||
"title":"Add Success",
|
"title":"Add Success",
|
||||||
"timeout":3,
|
"timeout":3,
|
||||||
"message":"ok",
|
"message":"ok"
|
||||||
"user_data": _user_data
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,18 +8,155 @@ if not ns.get('page'):
|
|||||||
if not ns.get('sort'):
|
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
|
sql = '''select a.*, b.source_text
|
||||||
from (select * from bid_tenders where 1=1 [[filterstr]]) a left join (select k as status,
|
from (select * from bid_tenders where 1=1 [[filterstr]]) a left join (select k as source,
|
||||||
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') b on a.source = b.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"}]}')
|
|
||||||
|
|
||||||
|
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 = ns.copy()
|
||||||
filterdic['filterstr'] = ''
|
filterdic['filterstr'] = ''
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
|
|
||||||
{
|
{
|
||||||
"widgettype":"VBox",
|
"widgettype":"VBox",
|
||||||
"options":{"height":"100%","width":"100%"},
|
"options":{"cheight":40,"width":"100%"},
|
||||||
"subwidgets":[
|
"subwidgets":[{
|
||||||
{
|
|
||||||
"id":"bid_tenders_tbl",
|
"id":"bid_tenders_tbl",
|
||||||
"widgettype":"Tabular",
|
"widgettype":"Tabular",
|
||||||
"options":{
|
"options":{
|
||||||
@ -11,28 +10,23 @@
|
|||||||
"height":"100%",
|
"height":"100%",
|
||||||
|
|
||||||
|
|
||||||
"title":"招标信息",
|
"title":"招标商务要素",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"toolbar":{
|
|
||||||
"tools": []
|
|
||||||
},
|
|
||||||
|
|
||||||
"css":"card",
|
"css":"card",
|
||||||
|
|
||||||
|
|
||||||
"editable":{
|
"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": {
|
"browserfields": {
|
||||||
"exclouded": [
|
"exclouded": [
|
||||||
"raw_content",
|
|
||||||
"attach_json",
|
|
||||||
"key_dates",
|
"key_dates",
|
||||||
"org_id",
|
"org_id",
|
||||||
"account_id",
|
|
||||||
"mail_uid",
|
|
||||||
"risk_note"
|
"risk_note"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -62,7 +52,6 @@
|
|||||||
"id",
|
"id",
|
||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
"approval_task_id",
|
|
||||||
"project_id"
|
"project_id"
|
||||||
],
|
],
|
||||||
|
|
||||||
@ -90,14 +79,24 @@
|
|||||||
"datatype": "str",
|
"datatype": "str",
|
||||||
"label": "机构ID"
|
"label": "机构ID"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "project_id",
|
||||||
|
"title": "项目ID",
|
||||||
|
"type": "str",
|
||||||
|
"length": 32,
|
||||||
|
"cwidth": 18,
|
||||||
|
"uitype": "str",
|
||||||
|
"datatype": "str",
|
||||||
|
"label": "项目ID"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "source",
|
"name": "source",
|
||||||
"title": "信息来源",
|
"title": "来源",
|
||||||
"type": "str",
|
"type": "str",
|
||||||
"length": 16,
|
"length": 16,
|
||||||
"nullable": "no",
|
"nullable": "no",
|
||||||
"default": "mail",
|
"default": "tender_doc",
|
||||||
"label": "信息来源",
|
"label": "来源",
|
||||||
"uitype": "code",
|
"uitype": "code",
|
||||||
"valueField": "source",
|
"valueField": "source",
|
||||||
"textField": "source_text",
|
"textField": "source_text",
|
||||||
@ -112,73 +111,6 @@
|
|||||||
},
|
},
|
||||||
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}"
|
"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",
|
"name": "title",
|
||||||
"title": "项目名称",
|
"title": "项目名称",
|
||||||
@ -221,29 +153,31 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "summary",
|
"name": "summary",
|
||||||
"title": "内容摘要",
|
"title": "项目概况摘要",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"length": 0,
|
"length": 0,
|
||||||
"uitype": "text",
|
"uitype": "text",
|
||||||
"datatype": "text",
|
"datatype": "text",
|
||||||
"label": "内容摘要"
|
"label": "项目概况摘要"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "bid_start_at",
|
"name": "bid_start_at",
|
||||||
"title": "投标开始时间",
|
"title": "投标开始时间",
|
||||||
"type": "date",
|
"type": "str",
|
||||||
"length": 0,
|
"length": 16,
|
||||||
"uitype": "date",
|
"cwidth": 16,
|
||||||
"datatype": "date",
|
"uitype": "str",
|
||||||
|
"datatype": "str",
|
||||||
"label": "投标开始时间"
|
"label": "投标开始时间"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "bid_deadline",
|
"name": "bid_deadline",
|
||||||
"title": "投标截止时间",
|
"title": "投标截止时间",
|
||||||
"type": "date",
|
"type": "str",
|
||||||
"length": 0,
|
"length": 16,
|
||||||
"uitype": "date",
|
"cwidth": 16,
|
||||||
"datatype": "date",
|
"uitype": "str",
|
||||||
|
"datatype": "str",
|
||||||
"label": "投标截止时间"
|
"label": "投标截止时间"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -260,9 +194,9 @@
|
|||||||
"name": "budget_amount",
|
"name": "budget_amount",
|
||||||
"title": "招标金额",
|
"title": "招标金额",
|
||||||
"type": "float",
|
"type": "float",
|
||||||
"length": 18,
|
"length": 14,
|
||||||
"dec": 2,
|
"dec": 2,
|
||||||
"cwidth": 18,
|
"cwidth": 14,
|
||||||
"uitype": "float",
|
"uitype": "float",
|
||||||
"datatype": "float",
|
"datatype": "float",
|
||||||
"label": "招标金额"
|
"label": "招标金额"
|
||||||
@ -271,9 +205,9 @@
|
|||||||
"name": "deposit_amount",
|
"name": "deposit_amount",
|
||||||
"title": "投标保证金",
|
"title": "投标保证金",
|
||||||
"type": "float",
|
"type": "float",
|
||||||
"length": 18,
|
"length": 14,
|
||||||
"dec": 2,
|
"dec": 2,
|
||||||
"cwidth": 18,
|
"cwidth": 14,
|
||||||
"uitype": "float",
|
"uitype": "float",
|
||||||
"datatype": "float",
|
"datatype": "float",
|
||||||
"label": "投标保证金"
|
"label": "投标保证金"
|
||||||
@ -289,12 +223,12 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "key_dates",
|
"name": "key_dates",
|
||||||
"title": "关键时间节点(JSON)",
|
"title": "关键节点(JSON)",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"length": 0,
|
"length": 0,
|
||||||
"uitype": "text",
|
"uitype": "text",
|
||||||
"datatype": "text",
|
"datatype": "text",
|
||||||
"label": "关键时间节点(JSON)"
|
"label": "关键节点(JSON)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "risk_note",
|
"name": "risk_note",
|
||||||
@ -310,58 +244,17 @@
|
|||||||
"title": "状态",
|
"title": "状态",
|
||||||
"type": "str",
|
"type": "str",
|
||||||
"length": 16,
|
"length": 16,
|
||||||
"nullable": "no",
|
"default": "summarized",
|
||||||
"default": "new",
|
"cwidth": 16,
|
||||||
"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,
|
|
||||||
"uitype": "str",
|
"uitype": "str",
|
||||||
"datatype": "str",
|
"datatype": "str",
|
||||||
"label": "审批任务ID"
|
"label": "状态"
|
||||||
},
|
|
||||||
{
|
|
||||||
"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": "作废/存档原因"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "created_by",
|
"name": "created_by",
|
||||||
"title": "创建人",
|
"title": "创建人",
|
||||||
"type": "str",
|
"type": "str",
|
||||||
"length": 32,
|
"length": 64,
|
||||||
"cwidth": 18,
|
"cwidth": 18,
|
||||||
"uitype": "str",
|
"uitype": "str",
|
||||||
"datatype": "str",
|
"datatype": "str",
|
||||||
@ -391,45 +284,25 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
"search_form":{
|
"data_filter":{
|
||||||
"css":"card",
|
"AND": [
|
||||||
"padding":"8px",
|
{
|
||||||
"show_label":false,
|
"field": "title",
|
||||||
"submit_label":"搜索",
|
"op": "LIKE",
|
||||||
"submit_css":"primary",
|
"var": "title_input"
|
||||||
"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'"
|
|
||||||
},
|
},
|
||||||
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}?prepend_all=1"
|
{
|
||||||
},
|
"field": "project_id",
|
||||||
{
|
"op": "=",
|
||||||
"name": "purchaser_input",
|
"var": "project_id"
|
||||||
"uitype": "str",
|
},
|
||||||
"placeholder": "招标人/采购人",
|
{
|
||||||
"cwidth": 15
|
"field": "purchaser",
|
||||||
}
|
"op": "LIKE",
|
||||||
]
|
"var": "purchaser_input"
|
||||||
},
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,8 @@
|
|||||||
|
|
||||||
ns = params_kw.copy()
|
ns = params_kw.copy()
|
||||||
for k,v in list(ns.items()):
|
for k,v in ns.items():
|
||||||
if v == 'NaN' or v == 'null' or v == '' or v is None:
|
if v == 'NaN' or v == 'null':
|
||||||
if k != 'id':
|
ns[k] = None
|
||||||
del ns[k]
|
|
||||||
else:
|
|
||||||
ns[k] = None if v in ('NaN', 'null') else v
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -15,11 +11,7 @@ db = DBPools()
|
|||||||
dbname = get_module_dbname('pipeline_bidding')
|
dbname = get_module_dbname('pipeline_bidding')
|
||||||
async with db.sqlorContext(dbname) as sor:
|
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"}]')
|
r = await sor.U('bid_tenders', ns)
|
||||||
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)
|
|
||||||
debug('update success');
|
debug('update success');
|
||||||
return {
|
return {
|
||||||
"widgettype":"Message",
|
"widgettype":"Message",
|
||||||
|
|||||||
@ -7,15 +7,15 @@
|
|||||||
"options": {"width": "100%", "alignItems": "center", "padding": "16px 24px 8px 24px", "cheight": 6, "gap": "12px"},
|
"options": {"width": "100%", "alignItems": "center", "padding": "16px 24px 8px 24px", "cheight": 6, "gap": "12px"},
|
||||||
"subwidgets": [
|
"subwidgets": [
|
||||||
{"widgettype": "Title2", "options": {"text": "投标产线"}},
|
{"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": "Filler"},
|
||||||
{
|
{
|
||||||
"widgettype": "Button",
|
"widgettype": "Button",
|
||||||
"options": {"label": "📨 招标信息", "css": "small"},
|
"options": {"label": "📨 招标商务要素", "css": "small"},
|
||||||
"binds": [{
|
"binds": [{
|
||||||
"wid": "self", "event": "click", "actiontype": "urlwidget",
|
"wid": "self", "event": "click", "actiontype": "urlwidget",
|
||||||
"target": "Popup",
|
"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"
|
"url": "/pipeline-bidding/bid_tenders/index.ui"
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
@ -28,16 +28,6 @@
|
|||||||
"popup_options": {"title": "公司投标知识库", "width": "88%", "height": "82%", "auto_open": true},
|
"popup_options": {"title": "公司投标知识库", "width": "88%", "height": "82%", "auto_open": true},
|
||||||
"url": "/pipeline-bidding/bid_kb_docs/index.ui"
|
"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"},
|
"options": {"width": "100%", "padding": "16px", "bgcolor": "#ffffff", "border": "1px solid #e2e8f0", "borderRadius": "8px", "spacing": "8px"},
|
||||||
"subwidgets": [
|
"subwidgets": [
|
||||||
{"widgettype": "Title3", "options": {"text": "使用流程"}},
|
{"widgettype": "Title3", "options": {"text": "使用流程"}},
|
||||||
{"widgettype": "Text", "options": {"text": "1. 邮箱配置:录入阿里云企业邮(imap.qiye.aliyun.com:993,开启安全登录需填客户端专用密码)", "cfontsize": 0.95}},
|
{"widgettype": "Text", "options": {"text": "1. 立项:投标驾驶舱(侧栏 产线管理→投标产线)说「创建投标项目」,设定参与人员", "cfontsize": 0.95}},
|
||||||
{"widgettype": "Text", "options": {"text": "2. 投标驾驶舱:从「开发产线」同款入口(侧栏 产线管理→驾驶舱)切换到投标项目,对话式推进;或直接说「采集招标信息」", "cfontsize": 0.95}},
|
{"widgettype": "Text", "options": {"text": "2. 上传招标文件:按待办任务上传招标文件(含答疑/补遗),上传后自动进入解析", "cfontsize": 0.95}},
|
||||||
{"widgettype": "Text", "options": {"text": "3. 审批:招标信息摘要后提交审批,不通过自动存档作废;通过自动立项并发布上传招标文件任务", "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": "4. 编写与评审:资质准备(知识库匹配+人工文件清单)→ 一章一任务自动编写与评审循环", "cfontsize": 0.95}},
|
||||||
{"widgettype": "Text", "options": {"text": "5. 评分与交付:合成标书 → 整书按评分规则打分 → 不达标自动退回问题章节 → 通过后人工交付确认", "cfontsize": 0.95}}
|
{"widgettype": "Text", "options": {"text": "5. 评分与交付:合成标书 → 整书按评分规则打分 → 不达标自动退回问题章节 → 通过后人工交付确认", "cfontsize": 0.95}}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -106,6 +96,13 @@
|
|||||||
"binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "Popup",
|
"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_reviews/index.ui"}]
|
"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