feat: 投标产线模块(bidding_general)

- 12张 bid_* 表(models+DDL+CRUD):招标信息/邮箱配置/人员/招标文件/评分项/资质/
  投标文件要求/章节/评审/评分/知识库/标书
- 7个角色能力模块(bid_*_capability):采集摘要/解析/知识库资质/编写/评审/合成/评分
- bid_flow 状态对账器:角色任务唯一创建者+阻塞门禁(源头守门,引擎零特判)
- bid_mail:阿里云企业邮 IMAP 采集(凭据存DB AES,客户端专用密码兼容)
- 能力包注册(bid_ability)+ 角色工具自注册(role_tool_schemas)
- 技能:6 common 状态机/规范 + 9 角色技能(pipelines/bidding_general,随 pipeline-core 部署)
- i18n(zh/en)、init/data.json(appcodes+pipelines记录)、build.sh、RBAC load_path
This commit is contained in:
yumoqing 2026-08-26 22:52:41 +08:00
parent 73d19bbd49
commit 19a3d67c68
110 changed files with 13070 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
__pycache__/
*.pyc
*.egg-info/
build/
dist/

113
build.sh Normal file
View File

@ -0,0 +1,113 @@
#!/bin/bash
# pipeline-bidding 一键部署脚本(在宿主应用根目录执行或从本目录定位宿主)。
# 职责:建表(运行期禁止 schema 变更,建表只在部署期)+ 种子数据(appcodes + pipelines 产线记录)
# + wwwroot 软链 + pip install 本包。
# 参考模式:app_audit/build.sh + sage-database-design-conventions 的种子数据步骤。
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# 定位宿主应用根目录(含 wwwroot + py3)
APP_ROOT=""
for candidate in "$SCRIPT_DIR/../../pipeline-app" "$SCRIPT_DIR/../.." "$HOME/work/pipeline/pipeline-app"; do
if [ -d "$candidate/wwwroot" ] && [ -d "$candidate/py3" ]; then
APP_ROOT="$(cd "$candidate" && pwd)"
break
fi
done
if [ -z "$APP_ROOT" ]; then
echo "ERROR: host app (pipeline-app) root not found"
exit 1
fi
PYTHON="$APP_ROOT/py3/bin/python"
echo "Host app root: $APP_ROOT"
# ── 1. 建表(幂等:CREATE TABLE IF NOT EXISTS 由 json2ddl 产物改造)──
cd "$SCRIPT_DIR"
"$PYTHON" - <<'PYEOF'
import re, sys, os
ddl = open('mysql.ddl.sql', encoding='utf-8').read()
# json2ddl 产物含 drop table if exists,部署期改为 IF NOT EXISTS 幂等建表
ddl = re.sub(r'drop table if exists (\w+);', '', ddl)
ddl = re.sub(r'CREATE TABLE (\w+)\s*\(', r'CREATE TABLE IF NOT EXISTS \1 (', ddl)
open('/tmp/pipeline_bidding_ddl.sql', 'w', encoding='utf-8').write(ddl)
print('DDL prepared:', ddl.count('CREATE TABLE IF NOT EXISTS'), 'tables')
PYEOF
"$PYTHON" - <<'PYEOF'
import sys, os, subprocess
sys.path.insert(0, os.getcwd())
from appPublic.jsonConfig import getConfig
from appPublic.aes import aes_decode_b64
cfg = getConfig('.', {'workdir': '.'})
kw = cfg.databases['pipeline'].kwargs
pwd = aes_decode_b64(cfg.password_key, kw.password)
with open('/tmp/pipeline_bidding_ddl.sql', 'rb') as f:
r = subprocess.run(['mysql', '-h', str(kw.host), '-P', str(kw.port),
'-u', str(kw.user), '-p%s' % pwd, str(kw.db)],
stdin=f, capture_output=True)
if r.returncode == 0:
print('tables applied')
else:
print('ERROR:', r.stderr.decode('utf-8', 'replace').strip()[:300])
sys.exit(1)
PYEOF
# ── 2. 种子数据:appcodes 字典 + pipelines 产线记录(数据源 init/data.json,幂等)──
cd "$APP_ROOT"
"$PYTHON" - "$SCRIPT_DIR" <<'PYEOF'
import sys, os, json, asyncio
CDIR = sys.argv[1]
sys.path.insert(0, os.getcwd())
from appPublic.jsonConfig import getConfig
from sqlor.dbpools import DBPools
from appPublic.uniqueID import getID
data = json.load(open(os.path.join(CDIR, 'init', 'data.json')))
async def seed():
config = getConfig('.')
db = DBPools(config.databases)
async with db.sqlorContext('pipeline') as sor:
# pipelines 产线主记录(checklist 第 0 步)
for pl in data.get('pipelines', []):
recs = await sor.sqlExe("SELECT id FROM pipelines WHERE id=${i}$", {"i": pl['id']})
if not recs:
await sor.C('pipelines', {
'id': pl['id'], 'name': pl['name'],
'description': pl.get('description', ''),
'pipeline_type': pl.get('pipeline_type', 'bidding'),
'version': '1.0.0', 'status': pl.get('status', 'published'),
'org_id': pl.get('org_id', '0'), 'created_by': 'system',
})
print('pipeline registered:', pl['id'])
# appcodes 字典
for ac in data.get('appcodes', []):
pid = ac['parentid']
rows = await sor.R('appcodes', {'id': pid})
if not rows:
await sor.C('appcodes', {'id': pid, 'name': ac.get('parentname', pid),
'hierarchy_flg': '0'})
for item in ac.get('items', []):
chk = await sor.sqlExe(
"SELECT id FROM appcodes_kv WHERE parentid=${p}$ AND k=${k}$",
{'p': pid, 'k': item['k']})
if not chk:
await sor.C('appcodes_kv', {'id': getID(), 'parentid': pid,
'k': item['k'], 'v': item['v']})
await sor.sqlExe("COMMIT", {})
print('seeded %d code groups' % len(data.get('appcodes', [])))
asyncio.run(seed())
PYEOF
# ── 3. wwwroot 软链 + pip install ──
rm -f "$APP_ROOT/wwwroot/pipeline-bidding"
ln -sf "$SCRIPT_DIR/wwwroot" "$APP_ROOT/wwwroot/pipeline-bidding"
echo "wwwroot linked: pipeline-bidding"
"$APP_ROOT/py3/bin/pip" install "$SCRIPT_DIR/" 2>&1 | tail -1
echo "pipeline-bidding module deploy complete."
echo "后续(宿主负责):app 入口 import load_pipeline_bidding;RBAC 用 scripts/load_path.py;i18n 用 merge_i18n.py。"

227
docs/DESIGN.md Normal file
View File

@ -0,0 +1,227 @@
# 投标产线(bidding_general)设计文档
> 对照开发产线(`sdlc_general`)设计,复用平台通用内核(项目/会话/任务链/人类任务/交付件/问答冒泡/技能分层),
> 只增量定义投标产线自己的 **阶段、角色、工具、技能、任务流转规则、交付件**。
版本:v1.0.0  创建:2026-08-26
---
## 一、业务流程(需求原文映射)
| # | 业务动作 | 承担者 | 落库 |
|---|---|---|---|
| 1 | 从 yumoqing@opencomputing.cn(阿里云企业邮)获取招标信息 | agent.tender_scout | bid_tenders |
| 2 | 对每条招标信息内容摘要:投标开始时间、项目周期、招标金额等 | agent.tender_scout | bid_tenders.summary/bid_start_at/project_period/budget_amount |
| 3 | 走审批流程 | 人(human task + 可选钉钉审批) | pipeline_human_tasks(task_type=tender_approval) |
| 4 | 审批不通过 → 招标信息存档作废 | 机制 | bid_tenders.status=archived |
| 5 | 审批通过 → 创建项目 | 机制/主 agent | sd_projects(pipeline_id=bidding_general) |
| 6 | 设定参与人员 | 主 agent / 人 | bid_members |
| 7 | 等待上传招标文件 | 人(阻塞门禁) | pipeline_human_tasks(tender_file_upload) + bid_tender_files |
| 8 | 从招标文件抽取评分项及得分规则 | agent.tender_analyst | bid_scoring_items |
| 9 | 抽取所需资质 | agent.tender_analyst | bid_qualifications |
| 10 | 抽取投标文件要求(章节结构/格式/份数/密封) | agent.tender_analyst | bid_doc_requirements |
| 11 | 从公司投标知识库获取所需资质文档 | agent.bid_prep | bid_qualifications.kb_doc_id ← bid_kb_docs |
| 12 | 发布人类应提供的文件清单 | agent.bid_prep(阻塞门禁) | pipeline_human_tasks(human_docs_request) |
| 13 | 按投标文件要求分章节编写投标文件 | agent.bid_writer | bid_chapters |
| 14 | 每章节完成后对照招标/投标文件要求评审,分数不够给改进意见重做 | agent.bid_reviewer | bid_reviews(scope=chapter) → 章节循环 |
| 15 | 合成标书 | agent.bid_compositor | bid_documents(docx) |
| 16 | 对整个标书评审 + 按评分项和得分规则打分 | agent.bid_scorer | bid_reviews(scope=whole) + bid_scores |
| 17 | 不合适 → 改进意见 → 修改问题章节 → 评审 → …… 直到得分通过 | 机制(整书循环) | bid_scores.target_chapter_id → 章节回 rejected |
---
## 二、阶段与角色(对照开发产线)
| 开发产线 | 投标产线 | 角色规范名 | 职责 | next_role |
|---|---|---|---|---|
| requirement 需求分析 | 招标情报 | `agent.tender_scout` | 收邮件、去重入库、摘要与要素抽取、提交审批 | `agent.tender_analyst` |
| —(新增门禁) | 招标审批 | 人类任务 | 审批通过立项 / 不通过存档作废 | — |
| design 设计 | 招标文件解析 | `agent.tender_analyst` | 抽取评分项+得分规则、所需资质、投标文件要求,产出章节骨架 | `agent.bid_prep` |
| —(新增) | 资料准备 | `agent.bid_prep` | 知识库资质匹配、缺失项发布人类文件清单 | `agent.bid_writer` |
| develop 开发 | 章节编写 | `agent.bid_writer` | 按章节骨架逐章写作(一章一任务) | `agent.bid_reviewer` |
| test 测试 | 章节评审 | `agent.bid_reviewer` | 对照招标要求+得分规则逐章打分,不达标退回重写 | `agent.bid_compositor` |
| —(新增) | 标书合成 | `agent.bid_compositor` | 合成 docx(封面/目录/页码/偏离表),产出交付件 | `agent.bid_scorer` |
| deploy 部署 | 全书评分 | `agent.bid_scorer` | 按评分项+得分规则给整书打分、生成改进意见并映射章节 | ""(终结→人类交付确认) |
| agent.pm | 同名复用 | `agent.pm` | 任务审核/派发/验收 | — |
| agent.qc | 同名复用 | `agent.qc` | 交付件合规检查 | — |
> 角色命名遵守平台规范:agent 角色 = `agent.{role}`,人类角色 = RBAC `{orgtype}.{role}`。
> 同名角色跨产线隔离(按 pipeline_id 查 RoleSpec),`agent.pm`/`agent.qc` 在两产线各有自己的技能与提示词。
---
## 三、任务流转规则
### 3.1 主链(机制驱动,非 LLM 驱动)
```
[邮箱] --fetch_tenders--> bid_tenders(new)
--summarize_tender--> summarized
--submit_tender_approval--> pending_approval ┌── reject ──> archived(作废) ✗终止
└── approve ──> approved
--create_bid_project--> sd_projects + assign_members
--request_tender_file--> [人类任务:上传招标文件] (阻塞)
↓ 上传完成
agent.tender_analyst → 评分项 / 资质 / 投标文件要求 / 章节骨架
↓ approved (PM)
agent.bid_prep → 知识库资质匹配 + [人类任务:应提供文件清单](阻塞)
↓ approved
agent.bid_writer → 逐章写作(每章一个任务,可并行受并发上限约束)
↓ 章节 written
agent.bid_reviewer → 章节评审打分
↓ 全部章节 approved
agent.bid_compositor → 合成标书 docx
↓
agent.bid_scorer → 整书评分
↓ 通过
[人类任务:交付确认] → 项目 completed
```
### 3.2 循环一:章节级评审回退
- 触发:`review_chapter` 判定 `score / max_score < chapter_pass_ratio`(默认 0.8)
- 动作:章节 `status=rejected` + `review_comment` 落库 + `revise_count += 1`
- 机制:`bid_reviewer` 任务 approved 后,若存在 rejected 章节 → 自动派生 `agent.bid_writer` 重写任务(任务标题带章节号,params 带 `chapter_id` + `improvement`)
- 上限:`revise_count > bid_max_revise`(默认 3)→ 抛人类介入(fault_report + 人类任务),不无限重做
### 3.3 循环二:整书评分回退
- 触发:`score_bid` 总分率 `< bid_pass_ratio`(默认 0.85),或存在否决项(`is_veto`)未响应
- 动作:`bid_scores` 逐评分项落分 + `improvement` + `target_chapter_id`
- 机制:把 `target_chapter_id` 命中的章节置 `rejected` → 派生 `bid_writer` 任务 → 重写 → 章节评审 → 全部通过 → 重新合成 → 重新评分
- 轮次记录 `bid_reviews.round`,上限 `bid_max_round`(默认 3)→ 抛人类介入
### 3.4 阻塞门禁(人类任务)
| task_type | 语义 | 阻塞范围 |
|---|---|---|
| `tender_approval` | 招标信息审批 | 该 tender 不立项、不推进 |
| `tender_file_upload` | 等待上传招标文件 | 项目全部角色任务 |
| `human_docs_request` | 人类应提供的文件 | 项目全部角色任务 |
| `bid_delivery_confirm` | 最终标书交付确认 | 项目收尾 |
**门禁的守门位置:投标产线的角色任务唯一创建者是 `bid_flow` 对账器**(每 15 秒状态对账)。
对账器在创建任务前就检查 `pipeline_human_tasks.project_id = <项目id> AND status='pending'`,
有阻塞任务直接不派 —— 门禁在**创建源头**,不在引擎派发层。
> ⚠️ 概念区分(重要):`tenant_id` 是引擎的通用租户键(宿主可提供为 org_id / user_id 等)。
> **只有角色任务(pipeline_id='role_task')这个具体场景约定 `tenant_id = project_id`**
> (与开发产线的既有任务创建约定一致,`_create_next_task` 同款)。
> 这是角色任务派发机制的约定,不是引擎通用规则 —— 引擎层 SQL 不得引用该等价关系,
> 引擎对投标产线零特判;投标的阻塞语义全部收敛在投标产线自己的代码(bid_flow)里。
### 3.5 硬规则
- 审批不通过 = 终态(archived),不得再进入立项;需重新评估须人工 `reopen_tender`。
- 章节写作任务与章节一对一(`params.chapter_id`),禁止一个任务写多章(对照开发产线「单任务不要过大」)。
- 评审/评分只由 `agent.bid_reviewer` / `agent.bid_scorer` 执行,写作角色不得自评(源头杜绝越权:capability 按角色缩小)。
- 得分/改进意见必须落库(`bid_reviews`/`bid_scores`),不接受"文档里声称评审通过"。
- 案例与业绩一致性:技术方案引用的案例必须能对应到 `bid_kb_docs(doc_type=case)` 真实合同,禁止虚构客户/金额(写进 bid-doc-spec 技能,评审项硬检查)。
---
## 四、数据表(bid_ 前缀,与 sd_/pipeline_ 表共库 pipeline)
| 表 | 说明 | 关键字段 |
|---|---|---|
| `bid_mail_accounts` | 招标邮箱配置(凭据不写死代码) | protocol/host/port/username/password(AES)/folder/filter_keywords/last_uid/enabled |
| `bid_tenders` | 招标信息 | mail_uid/subject/received_at/raw_content/title/purchaser/tender_no/summary/bid_start_at/bid_deadline/project_period/budget_amount/deposit/qualification_req/status/project_id |
| `bid_members` | 项目参与人员 | project_id/user_id/member_role/duty |
| `bid_tender_files` | 招标文件及附件 | project_id/tender_id/file_name/file_path/file_type/extract_status |
| `bid_scoring_items` | 评分项 + 得分规则 | project_id/section/item_name/max_score/scoring_rule/is_veto/weight/source_ref |
| `bid_qualifications` | 所需资质 | project_id/qual_name/requirement/is_mandatory/kb_doc_id/match_status/owner |
| `bid_doc_requirements` | 投标文件要求 | project_id/req_type/chapter_no/chapter_title/requirement/page_limit |
| `bid_chapters` | 标书章节 | project_id/chapter_no/title/scoring_item_ids/status/version/content/file_path/review_score/review_comment/revise_count/assignee_role |
| `bid_reviews` | 评审记录(章节级/整书级) | project_id/scope/chapter_id/round/reviewer/score/max_score/passed/comments |
| `bid_scores` | 评分明细(按评分项) | project_id/review_id/scoring_item_id/score/max_score/reason/improvement/target_chapter_id |
| `bid_kb_docs` | 公司投标知识库 | org_id/doc_type/doc_name/tags/file_path/content_text/valid_until/status |
| `bid_documents` | 合成标书 | project_id/version/file_path/page_count/total_score/status |
复用平台表:`sd_projects`(项目,pipeline_id=bidding_general)、`pipeline_tasks`(角色任务链)、
`pipeline_human_tasks`(审批/人类任务)、`pipeline_deliverables`(交付件)、`pipeline_agent_questions`(问答冒泡)。
---
## 五、工具
### 5.1 主 agent(驾驶舱)工具 —— `PipelineAbility.tools`
**招标情报**:`fetch_tenders` `list_tenders` `tender_detail` `summarize_tender`
`submit_tender_approval` `approve_tender` `reject_tender` `archive_tender` `reopen_tender`
**立项与人员**:`create_bid_project` `assign_members` `list_members` `request_tender_file` `list_tender_files`
**解析结果查看**:`list_scoring_items` `list_qualifications` `list_doc_requirements`
**知识库**:`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_reviews` `bid_score_report`
**通用复用**(引擎级,来自 shared_ability):`create_task` `list_tasks` `task_detail` `approve_task`
`complete_task` `cancel_task` `reset_task_retry` `start_agents` `diagnose_project`
`list_deliverables` `view_deliverable` `list_questions` `answer_question` `escalate_question`
`list_projects` `start_project` `complete_project` `archive_project` `pause_project` `resume_project`
`list_role_models` `set_role_model` `submit_deliverable` `approve_deliverable` `reject_deliverable`
### 5.2 角色 agent 能力工具(capability → TOOL_SCHEMAS)
| capability | 工具 | 授予角色 |
|---|---|---|
| `bid_tender_capability` | `fetch_tenders` `summarize_tender` `set_tender_fields` `submit_tender_approval` `archive_tender` `list_tenders` | agent.tender_scout |
| `bid_analysis_capability` | `add_scoring_item` `add_qualification` `add_doc_requirement` `create_chapter_outline` `list_scoring_items` `list_doc_requirements` `mark_file_extracted` | agent.tender_analyst |
| `bid_kb_capability` | `search_bid_kb` `match_qualification` `mark_qualification_missing` `request_human_docs` `list_qualifications` | agent.bid_prep |
| `bid_write_capability` | `list_chapters` `chapter_detail` `write_chapter` `submit_chapter` `list_scoring_items` `search_bid_kb` | agent.bid_writer |
| `bid_review_capability` | `review_chapter` `reject_chapter` `approve_chapter` `list_chapters` `list_scoring_items` `list_doc_requirements` | agent.bid_reviewer |
| `bid_compose_capability` | `compose_bid` `list_chapters` `list_doc_requirements` | agent.bid_compositor |
| `bid_score_capability` | `score_bid` `add_score_item_result` `request_chapter_revise` `list_scoring_items` `list_bid_documents` | agent.bid_scorer |
> capability 按角色职责缩小 —— 写作角色拿不到 `review_chapter`/`score_bid`,评审角色拿不到 `write_chapter`。
> 越权在源头杜绝,不靠事后 RBAC 校验。
---
## 六、技能(skills_library/pipelines/bidding_general/)
### common/(产线通用规范,状态机 + 门禁)
| 技能 | 内容 |
|---|---|
| `bid-workflow` | 产线总纲:阶段链、门禁、两级循环、回退语义、机制 vs LLM 分界 |
| `tender` | 招标信息状态机:new→summarized→pending_approval→approved/rejected→archived |
| `bid-chapter` | 章节状态机 + 评审循环 + 重做上限 + 一章一任务 |
| `bid-review` | 评审与评分规范:对照评分项/得分规则打分、改进意见→章节映射、否决项处理 |
| `bid-qualification` | 资质匹配规范:知识库检索 → matched/missing/human_required → 人类文件清单 |
| `bid-doc-spec` | 投标文件编写规范:章节骨架来自评分标准原文、操作层内容、案例与业绩一一对应、禁虚构 |
| `team-communication` | 问答冒泡链(复用开发产线同名规范) |
### roles/(角色定义,frontmatter 声明 capability)
`agent.tender_scout` `agent.tender_analyst` `agent.bid_prep` `agent.bid_writer`
`agent.bid_reviewer` `agent.bid_compositor` `agent.bid_scorer` `agent.pm` `agent.qc`
---
## 七、代码落位
| 内容 | 位置 | 理由 |
|---|---|---|
| 主 agent 能力包 `bid_ability.py` | pipeline-service | 与 sdlc_ability 同构;ability 注册表在 core,任何模块可注册 |
| 角色能力工具 `bid_*_capability.py` | pipeline-service | `exec_capability_tool` 只 import `pipeline_service.*` |
| 邮箱采集 `bid_mail_fetcher.py` | pipeline-service | 被能力工具调用 |
| 通用任务 handler 汇总 `shared_ability.py` | pipeline-service | 复用而非重写 SDLC 的通用任务/交付件/问答 handler |
| 表 / CRUD / UI / dspy / RBAC | pipeline-bidding(新模块仓库) | 产线业务资产独立仓库,宿主 load_pipeline_bidding() 装载 |
| 技能 | pipeline-core/skills_library/pipelines/bidding_general | 技能库统一目录,部署期同步到机构工作空间 |
---
## 八、验收口径
1. `bid_*` 12 张表建表成功,CRUD 页面可增删改查。
2. `fetch_tenders` 真连阿里云企业邮 IMAP 取到邮件并入库(凭据来自 `bid_mail_accounts`)。
3. `summarize_tender` 真调 LLM 产出摘要与 5 要素并落库。
4. 审批不通过 → tender 状态 archived;通过 → sd_projects 出现 pipeline_id=bidding_general 的项目。
5. 上传招标文件人类任务 pending 时,poller 不派发该项目 agent 任务(项目级门禁生效)。
6. 章节评审不达标 → 章节 rejected + 自动派生重写任务(DB 可查)。
7. 整书评分不达标 → bid_scores 有 improvement + target_chapter_id,命中章节回 rejected。
8. 全流程任务链在 `pipeline_tasks` 中可追溯(role 从 tender_scout 到 bid_scorer)。

30
i18n/en/msg.txt Normal file
View File

@ -0,0 +1,30 @@
招标信息: Tender Notices
招标信息台: Tender Inbox
邮箱配置: Mail Account Config
投标知识库: Bid Knowledge Base
评分项: Scoring Items
标书章节: Bid Chapters
合成标书: Assembled Bid Document
采集招标信息: Fetch Tender Notices
提交审批: Submit for Approval
审批通过: Approved
审批不通过: Rejected
存档作废: Archived / Voided
立项: Create Project
参与人员: Team Members
上传招标文件: Upload Tender Document
资质要求: Qualification Requirements
投标文件要求: Bid Document Requirements
章节评审: Chapter Review
整书评分: Whole-document Scoring
改进意见: Improvement Suggestions
得分规则: Scoring Rules
评审通过: Review Passed
退回重写: Returned for Rewrite
交付确认: Delivery Confirmation
投标开始时间: Bid Start Time
投标截止时间: Bid Deadline
项目周期: Project Duration
招标金额: Budget Amount
投标保证金: Bid Bond
内容摘要: Summary

30
i18n/zh/msg.txt Normal file
View File

@ -0,0 +1,30 @@
招标信息: 招标信息
招标信息台: 招标信息台
邮箱配置: 邮箱配置
投标知识库: 投标知识库
评分项: 评分项
标书章节: 标书章节
合成标书: 合成标书
采集招标信息: 采集招标信息
提交审批: 提交审批
审批通过: 审批通过
审批不通过: 审批不通过
存档作废: 存档作废
立项: 立项
参与人员: 参与人员
上传招标文件: 上传招标文件
资质要求: 资质要求
投标文件要求: 投标文件要求
章节评审: 章节评审
整书评分: 整书评分
改进意见: 改进意见
得分规则: 得分规则
评审通过: 评审通过
退回重写: 退回重写
交付确认: 交付确认
投标开始时间: 投标开始时间
投标截止时间: 投标截止时间
项目周期: 项目周期
招标金额: 招标金额
投标保证金: 投标保证金
内容摘要: 内容摘要

136
init/data.json Normal file
View File

@ -0,0 +1,136 @@
{
"pipelines": [
{
"id": "bidding_general",
"name": "通用投标产线",
"pipeline_type": "bidding",
"description": "招标信息采集→内容摘要→审批→立项与人员→招标文件解析(评分项/得分规则/资质/投标文件要求)→资质准备→分章节编写→章节评审→合成标书→整书评分→交付",
"status": "published",
"org_id": "0"
}
],
"appcodes": [
{
"parentid": "bid_tender_status",
"parentname": "招标信息状态",
"items": [
{"k": "new", "v": "新采集"},
{"k": "summarized", "v": "已摘要"},
{"k": "pending_approval", "v": "待审批"},
{"k": "approved", "v": "审批通过"},
{"k": "rejected", "v": "审批不通过"},
{"k": "archived", "v": "存档作废"}
]
},
{
"parentid": "bid_tender_source",
"parentname": "招标信息来源",
"items": [
{"k": "mail", "v": "邮件采集"},
{"k": "manual", "v": "手工录入"}
]
},
{
"parentid": "bid_member_role",
"parentname": "投标人员角色",
"items": [
{"k": "leader", "v": "投标负责人"},
{"k": "writer", "v": "标书编写"},
{"k": "qual", "v": "资质资料"},
{"k": "price", "v": "报价"},
{"k": "reviewer", "v": "评审"}
]
},
{
"parentid": "bid_file_type",
"parentname": "招标文件类型",
"items": [
{"k": "tender_doc", "v": "招标文件"},
{"k": "qa", "v": "答疑补遗"},
{"k": "attachment", "v": "附件"}
]
},
{
"parentid": "bid_scoring_section",
"parentname": "评分部分",
"items": [
{"k": "business", "v": "商务"},
{"k": "technical", "v": "技术"},
{"k": "price", "v": "价格"}
]
},
{
"parentid": "bid_qual_match_status",
"parentname": "资质匹配状态",
"items": [
{"k": "pending", "v": "待匹配"},
{"k": "matched", "v": "已匹配知识库"},
{"k": "missing", "v": "缺失"},
{"k": "human_required", "v": "需人工提供"}
]
},
{
"parentid": "bid_qual_owner",
"parentname": "资质提供方",
"items": [
{"k": "kb", "v": "公司知识库"},
{"k": "human", "v": "人工提供"}
]
},
{
"parentid": "bid_req_type",
"parentname": "投标文件要求类型",
"items": [
{"k": "structure", "v": "章节结构"},
{"k": "format", "v": "格式要求"},
{"k": "seal", "v": "密封要求"},
{"k": "copies", "v": "份数要求"},
{"k": "deadline", "v": "时间节点"},
{"k": "other", "v": "其他"}
]
},
{
"parentid": "bid_chapter_status",
"parentname": "章节状态",
"items": [
{"k": "pending", "v": "待编写"},
{"k": "writing", "v": "编写中"},
{"k": "written", "v": "已提交评审"},
{"k": "reviewing", "v": "评审中"},
{"k": "approved", "v": "评审通过"},
{"k": "rejected", "v": "退回重写"}
]
},
{
"parentid": "bid_review_scope",
"parentname": "评审范围",
"items": [
{"k": "chapter", "v": "章节评审"},
{"k": "whole", "v": "整书评审"}
]
},
{
"parentid": "bid_kb_doc_type",
"parentname": "知识库文档类型",
"items": [
{"k": "qualification", "v": "资质证书"},
{"k": "cert", "v": "其他证照"},
{"k": "case", "v": "业绩案例"},
{"k": "template", "v": "模板"},
{"k": "finance", "v": "财务资料"},
{"k": "personnel", "v": "人员资料"}
]
},
{
"parentid": "bid_doc_status",
"parentname": "标书状态",
"items": [
{"k": "draft", "v": "已合成待评分"},
{"k": "reviewing", "v": "评分中"},
{"k": "passed", "v": "评分通过"},
{"k": "rejected", "v": "评分不通过"},
{"k": "delivered", "v": "已交付"}
]
}
]
}

50
json/bid_chapters.json Normal file
View File

@ -0,0 +1,50 @@
{
"tblname": "bid_chapters",
"title": "投标文件章节",
"params": {
"sortby": [
"order_no",
"chapter_no"
],
"confidential_fields": [],
"browserfields": {
"exclouded": [
"content",
"outline",
"scoring_item_ids"
]
},
"editexclouded": [
"id",
"created_at",
"updated_at",
"task_id",
"version",
"word_count"
],
"editable": {
"new_data_url": "{{entire_url('add_bid_chapters.dspy')}}",
"update_data_url": "{{entire_url('update_bid_chapters.dspy')}}",
"delete_data_url": "{{entire_url('delete_bid_chapters.dspy')}}"
},
"data_filter": {
"AND": [
{
"field": "project_id",
"op": "=",
"var": "project_id"
},
{
"field": "status",
"op": "=",
"var": "status_input"
},
{
"field": "title",
"op": "LIKE",
"var": "title_input"
}
]
}
}
}

View File

@ -0,0 +1,38 @@
{
"tblname": "bid_doc_requirements",
"title": "投标文件要求",
"params": {
"sortby": [
"req_type",
"order_no"
],
"confidential_fields": [],
"browserfields": {
"exclouded": []
},
"editexclouded": [
"id",
"created_at",
"updated_at"
],
"editable": {
"new_data_url": "{{entire_url('add_bid_doc_requirements.dspy')}}",
"update_data_url": "{{entire_url('update_bid_doc_requirements.dspy')}}",
"delete_data_url": "{{entire_url('delete_bid_doc_requirements.dspy')}}"
},
"data_filter": {
"AND": [
{
"field": "project_id",
"op": "=",
"var": "project_id"
},
{
"field": "req_type",
"op": "=",
"var": "type_input"
}
]
}
}
}

39
json/bid_documents.json Normal file
View File

@ -0,0 +1,39 @@
{
"tblname": "bid_documents",
"title": "合成标书",
"params": {
"sortby": [
"version desc"
],
"confidential_fields": [],
"browserfields": {
"exclouded": []
},
"editexclouded": [
"id",
"created_at",
"updated_at",
"task_id",
"review_id"
],
"editable": {
"new_data_url": "{{entire_url('add_bid_documents.dspy')}}",
"update_data_url": "{{entire_url('update_bid_documents.dspy')}}",
"delete_data_url": "{{entire_url('delete_bid_documents.dspy')}}"
},
"data_filter": {
"AND": [
{
"field": "project_id",
"op": "=",
"var": "project_id"
},
{
"field": "status",
"op": "=",
"var": "status_input"
}
]
}
}
}

45
json/bid_kb_docs.json Normal file
View File

@ -0,0 +1,45 @@
{
"tblname": "bid_kb_docs",
"title": "投标知识库",
"params": {
"sortby": [
"doc_type",
"created_at desc"
],
"confidential_fields": [],
"browserfields": {
"exclouded": [
"content_text"
]
},
"editexclouded": [
"id",
"created_at",
"updated_at"
],
"editable": {
"new_data_url": "{{entire_url('add_bid_kb_docs.dspy')}}",
"update_data_url": "{{entire_url('update_bid_kb_docs.dspy')}}",
"delete_data_url": "{{entire_url('delete_bid_kb_docs.dspy')}}"
},
"data_filter": {
"AND": [
{
"field": "doc_name",
"op": "LIKE",
"var": "name_input"
},
{
"field": "doc_type",
"op": "=",
"var": "type_input"
},
{
"field": "status",
"op": "=",
"var": "status_input"
}
]
}
}
}

View File

@ -0,0 +1,42 @@
{
"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"
}
]
}
}
}

37
json/bid_members.json Normal file
View File

@ -0,0 +1,37 @@
{
"tblname": "bid_members",
"title": "投标参与人员",
"params": {
"sortby": [
"created_at"
],
"confidential_fields": [],
"browserfields": {
"exclouded": []
},
"editexclouded": [
"id",
"created_at",
"updated_at"
],
"editable": {
"new_data_url": "{{entire_url('add_bid_members.dspy')}}",
"update_data_url": "{{entire_url('update_bid_members.dspy')}}",
"delete_data_url": "{{entire_url('delete_bid_members.dspy')}}"
},
"data_filter": {
"AND": [
{
"field": "project_id",
"op": "=",
"var": "project_id"
},
{
"field": "user_name",
"op": "LIKE",
"var": "name_input"
}
]
}
}
}

View File

@ -0,0 +1,38 @@
{
"tblname": "bid_qualifications",
"title": "招标所需资质",
"params": {
"sortby": [
"created_at"
],
"confidential_fields": [],
"browserfields": {
"exclouded": []
},
"editexclouded": [
"id",
"created_at",
"updated_at",
"human_task_id"
],
"editable": {
"new_data_url": "{{entire_url('add_bid_qualifications.dspy')}}",
"update_data_url": "{{entire_url('update_bid_qualifications.dspy')}}",
"delete_data_url": "{{entire_url('delete_bid_qualifications.dspy')}}"
},
"data_filter": {
"AND": [
{
"field": "project_id",
"op": "=",
"var": "project_id"
},
{
"field": "match_status",
"op": "=",
"var": "status_input"
}
]
}
}
}

48
json/bid_reviews.json Normal file
View File

@ -0,0 +1,48 @@
{
"tblname": "bid_reviews",
"title": "标书评审记录",
"params": {
"sortby": [
"created_at desc"
],
"confidential_fields": [],
"browserfields": {
"exclouded": [
"detail"
]
},
"editexclouded": [
"id",
"created_at",
"updated_at",
"task_id"
],
"editable": {
"new_data_url": "{{entire_url('add_bid_reviews.dspy')}}",
"update_data_url": "{{entire_url('update_bid_reviews.dspy')}}",
"delete_data_url": "{{entire_url('delete_bid_reviews.dspy')}}"
},
"data_filter": {
"AND": [
{
"field": "project_id",
"op": "=",
"var": "project_id"
},
{
"field": "scope",
"op": "=",
"var": "scope_input"
}
]
},
"subtables": [
{
"field": "review_id",
"title": "评分明细",
"url": "{{entire_url('../bid_scores?review_id=${id}')}}",
"subtable": "bid_scores"
}
]
}
}

37
json/bid_scores.json Normal file
View File

@ -0,0 +1,37 @@
{
"tblname": "bid_scores",
"title": "标书评分明细",
"params": {
"sortby": [
"created_at"
],
"confidential_fields": [],
"browserfields": {
"exclouded": []
},
"editexclouded": [
"id",
"created_at",
"updated_at"
],
"editable": {
"new_data_url": "{{entire_url('add_bid_scores.dspy')}}",
"update_data_url": "{{entire_url('update_bid_scores.dspy')}}",
"delete_data_url": "{{entire_url('delete_bid_scores.dspy')}}"
},
"data_filter": {
"AND": [
{
"field": "review_id",
"op": "=",
"var": "review_id"
},
{
"field": "project_id",
"op": "=",
"var": "project_id"
}
]
}
}
}

View File

@ -0,0 +1,43 @@
{
"tblname": "bid_scoring_items",
"title": "评分项与得分规则",
"params": {
"sortby": [
"section",
"order_no"
],
"confidential_fields": [],
"browserfields": {
"exclouded": []
},
"editexclouded": [
"id",
"created_at",
"updated_at"
],
"editable": {
"new_data_url": "{{entire_url('add_bid_scoring_items.dspy')}}",
"update_data_url": "{{entire_url('update_bid_scoring_items.dspy')}}",
"delete_data_url": "{{entire_url('delete_bid_scoring_items.dspy')}}"
},
"data_filter": {
"AND": [
{
"field": "project_id",
"op": "=",
"var": "project_id"
},
{
"field": "section",
"op": "=",
"var": "section_input"
},
{
"field": "item_name",
"op": "LIKE",
"var": "name_input"
}
]
}
}
}

View File

@ -0,0 +1,40 @@
{
"tblname": "bid_tender_files",
"title": "招标文件",
"params": {
"sortby": [
"created_at desc"
],
"confidential_fields": [],
"browserfields": {
"exclouded": [
"content_text"
]
},
"editexclouded": [
"id",
"created_at",
"updated_at",
"content_text"
],
"editable": {
"new_data_url": "{{entire_url('add_bid_tender_files.dspy')}}",
"update_data_url": "{{entire_url('update_bid_tender_files.dspy')}}",
"delete_data_url": "{{entire_url('delete_bid_tender_files.dspy')}}"
},
"data_filter": {
"AND": [
{
"field": "project_id",
"op": "=",
"var": "project_id"
},
{
"field": "extract_status",
"op": "=",
"var": "status_input"
}
]
}
}
}

52
json/bid_tenders.json Normal file
View File

@ -0,0 +1,52 @@
{
"tblname": "bid_tenders",
"title": "招标信息",
"params": {
"sortby": [
"received_at desc"
],
"confidential_fields": [],
"browserfields": {
"exclouded": [
"raw_content",
"attach_json",
"key_dates",
"org_id",
"account_id",
"mail_uid",
"risk_note"
]
},
"editexclouded": [
"id",
"created_at",
"updated_at",
"approval_task_id",
"project_id"
],
"editable": {
"new_data_url": "{{entire_url('add_bid_tenders.dspy')}}",
"update_data_url": "{{entire_url('update_bid_tenders.dspy')}}",
"delete_data_url": "{{entire_url('delete_bid_tenders.dspy')}}"
},
"data_filter": {
"AND": [
{
"field": "title",
"op": "LIKE",
"var": "title_input"
},
{
"field": "status",
"op": "=",
"var": "status_input"
},
{
"field": "purchaser",
"op": "LIKE",
"var": "purchaser_input"
}
]
}
}
}

4
json/build.sh Normal file
View File

@ -0,0 +1,4 @@
#!/bin/bash
# 由 models/*.json + json/*.json 生成 wwwroot 下 CRUD 页面
cd "$(dirname "$0")"
../../pipeline-app/py3/bin/xls2ui -m ../models -o ../wwwroot pipeline_bidding *.json

193
models/bid_chapters.json Normal file
View File

@ -0,0 +1,193 @@
{
"summary": [
{
"name": "bid_chapters",
"title": "投标文件章节",
"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": "chapter_no",
"title": "章节号",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "parent_no",
"title": "父章节号",
"type": "str",
"length": 32
},
{
"name": "title",
"title": "章节标题",
"type": "str",
"length": 255,
"nullable": "no"
},
{
"name": "section",
"title": "所属部分",
"type": "str",
"length": 32,
"default": "technical"
},
{
"name": "scoring_item_ids",
"title": "对应评分项(JSON)",
"type": "text"
},
{
"name": "outline",
"title": "章节要点",
"type": "text"
},
{
"name": "status",
"title": "章节状态",
"type": "str",
"length": 16,
"nullable": "no",
"default": "pending"
},
{
"name": "version",
"title": "版本号",
"type": "int",
"nullable": "no",
"default": "1"
},
{
"name": "content",
"title": "章节正文",
"type": "text"
},
{
"name": "file_path",
"title": "章节文件路径",
"type": "str",
"length": 500
},
{
"name": "word_count",
"title": "字数",
"type": "int",
"default": "0"
},
{
"name": "review_score",
"title": "评审得分",
"type": "float",
"length": 8,
"dec": 2
},
{
"name": "max_score",
"title": "对应满分",
"type": "float",
"length": 8,
"dec": 2
},
{
"name": "review_comment",
"title": "评审意见/改进意见",
"type": "text"
},
{
"name": "revise_count",
"title": "重做次数",
"type": "int",
"nullable": "no",
"default": "0"
},
{
"name": "assignee_role",
"title": "承办角色",
"type": "str",
"length": 64,
"default": "agent.bid_writer"
},
{
"name": "task_id",
"title": "关联任务ID",
"type": "str",
"length": 32
},
{
"name": "order_no",
"title": "排序",
"type": "int",
"default": "0"
},
{
"name": "created_at",
"title": "创建时间",
"type": "timestamp",
"nullable": "no"
},
{
"name": "updated_at",
"title": "更新时间",
"type": "timestamp"
}
],
"indexes": [
{
"name": "idx_bc_project",
"idxtype": "index",
"idxfields": [
"project_id"
]
},
{
"name": "idx_bc_status",
"idxtype": "index",
"idxfields": [
"status"
]
},
{
"name": "idx_bc_no",
"idxtype": "index",
"idxfields": [
"project_id",
"chapter_no"
]
}
],
"codes": [
{
"field": "status",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='bid_chapter_status'"
},
{
"field": "section",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='bid_scoring_section'"
}
]
}

View File

@ -0,0 +1,106 @@
{
"summary": [
{
"name": "bid_doc_requirements",
"title": "投标文件要求",
"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": "req_type",
"title": "要求类型",
"type": "str",
"length": 32,
"nullable": "no",
"default": "structure"
},
{
"name": "chapter_no",
"title": "章节号",
"type": "str",
"length": 32
},
{
"name": "chapter_title",
"title": "章节标题",
"type": "str",
"length": 255
},
{
"name": "requirement",
"title": "要求内容",
"type": "text"
},
{
"name": "page_limit",
"title": "页数限制",
"type": "int"
},
{
"name": "source_ref",
"title": "招标文件出处",
"type": "str",
"length": 500
},
{
"name": "order_no",
"title": "排序",
"type": "int",
"default": "0"
},
{
"name": "created_at",
"title": "创建时间",
"type": "timestamp",
"nullable": "no"
},
{
"name": "updated_at",
"title": "更新时间",
"type": "timestamp"
}
],
"indexes": [
{
"name": "idx_bdr_project",
"idxtype": "index",
"idxfields": [
"project_id"
]
},
{
"name": "idx_bdr_type",
"idxtype": "index",
"idxfields": [
"req_type"
]
}
],
"codes": [
{
"field": "req_type",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='bid_req_type'"
}
]
}

142
models/bid_documents.json Normal file
View File

@ -0,0 +1,142 @@
{
"summary": [
{
"name": "bid_documents",
"title": "合成标书",
"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": "version",
"title": "版本号",
"type": "int",
"nullable": "no",
"default": "1"
},
{
"name": "doc_name",
"title": "标书名称",
"type": "str",
"length": 255
},
{
"name": "file_path",
"title": "文件路径",
"type": "str",
"length": 500
},
{
"name": "page_count",
"title": "页数",
"type": "int",
"default": "0"
},
{
"name": "chapter_count",
"title": "章节数",
"type": "int",
"default": "0"
},
{
"name": "total_score",
"title": "总得分",
"type": "float",
"length": 8,
"dec": 2
},
{
"name": "max_score",
"title": "总满分",
"type": "float",
"length": 8,
"dec": 2
},
{
"name": "pass_ratio",
"title": "通过门限",
"type": "float",
"length": 5,
"dec": 4
},
{
"name": "status",
"title": "状态",
"type": "str",
"length": 16,
"nullable": "no",
"default": "draft"
},
{
"name": "review_id",
"title": "评审记录ID",
"type": "str",
"length": 32
},
{
"name": "task_id",
"title": "关联任务ID",
"type": "str",
"length": 32
},
{
"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_bdoc_project",
"idxtype": "index",
"idxfields": [
"project_id"
]
},
{
"name": "idx_bdoc_status",
"idxtype": "index",
"idxfields": [
"status"
]
}
],
"codes": [
{
"field": "status",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='bid_doc_status'"
}
]
}

152
models/bid_kb_docs.json Normal file
View File

@ -0,0 +1,152 @@
{
"summary": [
{
"name": "bid_kb_docs",
"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": "doc_type",
"title": "文档类型",
"type": "str",
"length": 32,
"nullable": "no",
"default": "qualification"
},
{
"name": "doc_name",
"title": "文档名称",
"type": "str",
"length": 255,
"nullable": "no"
},
{
"name": "doc_no",
"title": "证书/合同编号",
"type": "str",
"length": 64
},
{
"name": "tags",
"title": "标签(逗号分隔)",
"type": "str",
"length": 500
},
{
"name": "file_path",
"title": "文件路径",
"type": "str",
"length": 500
},
{
"name": "content_text",
"title": "正文内容",
"type": "text"
},
{
"name": "issuer",
"title": "颁发/签约方",
"type": "str",
"length": 255
},
{
"name": "customer",
"title": "案例客户",
"type": "str",
"length": 255
},
{
"name": "contract_amount",
"title": "合同金额",
"type": "float",
"length": 18,
"dec": 2
},
{
"name": "valid_from",
"title": "生效日期",
"type": "date"
},
{
"name": "valid_until",
"title": "有效期至",
"type": "date"
},
{
"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_bkd_org",
"idxtype": "index",
"idxfields": [
"org_id"
]
},
{
"name": "idx_bkd_type",
"idxtype": "index",
"idxfields": [
"doc_type"
]
},
{
"name": "idx_bkd_status",
"idxtype": "index",
"idxfields": [
"status"
]
}
],
"codes": [
{
"field": "doc_type",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='bid_kb_doc_type'"
}
]
}

View File

@ -0,0 +1,159 @@
{
"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": []
}

103
models/bid_members.json Normal file
View File

@ -0,0 +1,103 @@
{
"summary": [
{
"name": "bid_members",
"title": "投标项目参与人员",
"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": "user_id",
"title": "用户ID",
"type": "str",
"length": 32
},
{
"name": "user_name",
"title": "姓名",
"type": "str",
"length": 128
},
{
"name": "member_role",
"title": "承担角色",
"type": "str",
"length": 64,
"nullable": "no",
"default": "writer"
},
{
"name": "duty",
"title": "职责说明",
"type": "str",
"length": 500
},
{
"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_bm_project",
"idxtype": "index",
"idxfields": [
"project_id"
]
},
{
"name": "idx_bm_user",
"idxtype": "index",
"idxfields": [
"user_id"
]
}
],
"codes": [
{
"field": "member_role",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='bid_member_role'"
}
]
}

View File

@ -0,0 +1,131 @@
{
"summary": [
{
"name": "bid_qualifications",
"title": "招标所需资质",
"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": "qual_name",
"title": "资质名称",
"type": "str",
"length": 255,
"nullable": "no"
},
{
"name": "requirement",
"title": "资质要求原文",
"type": "text"
},
{
"name": "is_mandatory",
"title": "是否必需",
"type": "str",
"length": 1,
"nullable": "no",
"default": "1"
},
{
"name": "owner",
"title": "提供方",
"type": "str",
"length": 16,
"nullable": "no",
"default": "kb"
},
{
"name": "kb_doc_id",
"title": "知识库文档ID",
"type": "str",
"length": 32
},
{
"name": "match_status",
"title": "匹配状态",
"type": "str",
"length": 16,
"nullable": "no",
"default": "pending"
},
{
"name": "file_path",
"title": "文档路径",
"type": "str",
"length": 500
},
{
"name": "note",
"title": "说明",
"type": "str",
"length": 500
},
{
"name": "human_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_bq_project",
"idxtype": "index",
"idxfields": [
"project_id"
]
},
{
"name": "idx_bq_match",
"idxtype": "index",
"idxfields": [
"match_status"
]
}
],
"codes": [
{
"field": "match_status",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='bid_qual_match_status'"
},
{
"field": "owner",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='bid_qual_owner'"
}
]
}

156
models/bid_reviews.json Normal file
View File

@ -0,0 +1,156 @@
{
"summary": [
{
"name": "bid_reviews",
"title": "标书评审记录",
"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": "scope",
"title": "评审范围",
"type": "str",
"length": 16,
"nullable": "no",
"default": "chapter"
},
{
"name": "chapter_id",
"title": "章节ID",
"type": "str",
"length": 32
},
{
"name": "document_id",
"title": "标书ID",
"type": "str",
"length": 32
},
{
"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": "score",
"title": "得分",
"type": "float",
"length": 8,
"dec": 2
},
{
"name": "max_score",
"title": "满分",
"type": "float",
"length": 8,
"dec": 2
},
{
"name": "pass_ratio",
"title": "通过门限",
"type": "float",
"length": 5,
"dec": 4
},
{
"name": "passed",
"title": "是否通过",
"type": "str",
"length": 1,
"nullable": "no",
"default": "0"
},
{
"name": "comments",
"title": "改进意见",
"type": "text"
},
{
"name": "detail",
"title": "评审明细(JSON)",
"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_br_project",
"idxtype": "index",
"idxfields": [
"project_id"
]
},
{
"name": "idx_br_scope",
"idxtype": "index",
"idxfields": [
"scope"
]
},
{
"name": "idx_br_chapter",
"idxtype": "index",
"idxfields": [
"chapter_id"
]
}
],
"codes": [
{
"field": "scope",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='bid_review_scope'"
}
]
}

111
models/bid_scores.json Normal file
View File

@ -0,0 +1,111 @@
{
"summary": [
{
"name": "bid_scores",
"title": "标书评分明细",
"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": "review_id",
"title": "评审记录ID",
"type": "str",
"length": 32
},
{
"name": "scoring_item_id",
"title": "评分项ID",
"type": "str",
"length": 32
},
{
"name": "item_name",
"title": "评分项名称",
"type": "str",
"length": 255
},
{
"name": "score",
"title": "得分",
"type": "float",
"length": 8,
"dec": 2
},
{
"name": "max_score",
"title": "满分",
"type": "float",
"length": 8,
"dec": 2
},
{
"name": "reason",
"title": "评分理由",
"type": "text"
},
{
"name": "improvement",
"title": "改进意见",
"type": "text"
},
{
"name": "target_chapter_id",
"title": "待改章节ID",
"type": "str",
"length": 32
},
{
"name": "created_at",
"title": "创建时间",
"type": "timestamp",
"nullable": "no"
},
{
"name": "updated_at",
"title": "更新时间",
"type": "timestamp"
}
],
"indexes": [
{
"name": "idx_bs_project",
"idxtype": "index",
"idxfields": [
"project_id"
]
},
{
"name": "idx_bs_review",
"idxtype": "index",
"idxfields": [
"review_id"
]
},
{
"name": "idx_bs_chapter",
"idxtype": "index",
"idxfields": [
"target_chapter_id"
]
}
],
"codes": []
}

View File

@ -0,0 +1,132 @@
{
"summary": [
{
"name": "bid_scoring_items",
"title": "评分项与得分规则",
"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": "section",
"title": "评分部分",
"type": "str",
"length": 32,
"nullable": "no",
"default": "technical"
},
{
"name": "item_no",
"title": "评分项编号",
"type": "str",
"length": 32
},
{
"name": "item_name",
"title": "评分项名称",
"type": "str",
"length": 255,
"nullable": "no"
},
{
"name": "max_score",
"title": "满分",
"type": "float",
"length": 8,
"dec": 2,
"nullable": "no",
"default": "0"
},
{
"name": "scoring_rule",
"title": "得分规则",
"type": "text"
},
{
"name": "is_veto",
"title": "否决项",
"type": "str",
"length": 1,
"nullable": "no",
"default": "0"
},
{
"name": "weight",
"title": "权重",
"type": "float",
"length": 8,
"dec": 4
},
{
"name": "source_ref",
"title": "招标文件出处",
"type": "str",
"length": 500
},
{
"name": "target_chapter_no",
"title": "对应章节号",
"type": "str",
"length": 32
},
{
"name": "order_no",
"title": "排序",
"type": "int",
"default": "0"
},
{
"name": "created_at",
"title": "创建时间",
"type": "timestamp",
"nullable": "no"
},
{
"name": "updated_at",
"title": "更新时间",
"type": "timestamp"
}
],
"indexes": [
{
"name": "idx_bsi_project",
"idxtype": "index",
"idxfields": [
"project_id"
]
},
{
"name": "idx_bsi_section",
"idxtype": "index",
"idxfields": [
"section"
]
}
],
"codes": [
{
"field": "section",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='bid_scoring_section'"
}
]
}

View File

@ -0,0 +1,129 @@
{
"summary": [
{
"name": "bid_tender_files",
"title": "招标文件",
"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": "tender_id",
"title": "招标信息ID",
"type": "str",
"length": 32
},
{
"name": "file_name",
"title": "文件名",
"type": "str",
"length": 255,
"nullable": "no"
},
{
"name": "file_path",
"title": "文件路径",
"type": "str",
"length": 500
},
{
"name": "file_type",
"title": "文件类型",
"type": "str",
"length": 32,
"nullable": "no",
"default": "tender_doc"
},
{
"name": "file_size",
"title": "文件大小",
"type": "int",
"default": "0"
},
{
"name": "content_text",
"title": "正文提取",
"type": "text"
},
{
"name": "extract_status",
"title": "抽取状态",
"type": "str",
"length": 16,
"nullable": "no",
"default": "pending"
},
{
"name": "extract_note",
"title": "抽取说明",
"type": "str",
"length": 500
},
{
"name": "uploaded_by",
"title": "上传人",
"type": "str",
"length": 32
},
{
"name": "created_at",
"title": "创建时间",
"type": "timestamp",
"nullable": "no"
},
{
"name": "updated_at",
"title": "更新时间",
"type": "timestamp"
}
],
"indexes": [
{
"name": "idx_btf_project",
"idxtype": "index",
"idxfields": [
"project_id"
]
},
{
"name": "idx_btf_tender",
"idxtype": "index",
"idxfields": [
"tender_id"
]
},
{
"name": "idx_btf_status",
"idxtype": "index",
"idxfields": [
"extract_status"
]
}
],
"codes": [
{
"field": "file_type",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='bid_file_type'"
}
]
}

246
models/bid_tenders.json Normal file
View File

@ -0,0 +1,246 @@
{
"summary": [
{
"name": "bid_tenders",
"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": "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"
}
],
"indexes": [
{
"name": "idx_bt_org",
"idxtype": "index",
"idxfields": [
"org_id"
]
},
{
"name": "idx_bt_status",
"idxtype": "index",
"idxfields": [
"status"
]
},
{
"name": "idx_bt_mailuid",
"idxtype": "index",
"idxfields": [
"mail_uid"
]
},
{
"name": "idx_bt_project",
"idxtype": "index",
"idxfields": [
"project_id"
]
},
{
"name": "idx_bt_deadline",
"idxtype": "index",
"idxfields": [
"bid_deadline"
]
}
],
"codes": [
{
"field": "status",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='bid_tender_status'"
},
{
"field": "source",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='bid_tender_source'"
}
]
}

526
mysql.ddl.sql Normal file
View File

@ -0,0 +1,526 @@
-- /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
-- 建库时请用以下语句,支持emoji字符
-- CREATE DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
drop table if exists bid_scoring_items;
CREATE TABLE bid_scoring_items
(
`id` VARCHAR(32) NOT NULL comment '主键ID',
`project_id` VARCHAR(32) NOT NULL comment '项目ID',
`section` VARCHAR(32) NOT NULL DEFAULT 'technical' comment '评分部分',
`item_no` VARCHAR(32) comment '评分项编号',
`item_name` VARCHAR(255) NOT NULL comment '评分项名称',
`max_score` double(8,2) NOT NULL DEFAULT '0' comment '满分',
`scoring_rule` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci comment '得分规则',
`is_veto` VARCHAR(1) NOT NULL DEFAULT '0' comment '否决项',
`weight` double(8,4) comment '权重',
`source_ref` VARCHAR(500) comment '招标文件出处',
`target_chapter_no` VARCHAR(32) comment '对应章节号',
`order_no` int DEFAULT '0' 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_scoring_items_idx_bsi_project ON bid_scoring_items(project_id);
CREATE INDEX bid_scoring_items_idx_bsi_section ON bid_scoring_items(section);
-- /home/ymq/work/pipeline/pipeline-bidding/models/bid_doc_requirements.json
-- 建库时请用以下语句,支持emoji字符
-- CREATE DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
drop table if exists bid_doc_requirements;
CREATE TABLE bid_doc_requirements
(
`id` VARCHAR(32) NOT NULL comment '主键ID',
`project_id` VARCHAR(32) NOT NULL comment '项目ID',
`req_type` VARCHAR(32) NOT NULL DEFAULT 'structure' comment '要求类型',
`chapter_no` VARCHAR(32) comment '章节号',
`chapter_title` VARCHAR(255) comment '章节标题',
`requirement` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci comment '要求内容',
`page_limit` int comment '页数限制',
`source_ref` VARCHAR(500) comment '招标文件出处',
`order_no` int DEFAULT '0' 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_doc_requirements_idx_bdr_project ON bid_doc_requirements(project_id);
CREATE INDEX bid_doc_requirements_idx_bdr_type ON bid_doc_requirements(req_type);
-- /home/ymq/work/pipeline/pipeline-bidding/models/bid_members.json
-- 建库时请用以下语句,支持emoji字符
-- CREATE DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
drop table if exists bid_members;
CREATE TABLE bid_members
(
`id` VARCHAR(32) NOT NULL comment '主键ID',
`project_id` VARCHAR(32) NOT NULL comment '项目ID',
`user_id` VARCHAR(32) comment '用户ID',
`user_name` VARCHAR(128) comment '姓名',
`member_role` VARCHAR(64) NOT NULL DEFAULT 'writer' comment '承担角色',
`duty` VARCHAR(500) 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_members_idx_bm_project ON bid_members(project_id);
CREATE INDEX bid_members_idx_bm_user ON bid_members(user_id);
-- /home/ymq/work/pipeline/pipeline-bidding/models/bid_tender_files.json
-- 建库时请用以下语句,支持emoji字符
-- CREATE DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
drop table if exists bid_tender_files;
CREATE TABLE bid_tender_files
(
`id` VARCHAR(32) NOT NULL comment '主键ID',
`project_id` VARCHAR(32) NOT NULL comment '项目ID',
`tender_id` VARCHAR(32) comment '招标信息ID',
`file_name` VARCHAR(255) NOT NULL comment '文件名',
`file_path` VARCHAR(500) comment '文件路径',
`file_type` VARCHAR(32) NOT NULL DEFAULT 'tender_doc' comment '文件类型',
`file_size` int DEFAULT '0' comment '文件大小',
`content_text` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci comment '正文提取',
`extract_status` VARCHAR(16) NOT NULL DEFAULT 'pending' comment '抽取状态',
`extract_note` VARCHAR(500) comment '抽取说明',
`uploaded_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_tender_files_idx_btf_project ON bid_tender_files(project_id);
CREATE INDEX bid_tender_files_idx_btf_tender ON bid_tender_files(tender_id);
CREATE INDEX bid_tender_files_idx_btf_status ON bid_tender_files(extract_status);
-- /home/ymq/work/pipeline/pipeline-bidding/models/bid_chapters.json
-- 建库时请用以下语句,支持emoji字符
-- CREATE DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
drop table if exists bid_chapters;
CREATE TABLE bid_chapters
(
`id` VARCHAR(32) NOT NULL comment '主键ID',
`project_id` VARCHAR(32) NOT NULL comment '项目ID',
`chapter_no` VARCHAR(32) NOT NULL comment '章节号',
`parent_no` VARCHAR(32) comment '父章节号',
`title` VARCHAR(255) NOT NULL comment '章节标题',
`section` VARCHAR(32) DEFAULT 'technical' comment '所属部分',
`scoring_item_ids` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci comment '对应评分项(JSON)',
`outline` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci comment '章节要点',
`status` VARCHAR(16) NOT NULL DEFAULT 'pending' comment '章节状态',
`version` int NOT NULL DEFAULT '1' comment '版本号',
`content` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci comment '章节正文',
`file_path` VARCHAR(500) comment '章节文件路径',
`word_count` int DEFAULT '0' comment '字数',
`review_score` double(8,2) comment '评审得分',
`max_score` double(8,2) comment '对应满分',
`review_comment` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci comment '评审意见/改进意见',
`revise_count` int NOT NULL DEFAULT '0' comment '重做次数',
`assignee_role` VARCHAR(64) DEFAULT 'agent.bid_writer' comment '承办角色',
`task_id` VARCHAR(32) comment '关联任务ID',
`order_no` int DEFAULT '0' 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_chapters_idx_bc_project ON bid_chapters(project_id);
CREATE INDEX bid_chapters_idx_bc_status ON bid_chapters(status);
CREATE INDEX bid_chapters_idx_bc_no ON bid_chapters(project_id,chapter_no);
-- /home/ymq/work/pipeline/pipeline-bidding/models/bid_reviews.json
-- 建库时请用以下语句,支持emoji字符
-- CREATE DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
drop table if exists bid_reviews;
CREATE TABLE bid_reviews
(
`id` VARCHAR(32) NOT NULL comment '主键ID',
`project_id` VARCHAR(32) NOT NULL comment '项目ID',
`scope` VARCHAR(16) NOT NULL DEFAULT 'chapter' comment '评审范围',
`chapter_id` VARCHAR(32) comment '章节ID',
`document_id` VARCHAR(32) comment '标书ID',
`round` int NOT NULL DEFAULT '1' comment '评审轮次',
`reviewer` VARCHAR(64) comment '评审方',
`review_type` VARCHAR(16) DEFAULT 'agent' comment '评审类型',
`score` double(8,2) comment '得分',
`max_score` double(8,2) comment '满分',
`pass_ratio` double(5,4) comment '通过门限',
`passed` VARCHAR(1) NOT NULL DEFAULT '0' comment '是否通过',
`comments` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci comment '改进意见',
`detail` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci comment '评审明细(JSON)',
`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 '标书评审记录'
;
CREATE INDEX bid_reviews_idx_br_project ON bid_reviews(project_id);
CREATE INDEX bid_reviews_idx_br_scope ON bid_reviews(scope);
CREATE INDEX bid_reviews_idx_br_chapter ON bid_reviews(chapter_id);
-- /home/ymq/work/pipeline/pipeline-bidding/models/bid_tenders.json
-- 建库时请用以下语句,支持emoji字符
-- CREATE DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
drop table if exists bid_tenders;
CREATE TABLE bid_tenders
(
`id` VARCHAR(32) NOT NULL comment '主键ID',
`org_id` VARCHAR(32) NOT NULL DEFAULT '0' comment '机构ID',
`source` VARCHAR(16) NOT NULL DEFAULT 'mail' comment '信息来源',
`account_id` VARCHAR(32) comment '邮箱配置ID',
`mail_uid` VARCHAR(64) comment '邮件UID',
`mail_from` VARCHAR(255) comment '发件人',
`mail_subject` VARCHAR(500) comment '邮件主题',
`received_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP comment '收信时间',
`raw_content` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci comment '邮件原文',
`attach_json` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci comment '附件清单(JSON)',
`title` VARCHAR(255) comment '项目名称',
`purchaser` VARCHAR(255) comment '招标人/采购人',
`tender_no` VARCHAR(64) comment '招标编号',
`industry` VARCHAR(64) comment '行业',
`summary` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci comment '内容摘要',
`bid_start_at` date comment '投标开始时间',
`bid_deadline` date comment '投标截止时间',
`project_period` VARCHAR(64) comment '项目周期',
`budget_amount` double(18,2) comment '招标金额',
`deposit_amount` double(18,2) comment '投标保证金',
`qualification_req` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci comment '资质要求摘要',
`key_dates` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci comment '关键时间节点(JSON)',
`risk_note` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci comment '风险提示',
`status` VARCHAR(16) NOT NULL DEFAULT 'new' comment '状态',
`approval_task_id` VARCHAR(32) comment '审批任务ID',
`project_id` VARCHAR(32) comment '立项项目ID',
`archive_reason` VARCHAR(500) comment '作废/存档原因',
`created_by` VARCHAR(32) comment '创建人',
`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_tenders_idx_bt_org ON bid_tenders(org_id);
CREATE INDEX bid_tenders_idx_bt_status ON bid_tenders(status);
CREATE INDEX bid_tenders_idx_bt_mailuid ON bid_tenders(mail_uid);
CREATE INDEX bid_tenders_idx_bt_project ON bid_tenders(project_id);
CREATE INDEX bid_tenders_idx_bt_deadline ON bid_tenders(bid_deadline);
-- /home/ymq/work/pipeline/pipeline-bidding/models/bid_documents.json
-- 建库时请用以下语句,支持emoji字符
-- CREATE DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
drop table if exists bid_documents;
CREATE TABLE bid_documents
(
`id` VARCHAR(32) NOT NULL comment '主键ID',
`project_id` VARCHAR(32) NOT NULL comment '项目ID',
`version` int NOT NULL DEFAULT '1' comment '版本号',
`doc_name` VARCHAR(255) comment '标书名称',
`file_path` VARCHAR(500) comment '文件路径',
`page_count` int DEFAULT '0' comment '页数',
`chapter_count` int DEFAULT '0' comment '章节数',
`total_score` double(8,2) comment '总得分',
`max_score` double(8,2) comment '总满分',
`pass_ratio` double(5,4) comment '通过门限',
`status` VARCHAR(16) NOT NULL DEFAULT 'draft' comment '状态',
`review_id` VARCHAR(32) comment '评审记录ID',
`task_id` VARCHAR(32) comment '关联任务ID',
`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_documents_idx_bdoc_project ON bid_documents(project_id);
CREATE INDEX bid_documents_idx_bdoc_status ON bid_documents(status);
-- /home/ymq/work/pipeline/pipeline-bidding/models/bid_kb_docs.json
-- 建库时请用以下语句,支持emoji字符
-- CREATE DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
drop table if exists bid_kb_docs;
CREATE TABLE bid_kb_docs
(
`id` VARCHAR(32) NOT NULL comment '主键ID',
`org_id` VARCHAR(32) NOT NULL DEFAULT '0' comment '机构ID',
`doc_type` VARCHAR(32) NOT NULL DEFAULT 'qualification' comment '文档类型',
`doc_name` VARCHAR(255) NOT NULL comment '文档名称',
`doc_no` VARCHAR(64) comment '证书/合同编号',
`tags` VARCHAR(500) comment '标签(逗号分隔)',
`file_path` VARCHAR(500) comment '文件路径',
`content_text` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci comment '正文内容',
`issuer` VARCHAR(255) comment '颁发/签约方',
`customer` VARCHAR(255) comment '案例客户',
`contract_amount` double(18,2) comment '合同金额',
`valid_from` date comment '生效日期',
`valid_until` date 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_kb_docs_idx_bkd_org ON bid_kb_docs(org_id);
CREATE INDEX bid_kb_docs_idx_bkd_type ON bid_kb_docs(doc_type);
CREATE INDEX bid_kb_docs_idx_bkd_status ON bid_kb_docs(status);
-- /home/ymq/work/pipeline/pipeline-bidding/models/bid_scores.json
-- 建库时请用以下语句,支持emoji字符
-- CREATE DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
drop table if exists bid_scores;
CREATE TABLE bid_scores
(
`id` VARCHAR(32) NOT NULL comment '主键ID',
`project_id` VARCHAR(32) NOT NULL comment '项目ID',
`review_id` VARCHAR(32) comment '评审记录ID',
`scoring_item_id` VARCHAR(32) comment '评分项ID',
`item_name` VARCHAR(255) comment '评分项名称',
`score` double(8,2) comment '得分',
`max_score` double(8,2) comment '满分',
`reason` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci comment '评分理由',
`improvement` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci comment '改进意见',
`target_chapter_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 '标书评分明细'
;
CREATE INDEX bid_scores_idx_bs_project ON bid_scores(project_id);
CREATE INDEX bid_scores_idx_bs_review ON bid_scores(review_id);
CREATE INDEX bid_scores_idx_bs_chapter ON bid_scores(target_chapter_id);
-- /home/ymq/work/pipeline/pipeline-bidding/models/bid_qualifications.json
-- 建库时请用以下语句,支持emoji字符
-- CREATE DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
drop table if exists bid_qualifications;
CREATE TABLE bid_qualifications
(
`id` VARCHAR(32) NOT NULL comment '主键ID',
`project_id` VARCHAR(32) NOT NULL comment '项目ID',
`qual_name` VARCHAR(255) NOT NULL comment '资质名称',
`requirement` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci comment '资质要求原文',
`is_mandatory` VARCHAR(1) NOT NULL DEFAULT '1' comment '是否必需',
`owner` VARCHAR(16) NOT NULL DEFAULT 'kb' comment '提供方',
`kb_doc_id` VARCHAR(32) comment '知识库文档ID',
`match_status` VARCHAR(16) NOT NULL DEFAULT 'pending' comment '匹配状态',
`file_path` VARCHAR(500) comment '文档路径',
`note` VARCHAR(500) comment '说明',
`human_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 '招标所需资质'
;
CREATE INDEX bid_qualifications_idx_bq_project ON bid_qualifications(project_id);
CREATE INDEX bid_qualifications_idx_bq_match ON bid_qualifications(match_status);

View File

@ -0,0 +1,9 @@
"""pipeline_bidding — 投标产线模块(产线能力包,宿主不装则零接触)。
模块 = 数据(bid_* 表 + CRUD)+ 能力(bid_*_capability)+ 流转(bid_flow 对账器)
+ 主 agent 能力包(bid_ability)+ 角色技能(部署期随 pipeline-core skills_library 分发)。
"""
__version__ = "0.1.0"
PIPELINE_ID = "bidding_general"

View File

@ -0,0 +1,611 @@
"""投标产线能力包(bidding_general):主 agent(驾驶舱)工具 + 角色定义 + 菜单 + slash 命令。
与开发产线(sdlc_general)同构,通过 pipeline-core 的 PipelineAbility 注册表挂载,
零侵入 core / AgentExecutor:项目 sd_projects.pipeline_id='bidding_general' 时自动生效。
角色任务流转不用 RoleSpec.next_role(全部留空),一律由 bid_flow 状态对账器驱动 —— 因为投标
产线是「一章一任务 + 两级回退循环」的扇出结构,不是线性任务链。
"""
import json
import logging
from pipeline_core import (
ToolDefinition, PipelineAbility, RoleSpec, register_ability,
SlashCommand, register_slash_command,
)
logger = logging.getLogger("pipeline.bidding.ability")
PIPELINE_ID = "bidding_general"
# ══════════════════ 工具定义(主 agent / 驾驶舱)══════════════════
BID_TOOLS = [
# ── 招标情报 ──
ToolDefinition(name="fetch_tenders", description="从配置的招标邮箱采集招标信息入库(按关键词过滤+去重)",
parameters={"days": "回溯天数(可选,默认按邮箱配置)", "account_id": "邮箱配置ID(可选,默认全部启用)"},
category="tender"),
ToolDefinition(name="import_tender", description="手工导入一条招标信息(邮箱不可用或线下拿到公告时用)",
parameters={"title": "项目名称", "content": "公告正文", "purchaser": "招标人(可选)",
"tender_no": "招标编号(可选)"}, category="tender"),
ToolDefinition(name="list_tenders", description="列出招标信息(可按状态筛选:new/summarized/pending_approval/approved/rejected/archived)",
parameters={"status": "状态(可选)", "limit": "条数(可选,默认50)"}, category="tender"),
ToolDefinition(name="tender_detail", description="查看一条招标信息详情(含公告正文)",
parameters={"tender_id": "招标信息ID"}, category="tender"),
ToolDefinition(name="summarize_tender", description="对招标信息做内容摘要与要素抽取(投标开始时间/项目周期/招标金额/保证金/资质要求)",
parameters={"tender_id": "招标信息ID"}, category="tender"),
ToolDefinition(name="set_tender_fields", description="修正招标信息要素(摘要抽错时人工纠正)",
parameters={"tender_id": "招标信息ID", "title": "项目名称(可选)", "purchaser": "招标人(可选)",
"bid_start_at": "投标开始时间(可选)", "bid_deadline": "投标截止(可选)",
"project_period": "项目周期(可选)", "budget_amount": "招标金额(可选)"},
category="tender"),
ToolDefinition(name="submit_tender_approval", description="提交招标信息审批(生成人类审批任务,等待通过/不通过)",
parameters={"tender_id": "招标信息ID", "assignee_role": "审批角色(可选,默认owner.superuser)",
"note": "提交说明(可选)"}, category="tender"),
ToolDefinition(name="approve_tender", description="审批通过招标信息 → 自动立项并发布「上传招标文件」任务",
parameters={"tender_id": "招标信息ID", "comment": "审批意见(可选)"}, category="tender"),
ToolDefinition(name="reject_tender", description="审批不通过招标信息 → 该招标信息自动存档作废",
parameters={"tender_id": "招标信息ID", "comment": "不通过原因"}, category="tender"),
ToolDefinition(name="archive_tender", description="存档作废一条招标信息(不再跟进)",
parameters={"tender_id": "招标信息ID", "reason": "原因"}, category="tender"),
ToolDefinition(name="reopen_tender", description="重新评估已作废/已驳回的招标信息",
parameters={"tender_id": "招标信息ID", "reason": "原因(可选)"}, category="tender"),
# ── 立项 / 人员 / 招标文件 ──
ToolDefinition(name="create_bid_project", description="由已审批通过的招标信息立项(创建投标项目)",
parameters={"tender_id": "招标信息ID", "name": "项目名称(可选)"}, category="project"),
ToolDefinition(name="assign_members", description="设定投标项目参与人员,如「张三:负责人,李四:writer」",
parameters={"members": "人员清单(姓名:角色 逗号分隔,或JSON数组)"}, category="project"),
ToolDefinition(name="list_members", description="查看投标项目参与人员", parameters={}, category="project"),
ToolDefinition(name="request_tender_file", description="发布/重发「等待上传招标文件」人类任务",
parameters={"note": "说明(可选)"}, category="project"),
ToolDefinition(name="list_tender_files", description="查看已上传的招标文件及抽取状态", parameters={}, category="project"),
# ── 解析结果 ──
ToolDefinition(name="list_scoring_items", description="查看评分项与得分规则(从招标文件抽取的结果)",
parameters={"section": "评分部分(可选 business/technical/price)"}, category="analysis"),
ToolDefinition(name="list_doc_requirements", description="查看投标文件要求(章节结构/格式/密封/份数/截止)",
parameters={"req_type": "类型(可选 structure/format/seal/copies/deadline)"}, category="analysis"),
ToolDefinition(name="list_qualifications", description="查看招标所需资质及匹配状态(matched/missing/human_required)",
parameters={"match_status": "匹配状态(可选)"}, category="analysis"),
# ── 知识库 ──
ToolDefinition(name="search_bid_kb", description="检索公司投标知识库(资质证书/业绩案例/模板/财务/人员)",
parameters={"query": "关键词", "doc_type": "类型(可选 qualification/cert/case/template/finance/personnel)"},
category="kb"),
ToolDefinition(name="list_kb_docs", description="列出公司投标知识库文档",
parameters={"doc_type": "类型(可选)"}, category="kb"),
ToolDefinition(name="add_kb_doc", description="向公司投标知识库录入文档(资质证书/业绩案例等)",
parameters={"doc_name": "文档名称", "doc_type": "类型", "doc_no": "编号(可选)",
"tags": "标签(可选)", "file_path": "文件路径(可选)", "customer": "案例客户(可选)",
"contract_amount": "合同金额(可选)"}, category="kb"),
ToolDefinition(name="request_human_docs", description="发布「人类应提供的文件」清单任务(知识库缺失的资质由人工提供)",
parameters={"items": "清单(可选,默认自动汇总缺失资质)", "note": "说明(可选)"}, category="kb"),
# ── 章节 / 标书 / 评分 ──
ToolDefinition(name="list_chapters", description="查看投标文件章节及状态(pending/writing/written/approved/rejected)",
parameters={"status": "状态(可选)"}, category="bid"),
ToolDefinition(name="chapter_detail", description="查看章节详情(要点/对应评分项/评审意见/正文)",
parameters={"chapter_id": "章节ID"}, category="bid"),
ToolDefinition(name="list_bid_documents", description="查看已合成的标书版本及评分", parameters={}, category="bid"),
ToolDefinition(name="list_reviews", description="查看评审记录(章节级/整书级)",
parameters={"scope": "范围(可选 chapter/whole)", "chapter_id": "章节ID(可选)"}, category="bid"),
ToolDefinition(name="bid_score_report", description="查看最近一轮整书评分报告(总分+逐项明细+改进意见)",
parameters={}, category="bid"),
# ── 诊断 / 推进 ──
ToolDefinition(name="diagnose_bid", description="诊断投标项目:卡在哪个阶段、门禁、章节分布、在办任务、最近评分",
parameters={}, category="agent"),
ToolDefinition(name="advance_bid", description="立即触发投标流程状态对账(补派应有的角色任务,通常无需手动调用)",
parameters={}, category="agent"),
]
# ══════════════════ 产线 prompt 片段 ══════════════════
BID_PROMPT = """你是「投标产线」的驾驶舱 agent,负责从招标信息到标书交付的全流程管理:
招标信息采集 → 内容摘要 → 审批 → 立项与人员 → 上传招标文件 → 解析(评分项/得分规则/资质/投标文件要求)
→ 资质文档准备 → 分章节编写 → 章节评审 → 合成标书 → 整书评分 → 交付确认。
## 流转由机制驱动(重要)
角色任务由系统状态对账器自动派发(每 15 秒一轮),你**不需要**手动为每个章节创建任务:
- 章节评审不达标 → 章节自动退回重写
- 整书评分不达标 → 改进意见自动落到问题章节并退回重写 → 重新合成 → 重新评分
你的职责是:采集与摘要、推动审批、立项与配人、处理卡点与提问、回答评审/评分中的人类决策。
只有诊断到「机制该派任务却没派」时,才用 advance_bid 手动触发一次对账,或用 diagnose_bid 查根因。
## 阻塞门禁(不要绕过)
以下人类任务 pending 时,系统不会给该项目派发任何 agent 任务:
- 招标信息审批未决(tender_approval)
- 等待上传招标文件(tender_file_upload,上传后自动完成)
- 等待人类提供资质文件(human_docs_request)
遇到项目"不动了",先 diagnose_bid 看是不是卡在这三个门禁上,缺什么就催人补什么,不要硬推。
## 典型场景
- 用户说「看看有什么新招标 / 收一下招标邮件」→ fetch_tenders,再 list_tenders 看结果。
- 用户说「这条招标怎么样 / 帮我分析下」→ tender_detail + summarize_tender,把投标开始时间、
项目周期、招标金额、保证金、资质门槛、风险点讲清楚,再问是否提交审批。
- 用户说「提交审批」→ submit_tender_approval;用户直接拍板通过/不通过 → approve_tender / reject_tender。
**审批不通过就是终态**:招标信息自动存档作废,不要再立项。
- 审批通过后 → 自动立项并发上传招标文件任务;接着 assign_members 设定参与人员。
- 用户问进展 → diagnose_bid(含章节分布、在办任务、最近评分),不要只报"正在进行"。
- 用户问「能得多少分 / 差在哪」→ bid_score_report + list_reviews,讲清失分项与改进意见落到了哪个章节。
- 用户要补资质/案例 → add_kb_doc 录入公司投标知识库;知识库缺的用 request_human_docs 让人工提供。
## 硬规则
- 招标金额、投标时间、资质要求这些要素,只能来自招标公告/招标文件原文或人工确认,**禁止编造**;
抽取不到就如实说"公告未载明",必要时 ask_user。
- 标书内容的案例与业绩必须来自公司投标知识库的真实合同(bid_kb_docs 里 doc_type=case),
禁止虚构客户名、合同金额、历史成果数字。
- 章节没全部评审通过,不允许合成标书;评分没过门限,不允许当作可交付。"""
# ══════════════════ 角色集 ══════════════════
BID_ROLES = [
RoleSpec(
name="agent.tender_scout",
description="招标信息员",
aliases=["tender_scout", "scout", "tender", "tender_intake"],
system_prompt="你是招标信息员。先 load_skill 加载 role 技能,按其中的职责与规范执行任务。",
next_role="",
task_title="招标信息采集与摘要",
),
RoleSpec(
name="agent.tender_analyst",
description="招标文件分析师",
aliases=["tender_analyst", "analyst", "analysis"],
system_prompt="你是招标文件分析师。先 load_skill 加载 role 技能,按其中的职责与规范执行任务。",
next_role="",
task_title="招标文件解析",
),
RoleSpec(
name="agent.bid_prep",
description="投标资料专员",
aliases=["bid_prep", "prep", "qualification"],
system_prompt="你是投标资料专员。先 load_skill 加载 role 技能,按其中的职责与规范执行任务。",
next_role="",
task_title="投标资料准备",
),
RoleSpec(
name="agent.bid_writer",
description="标书撰写工程师",
aliases=["bid_writer", "writer", "write"],
system_prompt="你是标书撰写工程师。先 load_skill 加载 role 技能,按其中的职责与规范执行任务。",
next_role="",
task_title="章节编写",
),
RoleSpec(
name="agent.bid_reviewer",
description="标书评审员",
aliases=["bid_reviewer", "reviewer", "chapter_review"],
system_prompt="你是标书评审员。先 load_skill 加载 role 技能,按其中的职责与规范执行任务。",
next_role="",
task_title="章节评审",
),
RoleSpec(
name="agent.bid_compositor",
description="标书合成工程师",
aliases=["bid_compositor", "compositor", "compose"],
system_prompt="你是标书合成工程师。先 load_skill 加载 role 技能,按其中的职责与规范执行任务。",
next_role="",
task_title="标书合成",
),
RoleSpec(
name="agent.bid_scorer",
description="标书评分员(模拟评标)",
aliases=["bid_scorer", "scorer", "score"],
system_prompt="你是标书评分员(模拟评标专家)。先 load_skill 加载 role 技能,按其中的职责与规范执行任务。",
next_role="",
task_title="整书评审与评分",
),
RoleSpec(
name="agent.pm",
description="投标项目经理(任务验收)",
aliases=["pm", "manager"],
system_prompt="你是投标项目经理。先 load_skill 加载 role 技能,按其中的职责与规范验收任务。",
next_role="",
),
RoleSpec(
name="agent.qc",
description="投标质量控制(合规检查)",
aliases=["qc", "quality"],
system_prompt="你是投标质量控制工程师。先 load_skill 加载 role 技能,按其中的职责与规范检查交付件。",
next_role="",
),
]
# ══════════════════ handler(签名 async def handler(sor, params, ctx) -> str)══════════════════
def _need_project(ctx):
pid = ctx.get("project_id", "") or ""
return pid, ("请先切换到投标项目(用 switch_project 或在项目列表选择)" if not pid else "")
def _fmt(ok, msg):
return ("OK: " if ok else "ERROR: ") + str(msg)
def _fmt_rows(rows, empty="(空)"):
if not rows:
return empty
if isinstance(rows, str):
return rows
return "\n".join(json.dumps(r, ensure_ascii=False, default=str) for r in rows[:80])
async def _h_fetch_tenders(sor, p, ctx):
from .bid_tender_capability import fetch_tenders
ok, msg = await fetch_tenders(org_id=ctx.get("org_id", "") or "",
account_id=p.get("account_id", "") or "",
days=p.get("days") or None,
who="agent.main_agent")
return _fmt(ok, msg)
async def _h_import_tender(sor, p, ctx):
from .bid_tender_capability import import_tender
ok, msg = await import_tender(p.get("title", ""), p.get("content", ""),
purchaser=p.get("purchaser", ""),
tender_no=p.get("tender_no", ""),
org_id=ctx.get("org_id", "") or "0",
who="agent.main_agent",
created_by=ctx.get("user_id", "") or "")
return _fmt(ok, "已导入招标信息 id=%s" % msg if ok else msg)
async def _h_list_tenders(sor, p, ctx):
from .bid_tender_capability import list_tenders
return _fmt_rows(await list_tenders(status=p.get("status", "") or "",
org_id=ctx.get("org_id", "") or "",
limit=p.get("limit") or 50),
"暂无招标信息(可用 fetch_tenders 采集或 import_tender 手工导入)")
async def _h_tender_detail(sor, p, ctx):
from .bid_tender_capability import tender_detail
return await tender_detail(p.get("tender_id", ""))
async def _h_summarize_tender(sor, p, ctx):
from .bid_tender_capability import summarize_tender
ok, msg = await summarize_tender(p.get("tender_id", ""),
model_name=ctx.get("model_name", "") or "",
who="agent.main_agent")
return _fmt(ok, msg)
async def _h_set_tender_fields(sor, p, ctx):
from .bid_tender_capability import set_tender_fields
ok, msg = await set_tender_fields(
p.get("tender_id", ""), title=p.get("title", ""), purchaser=p.get("purchaser", ""),
tender_no=p.get("tender_no", ""), bid_start_at=p.get("bid_start_at", ""),
bid_deadline=p.get("bid_deadline", ""), project_period=p.get("project_period", ""),
budget_amount=p.get("budget_amount", ""), deposit_amount=p.get("deposit_amount", ""),
summary=p.get("summary", ""), qualification_req=p.get("qualification_req", ""),
who="agent.main_agent")
return _fmt(ok, msg)
async def _h_submit_tender_approval(sor, p, ctx):
from .bid_tender_capability import submit_tender_approval
ok, msg = await submit_tender_approval(
p.get("tender_id", ""), assignee_role=p.get("assignee_role", "") or "owner.superuser",
note=p.get("note", ""), who="agent.main_agent")
return _fmt(ok, msg)
async def _h_approve_tender(sor, p, ctx):
from .bid_tender_capability import approve_tender
ok, msg = await approve_tender(p.get("tender_id", ""), comment=p.get("comment", ""),
who=ctx.get("user_id", "") or "owner.superuser")
return _fmt(ok, msg)
async def _h_reject_tender(sor, p, ctx):
from .bid_tender_capability import reject_tender
ok, msg = await reject_tender(p.get("tender_id", ""), comment=p.get("comment", ""),
who=ctx.get("user_id", "") or "owner.superuser")
return _fmt(ok, msg)
async def _h_archive_tender(sor, p, ctx):
from .bid_tender_capability import archive_tender
ok, msg = await archive_tender(p.get("tender_id", ""), reason=p.get("reason", ""),
who="agent.main_agent")
return _fmt(ok, msg)
async def _h_reopen_tender(sor, p, ctx):
from .bid_tender_capability import reopen_tender
ok, msg = await reopen_tender(p.get("tender_id", ""), reason=p.get("reason", ""),
who="agent.main_agent")
return _fmt(ok, msg)
async def _h_create_bid_project(sor, p, ctx):
from .bid_tender_capability import create_bid_project
ok, msg = await create_bid_project(p.get("tender_id", ""), name=p.get("name", ""),
org_id=ctx.get("org_id", "") or "",
created_by=ctx.get("user_id", "") or "",
who="agent.main_agent")
return _fmt(ok, msg)
async def _h_assign_members(sor, p, ctx):
pid, err = _need_project(ctx)
if err:
return err
from .bid_tender_capability import assign_members
ok, msg = await assign_members(pid, members=p.get("members", ""), who="agent.main_agent")
return _fmt(ok, msg)
async def _h_list_members(sor, p, ctx):
pid, err = _need_project(ctx)
if err:
return err
from .bid_tender_capability import list_members
return _fmt_rows(await list_members(pid), "尚未设定参与人员")
async def _h_request_tender_file(sor, p, ctx):
pid, err = _need_project(ctx)
if err:
return err
from .bid_tender_capability import request_tender_file
ok, msg = await request_tender_file(pid, note=p.get("note", ""), who="agent.main_agent")
return _fmt(ok, msg)
async def _h_list_tender_files(sor, p, ctx):
pid, err = _need_project(ctx)
if err:
return err
from .bid_tender_capability import list_tender_files
return _fmt_rows(await list_tender_files(pid), "尚未上传招标文件")
async def _h_list_scoring_items(sor, p, ctx):
pid, err = _need_project(ctx)
if err:
return err
from .bid_analysis_capability import list_scoring_items
return _fmt_rows(await list_scoring_items(pid, section=p.get("section", "") or ""),
"尚无评分项(等招标文件解析)")
async def _h_list_doc_requirements(sor, p, ctx):
pid, err = _need_project(ctx)
if err:
return err
from .bid_analysis_capability import list_doc_requirements
return _fmt_rows(await list_doc_requirements(pid, req_type=p.get("req_type", "") or ""),
"尚无投标文件要求(等招标文件解析)")
async def _h_list_qualifications(sor, p, ctx):
pid, err = _need_project(ctx)
if err:
return err
from .bid_kb_capability import list_qualifications
return _fmt_rows(await list_qualifications(pid, match_status=p.get("match_status", "") or ""),
"尚无资质要求(等招标文件解析)")
async def _h_search_bid_kb(sor, p, ctx):
from .bid_kb_capability import search_bid_kb
return _fmt_rows(await search_bid_kb(query=p.get("query", "") or "",
doc_type=p.get("doc_type", "") or "",
org_id=ctx.get("org_id", "") or ""),
"知识库无命中")
async def _h_list_kb_docs(sor, p, ctx):
from .bid_kb_capability import list_kb_docs
return _fmt_rows(await list_kb_docs(doc_type=p.get("doc_type", "") or "",
org_id=ctx.get("org_id", "") or ""),
"公司投标知识库为空(用 add_kb_doc 录入资质证书/业绩案例)")
async def _h_add_kb_doc(sor, p, ctx):
from .bid_kb_capability import add_kb_doc
ok, msg = await add_kb_doc(
p.get("doc_name", ""), doc_type=p.get("doc_type", "") or "qualification",
doc_no=p.get("doc_no", ""), tags=p.get("tags", ""), file_path=p.get("file_path", ""),
content_text=p.get("content_text", ""), issuer=p.get("issuer", ""),
customer=p.get("customer", ""), contract_amount=p.get("contract_amount", ""),
valid_until=p.get("valid_until", ""), org_id=ctx.get("org_id", "") or "0",
who="agent.main_agent")
return _fmt(ok, msg)
async def _h_request_human_docs(sor, p, ctx):
pid, err = _need_project(ctx)
if err:
return err
from .bid_kb_capability import request_human_docs
ok, msg = await request_human_docs(pid, items=p.get("items", ""), note=p.get("note", ""),
who="agent.main_agent")
return _fmt(ok, msg)
async def _h_list_chapters(sor, p, ctx):
pid, err = _need_project(ctx)
if err:
return err
from .bid_write_capability import list_chapters
return _fmt_rows(await list_chapters(pid, status=p.get("status", "") or ""),
"尚无章节(等招标文件解析生成章节骨架)")
async def _h_chapter_detail(sor, p, ctx):
from .bid_write_capability import chapter_detail
return await chapter_detail(p.get("chapter_id", ""))
async def _h_list_bid_documents(sor, p, ctx):
pid, err = _need_project(ctx)
if err:
return err
from .bid_compose_capability import list_bid_documents
return _fmt_rows(await list_bid_documents(pid), "尚未合成标书")
async def _h_list_reviews(sor, p, ctx):
pid, err = _need_project(ctx)
if err:
return err
from .bid_review_capability import list_reviews
return _fmt_rows(await list_reviews(pid, scope=p.get("scope", "") or "",
chapter_id=p.get("chapter_id", "") or ""),
"尚无评审记录")
async def _h_bid_score_report(sor, p, ctx):
pid, err = _need_project(ctx)
if err:
return err
from .bid_score_capability import bid_score_report
return await bid_score_report(pid)
async def _h_diagnose_bid(sor, p, ctx):
pid, err = _need_project(ctx)
if err:
return err
from .bid_flow import diagnose_bid_project
return await diagnose_bid_project(pid)
async def _h_advance_bid(sor, p, ctx):
pid, err = _need_project(ctx)
if err:
return err
from .bid_flow import reconcile_project
acts = await reconcile_project(sor, pid)
return "对账结果:\n" + "\n".join("- " + a for a in (acts or ["无动作"]))
BID_HANDLERS = {
"fetch_tenders": _h_fetch_tenders,
"import_tender": _h_import_tender,
"list_tenders": _h_list_tenders,
"tender_detail": _h_tender_detail,
"summarize_tender": _h_summarize_tender,
"set_tender_fields": _h_set_tender_fields,
"submit_tender_approval": _h_submit_tender_approval,
"approve_tender": _h_approve_tender,
"reject_tender": _h_reject_tender,
"archive_tender": _h_archive_tender,
"reopen_tender": _h_reopen_tender,
"create_bid_project": _h_create_bid_project,
"assign_members": _h_assign_members,
"list_members": _h_list_members,
"request_tender_file": _h_request_tender_file,
"list_tender_files": _h_list_tender_files,
"list_scoring_items": _h_list_scoring_items,
"list_doc_requirements": _h_list_doc_requirements,
"list_qualifications": _h_list_qualifications,
"search_bid_kb": _h_search_bid_kb,
"list_kb_docs": _h_list_kb_docs,
"add_kb_doc": _h_add_kb_doc,
"request_human_docs": _h_request_human_docs,
"list_chapters": _h_list_chapters,
"chapter_detail": _h_chapter_detail,
"list_bid_documents": _h_list_bid_documents,
"list_reviews": _h_list_reviews,
"bid_score_report": _h_bid_score_report,
"diagnose_bid": _h_diagnose_bid,
"advance_bid": _h_advance_bid,
}
def _merge_shared():
"""并入引擎级通用任务/交付件/问答/项目工具(不重复实现,见 pipeline_service.shared_ability)。"""
try:
from pipeline_service.shared_ability import SHARED_TOOLS, SHARED_HANDLERS
except Exception as e: # pipeline_service 未安装/版本旧 → 只用投标专属工具,诚实降级
logger.warning("shared_ability 不可用,投标产线仅挂载专属工具: %s", str(e)[:120])
return list(BID_TOOLS), dict(BID_HANDLERS)
names = {t.name for t in BID_TOOLS}
tools = list(BID_TOOLS) + [t for t in SHARED_TOOLS if t.name not in names]
handlers = dict(SHARED_HANDLERS)
handlers.update(BID_HANDLERS) # 同名以投标产线实现为准
return tools, handlers
def register_bid_ability():
"""注册投标产线能力包(幂等)。"""
tools, handlers = _merge_shared()
ability = PipelineAbility(
pipeline_id=PIPELINE_ID,
name="通用投标产线",
tools=tools,
system_prompt=BID_PROMPT,
handlers=handlers,
roles=BID_ROLES,
menus=[
{"label": "📨 招标信息", "icon": "", "url": "/pipeline-bidding/bid_tenders/index.ui",
"type": "popup", "width": "88%", "height": "82%"},
{"label": "📑 评分项", "icon": "", "url": "/pipeline-bidding/bid_scoring_items/index.ui",
"type": "popup", "width": "88%", "height": "82%", "require_project": True},
{"label": "📝 标书章节", "icon": "", "url": "/pipeline-bidding/bid_chapters/index.ui",
"type": "popup", "width": "88%", "height": "82%", "require_project": True},
{"label": "🎯 资质", "icon": "", "url": "/pipeline-bidding/bid_qualifications/index.ui",
"type": "popup", "width": "88%", "height": "82%", "require_project": True},
{"label": "📚 投标知识库", "icon": "", "url": "/pipeline-bidding/bid_kb_docs/index.ui",
"type": "popup", "width": "88%", "height": "82%"},
{"label": "📦 标书", "icon": "", "url": "/pipeline-bidding/bid_documents/index.ui",
"type": "popup", "width": "88%", "height": "82%", "require_project": True},
{"label": "✉️ 邮箱配置", "icon": "", "url": "/pipeline-bidding/bid_mail_accounts/index.ui",
"type": "popup", "width": "88%", "height": "82%"},
],
)
register_ability(ability)
return ability
# ══════════════════ slash 命令(产线专属)══════════════════
async def _slash_tenders(args, ctx):
ex = ctx.get("executor")
if not ex:
return "无执行器上下文"
return await ex._execute_ability_tool("list_tenders", {"status": (args or "").strip()})
async def _slash_bid(args, ctx):
ex = ctx.get("executor")
if not ex:
return "无执行器上下文"
return await ex._execute_ability_tool("diagnose_bid", {})
async def _slash_chapters(args, ctx):
ex = ctx.get("executor")
if not ex:
return "无执行器上下文"
return await ex._execute_ability_tool("list_chapters", {"status": (args or "").strip()})
async def _slash_score(args, ctx):
ex = ctx.get("executor")
if not ex:
return "无执行器上下文"
return await ex._execute_ability_tool("bid_score_report", {})
def register_bid_slash_commands():
for cmd in [
SlashCommand("tenders", "列出招标信息(可带状态)", _slash_tenders, "pipeline", PIPELINE_ID),
SlashCommand("bid", "诊断当前投标项目卡点", _slash_bid, "pipeline", PIPELINE_ID),
SlashCommand("chapters", "列出标书章节及状态", _slash_chapters, "pipeline", PIPELINE_ID),
SlashCommand("score", "查看最近一轮整书评分报告", _slash_score, "pipeline", PIPELINE_ID),
]:
register_slash_command(cmd)
# import 即注册(与 sdlc_ability 一致:宿主 import pipeline_bidding 即生效)
register_bid_ability()
register_bid_slash_commands()

View File

@ -0,0 +1,444 @@
"""招标文件解析能力:抽取评分项+得分规则、所需资质、投标文件要求,生成章节骨架。
对应业务:招标文件获得后,从招标文件中抽取评分项以及得分规则、所需资质、投标文件要求。
章节骨架规则(写进 bid-doc-spec 技能):章节子项直接来自评分标准原文枚举的子维度,不是通用目录。
"""
import json
import logging
from .bid_common import (
get_db, new_id, rec_to_dict, rows_to_dicts, to_float, to_int, json_loads,
llm_json, record, resolve_project_id, CH_PENDING,
)
logger = logging.getLogger("pipeline.bidding.analysis")
EXTRACT_PROMPT = """你是招标文件分析师。下面是招标文件正文(可能被截断),请抽取三类结构化信息。
【招标文件正文】
__CONTENT__
严格只输出一个 JSON 对象:
{
"scoring_items": [
{"section": "business|technical|price", "item_no": "评分项编号", "item_name": "评分项名称",
"max_score": 数字满分, "scoring_rule": "得分规则原文(含档位/扣分/加分细则)",
"is_veto": "0或1(是否否决项/废标项)", "source_ref": "出处,如 第三章 评标办法 表1"}
],
"qualifications": [
{"qual_name": "资质名称", "requirement": "要求原文", "is_mandatory": "0或1"}
],
"doc_requirements": [
{"req_type": "structure|format|seal|copies|deadline|other",
"chapter_no": "章节号(structure 类必填,如 1 / 1.1)",
"chapter_title": "章节标题(structure 类必填)",
"requirement": "要求内容", "page_limit": 数字或null, "source_ref": "出处"}
]
}
要求:
1. scoring_items 必须逐条覆盖评标办法中的每个评分项,满分数字准确;否决项/废标条款 is_veto=1。
2. doc_requirements 中 req_type=structure 的条目就是投标文件应有的章节结构,按招标文件要求的顺序给出。
3. 只抽取正文中真实存在的内容,缺失就不要输出该条,禁止编造。"""
# ══════════════ 招标文件读取 ══════════════
async def list_tender_files(project_id, extract_status=""):
"""列出项目的招标文件。"""
db, dbname = get_db()
async with db.sqlorContext(dbname) as sor:
sql = ("SELECT id, file_name, file_type, extract_status, file_size, file_path "
"FROM bid_tender_files WHERE project_id=${pid}$")
p = {"pid": project_id}
if extract_status:
sql += " AND extract_status=${st}$"
p["st"] = extract_status
sql += " ORDER BY created_at"
recs = await sor.sqlExe(sql, p)
await sor.sqlExe("COMMIT", {})
return rows_to_dicts(recs)
async def read_tender_file(file_id, offset=0, length=20000):
"""读招标文件正文(分段读,供 LLM 分析)。正文为空时尝试从磁盘提取。"""
db, dbname = get_db()
async with db.sqlorContext(dbname) as sor:
recs = await sor.sqlExe(
"SELECT id, project_id, file_name, file_path, content_text "
"FROM bid_tender_files WHERE id=${fid}$", {"fid": file_id})
await sor.sqlExe("COMMIT", {})
if not recs:
return "招标文件不存在: %s" % file_id
d = rec_to_dict(recs[0])
txt = d.get("content_text") or ""
if not txt.strip() and d.get("file_path"):
txt = _extract_file_text(d["file_path"])
if txt:
await sor.sqlExe(
"UPDATE bid_tender_files SET content_text=${c}$, updated_at=NOW() "
"WHERE id=${fid}$", {"c": txt[:2000000], "fid": file_id})
await sor.sqlExe("COMMIT", {})
if not txt.strip():
return ("招标文件正文为空(file_path=%s)。请确认文件已上传且格式为 docx/pdf/txt。"
% d.get("file_path", ""))
o = to_int(offset, 0)
n = to_int(length, 20000)
seg = txt[o:o + n]
return ("【%s】字符 %d-%d / 共 %d\n%s"
% (d.get("file_name", ""), o, o + len(seg), len(txt), seg))
def _extract_file_text(path):
"""从 docx/pdf/txt 提取纯文本(无依赖时返回空串,不抛异常)。"""
import os
import re
import zipfile
if not path or not os.path.isfile(path):
return ""
ext = os.path.splitext(path)[1].lower()
try:
if ext in (".txt", ".md", ".csv", ".json"):
with open(path, "r", encoding="utf-8", errors="ignore") as f:
return f.read()
if ext == ".docx":
with zipfile.ZipFile(path) as z:
out = []
for name in z.namelist():
if not (name.startswith("word/") and name.endswith(".xml")):
continue
if "document" not in name and "header" not in name and "footer" not in name:
continue
xml = z.read(name).decode("utf-8", errors="ignore")
xml = xml.replace("</w:p>", "\n")
out.append("".join(re.findall(r"<w:t[^>]*>(.*?)</w:t>", xml, flags=re.S)))
return "\n".join(out)
if ext == ".pdf":
try:
import fitz
doc = fitz.open(path)
return "\n".join(pg.get_text() for pg in doc)
except ImportError:
return ""
except Exception as e:
logger.warning("extract file text failed %s: %s", path, str(e)[:120])
return ""
async def mark_file_extracted(file_id, note="", who=None, agent_id=None):
"""标记招标文件已抽取完成。"""
db, dbname = get_db()
async with db.sqlorContext(dbname) as sor:
await sor.sqlExe(
"UPDATE bid_tender_files SET extract_status='extracted', extract_note=${n}$, "
"updated_at=NOW() WHERE id=${fid}$", {"n": (note or "")[:480], "fid": file_id})
await sor.sqlExe("COMMIT", {})
return True, "已标记抽取完成: %s" % file_id
# ══════════════ 抽取落库(逐条) ══════════════
async def add_scoring_item(project_id, item_name, max_score, scoring_rule="",
section="technical", item_no="", is_veto="0",
source_ref="", target_chapter_no="", weight="",
who=None, agent_id=None):
"""新增一个评分项及得分规则。"""
try:
project_id = await resolve_project_id(project_id)
except ValueError as e:
return False, str(e)
if not item_name:
return False, "缺少 item_name"
db, dbname = get_db()
async with db.sqlorContext(dbname) as sor:
exist = await sor.sqlExe(
"SELECT id FROM bid_scoring_items WHERE project_id=${pid}$ AND item_name=${nm}$ LIMIT 1",
{"pid": project_id, "nm": str(item_name)[:250]})
await sor.sqlExe("COMMIT", {})
if exist:
return True, "评分项已存在: %s" % item_name
n = await sor.sqlExe(
"SELECT COUNT(*) AS c FROM bid_scoring_items WHERE project_id=${pid}$",
{"pid": project_id})
order_no = to_int(getattr(n[0], "c", 0) if n else 0) + 1
sid = new_id()
await sor.C("bid_scoring_items", {
"id": sid, "project_id": project_id,
"section": (section or "technical")[:30],
"item_no": str(item_no or "")[:30],
"item_name": str(item_name)[:250],
"max_score": to_float(max_score, 0.0),
"scoring_rule": scoring_rule or "",
"is_veto": "1" if str(is_veto) in ("1", "true", "True", "是") else "0",
"weight": to_float(weight, 0.0) or None,
"source_ref": str(source_ref or "")[:480],
"target_chapter_no": str(target_chapter_no or "")[:30],
"order_no": order_no,
})
await sor.sqlExe("COMMIT", {})
await record(sor, project_id, "bid_scoring_items", sid, "add",
who=who, agent_id=agent_id, detail=str(item_name)[:200])
return True, "评分项已录入: %s (%s分)" % (item_name, max_score)
async def add_qualification(project_id, qual_name, requirement="", is_mandatory="1",
owner="kb", who=None, agent_id=None):
"""新增一项招标所需资质。"""
try:
project_id = await resolve_project_id(project_id)
except ValueError as e:
return False, str(e)
if not qual_name:
return False, "缺少 qual_name"
db, dbname = get_db()
async with db.sqlorContext(dbname) as sor:
exist = await sor.sqlExe(
"SELECT id FROM bid_qualifications WHERE project_id=${pid}$ AND qual_name=${nm}$ LIMIT 1",
{"pid": project_id, "nm": str(qual_name)[:250]})
await sor.sqlExe("COMMIT", {})
if exist:
return True, "资质项已存在: %s" % qual_name
qid = new_id()
await sor.C("bid_qualifications", {
"id": qid, "project_id": project_id,
"qual_name": str(qual_name)[:250],
"requirement": requirement or "",
"is_mandatory": "1" if str(is_mandatory) in ("1", "true", "True", "是") else "0",
"owner": (owner or "kb")[:15],
"match_status": "pending",
})
await sor.sqlExe("COMMIT", {})
await record(sor, project_id, "bid_qualifications", qid, "add",
who=who, agent_id=agent_id, detail=str(qual_name)[:200])
return True, "资质要求已录入: %s" % qual_name
async def add_doc_requirement(project_id, requirement, req_type="structure",
chapter_no="", chapter_title="", page_limit="",
source_ref="", who=None, agent_id=None):
"""新增一条投标文件要求(structure 类同时是章节骨架来源)。"""
try:
project_id = await resolve_project_id(project_id)
except ValueError as e:
return False, str(e)
if not requirement:
return False, "缺少 requirement"
db, dbname = get_db()
async with db.sqlorContext(dbname) as sor:
n = await sor.sqlExe(
"SELECT COUNT(*) AS c FROM bid_doc_requirements WHERE project_id=${pid}$",
{"pid": project_id})
await sor.sqlExe("COMMIT", {})
order_no = to_int(getattr(n[0], "c", 0) if n else 0) + 1
rid = new_id()
await sor.C("bid_doc_requirements", {
"id": rid, "project_id": project_id,
"req_type": (req_type or "structure")[:30],
"chapter_no": str(chapter_no or "")[:30],
"chapter_title": str(chapter_title or "")[:250],
"requirement": requirement or "",
"page_limit": to_int(page_limit, 0) or None,
"source_ref": str(source_ref or "")[:480],
"order_no": order_no,
})
await sor.sqlExe("COMMIT", {})
return True, "投标文件要求已录入: %s %s" % (req_type, chapter_title or "")
async def extract_from_file(project_id, file_id="", model_name="", offset=0,
length=30000, who=None, agent_id=None):
"""LLM 一次性抽取三类信息并批量落库(评分项/资质/投标文件要求)。
大文件分段调用(offset 递增),重复项按名称去重,可安全多次调用。
"""
try:
project_id = await resolve_project_id(project_id, file_id=file_id)
except ValueError as e:
return False, str(e)
db, dbname = get_db()
async with db.sqlorContext(dbname) as sor:
if file_id:
recs = await sor.sqlExe(
"SELECT id, content_text, file_path, file_name FROM bid_tender_files "
"WHERE id=${fid}$", {"fid": file_id})
else:
recs = await sor.sqlExe(
"SELECT id, content_text, file_path, file_name FROM bid_tender_files "
"WHERE project_id=${pid}$ AND file_type='tender_doc' ORDER BY created_at LIMIT 1",
{"pid": project_id})
await sor.sqlExe("COMMIT", {})
if not recs:
return False, "项目下没有招标文件(bid_tender_files),无法抽取"
d = rec_to_dict(recs[0])
txt = d.get("content_text") or ""
if not txt.strip() and d.get("file_path"):
txt = _extract_file_text(d["file_path"])
if txt:
await sor.sqlExe(
"UPDATE bid_tender_files SET content_text=${c}$ WHERE id=${fid}$",
{"c": txt[:2000000], "fid": d["id"]})
await sor.sqlExe("COMMIT", {})
if not txt.strip():
return False, "招标文件正文为空,无法抽取(file=%s)" % d.get("file_name", "")
o = to_int(offset, 0)
seg = txt[o:o + to_int(length, 30000)]
data, err = await llm_json(EXTRACT_PROMPT.replace("__CONTENT__", seg),
model_name=model_name, retries=1)
if data is None:
return False, "LLM 抽取失败: %s" % err
cnt = {"scoring_items": 0, "qualifications": 0, "doc_requirements": 0}
for it in (data.get("scoring_items") or []):
if not isinstance(it, dict) or not it.get("item_name"):
continue
ok, _ = await add_scoring_item(
project_id, it.get("item_name"), it.get("max_score", 0),
scoring_rule=it.get("scoring_rule", ""), section=it.get("section", "technical"),
item_no=it.get("item_no", ""), is_veto=it.get("is_veto", "0"),
source_ref=it.get("source_ref", ""), who=who, agent_id=agent_id)
cnt["scoring_items"] += 1 if ok else 0
for it in (data.get("qualifications") or []):
if not isinstance(it, dict) or not it.get("qual_name"):
continue
ok, _ = await add_qualification(
project_id, it.get("qual_name"), requirement=it.get("requirement", ""),
is_mandatory=it.get("is_mandatory", "1"), who=who, agent_id=agent_id)
cnt["qualifications"] += 1 if ok else 0
for it in (data.get("doc_requirements") or []):
if not isinstance(it, dict) or not it.get("requirement"):
continue
ok, _ = await add_doc_requirement(
project_id, it.get("requirement"), req_type=it.get("req_type", "structure"),
chapter_no=it.get("chapter_no", ""), chapter_title=it.get("chapter_title", ""),
page_limit=it.get("page_limit", ""), source_ref=it.get("source_ref", ""),
who=who, agent_id=agent_id)
cnt["doc_requirements"] += 1 if ok else 0
return True, ("抽取完成(文件 %s,字符 %d-%d):评分项 %d,资质 %d,投标文件要求 %d"
% (d.get("file_name", ""), o, o + len(seg),
cnt["scoring_items"], cnt["qualifications"], cnt["doc_requirements"]))
# ══════════════ 查询 ══════════════
async def list_scoring_items(project_id, section=""):
try:
project_id = await resolve_project_id(project_id)
except ValueError as e:
return []
db, dbname = get_db()
async with db.sqlorContext(dbname) as sor:
sql = ("SELECT id, section, item_no, item_name, max_score, is_veto, "
"target_chapter_no, scoring_rule FROM bid_scoring_items WHERE project_id=${pid}$")
p = {"pid": project_id}
if section:
sql += " AND section=${sec}$"
p["sec"] = section
sql += " ORDER BY section, order_no"
recs = await sor.sqlExe(sql, p)
await sor.sqlExe("COMMIT", {})
out = rows_to_dicts(recs)
for r in out:
r["scoring_rule"] = (r.get("scoring_rule") or "")[:600]
return out
async def list_doc_requirements(project_id, req_type=""):
try:
project_id = await resolve_project_id(project_id)
except ValueError as e:
return []
db, dbname = get_db()
async with db.sqlorContext(dbname) as sor:
sql = ("SELECT id, req_type, chapter_no, chapter_title, requirement, page_limit "
"FROM bid_doc_requirements WHERE project_id=${pid}$")
p = {"pid": project_id}
if req_type:
sql += " AND req_type=${rt}$"
p["rt"] = req_type
sql += " ORDER BY req_type, order_no"
recs = await sor.sqlExe(sql, p)
await sor.sqlExe("COMMIT", {})
out = rows_to_dicts(recs)
for r in out:
r["requirement"] = (r.get("requirement") or "")[:600]
return out
# ══════════════ 章节骨架 ══════════════
async def create_chapter_outline(project_id, chapters="", who=None, agent_id=None):
"""生成章节骨架 → bid_chapters(status=pending)。
chapters 为空时:自动用 bid_doc_requirements(req_type=structure) 生成,
并按 target_chapter_no / 章节标题关键词把评分项挂到章节上(scoring_item_ids + max_score)。
"""
try:
project_id = await resolve_project_id(project_id)
except ValueError as e:
return False, str(e)
items = json_loads(chapters, []) if chapters else []
db, dbname = get_db()
async with db.sqlorContext(dbname) as sor:
if not items:
recs = await sor.sqlExe(
"SELECT chapter_no, chapter_title, requirement, page_limit FROM bid_doc_requirements "
"WHERE project_id=${pid}$ AND req_type='structure' ORDER BY order_no",
{"pid": project_id})
await sor.sqlExe("COMMIT", {})
for r in (recs or []):
d = rec_to_dict(r)
if not (d.get("chapter_title") or d.get("chapter_no")):
continue
items.append({
"chapter_no": d.get("chapter_no") or "",
"title": d.get("chapter_title") or "",
"outline": (d.get("requirement") or "")[:4000],
})
if not items:
return False, ("无章节来源:请先抽取 req_type=structure 的投标文件要求,"
"或显式传 chapters=[{\"chapter_no\":\"1\",\"title\":\"...\"}]")
sitems = await sor.sqlExe(
"SELECT id, item_name, max_score, section, target_chapter_no "
"FROM bid_scoring_items WHERE project_id=${pid}$", {"pid": project_id})
await sor.sqlExe("COMMIT", {})
sc = rows_to_dicts(sitems)
created, skipped = 0, 0
for idx, it in enumerate(items):
if not isinstance(it, dict):
continue
cno = str(it.get("chapter_no") or (idx + 1))[:30]
title = str(it.get("title") or it.get("chapter_title") or "").strip()[:250]
if not title:
continue
exist = await sor.sqlExe(
"SELECT id FROM bid_chapters WHERE project_id=${pid}$ AND chapter_no=${cno}$ LIMIT 1",
{"pid": project_id, "cno": cno})
await sor.sqlExe("COMMIT", {})
if exist:
skipped += 1
continue
matched, msum = [], 0.0
for s in sc:
tgt = (s.get("target_chapter_no") or "").strip()
nm = s.get("item_name") or ""
if (tgt and tgt == cno) or (nm and (nm in title or title in nm)):
matched.append(s["id"])
msum += to_float(s.get("max_score"), 0.0)
await sor.C("bid_chapters", {
"id": new_id(), "project_id": project_id,
"chapter_no": cno, "parent_no": str(it.get("parent_no") or "")[:30],
"title": title, "section": str(it.get("section") or "technical")[:30],
"scoring_item_ids": json.dumps(matched, ensure_ascii=False),
"outline": str(it.get("outline") or "")[:20000],
"status": CH_PENDING, "version": 1, "revise_count": 0,
"max_score": msum or None,
"assignee_role": "agent.bid_writer",
"order_no": idx + 1,
})
created += 1
await sor.sqlExe("COMMIT", {})
await record(sor, project_id, "bid_chapters", project_id, "create_outline",
who=who, agent_id=agent_id, detail="新增 %d 章" % created)
return True, ("章节骨架已生成:新增 %d 章,已存在跳过 %d 章。"
"系统将自动为每个章节派发编写任务。" % (created, skipped))

View File

@ -0,0 +1,333 @@
"""投标产线公共层:DB 上下文、角色规范化、门限参数、LLM 调用、项目/招标信息取数。
所有 bid_* capability 模块共用本模块,禁止各自重复实现(dspy/capability 都只调这里)。
"""
import json
import logging
from sqlor.dbpools import DBPools
from appPublic.uniqueID import getID
DBNAME = "pipeline"
PIPELINE_ID = "bidding_general"
logger = logging.getLogger("pipeline.bidding")
# ── 章节 / 标书状态 ──
CH_PENDING = "pending"
CH_WRITING = "writing"
CH_WRITTEN = "written"
CH_REVIEWING = "reviewing"
CH_APPROVED = "approved"
CH_REJECTED = "rejected"
# ── 招标信息状态 ──
TD_NEW = "new"
TD_SUMMARIZED = "summarized"
TD_PENDING_APPROVAL = "pending_approval"
TD_APPROVED = "approved"
TD_REJECTED = "rejected"
TD_ARCHIVED = "archived"
# ── 标书状态 ──
DOC_DRAFT = "draft"
DOC_REVIEWING = "reviewing"
DOC_PASSED = "passed"
DOC_REJECTED = "rejected"
DOC_DELIVERED = "delivered"
# ── 人类任务类型(阻塞门禁)──
HT_TENDER_APPROVAL = "tender_approval"
HT_TENDER_FILE = "tender_file_upload"
HT_HUMAN_DOCS = "human_docs_request"
HT_DELIVERY_CONFIRM = "bid_delivery_confirm"
BLOCKING_HT_TYPES = (HT_TENDER_FILE, HT_HUMAN_DOCS)
# 任务未终结状态(判断「是否已有在办任务」用)
OPEN_TASK_STATES = ("submitted", "running", "review", "qc_review")
# ── 门限默认值(可用 appbase params 表覆盖)──
DEFAULT_PARAMS = {
"bid_chapter_pass_ratio": "0.8", # 章节评审通过门限
"bid_pass_ratio": "0.85", # 整书评分通过门限
"bid_max_revise": "3", # 单章节最大重做次数
"bid_max_round": "3", # 整书最大评分轮次
}
def get_db():
"""取 DBPools(databases 为空时从 config 兜底注入,与引擎其他模块一致)。"""
db = DBPools()
if not db.databases:
from appPublic.jsonConfig import getConfig
config = getConfig()
if config and config.databases:
db.databases = config.databases
return db, DBNAME
def new_id():
return getID()
def normalize_role(role):
"""agent 角色补 agent. 前缀;人角色 {orgtype}.{role} 原样返回。"""
role = (role or "").strip()
if not role:
return ""
if "." in role:
return role
return "agent." + role
def rec_to_dict(rec):
"""sqlor 行对象 → dict(过滤方法名,避免 DictObject 序列化泄漏内置方法)。"""
if rec is None:
return {}
if isinstance(rec, dict):
return {k: v for k, v in rec.items() if not callable(v)}
d = {}
for k, v in vars(rec).items():
if k.startswith("_") or callable(v):
continue
d[k] = v
return d
def rows_to_dicts(recs, limit=200):
return [rec_to_dict(r) for r in (recs or [])[:limit]]
def to_float(v, default=0.0):
try:
if v is None or v == "":
return default
return float(v)
except (TypeError, ValueError):
return default
def to_int(v, default=0):
try:
if v is None or v == "":
return default
return int(float(v))
except (TypeError, ValueError):
return default
def json_loads(s, default=None):
if not s:
return default if default is not None else []
if isinstance(s, (list, dict)):
return s
try:
return json.loads(s)
except (json.JSONDecodeError, TypeError, ValueError):
return default if default is not None else []
async def get_param(sor, key, default=""):
"""读 appbase params 表配置(复用引擎 workspace.get_param,不重复实现)+ 默认兜底。"""
try:
from pipeline_service.workspace import get_param as _gp
v = await _gp(sor, key, "")
if v not in (None, ""):
return str(v)
except Exception:
pass
return str(DEFAULT_PARAMS.get(key, default))
async def resolve_project_id(project_id="", **entity_ids):
"""补齐 project_id:调用方(会话上下文)已给则原样返回;否则从实体反查项目。
角色工具经引擎分发时默认注入会话当前项目;为空时(无会话上下文)按实体反查,
避免「项目不明」导致跨项目读写。全部反查失败抛 ValueError。
"""
if project_id:
return project_id
db, dbname = get_db()
async with db.sqlorContext(dbname) as sor:
for tbl, col, val in (("bid_chapters", "chapter_id", entity_ids.get("chapter_id")),
("bid_reviews", "review_id", entity_ids.get("review_id")),
("bid_documents", "document_id", entity_ids.get("document_id")),
("bid_tender_files", "file_id", entity_ids.get("file_id"))):
if not val:
continue
recs = await sor.sqlExe(
"SELECT project_id FROM " + tbl + " WHERE id=${v}$ LIMIT 1", {"v": val})
await sor.sqlExe("COMMIT", {})
if recs and getattr(recs[0], "project_id", ""):
return getattr(recs[0], "project_id", "")
raise ValueError("缺少 project_id(请指定项目或切换到项目)")
async def get_thresholds(sor):
"""取四个门限参数(章节门限/整书门限/章节重做上限/整书轮次上限)。"""
return {
"chapter_pass_ratio": to_float(await get_param(sor, "bid_chapter_pass_ratio"), 0.8),
"bid_pass_ratio": to_float(await get_param(sor, "bid_pass_ratio"), 0.85),
"max_revise": to_int(await get_param(sor, "bid_max_revise"), 3),
"max_round": to_int(await get_param(sor, "bid_max_round"), 3),
}
async def get_project(sor, project_id):
"""取项目记录(dict),不存在返回 {}。"""
if not project_id:
return {}
recs = await sor.sqlExe(
"SELECT id, name, org_id, status, pipeline_id, created_by, default_model "
"FROM sd_projects WHERE id=${pid}$", {"pid": project_id})
await sor.sqlExe("COMMIT", {})
return rec_to_dict(recs[0]) if recs else {}
async def get_tender_by_project(sor, project_id):
"""取项目关联的招标信息(dict)。"""
if not project_id:
return {}
recs = await sor.sqlExe(
"SELECT * FROM bid_tenders WHERE project_id=${pid}$ ORDER BY created_at DESC LIMIT 1",
{"pid": project_id})
await sor.sqlExe("COMMIT", {})
return rec_to_dict(recs[0]) if recs else {}
async def create_role_task(sor, project_id, role, title, params=None, owner_id="bid_flow"):
"""创建角色任务(pipeline_tasks,tenant_id=project_id,pipeline_id='role_task')。
与开发产线共用同一套任务表和 poller —— 投标产线不另建任务机制。
"""
tid = new_id()
await sor.C("pipeline_tasks", {
"id": tid,
"tenant_id": project_id,
"pipeline_id": "role_task",
"owner_id": owner_id,
"title": (title or "")[:250],
"params": json.dumps(params or {}, ensure_ascii=False),
"role": normalize_role(role),
"state": "submitted",
"claimed_by": None,
})
logger.info("bid create_role_task role=%s task=%s project=%s", role, tid, project_id)
return tid
async def count_open_tasks(sor, project_id, role=None, chapter_id=None):
"""统计项目下未终结任务数(可按角色/章节过滤)。用于「已有在办任务就不重复派」。"""
states = "','".join(OPEN_TASK_STATES)
sql = ("SELECT id, params FROM pipeline_tasks WHERE tenant_id=${pid}$ "
"AND pipeline_id='role_task' AND state IN ('" + states + "')")
p = {"pid": project_id}
if role:
sql += " AND role=${role}$"
p["role"] = normalize_role(role)
recs = await sor.sqlExe(sql, p)
await sor.sqlExe("COMMIT", {})
if chapter_id is None:
return len(recs or [])
n = 0
for r in (recs or []):
prm = json_loads(getattr(r, "params", "") or "{}", {})
if isinstance(prm, dict) and prm.get("chapter_id") == chapter_id:
n += 1
return n
async def has_done_task(sor, project_id, role):
"""项目下该角色是否已有验收通过(approved/completed)的任务。"""
recs = await sor.sqlExe(
"SELECT id FROM pipeline_tasks WHERE tenant_id=${pid}$ AND pipeline_id='role_task' "
"AND role=${role}$ AND state IN ('approved','completed') LIMIT 1",
{"pid": project_id, "role": normalize_role(role)})
await sor.sqlExe("COMMIT", {})
return bool(recs)
async def has_pending_blocking_human_task(sor, project_id):
"""项目是否有阻塞中的人类任务(投标产线无迭代,按 project_id 判定)。"""
types = "','".join(BLOCKING_HT_TYPES + (HT_TENDER_APPROVAL,))
recs = await sor.sqlExe(
"SELECT COUNT(*) AS c FROM pipeline_human_tasks "
"WHERE project_id=${pid}$ AND status='pending' "
"AND task_type IN ('" + types + "')", {"pid": project_id})
await sor.sqlExe("COMMIT", {})
return to_int(getattr(recs[0], "c", 0) if recs else 0) > 0
async def create_human_task(sor, project_id, task_type, title, description="",
assignee_role="owner.superuser", assignee_id="",
form_schema=None):
"""创建人类任务(pipeline_human_tasks)。iteration_id 留空 → 走项目级门禁。"""
hid = new_id()
await sor.C("pipeline_human_tasks", {
"id": hid,
"task_id": "",
"step_name": task_type,
"version": 1,
"task_type": task_type,
"assignee_role": assignee_role or "owner.superuser",
"assignee_id": assignee_id or "",
"form_schema": json.dumps(form_schema or {}, ensure_ascii=False),
"status": "pending",
"project_id": project_id or "",
"title": (title or "")[:480],
"description": description or "",
})
logger.info("bid create_human_task type=%s id=%s project=%s", task_type, hid, project_id)
return hid
async def find_human_task(sor, project_id, task_type, status=None):
"""查项目下指定类型人类任务(最新一条)。"""
sql = ("SELECT * FROM pipeline_human_tasks WHERE project_id=${pid}$ "
"AND task_type=${tt}$")
p = {"pid": project_id, "tt": task_type}
if status:
sql += " AND status=${st}$"
p["st"] = status
sql += " ORDER BY created_at DESC LIMIT 1"
recs = await sor.sqlExe(sql, p)
await sor.sqlExe("COMMIT", {})
return rec_to_dict(recs[0]) if recs else {}
async def llm_json(prompt, model_name="", retries=1):
"""调 LLM 并解析 JSON(剥离 markdown code fence)。失败返回 (None, err)。"""
from pipeline_service.llm_bridge import llm_call
last_err = ""
for _ in range(max(1, retries + 1)):
try:
raw = await llm_call(prompt, model=model_name or None)
txt = (raw or "").strip()
if txt.startswith("```"):
parts = txt.split("\n", 1)
txt = parts[1] if len(parts) > 1 else txt
if "```" in txt:
txt = txt.rsplit("```", 1)[0]
txt = txt.strip()
# 容错:截取第一个 { 到最后一个 }
if not txt.startswith("{") and "{" in txt:
txt = txt[txt.find("{"):txt.rfind("}") + 1]
return json.loads(txt), ""
except Exception as e:
last_err = str(e)[:200]
return None, last_err or "LLM 返回无法解析为 JSON"
async def record(sor, project_id, entity, entity_id, action,
from_state=None, to_state=None, who=None, agent_id=None, detail=None):
"""写审计(复用引擎 audit,失败不影响主流程)。"""
try:
from pipeline_service.audit import record_audit
await record_audit(project_id, entity, entity_id, action,
from_state=from_state, to_state=to_state,
who=normalize_role(who), agent_id=agent_id,
detail=detail, sor=sor)
except Exception as e:
logger.warning("bid audit failed: %s", str(e)[:120])

View File

@ -0,0 +1,221 @@
"""标书合成能力(agent.bid_compositor):把已通过的章节合成完整标书(md + docx)。
对应业务:最后合成标书。
产出落 bid_documents(version 递增)+ 工作空间文件,并登记为交付件。
docx 生成用 python-docx;不可用时降级为 md(诚实降级,不假装成功)。
"""
import json
import logging
import os
from .bid_common import (
get_db, new_id, rec_to_dict, rows_to_dicts, to_float, to_int, record,
get_project, get_tender_by_project, resolve_project_id, CH_APPROVED, DOC_DRAFT,
)
logger = logging.getLogger("pipeline.bidding.compose")
async def _workspace_dir(sor, project_id):
"""项目工作目录:复用引擎 build_workspace_path({base}/{org}/{pipeline_id}/{项目名})。
不走 get_workspace_dir(那个依赖会话当前项目),角色 agent 场景下按项目自身字段解析。
"""
try:
from pipeline_service.workspace import get_workspace_base, build_workspace_path
base = await get_workspace_base(sor)
recs = await sor.sqlExe(
"SELECT name, org_id, pipeline_id FROM sd_projects WHERE id=${p}$", {"p": project_id})
await sor.sqlExe("COMMIT", {})
if base and recs:
r = recs[0]
d = build_workspace_path(base, getattr(r, "org_id", "0") or "0",
getattr(r, "pipeline_id", "") or "bidding_general",
getattr(r, "name", "") or project_id, project_id)
os.makedirs(d, exist_ok=True)
return d
except Exception as e:
logger.warning("workspace resolve failed: %s", str(e)[:120])
d = os.path.join("/tmp", "pipeline_bidding", project_id)
os.makedirs(d, exist_ok=True)
return d
def _write_docx(path, title, subtitle, chapters):
"""用 python-docx 写标书。返回 (ok, page_hint_or_err)。"""
try:
from docx import Document
from docx.shared import Pt
from docx.enum.text import WD_ALIGN_PARAGRAPH
except ImportError:
return False, "python-docx 未安装"
try:
doc = Document()
# 中文字体(python-docx 对 CJK 需显式设置 eastasia)
style = doc.styles["Normal"]
style.font.name = "宋体"
style.font.size = Pt(12)
try:
from docx.oxml.ns import qn
style.element.rPr.rFonts.set(qn("w:eastAsia"), "宋体")
except Exception:
pass
p = doc.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
r = p.add_run(title)
r.bold = True
r.font.size = Pt(26)
if subtitle:
p2 = doc.add_paragraph()
p2.alignment = WD_ALIGN_PARAGRAPH.CENTER
r2 = p2.add_run(subtitle)
r2.font.size = Pt(14)
doc.add_page_break()
doc.add_heading("目 录", level=1)
for c in chapters:
doc.add_paragraph("%s %s" % (c.get("chapter_no", ""), c.get("title", "")))
doc.add_page_break()
for c in chapters:
doc.add_heading("%s %s" % (c.get("chapter_no", ""), c.get("title", "")), level=1)
for line in (c.get("content") or "").split("\n"):
t = line.rstrip()
if not t:
continue
if t.startswith("### "):
doc.add_heading(t[4:], level=3)
elif t.startswith("## "):
doc.add_heading(t[3:], level=2)
elif t.startswith("# "):
doc.add_heading(t[2:], level=2)
elif t.lstrip().startswith(("- ", "* ")):
doc.add_paragraph(t.lstrip()[2:], style="List Bullet")
else:
doc.add_paragraph(t)
doc.add_page_break()
doc.save(path)
return True, ""
except Exception as e:
return False, "%s: %s" % (type(e).__name__, str(e)[:200])
async def compose_bid(project_id, doc_name="", who=None, agent_id=None, task_id=""):
"""合成标书:要求全部章节 approved(否则拒绝,避免拿半成品合成)。"""
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:
proj = await get_project(sor, project_id)
if not proj:
return False, "项目不存在: %s" % project_id
recs = await sor.sqlExe(
"SELECT id, chapter_no, title, status, content, max_score, review_score "
"FROM bid_chapters WHERE project_id=${pid}$ ORDER BY order_no, chapter_no",
{"pid": project_id})
await sor.sqlExe("COMMIT", {})
chapters = rows_to_dicts(recs, limit=500)
if not chapters:
return False, "项目下无章节,无法合成(请先生成章节骨架并编写)"
bad = [c for c in chapters if c.get("status") != CH_APPROVED]
if bad:
names = ", ".join("%s %s(%s)" % (c.get("chapter_no"), c.get("title"), c.get("status"))
for c in bad[:8])
return False, ("仍有 %d 个章节未评审通过,不可合成:%s%s"
% (len(bad), names, " ..." if len(bad) > 8 else ""))
tender = await get_tender_by_project(sor, project_id)
vrec = await sor.sqlExe(
"SELECT COALESCE(MAX(version),0) AS v FROM bid_documents WHERE project_id=${pid}$",
{"pid": project_id})
await sor.sqlExe("COMMIT", {})
ver = to_int(getattr(vrec[0], "v", 0) if vrec else 0) + 1
ws = await _workspace_dir(sor, project_id)
title = doc_name or ("%s 投标文件" % (tender.get("title") or proj.get("name") or "项目"))
subtitle = "投标人:(本单位)  招标编号:%s" % (tender.get("tender_no") or "-")
md_lines = ["# " + title, "", subtitle, "", "## 目录", ""]
for c in chapters:
md_lines.append("- %s %s" % (c.get("chapter_no"), c.get("title")))
md_lines.append("")
for c in chapters:
md_lines.append("# %s %s" % (c.get("chapter_no"), c.get("title")))
md_lines.append("")
md_lines.append(c.get("content") or "")
md_lines.append("")
md = "\n".join(md_lines)
md_path = os.path.join(ws, "bid_v%d.md" % ver)
with open(md_path, "w", encoding="utf-8") as f:
f.write(md)
docx_path = os.path.join(ws, "bid_v%d.docx" % ver)
ok_docx, err = _write_docx(docx_path, title, subtitle, chapters)
file_path = docx_path if ok_docx else md_path
total_chars = sum(len(c.get("content") or "") for c in chapters)
page_est = max(1, total_chars // 900) # 约 900 字/页(中文小四单倍行距)
max_sum = sum(to_float(c.get("max_score"), 0.0) for c in chapters)
did = new_id()
await sor.C("bid_documents", {
"id": did, "project_id": project_id, "version": ver,
"doc_name": title[:250], "file_path": file_path[:480],
"page_count": page_est, "chapter_count": len(chapters),
"max_score": max_sum or None, "status": DOC_DRAFT,
"task_id": task_id or "", "created_by": who or "agent.bid_compositor",
})
await sor.sqlExe("COMMIT", {})
await record(sor, project_id, "bid_documents", did, "compose",
to_state=DOC_DRAFT, who=who, agent_id=agent_id,
detail="v%d %d章 %d字" % (ver, len(chapters), total_chars))
note = "" if ok_docx else ("(docx 生成失败,已产出 markdown:%s)" % err)
return True, ("标书已合成 v%d:%s\n章节 %d 个,约 %d 字,估算 %d 页,对应满分 %.1f%s\n"
"系统将自动派发整书评分任务。"
% (ver, file_path, len(chapters), total_chars, page_est, max_sum, note))
async def list_bid_documents(project_id, limit=20):
try:
project_id = await resolve_project_id(project_id)
except ValueError as e:
return []
db, dbname = get_db()
async with db.sqlorContext(dbname) as sor:
recs = await sor.sqlExe(
"SELECT id, version, doc_name, file_path, page_count, chapter_count, "
"total_score, max_score, status, created_at FROM bid_documents "
"WHERE project_id=${pid}$ ORDER BY version DESC LIMIT " + str(to_int(limit, 20)),
{"pid": project_id})
await sor.sqlExe("COMMIT", {})
return rows_to_dicts(recs)
async def read_bid_document(document_id, offset=0, length=15000):
"""读合成标书正文(分段),供整书评审用。"""
db, dbname = get_db()
async with db.sqlorContext(dbname) as sor:
recs = await sor.sqlExe(
"SELECT id, project_id, version, file_path FROM bid_documents WHERE id=${did}$",
{"did": document_id})
await sor.sqlExe("COMMIT", {})
if not recs:
return "标书不存在: %s" % document_id
d = rec_to_dict(recs[0])
path = d.get("file_path") or ""
md = os.path.splitext(path)[0] + ".md"
src = md if os.path.isfile(md) else path
if not src or not os.path.isfile(src):
return "标书文件不存在于磁盘: %s" % path
if src.endswith(".docx"):
from .bid_analysis_capability import _extract_file_text
txt = _extract_file_text(src)
else:
with open(src, "r", encoding="utf-8", errors="ignore") as f:
txt = f.read()
o = to_int(offset, 0)
n = to_int(length, 15000)
return ("【标书 v%s】字符 %d-%d / 共 %d\n%s"
% (d.get("version"), o, o + n, len(txt), txt[o:o + n]))

View File

@ -0,0 +1,355 @@
"""投标产线任务流转引擎:状态收敛器(reconciler)+ 后台 poller。
设计原则(与开发产线一致):**流转归机制,不归 LLM**。
不依赖 agent 主动"调下一步工具",而是每 15 秒对每个投标项目做一次幂等状态对账:
按「领域状态」(招标文件/评分项/章节/标书)计算应存在哪些角色任务,缺则补建,已有则跳过。
因此:
- 章节评审不达标 → 章节置 rejected → 下一轮对账自动派重写任务(章节级循环)
- 整书评分不达标 → 命中章节置 rejected → 自动重写 → 重新评审 → 重新合成 → 重新评分(整书级循环)
- 人类任务 pending(等招标文件 / 等人类资料)→ 对账直接跳过该项目(阻塞门禁)
RoleSpec.next_role 全部留空:投标产线不用「任务链跳下一角色」,一律由本对账器决定。
"""
import asyncio
import json
import logging
from .bid_common import (
get_db, rows_to_dicts, rec_to_dict, to_int, to_float, json_loads,
create_role_task, count_open_tasks, has_done_task,
has_pending_blocking_human_task, find_human_task, get_thresholds,
create_human_task, PIPELINE_ID,
CH_PENDING, CH_WRITING, CH_WRITTEN, CH_REJECTED, CH_APPROVED,
DOC_DRAFT, DOC_REVIEWING, DOC_PASSED, DOC_REJECTED, DOC_DELIVERED,
HT_TENDER_FILE, HT_HUMAN_DOCS, HT_DELIVERY_CONFIRM,
)
logger = logging.getLogger("pipeline.bidding.flow")
R_ANALYST = "agent.tender_analyst"
R_PREP = "agent.bid_prep"
R_WRITER = "agent.bid_writer"
R_REVIEWER = "agent.bid_reviewer"
R_COMPOSITOR = "agent.bid_compositor"
R_SCORER = "agent.bid_scorer"
MAX_ANALYST_ATTEMPT = 2 # 解析阶段最多重派次数(防死循环)
MAX_PREP_ATTEMPT = 2
POLL_INTERVAL = 15
async def _count(sor, sql, p):
recs = await sor.sqlExe(sql, p)
await sor.sqlExe("COMMIT", {})
return to_int(getattr(recs[0], "c", 0) if recs else 0)
async def _done_task_count(sor, project_id, role):
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')",
{"p": project_id, "r": role})
async def _auto_close_file_upload_task(sor, project_id):
"""招标文件已上传 → 自动完成「等待上传招标文件」人类任务(上传即完成,不必人工再点一次)。"""
ht = await find_human_task(sor, project_id, HT_TENDER_FILE, status="pending")
if not ht:
return False
n = await _count(sor, "SELECT COUNT(*) AS c FROM bid_tender_files WHERE project_id=${p}$",
{"p": project_id})
if n <= 0:
return False
await sor.sqlExe(
"UPDATE pipeline_human_tasks SET status='done', qc_status='passed', "
"comments='招标文件已上传,系统自动完成', submitted_at=NOW() WHERE id=${h}$",
{"h": ht.get("id")})
await sor.sqlExe("COMMIT", {})
logger.info("bid_flow: auto-closed tender_file_upload task project=%s", project_id)
return True
async def reconcile_project(sor, project_id, project_name=""):
"""对单个投标项目做一次状态对账。返回动作说明列表(幂等,可反复调用)。"""
acts = []
pname = project_name or project_id
# ── 0. 招标文件到位则自动关闭上传任务,再判阻塞门禁 ──
await _auto_close_file_upload_task(sor, project_id)
if await has_pending_blocking_human_task(sor, project_id):
return ["blocked: 有待办人类任务(等招标文件/等人类资料/等审批),本轮不派发"]
th = await get_thresholds(sor)
n_files = await _count(sor, "SELECT COUNT(*) AS c FROM bid_tender_files "
"WHERE project_id=${p}$", {"p": project_id})
n_items = await _count(sor, "SELECT COUNT(*) AS c FROM bid_scoring_items "
"WHERE project_id=${p}$", {"p": project_id})
n_struct = await _count(sor, "SELECT COUNT(*) AS c FROM bid_doc_requirements "
"WHERE project_id=${p}$ AND req_type='structure'",
{"p": project_id})
n_quals = await _count(sor, "SELECT COUNT(*) AS c FROM bid_qualifications "
"WHERE project_id=${p}$", {"p": project_id})
n_quals_pending = await _count(sor, "SELECT COUNT(*) AS c FROM bid_qualifications "
"WHERE project_id=${p}$ AND match_status='pending'",
{"p": project_id})
chs = await sor.sqlExe(
"SELECT id, chapter_no, title, status, revise_count FROM bid_chapters "
"WHERE project_id=${p}$ ORDER BY order_no, chapter_no", {"p": project_id})
await sor.sqlExe("COMMIT", {})
chapters = rows_to_dicts(chs, limit=500)
docs = await sor.sqlExe(
"SELECT id, version, status FROM bid_documents WHERE project_id=${p}$ "
"ORDER BY version DESC LIMIT 1", {"p": project_id})
await sor.sqlExe("COMMIT", {})
doc = rec_to_dict(docs[0]) if docs else {}
# ── A. 招标文件解析(评分项 / 资质 / 投标文件要求 / 章节骨架)──
need_analysis = n_files > 0 and (n_items == 0 or n_struct == 0 or not chapters)
if need_analysis:
if await count_open_tasks(sor, project_id, role=R_ANALYST) > 0:
return acts + ["waiting: 招标文件解析任务在办"]
attempts = await _done_task_count(sor, project_id, R_ANALYST)
if attempts >= MAX_ANALYST_ATTEMPT:
ht = await find_human_task(sor, project_id, "general", status="pending")
if not ht:
await create_human_task(
sor, project_id, "general", "招标文件解析未产出完整结果,需人工介入",
("解析任务已完成 %d 次,但仍缺少:%s。\n"
"请人工检查招标文件是否可读(bid_tender_files.content_text),"
"或人工补录评分项/投标文件要求后完成本任务。"
% (attempts,
"、".join(x for x in [
"评分项" if n_items == 0 else "",
"投标文件章节结构" if n_struct == 0 else "",
"章节骨架" if not chapters else ""] if x))),
assignee_role="owner.superuser")
await sor.sqlExe("COMMIT", {})
acts.append("escalated: 解析重试超限 → 人工介入")
return acts
tid = await create_role_task(
sor, project_id, R_ANALYST,
"%s 招标文件解析(评分项/资质/投标文件要求/章节骨架)" % pname,
{"stage": "analysis", "task_kind": "bid_analysis"})
await sor.sqlExe("COMMIT", {})
return acts + ["created: 解析任务 %s" % tid]
# ── B. 资料准备(知识库资质匹配 + 人类文件清单)──
prep_done = await has_done_task(sor, project_id, R_PREP)
if n_quals > 0 and not prep_done:
if await count_open_tasks(sor, project_id, role=R_PREP) > 0:
return acts + ["waiting: 资料准备任务在办"]
tid = await create_role_task(
sor, project_id, R_PREP,
"%s 投标资料准备(资质匹配 + 人类文件清单)" % pname,
{"stage": "prep", "task_kind": "bid_prep", "qual_pending": n_quals_pending})
await sor.sqlExe("COMMIT", {})
return acts + ["created: 资料准备任务 %s" % tid]
if not chapters:
return acts + ["idle: 无章节,等待解析产出章节骨架"]
# ── C. 章节编写(一章一任务;pending/rejected 且无在办任务)──
created_w = 0
for c in chapters:
if c.get("status") not in (CH_PENDING, CH_REJECTED):
continue
if to_int(c.get("revise_count"), 0) > th["max_revise"]:
continue # 已超重做上限,等人工介入(review 阶段已抛任务)
if await count_open_tasks(sor, project_id, role=R_WRITER, chapter_id=c["id"]) > 0:
continue
stage = "revise" if c.get("status") == CH_REJECTED else "write"
tid = await create_role_task(
sor, project_id, R_WRITER,
"%s 第%s章《%s》%s" % (pname, c.get("chapter_no"), c.get("title"),
"修改重写" if stage == "revise" else "编写"),
{"stage": stage, "task_kind": "bid_write", "chapter_id": c["id"],
"chapter_no": c.get("chapter_no"), "revise_count": c.get("revise_count")})
created_w += 1
acts.append("created: 章节编写任务 %s (%s)" % (tid, c.get("chapter_no")))
if created_w:
await sor.sqlExe("COMMIT", {})
# ── D. 章节评审(written 且无在办评审任务)──
created_r = 0
for c in chapters:
if c.get("status") != CH_WRITTEN:
continue
if await count_open_tasks(sor, project_id, role=R_REVIEWER, chapter_id=c["id"]) > 0:
continue
tid = await create_role_task(
sor, project_id, R_REVIEWER,
"%s 第%s章《%s》评审打分" % (pname, c.get("chapter_no"), c.get("title")),
{"stage": "review", "task_kind": "bid_review", "chapter_id": c["id"],
"chapter_no": c.get("chapter_no")})
created_r += 1
acts.append("created: 章节评审任务 %s (%s)" % (tid, c.get("chapter_no")))
if created_r:
await sor.sqlExe("COMMIT", {})
if created_w or created_r:
return acts
# ── E. 合成标书(全部章节 approved;无标书或最新标书已被打回)──
all_approved = all(c.get("status") == CH_APPROVED for c in chapters)
if all_approved:
if not doc or doc.get("status") == DOC_REJECTED:
if await count_open_tasks(sor, project_id, role=R_COMPOSITOR) > 0:
return acts + ["waiting: 标书合成任务在办"]
tid = await create_role_task(
sor, project_id, R_COMPOSITOR, "%s 合成投标文件(标书)" % pname,
{"stage": "compose", "task_kind": "bid_compose",
"prev_version": doc.get("version") if doc else 0})
await sor.sqlExe("COMMIT", {})
return acts + ["created: 标书合成任务 %s" % tid]
# ── F. 整书评分 ──
if doc.get("status") in (DOC_DRAFT, DOC_REVIEWING):
if await count_open_tasks(sor, project_id, role=R_SCORER) > 0:
return acts + ["waiting: 整书评分任务在办"]
tid = await create_role_task(
sor, project_id, R_SCORER,
"%s 标书整体评审与评分(v%s)" % (pname, doc.get("version")),
{"stage": "score", "task_kind": "bid_score",
"document_id": doc.get("id"), "version": doc.get("version")})
await sor.sqlExe("COMMIT", {})
return acts + ["created: 整书评分任务 %s" % tid]
# ── G. 通过 → 交付确认 → 项目完成 ──
if doc.get("status") == DOC_PASSED:
ht = await find_human_task(sor, project_id, HT_DELIVERY_CONFIRM)
if not ht:
hid = await create_human_task(
sor, project_id, HT_DELIVERY_CONFIRM, "标书评分通过,请确认交付",
"标书已通过评分门限,请人工复核用印/密封/份数/递交后确认交付。")
await sor.sqlExe("COMMIT", {})
return acts + ["created: 交付确认任务 %s" % hid]
if ht.get("status") == "done":
await sor.sqlExe(
"UPDATE bid_documents SET status=${st}$, updated_at=NOW() WHERE id=${d}$",
{"st": DOC_DELIVERED, "d": doc.get("id")})
await sor.sqlExe(
"UPDATE sd_projects SET status='completed', updated_at=NOW() "
"WHERE id=${p}$ AND status<>'completed'", {"p": project_id})
await sor.sqlExe("COMMIT", {})
return acts + ["completed: 标书已交付,项目完成"]
return acts + ["waiting: 等待人工交付确认"]
return acts or ["idle: 无待办动作"]
async def reconcile_all(sor=None):
"""对账全部活跃投标项目。返回 {project_id: [actions]}。"""
if sor is not None:
return await _reconcile_all_with(sor)
db, dbname = get_db()
async with db.sqlorContext(dbname) as s:
return await _reconcile_all_with(s)
async def _reconcile_all_with(sor):
recs = await sor.sqlExe(
"SELECT id, name FROM sd_projects WHERE pipeline_id=${pl}$ "
"AND status NOT IN ('paused','archived','completed','cancelled') LIMIT 200",
{"pl": PIPELINE_ID})
await sor.sqlExe("COMMIT", {})
out = {}
for r in (recs or []):
pid = getattr(r, "id", "")
pname = getattr(r, "name", "")
if not pid:
continue
try:
out[pid] = await reconcile_project(sor, pid, pname)
except Exception as e:
logger.exception("reconcile project %s failed", pid)
out[pid] = ["error: %s" % str(e)[:200]]
return out
async def diagnose_bid_project(project_id):
"""诊断投标项目:阶段/门禁/章节分布/在办任务/最近评分,一次看清卡在哪。"""
db, dbname = get_db()
async with db.sqlorContext(dbname) as sor:
prec = await sor.sqlExe(
"SELECT id, name, status, pipeline_id FROM sd_projects WHERE id=${p}$",
{"p": project_id})
await sor.sqlExe("COMMIT", {})
if not prec:
return "项目不存在: %s" % project_id
proj = rec_to_dict(prec[0])
hts = await sor.sqlExe(
"SELECT id, task_type, title, status FROM pipeline_human_tasks "
"WHERE project_id=${p}$ ORDER BY created_at DESC LIMIT 20", {"p": project_id})
await sor.sqlExe("COMMIT", {})
tasks = await sor.sqlExe(
"SELECT id, role, title, state, retry_count FROM pipeline_tasks "
"WHERE tenant_id=${p}$ AND pipeline_id='role_task' "
"ORDER BY created_at DESC LIMIT 30", {"p": project_id})
await sor.sqlExe("COMMIT", {})
chs = await sor.sqlExe(
"SELECT status, COUNT(*) AS c FROM bid_chapters WHERE project_id=${p}$ "
"GROUP BY status", {"p": project_id})
await sor.sqlExe("COMMIT", {})
docs = await sor.sqlExe(
"SELECT version, status, total_score, max_score FROM bid_documents "
"WHERE project_id=${p}$ ORDER BY version DESC LIMIT 5", {"p": project_id})
await sor.sqlExe("COMMIT", {})
counts = {}
for tbl in ("bid_tender_files", "bid_scoring_items", "bid_qualifications",
"bid_doc_requirements", "bid_chapters"):
counts[tbl] = await _count(
sor, "SELECT COUNT(*) AS c FROM " + tbl + " WHERE project_id=${p}$",
{"p": project_id})
acts = await reconcile_project(sor, project_id, proj.get("name", ""))
return json.dumps({
"project": proj, "counts": counts,
"chapter_status": {getattr(r, "status", ""): to_int(getattr(r, "c", 0))
for r in (chs or [])},
"documents": rows_to_dicts(docs),
"human_tasks": rows_to_dicts(hts),
"role_tasks": rows_to_dicts(tasks),
"reconcile_actions": acts,
}, ensure_ascii=False, default=str)
# ══════════════ 后台 poller ══════════════
def start_poller():
"""注册投标产线对账 poller(ahserver 启动钩子)。
on_startup handler 只 spawn 后台任务,绝不内联无限循环(否则阻塞 aiohttp 启动)。
"""
try:
from ahserver.configuredServer import add_startup
except ImportError:
logger.warning("ahserver.add_startup 不可用,投标 poller 未启动")
return False
async def _bid_poller(app):
async def _loop():
db, dbname = get_db()
while True:
try:
async def _once():
async with db.sqlorContext(dbname) as sor:
res = await _reconcile_all_with(sor)
for pid, acts in (res or {}).items():
for a in acts:
if a.startswith(("created", "escalated", "completed", "error")):
logger.info("bid_flow %s: %s", pid, a)
# watchdog:每轮最多 60 秒,防连接池/MDL 锁卡死导致 poller 永久停摆
await asyncio.wait_for(_once(), timeout=60)
except Exception as e:
logger.warning("bid_poller error/timeout: %s", str(e)[:200])
await asyncio.sleep(POLL_INTERVAL)
asyncio.create_task(_loop())
logger.info("bid_flow poller started (interval=%ss)", POLL_INTERVAL)
add_startup(_bid_poller)
return True

View File

@ -0,0 +1,263 @@
"""投标知识库与资质能力:检索公司投标知识库、匹配所需资质、发布人类应提供的文件清单。
对应业务:从公司投标知识库中获取招标所需资质文档;发布人类应提供的文件(清单式人类任务,阻塞门禁)。
"""
import json
import logging
from .bid_common import (
get_db, new_id, rec_to_dict, rows_to_dicts, to_int, to_float,
record, create_human_task, find_human_task, get_project, resolve_project_id, HT_HUMAN_DOCS,
)
logger = logging.getLogger("pipeline.bidding.kb")
# ══════════════ 知识库 ══════════════
async def search_bid_kb(query="", doc_type="", org_id="", limit=10):
"""检索公司投标知识库(按名称/标签/编号/正文关键词)。返回命中文档摘要列表。"""
db, dbname = get_db()
async with db.sqlorContext(dbname) as sor:
sql = ("SELECT id, doc_type, doc_name, doc_no, tags, issuer, customer, "
"contract_amount, valid_until, file_path, LEFT(content_text, 300) AS preview "
"FROM bid_kb_docs WHERE status='active'")
p = {}
if org_id:
sql += " AND org_id=${oid}$"
p["oid"] = org_id
if doc_type:
sql += " AND doc_type=${dt}$"
p["dt"] = doc_type
q = (query or "").strip()
if q:
sql += (" AND (doc_name LIKE ${kw}$ OR tags LIKE ${kw}$ OR doc_no LIKE ${kw}$ "
"OR issuer LIKE ${kw}$ OR customer LIKE ${kw}$ OR content_text LIKE ${kw}$)")
p["kw"] = "%" + q + "%"
sql += " ORDER BY doc_type, created_at DESC LIMIT " + str(to_int(limit, 10))
recs = await sor.sqlExe(sql, p)
await sor.sqlExe("COMMIT", {})
return rows_to_dicts(recs)
async def list_kb_docs(doc_type="", org_id="", limit=100):
"""列出知识库文档(不含正文)。"""
return await search_bid_kb(query="", doc_type=doc_type, org_id=org_id, limit=limit)
async def kb_doc_detail(kb_doc_id, length=6000):
"""取知识库文档正文(供撰写/资质引用)。"""
db, dbname = get_db()
async with db.sqlorContext(dbname) as sor:
recs = await sor.sqlExe("SELECT * FROM bid_kb_docs WHERE id=${kid}$", {"kid": kb_doc_id})
await sor.sqlExe("COMMIT", {})
if not recs:
return "知识库文档不存在: %s" % kb_doc_id
d = rec_to_dict(recs[0])
d["content_text"] = (d.get("content_text") or "")[:to_int(length, 6000)]
return json.dumps(d, ensure_ascii=False, default=str)
async def add_kb_doc(doc_name, doc_type="qualification", doc_no="", tags="",
file_path="", content_text="", issuer="", customer="",
contract_amount="", valid_from="", valid_until="",
org_id="0", who=None, agent_id=None):
"""录入知识库文档(资质证书/业绩案例/模板/财务/人员)。"""
if not doc_name:
return False, "缺少 doc_name"
db, dbname = get_db()
async with db.sqlorContext(dbname) as sor:
kid = new_id()
data = {
"id": kid, "org_id": org_id or "0",
"doc_type": (doc_type or "qualification")[:30],
"doc_name": str(doc_name)[:250], "doc_no": str(doc_no or "")[:60],
"tags": str(tags or "")[:480], "file_path": str(file_path or "")[:480],
"content_text": content_text or "", "issuer": str(issuer or "")[:250],
"customer": str(customer or "")[:250], "status": "active",
"created_by": who or "",
}
amt = to_float(contract_amount, 0.0)
if amt > 0:
data["contract_amount"] = amt
if valid_from:
data["valid_from"] = str(valid_from)[:10]
if valid_until:
data["valid_until"] = str(valid_until)[:10]
await sor.C("bid_kb_docs", data)
await sor.sqlExe("COMMIT", {})
return True, "知识库文档已录入: %s (%s)" % (doc_name, kid)
# ══════════════ 资质匹配 ══════════════
async def list_qualifications(project_id, match_status=""):
try:
project_id = await resolve_project_id(project_id)
except ValueError as e:
return []
db, dbname = get_db()
async with db.sqlorContext(dbname) as sor:
sql = ("SELECT id, qual_name, is_mandatory, owner, match_status, kb_doc_id, "
"file_path, note, requirement FROM bid_qualifications WHERE project_id=${pid}$")
p = {"pid": project_id}
if match_status:
sql += " AND match_status=${st}$"
p["st"] = match_status
sql += " ORDER BY is_mandatory DESC, created_at"
recs = await sor.sqlExe(sql, p)
await sor.sqlExe("COMMIT", {})
out = rows_to_dicts(recs)
for r in out:
r["requirement"] = (r.get("requirement") or "")[:400]
return out
async def match_qualification(qual_id, kb_doc_id, note="", who=None, agent_id=None):
"""把知识库文档挂到资质项上:match_status=matched。"""
if not (qual_id and kb_doc_id):
return False, "缺少 qual_id 或 kb_doc_id"
db, dbname = get_db()
async with db.sqlorContext(dbname) as sor:
kb = await sor.sqlExe(
"SELECT id, doc_name, file_path, valid_until FROM bid_kb_docs WHERE id=${kid}$",
{"kid": kb_doc_id})
await sor.sqlExe("COMMIT", {})
if not kb:
return False, "知识库文档不存在: %s" % kb_doc_id
d = rec_to_dict(kb[0])
await sor.sqlExe(
"UPDATE bid_qualifications SET kb_doc_id=${kid}$, match_status='matched', "
"owner='kb', file_path=${fp}$, note=${nt}$, updated_at=NOW() WHERE id=${qid}$",
{"kid": kb_doc_id, "fp": (d.get("file_path") or "")[:480],
"nt": (note or ("命中知识库:" + (d.get("doc_name") or "")))[:480], "qid": qual_id})
rec = await sor.sqlExe(
"SELECT project_id, qual_name, match_status FROM bid_qualifications WHERE id=${qid}$",
{"qid": qual_id})
await sor.sqlExe("COMMIT", {})
if not rec:
return False, "资质项不存在: %s" % qual_id
r = rec_to_dict(rec[0])
if r.get("match_status") != "matched":
return False, "资质匹配写入失败"
await record(sor, r.get("project_id"), "bid_qualifications", qual_id, "match",
to_state="matched", who=who, agent_id=agent_id,
detail="kb_doc=" + kb_doc_id)
return True, "资质「%s」已匹配知识库文档「%s」" % (r.get("qual_name"), d.get("doc_name"))
async def mark_qualification_missing(qual_id, note="", need_human="1",
who=None, agent_id=None):
"""知识库无此资质:标记 missing 或 human_required(需人类提供)。"""
st = "human_required" if str(need_human) in ("1", "true", "True", "是") else "missing"
db, dbname = get_db()
async with db.sqlorContext(dbname) as sor:
await sor.sqlExe(
"UPDATE bid_qualifications SET match_status=${st}$, owner='human', "
"note=${nt}$, updated_at=NOW() WHERE id=${qid}$",
{"st": st, "nt": (note or "")[:480], "qid": qual_id})
rec = await sor.sqlExe(
"SELECT project_id, qual_name FROM bid_qualifications WHERE id=${qid}$",
{"qid": qual_id})
await sor.sqlExe("COMMIT", {})
if not rec:
return False, "资质项不存在: %s" % qual_id
r = rec_to_dict(rec[0])
await record(sor, r.get("project_id"), "bid_qualifications", qual_id, "mark_missing",
to_state=st, who=who, agent_id=agent_id, detail=note)
return True, "资质「%s」标记为 %s" % (r.get("qual_name"), st)
async def request_human_docs(project_id, items="", note="", who=None, agent_id=None):
"""发布「人类应提供的文件」清单人类任务(阻塞门禁)。
items 为空时自动汇总 match_status in (missing, human_required) 的资质项。
"""
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:
p = await get_project(sor, project_id)
if not p:
return False, "项目不存在: %s" % project_id
exist = await find_human_task(sor, project_id, HT_HUMAN_DOCS, status="pending")
if exist:
return True, "已有待办的人类文件清单任务: %s" % exist.get("id")
lines = []
if items:
if isinstance(items, str):
try:
parsed = json.loads(items)
except (ValueError, TypeError):
parsed = [x.strip() for x in items.split(",") if x.strip()]
else:
parsed = items
for x in (parsed or []):
if isinstance(x, dict):
lines.append("- %s:%s" % (x.get("name", ""), x.get("note", "")))
else:
lines.append("- %s" % x)
else:
recs = await sor.sqlExe(
"SELECT qual_name, requirement, is_mandatory, note FROM bid_qualifications "
"WHERE project_id=${pid}$ AND match_status IN ('missing','human_required') "
"ORDER BY is_mandatory DESC", {"pid": project_id})
await sor.sqlExe("COMMIT", {})
for r in (recs or []):
d = rec_to_dict(r)
lines.append("- %s%s:%s"
% (d.get("qual_name", ""),
"(必需)" if str(d.get("is_mandatory")) == "1" else "",
(d.get("note") or d.get("requirement") or "")[:200]))
if not lines:
return True, "无需人类提供的文件(资质均已从知识库匹配)"
desc = ("以下资质/证明文件公司知识库中缺失,请人工提供原件扫描件并上传到项目工作空间:"
"\n" + "\n".join(lines))
if note:
desc += "\n\n补充说明:" + note
desc += "\n\n提供完毕后请完成本任务,系统将自动继续投标文件编写。"
hid = await create_human_task(
sor, project_id, HT_HUMAN_DOCS,
"需人工提供的投标资质文件(%d 项)" % len(lines), desc,
assignee_role="owner.superuser",
form_schema={"project_id": project_id, "count": len(lines)})
await sor.sqlExe(
"UPDATE bid_qualifications SET human_task_id=${hid}$ WHERE project_id=${pid}$ "
"AND match_status IN ('missing','human_required')", {"hid": hid, "pid": project_id})
await sor.sqlExe("COMMIT", {})
return True, "已发布人类文件清单任务 %s(%d 项)" % (hid, len(lines))
async def auto_match_qualifications(project_id, org_id="", who=None, agent_id=None):
"""批量自动匹配:对 pending 资质项按名称关键词检索知识库,命中即 matched,否则 human_required。"""
try:
project_id = await resolve_project_id(project_id)
except ValueError as e:
return False, str(e)
quals = await list_qualifications(project_id, match_status="pending")
if not quals:
return True, "无待匹配资质项"
matched, missing = 0, 0
for q in quals:
name = (q.get("qual_name") or "").strip()
if not name:
continue
hits = await search_bid_kb(query=name, org_id=org_id, limit=3)
if not hits:
key = name.replace("证书", "").replace("认证", "").strip()[:8]
if key:
hits = await search_bid_kb(query=key, org_id=org_id, limit=3)
if hits:
ok, _ = await match_qualification(q["id"], hits[0]["id"],
note="自动匹配:" + (hits[0].get("doc_name") or ""),
who=who, agent_id=agent_id)
matched += 1 if ok else 0
else:
await mark_qualification_missing(q["id"], note="知识库未命中,需人工提供",
need_human="1", who=who, agent_id=agent_id)
missing += 1
return True, ("资质自动匹配完成:命中知识库 %d 项,需人工提供 %d 项。"
"请对需人工提供的项调用 request_human_docs 发布清单。" % (matched, missing))

View File

@ -0,0 +1,307 @@
"""招标邮件采集: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"&nbsp;?", " ", 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

View File

@ -0,0 +1,182 @@
"""章节评审能力(agent.bid_reviewer):对照招标文件要求 + 投标文件要求 + 得分规则逐章打分。
对应业务:每章节完成后对照招标文件要求和投标文件要求评审,分数不够给出改进意见重做。
达标 → approved;不达标 → rejected + 改进意见 + revise_count+1(reconciler 自动派重写任务);
超过重做上限 → 抛人类介入任务,不无限重做。
"""
import json
import logging
from .bid_common import (
get_db, new_id, rec_to_dict, rows_to_dicts, json_loads, to_float, to_int,
get_thresholds, record, create_human_task, find_human_task, resolve_project_id,
CH_WRITTEN, CH_REVIEWING, CH_APPROVED, CH_REJECTED,
)
logger = logging.getLogger("pipeline.bidding.review")
TABLE = "bid_chapters"
async def _chapter(sor, chapter_id):
recs = await sor.sqlExe("SELECT * FROM " + TABLE + " WHERE id=${cid}$", {"cid": chapter_id})
await sor.sqlExe("COMMIT", {})
return rec_to_dict(recs[0]) if recs else {}
async def review_chapter(chapter_id, score, max_score="", comments="", detail="",
who=None, agent_id=None, task_id=""):
"""章节评审落库 + 按门限判定通过/退回。
score / max_score:按该章节对应评分项的得分规则打分(max_score 省略时取章节 max_score)。
comments:不通过时必须给出可执行的改进意见(缺失则拒绝,避免"退回但不说怎么改")。
"""
if not chapter_id:
return False, "缺少 chapter_id"
db, dbname = get_db()
async with db.sqlorContext(dbname) as sor:
c = await _chapter(sor, chapter_id)
if not c:
return False, "章节不存在: %s" % chapter_id
pid = c.get("project_id")
th = await get_thresholds(sor)
sc = to_float(score, -1.0)
if sc < 0:
return False, "score 必须为数字(0~满分)"
ms = to_float(max_score, 0.0) or to_float(c.get("max_score"), 0.0)
if ms <= 0:
ms = 100.0 # 章节未挂评分项时按百分制评审
ratio = sc / ms if ms > 0 else 0.0
passed = ratio >= th["chapter_pass_ratio"]
if not passed and not (comments or "").strip():
return False, ("评审不通过必须给出改进意见(comments),"
"写清缺哪些评分维度、应补什么内容、对照哪条得分规则")
# 轮次 = 该章节已有评审数 + 1
rn = await sor.sqlExe(
"SELECT COUNT(*) AS c FROM bid_reviews WHERE chapter_id=${cid}$ AND scope='chapter'",
{"cid": chapter_id})
await sor.sqlExe("COMMIT", {})
rnd = to_int(getattr(rn[0], "c", 0) if rn else 0) + 1
rid = new_id()
await sor.C("bid_reviews", {
"id": rid, "project_id": pid, "scope": "chapter", "chapter_id": chapter_id,
"round": rnd, "reviewer": (who or "agent.bid_reviewer")[:60],
"review_type": "agent", "score": sc, "max_score": ms,
"pass_ratio": th["chapter_pass_ratio"], "passed": "1" if passed else "0",
"comments": comments or "", "detail": (detail or "")[:60000],
"task_id": task_id or "",
})
await sor.sqlExe("COMMIT", {})
if passed:
await sor.sqlExe(
"UPDATE " + TABLE + " SET status=${st}$, review_score=${sc}$, max_score=${ms}$, "
"review_comment=${cm}$, updated_at=NOW() WHERE id=${cid}$",
{"st": CH_APPROVED, "sc": sc, "ms": ms,
"cm": (comments or "评审通过")[:60000], "cid": chapter_id})
await sor.sqlExe("COMMIT", {})
await record(sor, pid, TABLE, chapter_id, "review_pass",
from_state=c.get("status"), to_state=CH_APPROVED,
who=who, agent_id=agent_id,
detail="%.2f/%.2f (%.0f%%)" % (sc, ms, ratio * 100))
return True, ("章节「%s」评审通过 %.2f/%.2f(%.0f%%,门限 %.0f%%)"
% (c.get("title"), sc, ms, ratio * 100, th["chapter_pass_ratio"] * 100))
revise = to_int(c.get("revise_count"), 0) + 1
await sor.sqlExe(
"UPDATE " + TABLE + " SET status=${st}$, review_score=${sc}$, max_score=${ms}$, "
"review_comment=${cm}$, revise_count=${rc}$, updated_at=NOW() WHERE id=${cid}$",
{"st": CH_REJECTED, "sc": sc, "ms": ms, "cm": comments or "",
"rc": revise, "cid": chapter_id})
await sor.sqlExe("COMMIT", {})
await record(sor, pid, TABLE, chapter_id, "review_reject",
from_state=c.get("status"), to_state=CH_REJECTED,
who=who, agent_id=agent_id,
detail="%.2f/%.2f revise=%d" % (sc, ms, revise))
if revise > th["max_revise"]:
exist = await find_human_task(sor, pid, "general", status="pending")
title = "章节反复不达标需人工介入:%s %s" % (c.get("chapter_no"), c.get("title"))
if not (exist and exist.get("title") == title):
await create_human_task(
sor, pid, "general", title,
("章节「%s %s」已重做 %d 次仍未达门限(最近得分 %.2f/%.2f,门限 %.0f%%)。\n"
"最新改进意见:\n%s\n\n"
"请人工判断:补充素材/调整章节范围/降低目标分,处理完成后完成本任务。"
% (c.get("chapter_no"), c.get("title"), revise, sc, ms,
th["chapter_pass_ratio"] * 100, (comments or "")[:1500])),
assignee_role="owner.superuser")
await sor.sqlExe("COMMIT", {})
return True, ("章节「%s」评审不通过 %.2f/%.2f,已达重做上限 %d 次 → 已抛人工介入任务"
% (c.get("title"), sc, ms, th["max_revise"]))
return True, ("章节「%s」评审不通过 %.2f/%.2f(%.0f%% < 门限 %.0f%%),已退回重写"
"(第 %d 次),系统将自动派发重写任务"
% (c.get("title"), sc, ms, ratio * 100,
th["chapter_pass_ratio"] * 100, revise))
async def approve_chapter(chapter_id, comment="", who=None, agent_id=None):
"""人工/评审直接通过章节(written/reviewing/rejected → approved)。"""
db, dbname = get_db()
async with db.sqlorContext(dbname) as sor:
c = await _chapter(sor, chapter_id)
if not c:
return False, "章节不存在: %s" % chapter_id
await sor.sqlExe(
"UPDATE " + TABLE + " SET status=${st}$, review_comment=${cm}$, updated_at=NOW() "
"WHERE id=${cid}$ AND status IN ('written','reviewing','rejected','writing')",
{"st": CH_APPROVED, "cm": (comment or "人工通过")[:60000], "cid": chapter_id})
cur = await _chapter(sor, chapter_id)
if cur.get("status") != CH_APPROVED:
return False, "通过失败:当前状态 %s" % cur.get("status")
await record(sor, c.get("project_id"), TABLE, chapter_id, "approve",
from_state=c.get("status"), to_state=CH_APPROVED,
who=who, agent_id=agent_id, detail=comment)
return True, "章节「%s」已通过" % c.get("title")
async def reject_chapter(chapter_id, comment="", who=None, agent_id=None):
"""退回章节重写(需给改进意见)。"""
if not (comment or "").strip():
return False, "退回必须给出改进意见(comment)"
db, dbname = get_db()
async with db.sqlorContext(dbname) as sor:
c = await _chapter(sor, chapter_id)
if not c:
return False, "章节不存在: %s" % chapter_id
revise = to_int(c.get("revise_count"), 0) + 1
await sor.sqlExe(
"UPDATE " + TABLE + " SET status=${st}$, review_comment=${cm}$, "
"revise_count=${rc}$, updated_at=NOW() WHERE id=${cid}$",
{"st": CH_REJECTED, "cm": comment[:60000], "rc": revise, "cid": chapter_id})
await sor.sqlExe("COMMIT", {})
await record(sor, c.get("project_id"), TABLE, chapter_id, "reject",
from_state=c.get("status"), to_state=CH_REJECTED,
who=who, agent_id=agent_id, detail=comment[:480])
return True, "章节「%s」已退回重写(第 %d 次)" % (c.get("title"), revise)
async def list_reviews(project_id, scope="", chapter_id="", limit=50):
"""查评审记录(章节级/整书级)。"""
try:
project_id = await resolve_project_id(project_id)
except ValueError as e:
return []
db, dbname = get_db()
async with db.sqlorContext(dbname) as sor:
sql = ("SELECT id, scope, chapter_id, document_id, round, reviewer, score, max_score, "
"passed, pass_ratio, LEFT(comments, 500) AS comments, created_at "
"FROM bid_reviews WHERE project_id=${pid}$")
p = {"pid": project_id}
if scope:
sql += " AND scope=${sc}$"
p["sc"] = scope
if chapter_id:
sql += " AND chapter_id=${cid}$"
p["cid"] = chapter_id
sql += " ORDER BY created_at DESC LIMIT " + str(to_int(limit, 50))
recs = await sor.sqlExe(sql, p)
await sor.sqlExe("COMMIT", {})
return rows_to_dicts(recs)

View File

@ -0,0 +1,334 @@
"""整书评分能力(agent.bid_scorer):按评分项和得分规则对整个标书打分。
对应业务:对整个标书评审,并按照评分项和得分规则对标书进行评分;如果不合适,提出改进意见,
按照意见对问题所在章节修改、评审……直到得分通过。
机制:add_score_item_result 逐项落分(含 improvement + target_chapter_id)→ finalize_bid_score
汇总判定;不通过则把命中章节置 rejected(reconciler 自动派重写任务),通过则发人类交付确认任务。
"""
import json
import logging
from .bid_common import (
get_db, new_id, rec_to_dict, rows_to_dicts, to_float, to_int,
get_thresholds, record, create_human_task, find_human_task, resolve_project_id,
CH_APPROVED, CH_REJECTED, DOC_DRAFT, DOC_REVIEWING, DOC_PASSED, DOC_REJECTED,
HT_DELIVERY_CONFIRM,
)
logger = logging.getLogger("pipeline.bidding.score")
async def _latest_doc(sor, project_id, document_id=""):
if document_id:
recs = await sor.sqlExe("SELECT * FROM bid_documents WHERE id=${d}$", {"d": document_id})
else:
recs = await sor.sqlExe(
"SELECT * FROM bid_documents WHERE project_id=${p}$ ORDER BY version DESC LIMIT 1",
{"p": project_id})
await sor.sqlExe("COMMIT", {})
return rec_to_dict(recs[0]) if recs else {}
async def start_bid_score(project_id, document_id="", who=None, agent_id=None, task_id=""):
"""开启一轮整书评分:创建 bid_reviews(scope=whole) 并返回 review_id + 评分项清单。"""
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:
doc = await _latest_doc(sor, project_id, document_id)
if not doc:
return False, "项目下无合成标书,请先 compose_bid"
th = await get_thresholds(sor)
rn = await sor.sqlExe(
"SELECT COUNT(*) AS c FROM bid_reviews WHERE project_id=${p}$ AND scope='whole'",
{"p": project_id})
await sor.sqlExe("COMMIT", {})
rnd = to_int(getattr(rn[0], "c", 0) if rn else 0) + 1
# 同一标书版本已有未完成评审 → 复用,避免重复开轮
open_r = await sor.sqlExe(
"SELECT id FROM bid_reviews WHERE project_id=${p}$ AND scope='whole' "
"AND document_id=${d}$ AND score IS NULL ORDER BY created_at DESC LIMIT 1",
{"p": project_id, "d": doc.get("id")})
await sor.sqlExe("COMMIT", {})
if open_r:
rid = getattr(open_r[0], "id", "")
else:
rid = new_id()
await sor.C("bid_reviews", {
"id": rid, "project_id": project_id, "scope": "whole",
"document_id": doc.get("id"), "round": rnd,
"reviewer": (who or "agent.bid_scorer")[:60], "review_type": "agent",
"pass_ratio": th["bid_pass_ratio"], "passed": "0",
"max_score": to_float(doc.get("max_score"), 0.0) or None,
"task_id": task_id or "",
})
await sor.sqlExe(
"UPDATE bid_documents SET status=${st}$, review_id=${r}$, updated_at=NOW() "
"WHERE id=${d}$", {"st": DOC_REVIEWING, "r": rid, "d": doc.get("id")})
await sor.sqlExe("COMMIT", {})
items = await sor.sqlExe(
"SELECT s.id, s.item_no, s.item_name, s.max_score, s.is_veto, s.section, "
"s.scoring_rule, s.target_chapter_no FROM bid_scoring_items s "
"WHERE s.project_id=${p}$ ORDER BY s.section, s.order_no", {"p": project_id})
await sor.sqlExe("COMMIT", {})
rows = rows_to_dicts(items, limit=300)
for r in rows:
r["scoring_rule"] = (r.get("scoring_rule") or "")[:800]
return True, json.dumps({
"review_id": rid, "round": rnd, "document_id": doc.get("id"),
"document_version": doc.get("version"), "file_path": doc.get("file_path"),
"pass_ratio": th["bid_pass_ratio"], "max_round": th["max_round"],
"scoring_items": rows,
"note": ("请逐个评分项调用 add_score_item_result 落分(不合格项必须给 improvement 与 "
"target_chapter_no/target_chapter_id),全部落完后调用 finalize_bid_score 汇总。"),
}, ensure_ascii=False, default=str)
async def add_score_item_result(review_id, scoring_item_id="", score=0, max_score="",
reason="", improvement="", target_chapter_no="",
target_chapter_id="", item_name="", who=None, agent_id=None):
"""逐个评分项落分。不合格项(得分率 < 门限)必须给 improvement + 目标章节。"""
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_reviews WHERE id=${r}$", {"r": review_id})
await sor.sqlExe("COMMIT", {})
if not rv:
return False, "评审记录不存在: %s" % review_id
review = rec_to_dict(rv[0])
pid = review.get("project_id")
th = await get_thresholds(sor)
it = {}
if scoring_item_id:
irec = await sor.sqlExe(
"SELECT * FROM bid_scoring_items WHERE id=${i}$", {"i": scoring_item_id})
await sor.sqlExe("COMMIT", {})
it = rec_to_dict(irec[0]) if irec else {}
ms = to_float(max_score, 0.0) or to_float(it.get("max_score"), 0.0)
if ms <= 0:
return False, "缺少 max_score(评分项满分)"
sc = to_float(score, -1.0)
if sc < 0:
return False, "score 必须为数字"
ratio = sc / ms
weak = ratio < th["bid_pass_ratio"]
if weak and not (improvement or "").strip():
return False, ("该评分项得分率 %.0f%% 低于门限,必须给出 improvement(改进意见)"
"与目标章节 target_chapter_no/target_chapter_id" % (ratio * 100))
cid = target_chapter_id or ""
cno = (target_chapter_no or it.get("target_chapter_no") or "").strip()
if not cid and cno:
crec = await sor.sqlExe(
"SELECT id FROM bid_chapters WHERE project_id=${p}$ AND chapter_no=${c}$ LIMIT 1",
{"p": pid, "c": cno})
await sor.sqlExe("COMMIT", {})
cid = getattr(crec[0], "id", "") if crec else ""
if not cid and scoring_item_id:
crec = await sor.sqlExe(
"SELECT id FROM bid_chapters WHERE project_id=${p}$ "
"AND scoring_item_ids LIKE ${kw}$ LIMIT 1",
{"p": pid, "kw": "%" + scoring_item_id + "%"})
await sor.sqlExe("COMMIT", {})
cid = getattr(crec[0], "id", "") if crec else ""
exist = await sor.sqlExe(
"SELECT id FROM bid_scores WHERE review_id=${r}$ AND scoring_item_id=${i}$ LIMIT 1",
{"r": review_id, "i": scoring_item_id or ""})
await sor.sqlExe("COMMIT", {})
data = {
"project_id": pid, "review_id": review_id,
"scoring_item_id": scoring_item_id or "",
"item_name": (item_name or it.get("item_name") or "")[:250],
"score": sc, "max_score": ms, "reason": reason or "",
"improvement": improvement or "", "target_chapter_id": cid,
}
if exist:
data["id"] = getattr(exist[0], "id", "")
await sor.U("bid_scores", data)
else:
data["id"] = new_id()
await sor.C("bid_scores", data)
await sor.sqlExe("COMMIT", {})
return True, ("评分项「%s」已落分 %.2f/%.2f%s"
% (data["item_name"], sc, ms,
"(需改进 → 章节 %s)" % (cid or cno or "未映射") if weak else ""))
async def finalize_bid_score(review_id, comments="", who=None, agent_id=None):
"""汇总评分:判定通过/不通过。
不通过 → 命中章节置 rejected(带改进意见)→ reconciler 自动派重写任务;
通过 → 标书 passed + 发人类交付确认任务;
轮次超上限 → 抛人工介入。
"""
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_reviews WHERE id=${r}$", {"r": review_id})
await sor.sqlExe("COMMIT", {})
if not rv:
return False, "评审记录不存在: %s" % review_id
review = rec_to_dict(rv[0])
pid = review.get("project_id")
th = await get_thresholds(sor)
srecs = await sor.sqlExe(
"SELECT * FROM bid_scores WHERE review_id=${r}$", {"r": review_id})
await sor.sqlExe("COMMIT", {})
scores = rows_to_dicts(srecs, limit=500)
if not scores:
return False, "本轮尚无评分明细,请先用 add_score_item_result 逐项落分"
total = sum(to_float(s.get("score"), 0.0) for s in scores)
maxsum = sum(to_float(s.get("max_score"), 0.0) for s in scores)
# 否决项检查:满分项得 0 分且 is_veto=1 视为致命
veto_ids = await sor.sqlExe(
"SELECT id, item_name FROM bid_scoring_items WHERE project_id=${p}$ AND is_veto='1'",
{"p": pid})
await sor.sqlExe("COMMIT", {})
veto_map = {getattr(r, "id", ""): getattr(r, "item_name", "") for r in (veto_ids or [])}
veto_fail = [veto_map[s["scoring_item_id"]] for s in scores
if s.get("scoring_item_id") in veto_map
and to_float(s.get("score"), 0.0) <= 0]
ratio = (total / maxsum) if maxsum > 0 else 0.0
passed = (ratio >= th["bid_pass_ratio"]) and not veto_fail
weak = [s for s in scores
if to_float(s.get("max_score"), 0.0) > 0
and to_float(s.get("score"), 0.0) / to_float(s.get("max_score"), 1.0) < th["bid_pass_ratio"]]
summary_lines = ["总分 %.2f/%.2f(%.1f%%,门限 %.0f%%)"
% (total, maxsum, ratio * 100, th["bid_pass_ratio"] * 100)]
if veto_fail:
summary_lines.append("⛔ 否决项未响应:" + "、".join(veto_fail[:5]))
for s in weak[:20]:
summary_lines.append("- %s %.1f/%.1f → %s"
% (s.get("item_name"), to_float(s.get("score")),
to_float(s.get("max_score")),
(s.get("improvement") or "")[:200]))
summary = "\n".join(summary_lines)
if comments:
summary += "\n\n评审说明:" + comments
await sor.sqlExe(
"UPDATE bid_reviews SET score=${sc}$, max_score=${ms}$, passed=${ps}$, "
"comments=${cm}$, updated_at=NOW() WHERE id=${r}$",
{"sc": total, "ms": maxsum, "ps": "1" if passed else "0",
"cm": summary[:60000], "r": review_id})
doc_status = DOC_PASSED if passed else DOC_REJECTED
await sor.sqlExe(
"UPDATE bid_documents SET status=${st}$, total_score=${sc}$, max_score=${ms}$, "
"pass_ratio=${pr}$, updated_at=NOW() WHERE id=${d}$",
{"st": doc_status, "sc": total, "ms": maxsum, "pr": th["bid_pass_ratio"],
"d": review.get("document_id") or ""})
await sor.sqlExe("COMMIT", {})
await record(sor, pid, "bid_documents", review.get("document_id") or "", "score",
to_state=doc_status, who=who, agent_id=agent_id,
detail="%.2f/%.2f round=%s" % (total, maxsum, review.get("round")))
if passed:
exist = await find_human_task(sor, pid, HT_DELIVERY_CONFIRM, status="pending")
if not exist:
await create_human_task(
sor, pid, HT_DELIVERY_CONFIRM,
"标书评分通过,请确认交付",
("标书评分已达通过门限。\n%s\n\n"
"请人工复核并确认交付(用印/密封/份数/递交方式按招标文件要求执行)。"
% summary[:3000]),
assignee_role="owner.superuser")
await sor.sqlExe("COMMIT", {})
return True, "整书评分通过:%s\n已发人工交付确认任务。" % summary
# 不通过:轮次门禁
if to_int(review.get("round"), 1) >= th["max_round"]:
await create_human_task(
sor, pid, "general",
"标书评分连续不达标需人工介入(第 %s 轮)" % review.get("round"),
("整书评分已进行 %s 轮仍未达门限:\n%s\n\n"
"请人工决策:调整评分预期/补充关键素材/重构章节,处理完成后完成本任务。"
% (review.get("round"), summary[:3000])),
assignee_role="owner.superuser")
await sor.sqlExe("COMMIT", {})
return True, ("整书评分不通过且已达轮次上限 %d → 已抛人工介入。\n%s"
% (th["max_round"], summary))
# 把改进意见落到问题章节:approved → rejected(reconciler 会派重写任务)
touched = 0
for s in weak:
cid = s.get("target_chapter_id") or ""
if not cid:
continue
crec = await sor.sqlExe(
"SELECT id, status, revise_count, title FROM bid_chapters WHERE id=${c}$",
{"c": cid})
await sor.sqlExe("COMMIT", {})
if not crec:
continue
c = rec_to_dict(crec[0])
imp = ("【整书评分改进意见 · 第%s轮】评分项「%s」得 %.1f/%.1f:\n%s"
% (review.get("round"), s.get("item_name"),
to_float(s.get("score")), to_float(s.get("max_score")),
s.get("improvement") or ""))
await sor.sqlExe(
"UPDATE bid_chapters SET status=${st}$, review_comment=${cm}$, "
"revise_count=${rc}$, updated_at=NOW() WHERE id=${c}$",
{"st": CH_REJECTED, "cm": imp[:60000],
"rc": to_int(c.get("revise_count"), 0) + 1, "c": cid})
await sor.sqlExe("COMMIT", {})
await record(sor, pid, "bid_chapters", cid, "score_reject",
from_state=c.get("status"), to_state=CH_REJECTED,
who=who, agent_id=agent_id, detail=(s.get("improvement") or "")[:400])
touched += 1
if touched == 0 and weak:
await create_human_task(
sor, pid, "general", "整书评分不通过但改进意见未映射到章节",
("以下评分项不达标,但 target_chapter_id 为空,系统无法自动定位待改章节:\n%s\n\n"
"请人工指定应修改的章节(或补 target_chapter_no 后重新评分)。" % summary[:3000]),
assignee_role="owner.superuser")
await sor.sqlExe("COMMIT", {})
return True, ("整书评分不通过:%s\n已退回 %d 个问题章节重写,系统将自动派发重写任务。"
% (summary, touched))
async def request_chapter_revise(chapter_id, improvement, who=None, agent_id=None):
"""按改进意见直接退回单个章节(评分员定点回退用)。"""
if not (improvement or "").strip():
return False, "必须给出 improvement(改进意见)"
from .bid_review_capability import reject_chapter
return await reject_chapter(chapter_id, comment=improvement, who=who, agent_id=agent_id)
async def bid_score_report(project_id, limit=50):
"""评分报告:最近一轮整书评分的总分 + 逐项明细。"""
try:
project_id = await resolve_project_id(project_id)
except ValueError as e:
return "缺少 project_id"
db, dbname = get_db()
async with db.sqlorContext(dbname) as sor:
rv = await sor.sqlExe(
"SELECT * FROM bid_reviews WHERE project_id=${p}$ AND scope='whole' "
"ORDER BY created_at DESC LIMIT 1", {"p": project_id})
await sor.sqlExe("COMMIT", {})
if not rv:
return "尚无整书评分记录"
r = rec_to_dict(rv[0])
srecs = await sor.sqlExe(
"SELECT item_name, score, max_score, LEFT(reason,300) AS reason, "
"LEFT(improvement,300) AS improvement, target_chapter_id "
"FROM bid_scores WHERE review_id=${r}$ ORDER BY created_at LIMIT " + str(to_int(limit, 50)),
{"r": r.get("id")})
await sor.sqlExe("COMMIT", {})
return json.dumps({
"review_id": r.get("id"), "round": r.get("round"),
"score": r.get("score"), "max_score": r.get("max_score"),
"passed": r.get("passed"), "pass_ratio": r.get("pass_ratio"),
"comments": (r.get("comments") or "")[:3000],
"items": rows_to_dicts(srecs, limit=to_int(limit, 50)),
}, ensure_ascii=False, default=str)

View File

@ -0,0 +1,465 @@
"""招标信息能力:采集 / 摘要要素抽取 / 审批 / 存档作废 / 立项 / 人员 / 等待招标文件。
对应业务:从邮箱获取招标信息 → 内容摘要(投标开始时间/项目周期/招标金额…)→ 走审批流程
→ 不通过则存档作废;通过则创建项目、设定参与人员、等待上传招标文件。
状态机规范见技能 `tender`;本模块只固化操作原语(CAS 迁移 + 审计),合法性由 LLM 读技能判断。
"""
import json
import logging
from .bid_common import (
get_db, new_id, rec_to_dict, rows_to_dicts, json_loads, to_float, to_int,
llm_json, record, create_human_task, find_human_task, get_project,
PIPELINE_ID, TD_NEW, TD_SUMMARIZED, TD_PENDING_APPROVAL, TD_APPROVED,
TD_REJECTED, TD_ARCHIVED, HT_TENDER_APPROVAL, HT_TENDER_FILE,
)
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}$, comments=${cm}$, "
"submitted_at=NOW() WHERE id=${hid}$ AND status='pending'",
{"st": status, "cm": (comment or "")[:2000], "hid": hid})
await sor.sqlExe("COMMIT", {})
# ══════════════ 立项 / 人员 / 招标文件 ══════════════
async def create_bid_project(tender_id, name="", org_id="", created_by="",
who=None, agent_id=None):
"""由招标信息立项:写 sd_projects(pipeline_id=bidding_general) + 发「上传招标文件」人类任务。"""
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("project_id"):
return True, "项目已存在: %s" % t.get("project_id")
if t.get("status") != TD_APPROVED:
return False, "招标信息未审批通过(当前 %s),不可立项" % t.get("status")
pid = new_id()
pname = (name or t.get("title") or t.get("mail_subject") or "投标项目")[:190]
await sor.C("sd_projects", {
"id": pid,
"name": pname,
"description": (t.get("summary") or "")[:4000],
"project_type": "bidding",
"pipeline_id": PIPELINE_ID,
"status": "in_progress",
"org_id": org_id or t.get("org_id") or "0",
"created_by": created_by or (who or "agent.tender_scout"),
})
await sor.sqlExe(
"UPDATE " + TABLE + " SET project_id=${pid}$, updated_at=NOW() WHERE id=${tid}$",
{"pid": pid, "tid": tender_id})
await sor.sqlExe("COMMIT", {})
await record(sor, pid, "sd_projects", pid, "create_bid_project",
to_state="in_progress", who=who, agent_id=agent_id,
detail="tender_id=" + tender_id)
hid = await create_human_task(
sor, pid, HT_TENDER_FILE,
"上传招标文件:%s" % pname,
("项目已立项,请上传招标文件(含答疑/补遗文件)。\n"
"上传后系统自动进入招标文件解析(抽取评分项、得分规则、所需资质、投标文件要求)。\n"
"上传入口:投标产线 → 招标文件(bid_tender_files)。"),
assignee_role="owner.superuser",
form_schema={"project_id": pid, "tender_id": tender_id})
return True, "project_id=%s(已发上传招标文件任务 %s)" % (pid, hid)
async def assign_members(project_id, members="", who=None, agent_id=None):
"""设定参与人员。members 支持 JSON 数组或 "姓名:角色,姓名:角色" 简写。"""
if not project_id:
return False, "缺少 project_id"
items = []
ms = members
if isinstance(ms, str) and ms.strip():
parsed = json_loads(ms, None)
if isinstance(parsed, list):
items = parsed
else:
for seg in ms.split(","):
seg = seg.strip()
if not seg:
continue
if ":" in seg or ":" in seg:
nm, rl = seg.replace(":", ":").split(":", 1)
items.append({"user_name": nm.strip(), "member_role": rl.strip()})
else:
items.append({"user_name": seg, "member_role": "writer"})
elif isinstance(ms, list):
items = ms
if not items:
return False, "未解析出参与人员(示例:张三:负责人,李四:writer)"
db, dbname = get_db()
async with db.sqlorContext(dbname) as sor:
n = 0
for it in items:
if not isinstance(it, dict):
continue
nm = str(it.get("user_name") or it.get("name") or "").strip()
if not nm:
continue
exist = await sor.sqlExe(
"SELECT id FROM bid_members WHERE project_id=${pid}$ AND user_name=${nm}$ LIMIT 1",
{"pid": project_id, "nm": nm})
if exist:
continue
await sor.C("bid_members", {
"id": new_id(), "project_id": project_id,
"user_id": str(it.get("user_id") or ""), "user_name": nm[:120],
"member_role": str(it.get("member_role") or "writer")[:60],
"duty": str(it.get("duty") or "")[:480],
"status": "active", "created_by": who or "",
})
n += 1
await sor.sqlExe("COMMIT", {})
await record(sor, project_id, "bid_members", project_id, "assign_members",
who=who, agent_id=agent_id, detail="新增 %d 人" % n)
return True, "已设定参与人员 %d 人" % n
async def list_members(project_id):
db, dbname = get_db()
async with db.sqlorContext(dbname) as sor:
recs = await sor.sqlExe(
"SELECT user_name, member_role, duty, status FROM bid_members "
"WHERE project_id=${pid}$ ORDER BY created_at", {"pid": project_id})
await sor.sqlExe("COMMIT", {})
return rows_to_dicts(recs)
async def request_tender_file(project_id, note="", who=None, agent_id=None):
"""(重新)发布「等待上传招标文件」人类任务。"""
db, dbname = get_db()
async with db.sqlorContext(dbname) as sor:
p = await get_project(sor, project_id)
if not p:
return False, "项目不存在: %s" % project_id
exist = await find_human_task(sor, project_id, HT_TENDER_FILE, status="pending")
if exist:
return True, "已有待办上传任务: %s" % exist.get("id")
hid = await create_human_task(
sor, project_id, HT_TENDER_FILE,
"上传招标文件:%s" % p.get("name", ""),
note or "请上传招标文件(含答疑/补遗),上传后自动进入解析阶段。")
await sor.sqlExe("COMMIT", {})
return True, "已发布上传招标文件任务: %s" % hid
async def list_tender_files(project_id):
db, dbname = get_db()
async with db.sqlorContext(dbname) as sor:
recs = await sor.sqlExe(
"SELECT id, file_name, file_type, extract_status, file_size, created_at "
"FROM bid_tender_files WHERE project_id=${pid}$ ORDER BY created_at",
{"pid": project_id})
await sor.sqlExe("COMMIT", {})
return rows_to_dicts(recs)

View File

@ -0,0 +1,140 @@
"""章节编写能力(agent.bid_writer):查章节、看要求、写正文、提交评审。
对应业务:按照投标文件要求分章节编写投标文件。
一章一任务(任务 params.chapter_id),写完置 written 由评审角色接手(reconciler 自动派评审任务)。
"""
import json
import logging
from .bid_common import (
get_db, rec_to_dict, rows_to_dicts, json_loads, to_int, record,
resolve_project_id, CH_PENDING, CH_WRITING, CH_WRITTEN, CH_REJECTED, CH_APPROVED,
)
logger = logging.getLogger("pipeline.bidding.write")
TABLE = "bid_chapters"
MIN_CONTENT_LEN = 200 # 章节正文最小长度门槛(防空交付)
async def list_chapters(project_id, status=""):
"""列出章节(不含正文,避免上下文爆炸)。"""
try:
project_id = await resolve_project_id(project_id)
except ValueError as e:
return []
db, dbname = get_db()
async with db.sqlorContext(dbname) as sor:
sql = ("SELECT id, chapter_no, title, section, status, version, revise_count, "
"word_count, review_score, max_score, assignee_role "
"FROM " + TABLE + " WHERE project_id=${pid}$")
p = {"pid": project_id}
if status:
sql += " AND status=${st}$"
p["st"] = status
sql += " ORDER BY order_no, chapter_no"
recs = await sor.sqlExe(sql, p)
await sor.sqlExe("COMMIT", {})
return rows_to_dicts(recs)
async def chapter_detail(chapter_id, with_content="1", length=12000):
"""章节详情:要点、对应评分项及得分规则、上一轮评审改进意见、当前正文。"""
db, dbname = get_db()
async with db.sqlorContext(dbname) as sor:
recs = await sor.sqlExe("SELECT * FROM " + TABLE + " WHERE id=${cid}$", {"cid": chapter_id})
await sor.sqlExe("COMMIT", {})
if not recs:
return "章节不存在: %s" % chapter_id
d = rec_to_dict(recs[0])
sids = json_loads(d.get("scoring_item_ids"), [])
items = []
if sids:
inlist = ",".join("'" + str(x).replace("'", "") + "'" for x in sids)
srecs = await sor.sqlExe(
"SELECT item_no, item_name, max_score, is_veto, scoring_rule, source_ref "
"FROM bid_scoring_items WHERE id IN (" + inlist + ")", {})
await sor.sqlExe("COMMIT", {})
items = rows_to_dicts(srecs)
reqs = await sor.sqlExe(
"SELECT req_type, requirement, page_limit FROM bid_doc_requirements "
"WHERE project_id=${pid}$ AND (chapter_no=${cno}$ OR req_type IN ('format','seal','copies')) "
"ORDER BY order_no",
{"pid": d.get("project_id"), "cno": d.get("chapter_no")})
await sor.sqlExe("COMMIT", {})
out = {
"id": d.get("id"), "chapter_no": d.get("chapter_no"), "title": d.get("title"),
"section": d.get("section"), "status": d.get("status"), "version": d.get("version"),
"revise_count": d.get("revise_count"), "outline": (d.get("outline") or "")[:6000],
"max_score": d.get("max_score"), "review_score": d.get("review_score"),
"review_comment": (d.get("review_comment") or "")[:6000],
"scoring_items": items,
"doc_requirements": rows_to_dicts(reqs),
}
if str(with_content) in ("1", "true", "True"):
out["content"] = (d.get("content") or "")[:to_int(length, 12000)]
return json.dumps(out, ensure_ascii=False, default=str)
async def write_chapter(chapter_id, content, summary="", who=None, agent_id=None):
"""写入/覆盖章节正文:pending/rejected/writing → writing,版本+1,字数统计。"""
if not chapter_id:
return False, "缺少 chapter_id"
txt = content or ""
if len(txt.strip()) < MIN_CONTENT_LEN:
return False, ("章节正文过短(%d 字符 < %d),拒绝写入。请按招标文件要求与评分规则"
"写到操作层面(量化指标/流程步骤/责任人/表单),不要写空话。"
% (len(txt.strip()), MIN_CONTENT_LEN))
db, dbname = get_db()
async with db.sqlorContext(dbname) as sor:
recs = await sor.sqlExe(
"SELECT id, project_id, status, version, title FROM " + TABLE + " WHERE id=${cid}$",
{"cid": chapter_id})
await sor.sqlExe("COMMIT", {})
if not recs:
return False, "章节不存在: %s" % chapter_id
d = rec_to_dict(recs[0])
if d.get("status") == CH_APPROVED:
return False, "章节已评审通过(approved),不可覆盖;如需修改请先由评审/评分角色退回"
ver = to_int(d.get("version"), 1)
await sor.sqlExe(
"UPDATE " + TABLE + " SET content=${c}$, word_count=${w}$, status=${st}$, "
"version=${v}$, updated_at=NOW() WHERE id=${cid}$",
{"c": txt, "w": len(txt), "st": CH_WRITING, "v": ver + 1, "cid": chapter_id})
await sor.sqlExe("COMMIT", {})
await record(sor, d.get("project_id"), TABLE, chapter_id, "write",
from_state=d.get("status"), to_state=CH_WRITING,
who=who, agent_id=agent_id, detail=(summary or "")[:480])
return True, ("章节「%s」正文已保存(%d 字符,v%d)。确认完成后调用 submit_chapter 提交评审。"
% (d.get("title"), len(txt), ver + 1))
async def submit_chapter(chapter_id, note="", who=None, agent_id=None):
"""提交章节评审:writing → written(reconciler 会自动派发评审任务)。"""
db, dbname = get_db()
async with db.sqlorContext(dbname) as sor:
recs = await sor.sqlExe(
"SELECT id, project_id, status, title, content FROM " + TABLE + " WHERE id=${cid}$",
{"cid": chapter_id})
await sor.sqlExe("COMMIT", {})
if not recs:
return False, "章节不存在: %s" % chapter_id
d = rec_to_dict(recs[0])
if len((d.get("content") or "").strip()) < MIN_CONTENT_LEN:
return False, "章节正文为空或过短,请先 write_chapter 写入正文"
if d.get("status") not in (CH_WRITING, CH_PENDING, CH_REJECTED):
return False, "当前状态 %s 不可提交(需 writing/pending/rejected)" % d.get("status")
await sor.sqlExe(
"UPDATE " + TABLE + " SET status=${st}$, updated_at=NOW() WHERE id=${cid}$ "
"AND status=${old}$",
{"st": CH_WRITTEN, "old": d.get("status"), "cid": chapter_id})
chk = await sor.sqlExe("SELECT status FROM " + TABLE + " WHERE id=${cid}$", {"cid": chapter_id})
await sor.sqlExe("COMMIT", {})
cur = getattr(chk[0], "status", "") if chk else ""
if cur != CH_WRITTEN:
return False, "提交失败(CAS):当前状态 %s" % cur
await record(sor, d.get("project_id"), TABLE, chapter_id, "submit",
from_state=d.get("status"), to_state=CH_WRITTEN,
who=who, agent_id=agent_id, detail=(note or "")[:480])
return True, "章节「%s」已提交评审" % d.get("title")

58
pipeline_bidding/init.py Normal file
View File

@ -0,0 +1,58 @@
"""pipeline-bidding 模块装载入口。
宿主应用(pipeline-app)在 init() 中调用 load_pipeline_bidding() 即挂载投标产线:
能力包注册(bid_ability)+ 角色工具 schema 注册(role_tool_schemas)+ 对账器启动(bid_flow)。
未装本模块的宿主完全零接触(不 import 本包则以上全部不发生)。
表结构不在此处建(铁律:运行期不做 schema 变更),建表走部署期:
json2ddl 生成 → build.sh 应用 / 人工执行(见 build.sh)。
"""
import logging
logger = logging.getLogger("pipeline.bidding")
_loaded = False
def load_pipeline_bidding():
"""挂载投标产线(幂等)。"""
global _loaded
if _loaded:
return True
ok = True
# 1. 角色工具 schema → 引擎分发字典(全路径模块,引擎裸名约定不变)
try:
from .role_tool_schemas import register_role_tools
n = register_role_tools()
logger.info("[pipeline_bidding] role tool schemas: %d", n)
except Exception as e:
logger.warning("[pipeline_bidding] role tool schema 注册失败: %s", str(e)[:200])
ok = False
# 2. 主 agent 能力包 + slash 命令(import 即注册,与开发产线同机制)
try:
from . import bid_ability # noqa: F401
logger.info("[pipeline_bidding] ability registered: bidding_general")
except Exception as e:
logger.warning("[pipeline_bidding] ability 注册失败: %s", str(e)[:200])
ok = False
# 3. 状态对账器(角色任务唯一创建者 + 阻塞门禁守门)。
# PIPELINE_MODE=web 时不注册(与引擎 poller 同款模式开关)。
import os
mode = (os.environ.get("PIPELINE_MODE", "all") or "all").strip().lower()
if mode in ("all", "worker"):
try:
from .bid_flow import start_poller
start_poller()
except Exception as e:
logger.warning("[pipeline_bidding] poller 启动失败: %s", str(e)[:200])
ok = False
else:
logger.info("[pipeline_bidding] PIPELINE_MODE=web,poller 不启动")
_loaded = ok or _loaded
logger.info("[pipeline_bidding] v0.1.0 loaded")
return True

View File

@ -0,0 +1,305 @@
"""投标产线角色工具 schema —— 注册进引擎 capability_tools.TOOL_SCHEMAS。
分工:
- 引擎 pipeline_service.capability_tools 只保留产线无关的分发机制(module 支持裸名/全路径);
- 投标产线的工具 schema(工具名→实现模块→参数)定义在这里,模块加载时注册进引擎字典。
这样宿主不装投标产线时,引擎字典里不会出现任何投标字符串。
注册时机:init.py 的 load_pipeline_bidding() 调用 register_role_tools()(import 即注册)。
"""
import logging
logger = logging.getLogger("pipeline.bidding.tools")
_M = "pipeline_bidding"
BID_ROLE_TOOL_SCHEMAS = {
# ── bid_tender_capability:招标信息员 ──
"fetch_tenders": {
"module": f"{_M}.bid_tender_capability",
"description": "从配置的招标邮箱采集招标信息入库(按关键词过滤+去重)",
"params": {"days": "回溯天数(可选)", "account_id": "邮箱配置ID(可选)"},
"required": [],
},
"summarize_tender": {
"module": f"{_M}.bid_tender_capability",
"description": "对招标信息做内容摘要与要素抽取(投标开始时间/项目周期/招标金额/保证金/资质要求)并落库",
"params": {"tender_id": "招标信息ID", "model_name": "模型(可选)"},
"required": ["tender_id"],
},
"set_tender_fields": {
"module": f"{_M}.bid_tender_capability",
"description": "修正招标信息要素(摘要抽错时纠正)",
"params": {"tender_id": "招标信息ID", "title": "项目名称(可选)", "purchaser": "招标人(可选)",
"bid_start_at": "投标开始时间(可选)", "bid_deadline": "投标截止(可选)",
"project_period": "项目周期(可选)", "budget_amount": "招标金额(可选)",
"summary": "摘要(可选)", "qualification_req": "资质要求(可选)"},
"required": ["tender_id"],
},
"submit_tender_approval": {
"module": f"{_M}.bid_tender_capability",
"description": "提交招标信息审批(生成人类审批任务)",
"params": {"tender_id": "招标信息ID", "note": "提交说明(可选)"},
"required": ["tender_id"],
},
"list_tenders": {
"module": f"{_M}.bid_tender_capability",
"description": "列出招标信息",
"params": {"status": "状态(可选)", "limit": "条数(可选)"},
"required": [],
},
"archive_tender": {
"module": f"{_M}.bid_tender_capability",
"description": "存档作废招标信息",
"params": {"tender_id": "招标信息ID", "reason": "原因"},
"required": ["tender_id"],
},
"assign_members": {
"module": f"{_M}.bid_tender_capability",
"description": "设定投标项目参与人员,如「张三:负责人,李四:writer」",
"params": {"project_id": "项目ID(可选,默认当前)",
"members": "人员清单(姓名:角色 逗号分隔,或JSON数组)"},
"required": ["members"],
},
"list_members": {
"module": f"{_M}.bid_tender_capability",
"description": "查看投标项目参与人员",
"params": {"project_id": "项目ID(可选,默认当前)"},
"required": [],
},
# ── bid_analysis_capability:招标文件分析师 ──
"read_tender_file": {
"module": f"{_M}.bid_analysis_capability",
"description": "分段读取招标文件正文(供分析)",
"params": {"file_id": "招标文件ID", "offset": "起始字符(可选)", "length": "长度(可选)"},
"required": ["file_id"],
},
"list_tender_files": {
"module": f"{_M}.bid_analysis_capability",
"description": "列出项目招标文件及抽取状态",
"params": {"project_id": "项目ID(可选,默认当前)", "extract_status": "抽取状态(可选)"},
"required": [],
},
"extract_from_file": {
"module": f"{_M}.bid_analysis_capability",
"description": "LLM 一次性抽取评分项+得分规则/所需资质/投标文件要求并批量落库(大文件分段调)",
"params": {"project_id": "项目ID(可选,默认当前)", "file_id": "招标文件ID(可选)",
"offset": "分段起始字符(可选)", "length": "分段长度(可选)"},
"required": [],
},
"add_scoring_item": {
"module": f"{_M}.bid_analysis_capability",
"description": "录入评分项及得分规则(抽取漏项时人工补录)",
"params": {"project_id": "项目ID(可选,默认当前)", "item_name": "评分项名称", "max_score": "满分",
"scoring_rule": "得分规则原文", "section": "business|technical|price(可选)",
"is_veto": "是否否决项 0/1(可选)", "target_chapter_no": "对应章节号(可选)"},
"required": ["item_name", "max_score"],
},
"add_qualification": {
"module": f"{_M}.bid_analysis_capability",
"description": "录入招标所需资质",
"params": {"project_id": "项目ID(可选,默认当前)", "qual_name": "资质名称",
"requirement": "要求原文(可选)", "is_mandatory": "是否必需 0/1(可选)"},
"required": ["qual_name"],
},
"add_doc_requirement": {
"module": f"{_M}.bid_analysis_capability",
"description": "录入投标文件要求(structure 类即章节骨架来源)",
"params": {"project_id": "项目ID(可选,默认当前)", "requirement": "要求内容",
"req_type": "structure|format|seal|copies|deadline|other(可选)",
"chapter_no": "章节号(可选)", "chapter_title": "章节标题(可选)",
"page_limit": "页数限制(可选)"},
"required": ["requirement"],
},
"create_chapter_outline": {
"module": f"{_M}.bid_analysis_capability",
"description": "生成章节骨架(默认用 structure 类要求自动生成,并挂评分项)",
"params": {"project_id": "项目ID(可选,默认当前)", "chapters": "章节JSON数组(可选,默认自动生成)"},
"required": [],
},
"list_scoring_items": {
"module": f"{_M}.bid_analysis_capability",
"description": "列出评分项与得分规则",
"params": {"project_id": "项目ID(可选,默认当前)", "section": "评分部分(可选)"},
"required": [],
},
"list_doc_requirements": {
"module": f"{_M}.bid_analysis_capability",
"description": "列出投标文件要求",
"params": {"project_id": "项目ID(可选,默认当前)", "req_type": "类型(可选)"},
"required": [],
},
"mark_file_extracted": {
"module": f"{_M}.bid_analysis_capability",
"description": "标记招标文件抽取完成",
"params": {"file_id": "招标文件ID", "note": "说明(可选)"},
"required": ["file_id"],
},
# ── bid_kb_capability:投标资料专员 ──
"search_bid_kb": {
"module": f"{_M}.bid_kb_capability",
"description": "检索公司投标知识库(资质证书/业绩案例/模板/财务/人员)",
"params": {"query": "关键词", "doc_type": "类型(可选)", "org_id": "机构(可选)"},
"required": [],
},
"kb_doc_detail": {
"module": f"{_M}.bid_kb_capability",
"description": "取知识库文档正文",
"params": {"kb_doc_id": "知识库文档ID"},
"required": ["kb_doc_id"],
},
"list_qualifications": {
"module": f"{_M}.bid_kb_capability",
"description": "列出招标所需资质及匹配状态",
"params": {"project_id": "项目ID(可选,默认当前)", "match_status": "匹配状态(可选)"},
"required": [],
},
"match_qualification": {
"module": f"{_M}.bid_kb_capability",
"description": "把知识库文档挂到资质项上(pending→matched)",
"params": {"qual_id": "资质项ID", "kb_doc_id": "知识库文档ID", "note": "说明(可选)"},
"required": ["qual_id", "kb_doc_id"],
},
"mark_qualification_missing": {
"module": f"{_M}.bid_kb_capability",
"description": "知识库无此资质:标记 missing 或 human_required",
"params": {"qual_id": "资质项ID", "note": "说明(可选)", "need_human": "需人工提供 0/1(默认1)"},
"required": ["qual_id"],
},
"auto_match_qualifications": {
"module": f"{_M}.bid_kb_capability",
"description": "批量自动匹配全部 pending 资质项(命中知识库即 matched,否则 human_required)",
"params": {"project_id": "项目ID(可选,默认当前)"},
"required": [],
},
"request_human_docs": {
"module": f"{_M}.bid_kb_capability",
"description": "发布「人类应提供的文件」清单任务(阻塞门禁;默认自动汇总缺失资质)",
"params": {"project_id": "项目ID(可选,默认当前)", "items": "清单(可选)", "note": "说明(可选)"},
"required": [],
},
# ── bid_write_capability:标书撰写工程师 ──
"list_chapters": {
"module": f"{_M}.bid_write_capability",
"description": "列出章节及状态",
"params": {"project_id": "项目ID(可选,默认当前)", "status": "状态(可选)"},
"required": [],
},
"chapter_detail": {
"module": f"{_M}.bid_write_capability",
"description": "章节详情:要点/对应评分项及得分规则/上轮改进意见/当前正文",
"params": {"chapter_id": "章节ID"},
"required": ["chapter_id"],
},
"write_chapter": {
"module": f"{_M}.bid_write_capability",
"description": "写入章节正文(写到操作层面,禁止空话;有最小长度门槛)",
"params": {"chapter_id": "章节ID", "content": "章节正文", "summary": "本次写作要点(可选)"},
"required": ["chapter_id", "content"],
},
"submit_chapter": {
"module": f"{_M}.bid_write_capability",
"description": "提交章节评审(written 后系统自动派评审任务)",
"params": {"chapter_id": "章节ID", "note": "说明(可选)"},
"required": ["chapter_id"],
},
# ── bid_review_capability:标书评审员 ──
"review_chapter": {
"module": f"{_M}.bid_review_capability",
"description": "章节评审打分:对照评分项得分规则+招标文件/投标文件要求;不达标必须给改进意见",
"params": {"chapter_id": "章节ID", "score": "得分(数字)", "max_score": "满分(可选,默认取章节挂载满分)",
"comments": "评审意见/改进意见(不通过必填)", "detail": "评审明细(可选)"},
"required": ["chapter_id", "score"],
},
"approve_chapter": {
"module": f"{_M}.bid_review_capability",
"description": "直接通过章节(特殊情况)",
"params": {"chapter_id": "章节ID", "comment": "意见(可选)"},
"required": ["chapter_id"],
},
"reject_chapter": {
"module": f"{_M}.bid_review_capability",
"description": "退回章节重写(必须给改进意见)",
"params": {"chapter_id": "章节ID", "comment": "改进意见"},
"required": ["chapter_id", "comment"],
},
"list_reviews": {
"module": f"{_M}.bid_review_capability",
"description": "查评审记录",
"params": {"project_id": "项目ID(可选,默认当前)", "scope": "chapter|whole(可选)",
"chapter_id": "章节ID(可选)"},
"required": [],
},
# ── bid_compose_capability:标书合成工程师 ──
"compose_bid": {
"module": f"{_M}.bid_compose_capability",
"description": "合成标书(要求全部章节已评审通过;产出 md+docx 并登记版本)",
"params": {"project_id": "项目ID(可选,默认当前)", "doc_name": "标书名称(可选)"},
"required": [],
},
"list_bid_documents": {
"module": f"{_M}.bid_compose_capability",
"description": "列出已合成标书版本及评分",
"params": {"project_id": "项目ID(可选,默认当前)"},
"required": [],
},
"read_bid_document": {
"module": f"{_M}.bid_compose_capability",
"description": "分段读取合成标书正文(供整书评审)",
"params": {"document_id": "标书ID", "offset": "起始字符(可选)", "length": "长度(可选)"},
"required": ["document_id"],
},
# ── bid_score_capability:标书评分员(模拟评标)──
"start_bid_score": {
"module": f"{_M}.bid_score_capability",
"description": "开启一轮整书评分:返回评审ID+全部评分项清单",
"params": {"project_id": "项目ID(可选,默认当前)", "document_id": "标书ID(可选,默认最新)"},
"required": [],
},
"add_score_item_result": {
"module": f"{_M}.bid_score_capability",
"description": "逐个评分项落分;不合格项必须给 improvement+目标章节",
"params": {"review_id": "评审ID", "scoring_item_id": "评分项ID", "score": "得分",
"max_score": "满分(可选)", "reason": "评分理由(可选)",
"improvement": "改进意见(不合格必填)", "target_chapter_no": "待改章节号(可选)",
"target_chapter_id": "待改章节ID(可选)"},
"required": ["review_id", "scoring_item_id", "score"],
},
"finalize_bid_score": {
"module": f"{_M}.bid_score_capability",
"description": "汇总整书评分:通过→发交付确认任务;不通过→改进意见落到问题章节自动退回重写",
"params": {"review_id": "评审ID", "comments": "总体评审说明(可选)"},
"required": ["review_id"],
},
"request_chapter_revise": {
"module": f"{_M}.bid_score_capability",
"description": "按改进意见定点退回单个章节",
"params": {"chapter_id": "章节ID", "improvement": "改进意见"},
"required": ["chapter_id", "improvement"],
},
"bid_score_report": {
"module": f"{_M}.bid_score_capability",
"description": "最近一轮整书评分报告(总分+逐项明细)",
"params": {"project_id": "项目ID(可选,默认当前)"},
"required": [],
},
}
def register_role_tools():
"""把投标角色工具 schema 注册进引擎分发字典(幂等)。
引擎不可用(未装 pipeline-service)时诚实降级返回 0,不抛异常。
"""
try:
from pipeline_service.capability_tools import TOOL_SCHEMAS
except Exception as e:
logger.warning("引擎不可用,投标角色工具未注册: %s", str(e)[:160])
return 0
added = 0
for name, schema in BID_ROLE_TOOL_SCHEMAS.items():
TOOL_SCHEMAS[name] = schema
added += 1
logger.info("bid role tool schemas registered: %d", added)
return added

9
pyproject.toml Normal file
View File

@ -0,0 +1,9 @@
[project]
name = "pipeline_bidding"
version = "0.1.0"
description = "投标产线:招标信息采集→审批→立项→解析→资质准备→分章节编写→评审→合成→评分→交付"
dependencies = []
[tool.setuptools.packages.find]
where = ["."]
include = ["pipeline_bidding*"]

6
scripts/gen_ddl.sh Normal file
View File

@ -0,0 +1,6 @@
#!/bin/bash
# 生成 pipeline-bidding 的 MySQL DDL(json2ddl 需要 sqlor + appPublic 在 PYTHONPATH)
set -e
R=/home/ymq/work/repos
export PYTHONPATH="$R/xls2ddl:$R/sqlor:$R/apppublic"
python3 "$R/xls2ddl/gen_bidding_ddl.py"

64
scripts/load_path.py Normal file
View File

@ -0,0 +1,64 @@
#!/usr/bin/env python3
"""RBAC path registration for pipeline-bidding module.
与平台惯例一致:静态资源 → any(免登录),页面/CRUD/API → logined。
由宿主应用根目录执行:py3/bin/python pkgs/pipeline-bidding/scripts/load_path.py
"""
import subprocess
import sys
MOD = "pipeline-bidding"
TABLES = [
"bid_mail_accounts", "bid_tenders", "bid_members", "bid_tender_files",
"bid_scoring_items", "bid_qualifications", "bid_doc_requirements",
"bid_chapters", "bid_reviews", "bid_scores", "bid_kb_docs", "bid_documents",
]
PATHS_ANY = [
"/imgs/node-expand.svg",
"/imgs/node-collapse.svg",
"/imgs/open-folder.svg",
"/imgs/close-folder.svg",
]
PATHS_LOGINED = [
"/%s" % MOD,
"/%s/index.ui" % MOD,
]
for t in TABLES:
PATHS_LOGINED += [
"/%s/%s/index.ui" % (MOD, t),
"/%s/%s/get_%s.dspy" % (MOD, t, t),
"/%s/%s/add_%s.dspy" % (MOD, t, t),
"/%s/%s/update_%s.dspy" % (MOD, t, t),
"/%s/%s/delete_%s.dspy" % (MOD, t, t),
]
# 产线业务 API(当前无自写 dspy;保留结构,后续新增在此登记)
PATHS_LOGINED += [
"/%s/api/" % MOD,
]
def _run(role, path):
r = subprocess.run([sys.executable, "set_role_perm.py", role, path],
capture_output=True, text=True)
if r.returncode != 0:
print(" FAIL [%s] %s: %s" % (role, path, (r.stderr or "").strip()[:120]))
return r.returncode == 0
def main():
print("=== %s RBAC registration ===" % MOD)
n = 0
for p in PATHS_ANY:
n += _run("any", p)
print(" any: %s" % p)
for p in PATHS_LOGINED:
n += _run("logined", p)
print(" logined: %s" % p)
print("Done. %d/%d paths registered" % (n, len(PATHS_ANY) + len(PATHS_LOGINED)))
if __name__ == "__main__":
main()

View File

@ -0,0 +1,41 @@
ns = params_kw.copy()
for k,v in ns.items():
if v == 'NaN' or v == 'null':
ns[k] = None
id = params_kw.id
if not id or len(id) > 32:
id = uuid()
ns['id'] = id
db = DBPools()
dbname = get_module_dbname('pipeline_bidding')
async with db.sqlorContext(dbname) as sor:
r = await sor.C('bid_chapters', ns.copy())
_new_rows = await sor.sqlExe("SELECT * FROM bid_chapters WHERE id = ${id}$", {'id': id})
_user_data = _new_rows[0] if _new_rows else ns
return {
"widgettype":"Message",
"options":{
"cwidth":16,
"cheight":9,
"title":"Add Success",
"timeout":3,
"message":"ok",
"user_data": _user_data
}
}
return {
"widgettype":"Error",
"options":{
"title":"Add Error",
"cwidth":16,
"cheight":9,
"timeout":3,
"message":"failed"
}
}

View File

@ -0,0 +1,33 @@
ns = {
'id':params_kw['id'],
}
db = DBPools()
dbname = get_module_dbname('pipeline_bidding')
async with db.sqlorContext(dbname) as sor:
r = await sor.D('bid_chapters', ns)
debug('delete success');
return {
"widgettype":"Message",
"options":{
"title":"Delete Success",
"timeout":3,
"cwidth":16,
"cheight":9,
"message":"ok"
}
}
debug('Delete failed');
return {
"widgettype":"Error",
"options":{
"title":"Delete Error",
"timeout":3,
"cwidth":16,
"cheight":9,
"message":"failed"
}
}

View File

@ -0,0 +1,50 @@
ns = params_kw.copy()
debug(f'get_bid_chapters.dspy:{ns=}')
if not ns.get('page'):
ns['page'] = 1
if not ns.get('sort'):
ns['sort'] = json.loads('["order_no", "chapter_no"]')
sql = '''select a.*, b.status_text, c.section_text
from (select * from bid_chapters where 1=1 [[filterstr]]) a left join (select k as status,
v as status_text from appcodes_kv where parentid='bid_chapter_status') b on a.status = b.status left join (select k as section,
v as section_text from appcodes_kv where parentid='bid_scoring_section') c on a.section = c.section'''
filterjson = json.loads('{"AND": [{"field": "project_id", "op": "=", "var": "project_id"}, {"field": "status", "op": "=", "var": "status_input"}, {"field": "title", "op": "LIKE", "var": "title_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":[]
}

View File

@ -0,0 +1,370 @@
{
"widgettype":"VBox",
"options":{"height":"100%","width":"100%"},
"subwidgets":[
{
"id":"bid_chapters_tbl",
"widgettype":"Tabular",
"options":{
"width":"100%",
"height":"100%",
"title":"投标文件章节",
"toolbar":{
"tools": []
},
"css":"card",
"editable":{
"new_data_url": "{{entire_url('add_bid_chapters.dspy')}}",
"delete_data_url": "{{entire_url('delete_bid_chapters.dspy')}}",
"update_data_url": "{{entire_url('update_bid_chapters.dspy')}}"
},
"data_url":"{{entire_url('./get_bid_chapters.dspy')}}",
"data_method":"GET",
"data_params":{{json.dumps(params_kw, indent=4, ensure_ascii=False)}},
"row_options":{
"browserfields": {
"exclouded": [
"content",
"outline",
"scoring_item_ids"
]
},
"editexclouded":[
"id",
"created_at",
"updated_at",
"task_id",
"version",
"word_count"
],
"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": "chapter_no",
"title": "章节号",
"type": "str",
"length": 32,
"nullable": "no",
"cwidth": 18,
"uitype": "str",
"datatype": "str",
"label": "章节号"
},
{
"name": "parent_no",
"title": "父章节号",
"type": "str",
"length": 32,
"cwidth": 18,
"uitype": "str",
"datatype": "str",
"label": "父章节号"
},
{
"name": "title",
"title": "章节标题",
"type": "str",
"length": 255,
"nullable": "no",
"cwidth": 18,
"uitype": "str",
"datatype": "str",
"label": "章节标题"
},
{
"name": "section",
"title": "所属部分",
"type": "str",
"length": 32,
"default": "technical",
"label": "所属部分",
"uitype": "code",
"valueField": "section",
"textField": "section_text",
"params": {
"dbname": "{{get_module_dbname('pipeline_bidding')}}",
"table": "appcodes_kv",
"tblvalue": "k",
"tbltext": "v",
"valueField": "section",
"textField": "section_text",
"cond": "parentid='bid_scoring_section'"
},
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}"
},
{
"name": "scoring_item_ids",
"title": "对应评分项(JSON)",
"type": "text",
"length": 0,
"uitype": "text",
"datatype": "text",
"label": "对应评分项(JSON)"
},
{
"name": "outline",
"title": "章节要点",
"type": "text",
"length": 0,
"uitype": "text",
"datatype": "text",
"label": "章节要点"
},
{
"name": "status",
"title": "章节状态",
"type": "str",
"length": 16,
"nullable": "no",
"default": "pending",
"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_chapter_status'"
},
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}"
},
{
"name": "version",
"title": "版本号",
"type": "int",
"nullable": "no",
"default": "1",
"length": 0,
"uitype": "int",
"datatype": "int",
"label": "版本号"
},
{
"name": "content",
"title": "章节正文",
"type": "text",
"length": 0,
"uitype": "text",
"datatype": "text",
"label": "章节正文"
},
{
"name": "file_path",
"title": "章节文件路径",
"type": "str",
"length": 500,
"cwidth": 18,
"uitype": "str",
"datatype": "str",
"label": "章节文件路径"
},
{
"name": "word_count",
"title": "字数",
"type": "int",
"default": "0",
"length": 0,
"uitype": "int",
"datatype": "int",
"label": "字数"
},
{
"name": "review_score",
"title": "评审得分",
"type": "float",
"length": 8,
"dec": 2,
"cwidth": 8,
"uitype": "float",
"datatype": "float",
"label": "评审得分"
},
{
"name": "max_score",
"title": "对应满分",
"type": "float",
"length": 8,
"dec": 2,
"cwidth": 8,
"uitype": "float",
"datatype": "float",
"label": "对应满分"
},
{
"name": "review_comment",
"title": "评审意见/改进意见",
"type": "text",
"length": 0,
"uitype": "text",
"datatype": "text",
"label": "评审意见/改进意见"
},
{
"name": "revise_count",
"title": "重做次数",
"type": "int",
"nullable": "no",
"default": "0",
"length": 0,
"uitype": "int",
"datatype": "int",
"label": "重做次数"
},
{
"name": "assignee_role",
"title": "承办角色",
"type": "str",
"length": 64,
"default": "agent.bid_writer",
"cwidth": 18,
"uitype": "str",
"datatype": "str",
"label": "承办角色"
},
{
"name": "task_id",
"title": "关联任务ID",
"type": "str",
"length": 32,
"cwidth": 18,
"uitype": "str",
"datatype": "str",
"label": "关联任务ID"
},
{
"name": "order_no",
"title": "排序",
"type": "int",
"default": "0",
"length": 0,
"uitype": "int",
"datatype": "int",
"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": "project_id",
"uitype": "str",
"placeholder": "项目ID",
"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_chapter_status'"
},
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}?prepend_all=1"
},
{
"name": "title_input",
"uitype": "str",
"placeholder": "章节标题",
"cwidth": 15
}
]
},
"page_rows":160,
"cache_limit":5
}
,"binds":[]
}]
}

View File

@ -0,0 +1,44 @@
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": "project_id", "title": "项目ID", "type": "str", "length": 32, "nullable": "no"}, {"name": "chapter_no", "title": "章节号", "type": "str", "length": 32, "nullable": "no"}, {"name": "parent_no", "title": "父章节号", "type": "str", "length": 32}, {"name": "title", "title": "章节标题", "type": "str", "length": 255, "nullable": "no"}, {"name": "section", "title": "所属部分", "type": "str", "length": 32, "default": "technical"}, {"name": "scoring_item_ids", "title": "对应评分项(JSON)", "type": "text"}, {"name": "outline", "title": "章节要点", "type": "text"}, {"name": "status", "title": "章节状态", "type": "str", "length": 16, "nullable": "no", "default": "pending"}, {"name": "version", "title": "版本号", "type": "int", "nullable": "no", "default": "1"}, {"name": "content", "title": "章节正文", "type": "text"}, {"name": "file_path", "title": "章节文件路径", "type": "str", "length": 500}, {"name": "word_count", "title": "字数", "type": "int", "default": "0"}, {"name": "review_score", "title": "评审得分", "type": "float", "length": 8, "dec": 2}, {"name": "max_score", "title": "对应满分", "type": "float", "length": 8, "dec": 2}, {"name": "review_comment", "title": "评审意见/改进意见", "type": "text"}, {"name": "revise_count", "title": "重做次数", "type": "int", "nullable": "no", "default": "0"}, {"name": "assignee_role", "title": "承办角色", "type": "str", "length": 64, "default": "agent.bid_writer"}, {"name": "task_id", "title": "关联任务ID", "type": "str", "length": 32}, {"name": "order_no", "title": "排序", "type": "int", "default": "0"}, {"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_chapters', 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"
}
}

View File

@ -0,0 +1,41 @@
ns = params_kw.copy()
for k,v in ns.items():
if v == 'NaN' or v == 'null':
ns[k] = None
id = params_kw.id
if not id or len(id) > 32:
id = uuid()
ns['id'] = id
db = DBPools()
dbname = get_module_dbname('pipeline_bidding')
async with db.sqlorContext(dbname) as sor:
r = await sor.C('bid_doc_requirements', ns.copy())
_new_rows = await sor.sqlExe("SELECT * FROM bid_doc_requirements WHERE id = ${id}$", {'id': id})
_user_data = _new_rows[0] if _new_rows else ns
return {
"widgettype":"Message",
"options":{
"cwidth":16,
"cheight":9,
"title":"Add Success",
"timeout":3,
"message":"ok",
"user_data": _user_data
}
}
return {
"widgettype":"Error",
"options":{
"title":"Add Error",
"cwidth":16,
"cheight":9,
"timeout":3,
"message":"failed"
}
}

View File

@ -0,0 +1,33 @@
ns = {
'id':params_kw['id'],
}
db = DBPools()
dbname = get_module_dbname('pipeline_bidding')
async with db.sqlorContext(dbname) as sor:
r = await sor.D('bid_doc_requirements', ns)
debug('delete success');
return {
"widgettype":"Message",
"options":{
"title":"Delete Success",
"timeout":3,
"cwidth":16,
"cheight":9,
"message":"ok"
}
}
debug('Delete failed');
return {
"widgettype":"Error",
"options":{
"title":"Delete Error",
"timeout":3,
"cwidth":16,
"cheight":9,
"message":"failed"
}
}

View File

@ -0,0 +1,49 @@
ns = params_kw.copy()
debug(f'get_bid_doc_requirements.dspy:{ns=}')
if not ns.get('page'):
ns['page'] = 1
if not ns.get('sort'):
ns['sort'] = json.loads('["req_type", "order_no"]')
sql = '''select a.*, b.req_type_text
from (select * from bid_doc_requirements where 1=1 [[filterstr]]) a left join (select k as req_type,
v as req_type_text from appcodes_kv where parentid='bid_req_type') b on a.req_type = b.req_type'''
filterjson = json.loads('{"AND": [{"field": "project_id", "op": "=", "var": "project_id"}, {"field": "req_type", "op": "=", "var": "type_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":[]
}

View File

@ -0,0 +1,231 @@
{
"widgettype":"VBox",
"options":{"height":"100%","width":"100%"},
"subwidgets":[
{
"id":"bid_doc_requirements_tbl",
"widgettype":"Tabular",
"options":{
"width":"100%",
"height":"100%",
"title":"投标文件要求",
"toolbar":{
"tools": []
},
"css":"card",
"editable":{
"new_data_url": "{{entire_url('add_bid_doc_requirements.dspy')}}",
"delete_data_url": "{{entire_url('delete_bid_doc_requirements.dspy')}}",
"update_data_url": "{{entire_url('update_bid_doc_requirements.dspy')}}"
},
"data_url":"{{entire_url('./get_bid_doc_requirements.dspy')}}",
"data_method":"GET",
"data_params":{{json.dumps(params_kw, indent=4, ensure_ascii=False)}},
"row_options":{
"browserfields": {
"exclouded": []
},
"editexclouded":[
"id",
"created_at",
"updated_at"
],
"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": "req_type",
"title": "要求类型",
"type": "str",
"length": 32,
"nullable": "no",
"default": "structure",
"label": "要求类型",
"uitype": "code",
"valueField": "req_type",
"textField": "req_type_text",
"params": {
"dbname": "{{get_module_dbname('pipeline_bidding')}}",
"table": "appcodes_kv",
"tblvalue": "k",
"tbltext": "v",
"valueField": "req_type",
"textField": "req_type_text",
"cond": "parentid='bid_req_type'"
},
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}"
},
{
"name": "chapter_no",
"title": "章节号",
"type": "str",
"length": 32,
"cwidth": 18,
"uitype": "str",
"datatype": "str",
"label": "章节号"
},
{
"name": "chapter_title",
"title": "章节标题",
"type": "str",
"length": 255,
"cwidth": 18,
"uitype": "str",
"datatype": "str",
"label": "章节标题"
},
{
"name": "requirement",
"title": "要求内容",
"type": "text",
"length": 0,
"uitype": "text",
"datatype": "text",
"label": "要求内容"
},
{
"name": "page_limit",
"title": "页数限制",
"type": "int",
"length": 0,
"uitype": "int",
"datatype": "int",
"label": "页数限制"
},
{
"name": "source_ref",
"title": "招标文件出处",
"type": "str",
"length": 500,
"cwidth": 18,
"uitype": "str",
"datatype": "str",
"label": "招标文件出处"
},
{
"name": "order_no",
"title": "排序",
"type": "int",
"default": "0",
"length": 0,
"uitype": "int",
"datatype": "int",
"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": "project_id",
"uitype": "str",
"placeholder": "项目ID",
"cwidth": 15
},
{
"name": "type_input",
"uitype": "code",
"placeholder": "要求类型",
"cwidth": 15,
"valueField": "type_input",
"textField": "type_input_text",
"params": {
"dbname": "{{get_module_dbname('pipeline_bidding')}}",
"table": "appcodes_kv",
"tblvalue": "k",
"tbltext": "v",
"valueField": "type_input",
"textField": "type_input_text",
"cond": "parentid='bid_req_type'"
},
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}?prepend_all=1"
}
]
},
"page_rows":160,
"cache_limit":5
}
,"binds":[]
}]
}

View File

@ -0,0 +1,44 @@
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": "project_id", "title": "项目ID", "type": "str", "length": 32, "nullable": "no"}, {"name": "req_type", "title": "要求类型", "type": "str", "length": 32, "nullable": "no", "default": "structure"}, {"name": "chapter_no", "title": "章节号", "type": "str", "length": 32}, {"name": "chapter_title", "title": "章节标题", "type": "str", "length": 255}, {"name": "requirement", "title": "要求内容", "type": "text"}, {"name": "page_limit", "title": "页数限制", "type": "int"}, {"name": "source_ref", "title": "招标文件出处", "type": "str", "length": 500}, {"name": "order_no", "title": "排序", "type": "int", "default": "0"}, {"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_doc_requirements', 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"
}
}

View File

@ -0,0 +1,41 @@
ns = params_kw.copy()
for k,v in ns.items():
if v == 'NaN' or v == 'null':
ns[k] = None
id = params_kw.id
if not id or len(id) > 32:
id = uuid()
ns['id'] = id
db = DBPools()
dbname = get_module_dbname('pipeline_bidding')
async with db.sqlorContext(dbname) as sor:
r = await sor.C('bid_documents', ns.copy())
_new_rows = await sor.sqlExe("SELECT * FROM bid_documents WHERE id = ${id}$", {'id': id})
_user_data = _new_rows[0] if _new_rows else ns
return {
"widgettype":"Message",
"options":{
"cwidth":16,
"cheight":9,
"title":"Add Success",
"timeout":3,
"message":"ok",
"user_data": _user_data
}
}
return {
"widgettype":"Error",
"options":{
"title":"Add Error",
"cwidth":16,
"cheight":9,
"timeout":3,
"message":"failed"
}
}

View File

@ -0,0 +1,33 @@
ns = {
'id':params_kw['id'],
}
db = DBPools()
dbname = get_module_dbname('pipeline_bidding')
async with db.sqlorContext(dbname) as sor:
r = await sor.D('bid_documents', ns)
debug('delete success');
return {
"widgettype":"Message",
"options":{
"title":"Delete Success",
"timeout":3,
"cwidth":16,
"cheight":9,
"message":"ok"
}
}
debug('Delete failed');
return {
"widgettype":"Error",
"options":{
"title":"Delete Error",
"timeout":3,
"cwidth":16,
"cheight":9,
"message":"failed"
}
}

View File

@ -0,0 +1,49 @@
ns = params_kw.copy()
debug(f'get_bid_documents.dspy:{ns=}')
if not ns.get('page'):
ns['page'] = 1
if not ns.get('sort'):
ns['sort'] = json.loads('["version desc"]')
sql = '''select a.*, b.status_text
from (select * from bid_documents where 1=1 [[filterstr]]) a left join (select k as status,
v as status_text from appcodes_kv where parentid='bid_doc_status') b on a.status = b.status'''
filterjson = json.loads('{"AND": [{"field": "project_id", "op": "=", "var": "project_id"}, {"field": "status", "op": "=", "var": "status_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":[]
}

View File

@ -0,0 +1,289 @@
{
"widgettype":"VBox",
"options":{"height":"100%","width":"100%"},
"subwidgets":[
{
"id":"bid_documents_tbl",
"widgettype":"Tabular",
"options":{
"width":"100%",
"height":"100%",
"title":"合成标书",
"toolbar":{
"tools": []
},
"css":"card",
"editable":{
"new_data_url": "{{entire_url('add_bid_documents.dspy')}}",
"delete_data_url": "{{entire_url('delete_bid_documents.dspy')}}",
"update_data_url": "{{entire_url('update_bid_documents.dspy')}}"
},
"data_url":"{{entire_url('./get_bid_documents.dspy')}}",
"data_method":"GET",
"data_params":{{json.dumps(params_kw, indent=4, ensure_ascii=False)}},
"row_options":{
"browserfields": {
"exclouded": []
},
"editexclouded":[
"id",
"created_at",
"updated_at",
"task_id",
"review_id"
],
"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": "version",
"title": "版本号",
"type": "int",
"nullable": "no",
"default": "1",
"length": 0,
"uitype": "int",
"datatype": "int",
"label": "版本号"
},
{
"name": "doc_name",
"title": "标书名称",
"type": "str",
"length": 255,
"cwidth": 18,
"uitype": "str",
"datatype": "str",
"label": "标书名称"
},
{
"name": "file_path",
"title": "文件路径",
"type": "str",
"length": 500,
"cwidth": 18,
"uitype": "str",
"datatype": "str",
"label": "文件路径"
},
{
"name": "page_count",
"title": "页数",
"type": "int",
"default": "0",
"length": 0,
"uitype": "int",
"datatype": "int",
"label": "页数"
},
{
"name": "chapter_count",
"title": "章节数",
"type": "int",
"default": "0",
"length": 0,
"uitype": "int",
"datatype": "int",
"label": "章节数"
},
{
"name": "total_score",
"title": "总得分",
"type": "float",
"length": 8,
"dec": 2,
"cwidth": 8,
"uitype": "float",
"datatype": "float",
"label": "总得分"
},
{
"name": "max_score",
"title": "总满分",
"type": "float",
"length": 8,
"dec": 2,
"cwidth": 8,
"uitype": "float",
"datatype": "float",
"label": "总满分"
},
{
"name": "pass_ratio",
"title": "通过门限",
"type": "float",
"length": 5,
"dec": 4,
"cwidth": 5,
"uitype": "float",
"datatype": "float",
"label": "通过门限"
},
{
"name": "status",
"title": "状态",
"type": "str",
"length": 16,
"nullable": "no",
"default": "draft",
"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_doc_status'"
},
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}"
},
{
"name": "review_id",
"title": "评审记录ID",
"type": "str",
"length": 32,
"cwidth": 18,
"uitype": "str",
"datatype": "str",
"label": "评审记录ID"
},
{
"name": "task_id",
"title": "关联任务ID",
"type": "str",
"length": 32,
"cwidth": 18,
"uitype": "str",
"datatype": "str",
"label": "关联任务ID"
},
{
"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": "project_id",
"uitype": "str",
"placeholder": "项目ID",
"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_doc_status'"
},
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}?prepend_all=1"
}
]
},
"page_rows":160,
"cache_limit":5
}
,"binds":[]
}]
}

View File

@ -0,0 +1,44 @@
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": "project_id", "title": "项目ID", "type": "str", "length": 32, "nullable": "no"}, {"name": "version", "title": "版本号", "type": "int", "nullable": "no", "default": "1"}, {"name": "doc_name", "title": "标书名称", "type": "str", "length": 255}, {"name": "file_path", "title": "文件路径", "type": "str", "length": 500}, {"name": "page_count", "title": "页数", "type": "int", "default": "0"}, {"name": "chapter_count", "title": "章节数", "type": "int", "default": "0"}, {"name": "total_score", "title": "总得分", "type": "float", "length": 8, "dec": 2}, {"name": "max_score", "title": "总满分", "type": "float", "length": 8, "dec": 2}, {"name": "pass_ratio", "title": "通过门限", "type": "float", "length": 5, "dec": 4}, {"name": "status", "title": "状态", "type": "str", "length": 16, "nullable": "no", "default": "draft"}, {"name": "review_id", "title": "评审记录ID", "type": "str", "length": 32}, {"name": "task_id", "title": "关联任务ID", "type": "str", "length": 32}, {"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_documents', 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"
}
}

View File

@ -0,0 +1,41 @@
ns = params_kw.copy()
for k,v in ns.items():
if v == 'NaN' or v == 'null':
ns[k] = None
id = params_kw.id
if not id or len(id) > 32:
id = uuid()
ns['id'] = id
db = DBPools()
dbname = get_module_dbname('pipeline_bidding')
async with db.sqlorContext(dbname) as sor:
r = await sor.C('bid_kb_docs', ns.copy())
_new_rows = await sor.sqlExe("SELECT * FROM bid_kb_docs WHERE id = ${id}$", {'id': id})
_user_data = _new_rows[0] if _new_rows else ns
return {
"widgettype":"Message",
"options":{
"cwidth":16,
"cheight":9,
"title":"Add Success",
"timeout":3,
"message":"ok",
"user_data": _user_data
}
}
return {
"widgettype":"Error",
"options":{
"title":"Add Error",
"cwidth":16,
"cheight":9,
"timeout":3,
"message":"failed"
}
}

View File

@ -0,0 +1,33 @@
ns = {
'id':params_kw['id'],
}
db = DBPools()
dbname = get_module_dbname('pipeline_bidding')
async with db.sqlorContext(dbname) as sor:
r = await sor.D('bid_kb_docs', ns)
debug('delete success');
return {
"widgettype":"Message",
"options":{
"title":"Delete Success",
"timeout":3,
"cwidth":16,
"cheight":9,
"message":"ok"
}
}
debug('Delete failed');
return {
"widgettype":"Error",
"options":{
"title":"Delete Error",
"timeout":3,
"cwidth":16,
"cheight":9,
"message":"failed"
}
}

View File

@ -0,0 +1,49 @@
ns = params_kw.copy()
debug(f'get_bid_kb_docs.dspy:{ns=}')
if not ns.get('page'):
ns['page'] = 1
if not ns.get('sort'):
ns['sort'] = json.loads('["doc_type", "created_at desc"]')
sql = '''select a.*, b.doc_type_text
from (select * from bid_kb_docs where 1=1 [[filterstr]]) a left join (select k as doc_type,
v as doc_type_text from appcodes_kv where parentid='bid_kb_doc_type') b on a.doc_type = b.doc_type'''
filterjson = json.loads('{"AND": [{"field": "doc_name", "op": "LIKE", "var": "name_input"}, {"field": "doc_type", "op": "=", "var": "type_input"}, {"field": "status", "op": "=", "var": "status_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":[]
}

View File

@ -0,0 +1,302 @@
{
"widgettype":"VBox",
"options":{"height":"100%","width":"100%"},
"subwidgets":[
{
"id":"bid_kb_docs_tbl",
"widgettype":"Tabular",
"options":{
"width":"100%",
"height":"100%",
"title":"公司投标知识库",
"toolbar":{
"tools": []
},
"css":"card",
"editable":{
"new_data_url": "{{entire_url('add_bid_kb_docs.dspy')}}",
"delete_data_url": "{{entire_url('delete_bid_kb_docs.dspy')}}",
"update_data_url": "{{entire_url('update_bid_kb_docs.dspy')}}"
},
"data_url":"{{entire_url('./get_bid_kb_docs.dspy')}}",
"data_method":"GET",
"data_params":{{json.dumps(params_kw, indent=4, ensure_ascii=False)}},
"row_options":{
"browserfields": {
"exclouded": [
"content_text"
]
},
"editexclouded":[
"id",
"created_at",
"updated_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": "doc_type",
"title": "文档类型",
"type": "str",
"length": 32,
"nullable": "no",
"default": "qualification",
"label": "文档类型",
"uitype": "code",
"valueField": "doc_type",
"textField": "doc_type_text",
"params": {
"dbname": "{{get_module_dbname('pipeline_bidding')}}",
"table": "appcodes_kv",
"tblvalue": "k",
"tbltext": "v",
"valueField": "doc_type",
"textField": "doc_type_text",
"cond": "parentid='bid_kb_doc_type'"
},
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}"
},
{
"name": "doc_name",
"title": "文档名称",
"type": "str",
"length": 255,
"nullable": "no",
"cwidth": 18,
"uitype": "str",
"datatype": "str",
"label": "文档名称"
},
{
"name": "doc_no",
"title": "证书/合同编号",
"type": "str",
"length": 64,
"cwidth": 18,
"uitype": "str",
"datatype": "str",
"label": "证书/合同编号"
},
{
"name": "tags",
"title": "标签(逗号分隔)",
"type": "str",
"length": 500,
"cwidth": 18,
"uitype": "str",
"datatype": "str",
"label": "标签(逗号分隔)"
},
{
"name": "file_path",
"title": "文件路径",
"type": "str",
"length": 500,
"cwidth": 18,
"uitype": "str",
"datatype": "str",
"label": "文件路径"
},
{
"name": "content_text",
"title": "正文内容",
"type": "text",
"length": 0,
"uitype": "text",
"datatype": "text",
"label": "正文内容"
},
{
"name": "issuer",
"title": "颁发/签约方",
"type": "str",
"length": 255,
"cwidth": 18,
"uitype": "str",
"datatype": "str",
"label": "颁发/签约方"
},
{
"name": "customer",
"title": "案例客户",
"type": "str",
"length": 255,
"cwidth": 18,
"uitype": "str",
"datatype": "str",
"label": "案例客户"
},
{
"name": "contract_amount",
"title": "合同金额",
"type": "float",
"length": 18,
"dec": 2,
"cwidth": 18,
"uitype": "float",
"datatype": "float",
"label": "合同金额"
},
{
"name": "valid_from",
"title": "生效日期",
"type": "date",
"length": 0,
"uitype": "date",
"datatype": "date",
"label": "生效日期"
},
{
"name": "valid_until",
"title": "有效期至",
"type": "date",
"length": 0,
"uitype": "date",
"datatype": "date",
"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": "type_input",
"uitype": "code",
"placeholder": "文档类型",
"cwidth": 15,
"valueField": "type_input",
"textField": "type_input_text",
"params": {
"dbname": "{{get_module_dbname('pipeline_bidding')}}",
"table": "appcodes_kv",
"tblvalue": "k",
"tbltext": "v",
"valueField": "type_input",
"textField": "type_input_text",
"cond": "parentid='bid_kb_doc_type'"
},
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}?prepend_all=1"
},
{
"name": "status_input",
"uitype": "str",
"placeholder": "状态",
"cwidth": 15
}
]
},
"page_rows":160,
"cache_limit":5
}
,"binds":[]
}]
}

View File

@ -0,0 +1,44 @@
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": "doc_type", "title": "文档类型", "type": "str", "length": 32, "nullable": "no", "default": "qualification"}, {"name": "doc_name", "title": "文档名称", "type": "str", "length": 255, "nullable": "no"}, {"name": "doc_no", "title": "证书/合同编号", "type": "str", "length": 64}, {"name": "tags", "title": "标签(逗号分隔)", "type": "str", "length": 500}, {"name": "file_path", "title": "文件路径", "type": "str", "length": 500}, {"name": "content_text", "title": "正文内容", "type": "text"}, {"name": "issuer", "title": "颁发/签约方", "type": "str", "length": 255}, {"name": "customer", "title": "案例客户", "type": "str", "length": 255}, {"name": "contract_amount", "title": "合同金额", "type": "float", "length": 18, "dec": 2}, {"name": "valid_from", "title": "生效日期", "type": "date"}, {"name": "valid_until", "title": "有效期至", "type": "date"}, {"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_kb_docs', 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"
}
}

View File

@ -0,0 +1,41 @@
ns = params_kw.copy()
for k,v in ns.items():
if v == 'NaN' or v == 'null':
ns[k] = None
id = params_kw.id
if not id or len(id) > 32:
id = uuid()
ns['id'] = id
db = DBPools()
dbname = get_module_dbname('pipeline_bidding')
async with db.sqlorContext(dbname) as sor:
r = await sor.C('bid_mail_accounts', ns.copy())
_new_rows = await sor.sqlExe("SELECT * FROM bid_mail_accounts WHERE id = ${id}$", {'id': id})
_user_data = _new_rows[0] if _new_rows else ns
return {
"widgettype":"Message",
"options":{
"cwidth":16,
"cheight":9,
"title":"Add Success",
"timeout":3,
"message":"ok",
"user_data": _user_data
}
}
return {
"widgettype":"Error",
"options":{
"title":"Add Error",
"cwidth":16,
"cheight":9,
"timeout":3,
"message":"failed"
}
}

View File

@ -0,0 +1,33 @@
ns = {
'id':params_kw['id'],
}
db = DBPools()
dbname = get_module_dbname('pipeline_bidding')
async with db.sqlorContext(dbname) as sor:
r = await sor.D('bid_mail_accounts', ns)
debug('delete success');
return {
"widgettype":"Message",
"options":{
"title":"Delete Success",
"timeout":3,
"cwidth":16,
"cheight":9,
"message":"ok"
}
}
debug('Delete failed');
return {
"widgettype":"Error",
"options":{
"title":"Delete Error",
"timeout":3,
"cwidth":16,
"cheight":9,
"message":"failed"
}
}

View File

@ -0,0 +1,47 @@
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":[]
}

View File

@ -0,0 +1,307 @@
{
"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":[]
}]
}

View File

@ -0,0 +1,44 @@
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"
}
}

View File

@ -0,0 +1,41 @@
ns = params_kw.copy()
for k,v in ns.items():
if v == 'NaN' or v == 'null':
ns[k] = None
id = params_kw.id
if not id or len(id) > 32:
id = uuid()
ns['id'] = id
db = DBPools()
dbname = get_module_dbname('pipeline_bidding')
async with db.sqlorContext(dbname) as sor:
r = await sor.C('bid_members', ns.copy())
_new_rows = await sor.sqlExe("SELECT * FROM bid_members WHERE id = ${id}$", {'id': id})
_user_data = _new_rows[0] if _new_rows else ns
return {
"widgettype":"Message",
"options":{
"cwidth":16,
"cheight":9,
"title":"Add Success",
"timeout":3,
"message":"ok",
"user_data": _user_data
}
}
return {
"widgettype":"Error",
"options":{
"title":"Add Error",
"cwidth":16,
"cheight":9,
"timeout":3,
"message":"failed"
}
}

View File

@ -0,0 +1,33 @@
ns = {
'id':params_kw['id'],
}
db = DBPools()
dbname = get_module_dbname('pipeline_bidding')
async with db.sqlorContext(dbname) as sor:
r = await sor.D('bid_members', ns)
debug('delete success');
return {
"widgettype":"Message",
"options":{
"title":"Delete Success",
"timeout":3,
"cwidth":16,
"cheight":9,
"message":"ok"
}
}
debug('Delete failed');
return {
"widgettype":"Error",
"options":{
"title":"Delete Error",
"timeout":3,
"cwidth":16,
"cheight":9,
"message":"failed"
}
}

View File

@ -0,0 +1,49 @@
ns = params_kw.copy()
debug(f'get_bid_members.dspy:{ns=}')
if not ns.get('page'):
ns['page'] = 1
if not ns.get('sort'):
ns['sort'] = json.loads('["created_at"]')
sql = '''select a.*, b.member_role_text
from (select * from bid_members where 1=1 [[filterstr]]) a left join (select k as member_role,
v as member_role_text from appcodes_kv where parentid='bid_member_role') b on a.member_role = b.member_role'''
filterjson = json.loads('{"AND": [{"field": "project_id", "op": "=", "var": "project_id"}, {"field": "user_name", "op": "LIKE", "var": "name_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":[]
}

View File

@ -0,0 +1,212 @@
{
"widgettype":"VBox",
"options":{"height":"100%","width":"100%"},
"subwidgets":[
{
"id":"bid_members_tbl",
"widgettype":"Tabular",
"options":{
"width":"100%",
"height":"100%",
"title":"投标项目参与人员",
"toolbar":{
"tools": []
},
"css":"card",
"editable":{
"new_data_url": "{{entire_url('add_bid_members.dspy')}}",
"delete_data_url": "{{entire_url('delete_bid_members.dspy')}}",
"update_data_url": "{{entire_url('update_bid_members.dspy')}}"
},
"data_url":"{{entire_url('./get_bid_members.dspy')}}",
"data_method":"GET",
"data_params":{{json.dumps(params_kw, indent=4, ensure_ascii=False)}},
"row_options":{
"browserfields": {
"exclouded": []
},
"editexclouded":[
"id",
"created_at",
"updated_at"
],
"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": "user_id",
"title": "用户ID",
"type": "str",
"length": 32,
"cwidth": 18,
"uitype": "str",
"datatype": "str",
"label": "用户ID"
},
{
"name": "user_name",
"title": "姓名",
"type": "str",
"length": 128,
"cwidth": 18,
"uitype": "str",
"datatype": "str",
"label": "姓名"
},
{
"name": "member_role",
"title": "承担角色",
"type": "str",
"length": 64,
"nullable": "no",
"default": "writer",
"label": "承担角色",
"uitype": "code",
"valueField": "member_role",
"textField": "member_role_text",
"params": {
"dbname": "{{get_module_dbname('pipeline_bidding')}}",
"table": "appcodes_kv",
"tblvalue": "k",
"tbltext": "v",
"valueField": "member_role",
"textField": "member_role_text",
"cond": "parentid='bid_member_role'"
},
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}"
},
{
"name": "duty",
"title": "职责说明",
"type": "str",
"length": 500,
"cwidth": 18,
"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": "project_id",
"uitype": "str",
"placeholder": "项目ID",
"cwidth": 15
},
{
"name": "name_input",
"uitype": "str",
"placeholder": "姓名",
"cwidth": 15
}
]
},
"page_rows":160,
"cache_limit":5
}
,"binds":[]
}]
}

View File

@ -0,0 +1,44 @@
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": "project_id", "title": "项目ID", "type": "str", "length": 32, "nullable": "no"}, {"name": "user_id", "title": "用户ID", "type": "str", "length": 32}, {"name": "user_name", "title": "姓名", "type": "str", "length": 128}, {"name": "member_role", "title": "承担角色", "type": "str", "length": 64, "nullable": "no", "default": "writer"}, {"name": "duty", "title": "职责说明", "type": "str", "length": 500}, {"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_members', 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"
}
}

View File

@ -0,0 +1,41 @@
ns = params_kw.copy()
for k,v in ns.items():
if v == 'NaN' or v == 'null':
ns[k] = None
id = params_kw.id
if not id or len(id) > 32:
id = uuid()
ns['id'] = id
db = DBPools()
dbname = get_module_dbname('pipeline_bidding')
async with db.sqlorContext(dbname) as sor:
r = await sor.C('bid_qualifications', ns.copy())
_new_rows = await sor.sqlExe("SELECT * FROM bid_qualifications WHERE id = ${id}$", {'id': id})
_user_data = _new_rows[0] if _new_rows else ns
return {
"widgettype":"Message",
"options":{
"cwidth":16,
"cheight":9,
"title":"Add Success",
"timeout":3,
"message":"ok",
"user_data": _user_data
}
}
return {
"widgettype":"Error",
"options":{
"title":"Add Error",
"cwidth":16,
"cheight":9,
"timeout":3,
"message":"failed"
}
}

View File

@ -0,0 +1,33 @@
ns = {
'id':params_kw['id'],
}
db = DBPools()
dbname = get_module_dbname('pipeline_bidding')
async with db.sqlorContext(dbname) as sor:
r = await sor.D('bid_qualifications', ns)
debug('delete success');
return {
"widgettype":"Message",
"options":{
"title":"Delete Success",
"timeout":3,
"cwidth":16,
"cheight":9,
"message":"ok"
}
}
debug('Delete failed');
return {
"widgettype":"Error",
"options":{
"title":"Delete Error",
"timeout":3,
"cwidth":16,
"cheight":9,
"message":"failed"
}
}

View File

@ -0,0 +1,50 @@
ns = params_kw.copy()
debug(f'get_bid_qualifications.dspy:{ns=}')
if not ns.get('page'):
ns['page'] = 1
if not ns.get('sort'):
ns['sort'] = json.loads('["created_at"]')
sql = '''select a.*, b.match_status_text, c.owner_text
from (select * from bid_qualifications where 1=1 [[filterstr]]) a left join (select k as match_status,
v as match_status_text from appcodes_kv where parentid='bid_qual_match_status') b on a.match_status = b.match_status left join (select k as owner,
v as owner_text from appcodes_kv where parentid='bid_qual_owner') c on a.owner = c.owner'''
filterjson = json.loads('{"AND": [{"field": "project_id", "op": "=", "var": "project_id"}, {"field": "match_status", "op": "=", "var": "status_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":[]
}

View File

@ -0,0 +1,268 @@
{
"widgettype":"VBox",
"options":{"height":"100%","width":"100%"},
"subwidgets":[
{
"id":"bid_qualifications_tbl",
"widgettype":"Tabular",
"options":{
"width":"100%",
"height":"100%",
"title":"招标所需资质",
"toolbar":{
"tools": []
},
"css":"card",
"editable":{
"new_data_url": "{{entire_url('add_bid_qualifications.dspy')}}",
"delete_data_url": "{{entire_url('delete_bid_qualifications.dspy')}}",
"update_data_url": "{{entire_url('update_bid_qualifications.dspy')}}"
},
"data_url":"{{entire_url('./get_bid_qualifications.dspy')}}",
"data_method":"GET",
"data_params":{{json.dumps(params_kw, indent=4, ensure_ascii=False)}},
"row_options":{
"browserfields": {
"exclouded": []
},
"editexclouded":[
"id",
"created_at",
"updated_at",
"human_task_id"
],
"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": "qual_name",
"title": "资质名称",
"type": "str",
"length": 255,
"nullable": "no",
"cwidth": 18,
"uitype": "str",
"datatype": "str",
"label": "资质名称"
},
{
"name": "requirement",
"title": "资质要求原文",
"type": "text",
"length": 0,
"uitype": "text",
"datatype": "text",
"label": "资质要求原文"
},
{
"name": "is_mandatory",
"title": "是否必需",
"type": "str",
"length": 1,
"nullable": "no",
"default": "1",
"cwidth": 4,
"uitype": "str",
"datatype": "str",
"label": "是否必需"
},
{
"name": "owner",
"title": "提供方",
"type": "str",
"length": 16,
"nullable": "no",
"default": "kb",
"label": "提供方",
"uitype": "code",
"valueField": "owner",
"textField": "owner_text",
"params": {
"dbname": "{{get_module_dbname('pipeline_bidding')}}",
"table": "appcodes_kv",
"tblvalue": "k",
"tbltext": "v",
"valueField": "owner",
"textField": "owner_text",
"cond": "parentid='bid_qual_owner'"
},
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}"
},
{
"name": "kb_doc_id",
"title": "知识库文档ID",
"type": "str",
"length": 32,
"cwidth": 18,
"uitype": "str",
"datatype": "str",
"label": "知识库文档ID"
},
{
"name": "match_status",
"title": "匹配状态",
"type": "str",
"length": 16,
"nullable": "no",
"default": "pending",
"label": "匹配状态",
"uitype": "code",
"valueField": "match_status",
"textField": "match_status_text",
"params": {
"dbname": "{{get_module_dbname('pipeline_bidding')}}",
"table": "appcodes_kv",
"tblvalue": "k",
"tbltext": "v",
"valueField": "match_status",
"textField": "match_status_text",
"cond": "parentid='bid_qual_match_status'"
},
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}"
},
{
"name": "file_path",
"title": "文档路径",
"type": "str",
"length": 500,
"cwidth": 18,
"uitype": "str",
"datatype": "str",
"label": "文档路径"
},
{
"name": "note",
"title": "说明",
"type": "str",
"length": 500,
"cwidth": 18,
"uitype": "str",
"datatype": "str",
"label": "说明"
},
{
"name": "human_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": "更新时间"
}
]
},
"search_form":{
"css":"card",
"padding":"8px",
"show_label":false,
"submit_label":"搜索",
"submit_css":"primary",
"fields":[
{
"name": "project_id",
"uitype": "str",
"placeholder": "项目ID",
"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_qual_match_status'"
},
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}?prepend_all=1"
}
]
},
"page_rows":160,
"cache_limit":5
}
,"binds":[]
}]
}

View File

@ -0,0 +1,44 @@
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": "project_id", "title": "项目ID", "type": "str", "length": 32, "nullable": "no"}, {"name": "qual_name", "title": "资质名称", "type": "str", "length": 255, "nullable": "no"}, {"name": "requirement", "title": "资质要求原文", "type": "text"}, {"name": "is_mandatory", "title": "是否必需", "type": "str", "length": 1, "nullable": "no", "default": "1"}, {"name": "owner", "title": "提供方", "type": "str", "length": 16, "nullable": "no", "default": "kb"}, {"name": "kb_doc_id", "title": "知识库文档ID", "type": "str", "length": 32}, {"name": "match_status", "title": "匹配状态", "type": "str", "length": 16, "nullable": "no", "default": "pending"}, {"name": "file_path", "title": "文档路径", "type": "str", "length": 500}, {"name": "note", "title": "说明", "type": "str", "length": 500}, {"name": "human_task_id", "title": "人类任务ID", "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_qualifications', 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"
}
}

View File

@ -0,0 +1,41 @@
ns = params_kw.copy()
for k,v in ns.items():
if v == 'NaN' or v == 'null':
ns[k] = None
id = params_kw.id
if not id or len(id) > 32:
id = uuid()
ns['id'] = id
db = DBPools()
dbname = get_module_dbname('pipeline_bidding')
async with db.sqlorContext(dbname) as sor:
r = await sor.C('bid_reviews', ns.copy())
_new_rows = await sor.sqlExe("SELECT * FROM bid_reviews WHERE id = ${id}$", {'id': id})
_user_data = _new_rows[0] if _new_rows else ns
return {
"widgettype":"Message",
"options":{
"cwidth":16,
"cheight":9,
"title":"Add Success",
"timeout":3,
"message":"ok",
"user_data": _user_data
}
}
return {
"widgettype":"Error",
"options":{
"title":"Add Error",
"cwidth":16,
"cheight":9,
"timeout":3,
"message":"failed"
}
}

View File

@ -0,0 +1,33 @@
ns = {
'id':params_kw['id'],
}
db = DBPools()
dbname = get_module_dbname('pipeline_bidding')
async with db.sqlorContext(dbname) as sor:
r = await sor.D('bid_reviews', ns)
debug('delete success');
return {
"widgettype":"Message",
"options":{
"title":"Delete Success",
"timeout":3,
"cwidth":16,
"cheight":9,
"message":"ok"
}
}
debug('Delete failed');
return {
"widgettype":"Error",
"options":{
"title":"Delete Error",
"timeout":3,
"cwidth":16,
"cheight":9,
"message":"failed"
}
}

View File

@ -0,0 +1,49 @@
ns = params_kw.copy()
debug(f'get_bid_reviews.dspy:{ns=}')
if not ns.get('page'):
ns['page'] = 1
if not ns.get('sort'):
ns['sort'] = json.loads('["created_at desc"]')
sql = '''select a.*, b.scope_text
from (select * from bid_reviews where 1=1 [[filterstr]]) a left join (select k as scope,
v as scope_text from appcodes_kv where parentid='bid_review_scope') b on a.scope = b.scope'''
filterjson = json.loads('{"AND": [{"field": "project_id", "op": "=", "var": "project_id"}, {"field": "scope", "op": "=", "var": "scope_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":[]
}

View File

@ -0,0 +1,338 @@
{
"widgettype":"VBox",
"options":{"height":"100%","width":"100%"},
"subwidgets":[
{
"id":"bid_reviews_tbl",
"widgettype":"Tabular",
"options":{
"width":"100%",
"height":"100%",
"title":"标书评审记录",
"toolbar":{
"tools": [
{
"name": "filter",
"label": "搜索"
},
{
"selected_row": true,
"name": "bid_scores",
"icon": "{{entire_url('/imgs/bid_scores.svg')}}",
"label": "评分明细"
}
]
},
"css":"card",
"editable":{
"new_data_url": "{{entire_url('add_bid_reviews.dspy')}}",
"delete_data_url": "{{entire_url('delete_bid_reviews.dspy')}}",
"update_data_url": "{{entire_url('update_bid_reviews.dspy')}}"
},
"data_url":"{{entire_url('./get_bid_reviews.dspy')}}",
"data_method":"GET",
"data_params":{{json.dumps(params_kw, indent=4, ensure_ascii=False)}},
"row_options":{
"browserfields": {
"exclouded": [
"detail"
]
},
"editexclouded":[
"id",
"created_at",
"updated_at",
"task_id"
],
"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": "scope",
"title": "评审范围",
"type": "str",
"length": 16,
"nullable": "no",
"default": "chapter",
"label": "评审范围",
"uitype": "code",
"valueField": "scope",
"textField": "scope_text",
"params": {
"dbname": "{{get_module_dbname('pipeline_bidding')}}",
"table": "appcodes_kv",
"tblvalue": "k",
"tbltext": "v",
"valueField": "scope",
"textField": "scope_text",
"cond": "parentid='bid_review_scope'"
},
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}"
},
{
"name": "chapter_id",
"title": "章节ID",
"type": "str",
"length": 32,
"cwidth": 18,
"uitype": "str",
"datatype": "str",
"label": "章节ID"
},
{
"name": "document_id",
"title": "标书ID",
"type": "str",
"length": 32,
"cwidth": 18,
"uitype": "str",
"datatype": "str",
"label": "标书ID"
},
{
"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": "score",
"title": "得分",
"type": "float",
"length": 8,
"dec": 2,
"cwidth": 8,
"uitype": "float",
"datatype": "float",
"label": "得分"
},
{
"name": "max_score",
"title": "满分",
"type": "float",
"length": 8,
"dec": 2,
"cwidth": 8,
"uitype": "float",
"datatype": "float",
"label": "满分"
},
{
"name": "pass_ratio",
"title": "通过门限",
"type": "float",
"length": 5,
"dec": 4,
"cwidth": 5,
"uitype": "float",
"datatype": "float",
"label": "通过门限"
},
{
"name": "passed",
"title": "是否通过",
"type": "str",
"length": 1,
"nullable": "no",
"default": "0",
"cwidth": 4,
"uitype": "str",
"datatype": "str",
"label": "是否通过"
},
{
"name": "comments",
"title": "改进意见",
"type": "text",
"length": 0,
"uitype": "text",
"datatype": "text",
"label": "改进意见"
},
{
"name": "detail",
"title": "评审明细(JSON)",
"type": "text",
"length": 0,
"uitype": "text",
"datatype": "text",
"label": "评审明细(JSON)"
},
{
"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": "更新时间"
}
]
},
"search_form":{
"css":"card",
"padding":"8px",
"show_label":false,
"submit_label":"搜索",
"submit_css":"primary",
"fields":[
{
"name": "project_id",
"uitype": "str",
"placeholder": "项目ID",
"cwidth": 15
},
{
"name": "scope_input",
"uitype": "code",
"placeholder": "评审范围",
"cwidth": 15,
"valueField": "scope_input",
"textField": "scope_input_text",
"params": {
"dbname": "{{get_module_dbname('pipeline_bidding')}}",
"table": "appcodes_kv",
"tblvalue": "k",
"tbltext": "v",
"valueField": "scope_input",
"textField": "scope_input_text",
"cond": "parentid='bid_review_scope'"
},
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}?prepend_all=1"
}
]
},
"page_rows":160,
"cache_limit":5
}
,"binds":[
{
"wid": "self",
"event": "bid_scores",
"actiontype": "urlwidget",
"target": "PopupWindow",
"popup_options": {
"title": "评分明细",
"icon": "{{entire_url('/appbase/show_icon.dspy')}}?id=bid_scores",
"resizable": true,
"height": "70%",
"width": "70%"
},
"params_mapping": {
"mapping": {
"id": "review_id",
"referer_widget": "referer_widget"
},
"need_other": false
},
"options": {
"method": "POST",
"params": {},
"url": "{{entire_url('../bid_scores?review_id=${id}')}}"
}
}
]
}]
}

View File

@ -0,0 +1,44 @@
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": "project_id", "title": "项目ID", "type": "str", "length": 32, "nullable": "no"}, {"name": "scope", "title": "评审范围", "type": "str", "length": 16, "nullable": "no", "default": "chapter"}, {"name": "chapter_id", "title": "章节ID", "type": "str", "length": 32}, {"name": "document_id", "title": "标书ID", "type": "str", "length": 32}, {"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": "score", "title": "得分", "type": "float", "length": 8, "dec": 2}, {"name": "max_score", "title": "满分", "type": "float", "length": 8, "dec": 2}, {"name": "pass_ratio", "title": "通过门限", "type": "float", "length": 5, "dec": 4}, {"name": "passed", "title": "是否通过", "type": "str", "length": 1, "nullable": "no", "default": "0"}, {"name": "comments", "title": "改进意见", "type": "text"}, {"name": "detail", "title": "评审明细(JSON)", "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"}]')
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_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"
}
}

View File

@ -0,0 +1,41 @@
ns = params_kw.copy()
for k,v in ns.items():
if v == 'NaN' or v == 'null':
ns[k] = None
id = params_kw.id
if not id or len(id) > 32:
id = uuid()
ns['id'] = id
db = DBPools()
dbname = get_module_dbname('pipeline_bidding')
async with db.sqlorContext(dbname) as sor:
r = await sor.C('bid_scores', ns.copy())
_new_rows = await sor.sqlExe("SELECT * FROM bid_scores WHERE id = ${id}$", {'id': id})
_user_data = _new_rows[0] if _new_rows else ns
return {
"widgettype":"Message",
"options":{
"cwidth":16,
"cheight":9,
"title":"Add Success",
"timeout":3,
"message":"ok",
"user_data": _user_data
}
}
return {
"widgettype":"Error",
"options":{
"title":"Add Error",
"cwidth":16,
"cheight":9,
"timeout":3,
"message":"failed"
}
}

View File

@ -0,0 +1,33 @@
ns = {
'id':params_kw['id'],
}
db = DBPools()
dbname = get_module_dbname('pipeline_bidding')
async with db.sqlorContext(dbname) as sor:
r = await sor.D('bid_scores', ns)
debug('delete success');
return {
"widgettype":"Message",
"options":{
"title":"Delete Success",
"timeout":3,
"cwidth":16,
"cheight":9,
"message":"ok"
}
}
debug('Delete failed');
return {
"widgettype":"Error",
"options":{
"title":"Delete Error",
"timeout":3,
"cwidth":16,
"cheight":9,
"message":"failed"
}
}

View File

@ -0,0 +1,47 @@
ns = params_kw.copy()
debug(f'get_bid_scores.dspy:{ns=}')
if not ns.get('page'):
ns['page'] = 1
if not ns.get('sort'):
ns['sort'] = json.loads('["created_at"]')
sql = '''select * from bid_scores where 1=1 [[filterstr]]'''
filterjson = json.loads('{"AND": [{"field": "review_id", "op": "=", "var": "review_id"}, {"field": "project_id", "op": "=", "var": "project_id"}]}')
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":[]
}

219
wwwroot/bid_scores/index.ui Normal file
View File

@ -0,0 +1,219 @@
{
"widgettype":"VBox",
"options":{"height":"100%","width":"100%"},
"subwidgets":[
{
"id":"bid_scores_tbl",
"widgettype":"Tabular",
"options":{
"width":"100%",
"height":"100%",
"title":"标书评分明细",
"toolbar":{
"tools": []
},
"css":"card",
"editable":{
"new_data_url": "{{entire_url('add_bid_scores.dspy')}}",
"delete_data_url": "{{entire_url('delete_bid_scores.dspy')}}",
"update_data_url": "{{entire_url('update_bid_scores.dspy')}}"
},
"data_url":"{{entire_url('./get_bid_scores.dspy')}}",
"data_method":"GET",
"data_params":{{json.dumps(params_kw, indent=4, ensure_ascii=False)}},
"row_options":{
"browserfields": {
"exclouded": []
},
"editexclouded":[
"id",
"created_at",
"updated_at"
],
"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": "review_id",
"title": "评审记录ID",
"type": "str",
"length": 32,
"cwidth": 18,
"uitype": "str",
"datatype": "str",
"label": "评审记录ID"
},
{
"name": "scoring_item_id",
"title": "评分项ID",
"type": "str",
"length": 32,
"cwidth": 18,
"uitype": "str",
"datatype": "str",
"label": "评分项ID"
},
{
"name": "item_name",
"title": "评分项名称",
"type": "str",
"length": 255,
"cwidth": 18,
"uitype": "str",
"datatype": "str",
"label": "评分项名称"
},
{
"name": "score",
"title": "得分",
"type": "float",
"length": 8,
"dec": 2,
"cwidth": 8,
"uitype": "float",
"datatype": "float",
"label": "得分"
},
{
"name": "max_score",
"title": "满分",
"type": "float",
"length": 8,
"dec": 2,
"cwidth": 8,
"uitype": "float",
"datatype": "float",
"label": "满分"
},
{
"name": "reason",
"title": "评分理由",
"type": "text",
"length": 0,
"uitype": "text",
"datatype": "text",
"label": "评分理由"
},
{
"name": "improvement",
"title": "改进意见",
"type": "text",
"length": 0,
"uitype": "text",
"datatype": "text",
"label": "改进意见"
},
{
"name": "target_chapter_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": "更新时间"
}
]
},
"search_form":{
"css":"card",
"padding":"8px",
"show_label":false,
"submit_label":"搜索",
"submit_css":"primary",
"fields":[
{
"name": "review_id",
"uitype": "str",
"placeholder": "评审记录ID",
"cwidth": 15
},
{
"name": "project_id",
"uitype": "str",
"placeholder": "项目ID",
"cwidth": 15
}
]
},
"page_rows":160,
"cache_limit":5
}
,"binds":[]
}]
}

View File

@ -0,0 +1,44 @@
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": "project_id", "title": "项目ID", "type": "str", "length": 32, "nullable": "no"}, {"name": "review_id", "title": "评审记录ID", "type": "str", "length": 32}, {"name": "scoring_item_id", "title": "评分项ID", "type": "str", "length": 32}, {"name": "item_name", "title": "评分项名称", "type": "str", "length": 255}, {"name": "score", "title": "得分", "type": "float", "length": 8, "dec": 2}, {"name": "max_score", "title": "满分", "type": "float", "length": 8, "dec": 2}, {"name": "reason", "title": "评分理由", "type": "text"}, {"name": "improvement", "title": "改进意见", "type": "text"}, {"name": "target_chapter_id", "title": "待改章节ID", "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_scores', 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"
}
}

View File

@ -0,0 +1,41 @@
ns = params_kw.copy()
for k,v in ns.items():
if v == 'NaN' or v == 'null':
ns[k] = None
id = params_kw.id
if not id or len(id) > 32:
id = uuid()
ns['id'] = id
db = DBPools()
dbname = get_module_dbname('pipeline_bidding')
async with db.sqlorContext(dbname) as sor:
r = await sor.C('bid_scoring_items', ns.copy())
_new_rows = await sor.sqlExe("SELECT * FROM bid_scoring_items WHERE id = ${id}$", {'id': id})
_user_data = _new_rows[0] if _new_rows else ns
return {
"widgettype":"Message",
"options":{
"cwidth":16,
"cheight":9,
"title":"Add Success",
"timeout":3,
"message":"ok",
"user_data": _user_data
}
}
return {
"widgettype":"Error",
"options":{
"title":"Add Error",
"cwidth":16,
"cheight":9,
"timeout":3,
"message":"failed"
}
}

View File

@ -0,0 +1,33 @@
ns = {
'id':params_kw['id'],
}
db = DBPools()
dbname = get_module_dbname('pipeline_bidding')
async with db.sqlorContext(dbname) as sor:
r = await sor.D('bid_scoring_items', ns)
debug('delete success');
return {
"widgettype":"Message",
"options":{
"title":"Delete Success",
"timeout":3,
"cwidth":16,
"cheight":9,
"message":"ok"
}
}
debug('Delete failed');
return {
"widgettype":"Error",
"options":{
"title":"Delete Error",
"timeout":3,
"cwidth":16,
"cheight":9,
"message":"failed"
}
}

View File

@ -0,0 +1,49 @@
ns = params_kw.copy()
debug(f'get_bid_scoring_items.dspy:{ns=}')
if not ns.get('page'):
ns['page'] = 1
if not ns.get('sort'):
ns['sort'] = json.loads('["section", "order_no"]')
sql = '''select a.*, b.section_text
from (select * from bid_scoring_items where 1=1 [[filterstr]]) a left join (select k as section,
v as section_text from appcodes_kv where parentid='bid_scoring_section') b on a.section = b.section'''
filterjson = json.loads('{"AND": [{"field": "project_id", "op": "=", "var": "project_id"}, {"field": "section", "op": "=", "var": "section_input"}, {"field": "item_name", "op": "LIKE", "var": "name_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":[]
}

View File

@ -0,0 +1,275 @@
{
"widgettype":"VBox",
"options":{"height":"100%","width":"100%"},
"subwidgets":[
{
"id":"bid_scoring_items_tbl",
"widgettype":"Tabular",
"options":{
"width":"100%",
"height":"100%",
"title":"评分项与得分规则",
"toolbar":{
"tools": []
},
"css":"card",
"editable":{
"new_data_url": "{{entire_url('add_bid_scoring_items.dspy')}}",
"delete_data_url": "{{entire_url('delete_bid_scoring_items.dspy')}}",
"update_data_url": "{{entire_url('update_bid_scoring_items.dspy')}}"
},
"data_url":"{{entire_url('./get_bid_scoring_items.dspy')}}",
"data_method":"GET",
"data_params":{{json.dumps(params_kw, indent=4, ensure_ascii=False)}},
"row_options":{
"browserfields": {
"exclouded": []
},
"editexclouded":[
"id",
"created_at",
"updated_at"
],
"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": "section",
"title": "评分部分",
"type": "str",
"length": 32,
"nullable": "no",
"default": "technical",
"label": "评分部分",
"uitype": "code",
"valueField": "section",
"textField": "section_text",
"params": {
"dbname": "{{get_module_dbname('pipeline_bidding')}}",
"table": "appcodes_kv",
"tblvalue": "k",
"tbltext": "v",
"valueField": "section",
"textField": "section_text",
"cond": "parentid='bid_scoring_section'"
},
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}"
},
{
"name": "item_no",
"title": "评分项编号",
"type": "str",
"length": 32,
"cwidth": 18,
"uitype": "str",
"datatype": "str",
"label": "评分项编号"
},
{
"name": "item_name",
"title": "评分项名称",
"type": "str",
"length": 255,
"nullable": "no",
"cwidth": 18,
"uitype": "str",
"datatype": "str",
"label": "评分项名称"
},
{
"name": "max_score",
"title": "满分",
"type": "float",
"length": 8,
"dec": 2,
"nullable": "no",
"default": "0",
"cwidth": 8,
"uitype": "float",
"datatype": "float",
"label": "满分"
},
{
"name": "scoring_rule",
"title": "得分规则",
"type": "text",
"length": 0,
"uitype": "text",
"datatype": "text",
"label": "得分规则"
},
{
"name": "is_veto",
"title": "否决项",
"type": "str",
"length": 1,
"nullable": "no",
"default": "0",
"cwidth": 4,
"uitype": "str",
"datatype": "str",
"label": "否决项"
},
{
"name": "weight",
"title": "权重",
"type": "float",
"length": 8,
"dec": 4,
"cwidth": 8,
"uitype": "float",
"datatype": "float",
"label": "权重"
},
{
"name": "source_ref",
"title": "招标文件出处",
"type": "str",
"length": 500,
"cwidth": 18,
"uitype": "str",
"datatype": "str",
"label": "招标文件出处"
},
{
"name": "target_chapter_no",
"title": "对应章节号",
"type": "str",
"length": 32,
"cwidth": 18,
"uitype": "str",
"datatype": "str",
"label": "对应章节号"
},
{
"name": "order_no",
"title": "排序",
"type": "int",
"default": "0",
"length": 0,
"uitype": "int",
"datatype": "int",
"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": "project_id",
"uitype": "str",
"placeholder": "项目ID",
"cwidth": 15
},
{
"name": "section_input",
"uitype": "code",
"placeholder": "评分部分",
"cwidth": 15,
"valueField": "section_input",
"textField": "section_input_text",
"params": {
"dbname": "{{get_module_dbname('pipeline_bidding')}}",
"table": "appcodes_kv",
"tblvalue": "k",
"tbltext": "v",
"valueField": "section_input",
"textField": "section_input_text",
"cond": "parentid='bid_scoring_section'"
},
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}?prepend_all=1"
},
{
"name": "name_input",
"uitype": "str",
"placeholder": "评分项名称",
"cwidth": 15
}
]
},
"page_rows":160,
"cache_limit":5
}
,"binds":[]
}]
}

View File

@ -0,0 +1,44 @@
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": "project_id", "title": "项目ID", "type": "str", "length": 32, "nullable": "no"}, {"name": "section", "title": "评分部分", "type": "str", "length": 32, "nullable": "no", "default": "technical"}, {"name": "item_no", "title": "评分项编号", "type": "str", "length": 32}, {"name": "item_name", "title": "评分项名称", "type": "str", "length": 255, "nullable": "no"}, {"name": "max_score", "title": "满分", "type": "float", "length": 8, "dec": 2, "nullable": "no", "default": "0"}, {"name": "scoring_rule", "title": "得分规则", "type": "text"}, {"name": "is_veto", "title": "否决项", "type": "str", "length": 1, "nullable": "no", "default": "0"}, {"name": "weight", "title": "权重", "type": "float", "length": 8, "dec": 4}, {"name": "source_ref", "title": "招标文件出处", "type": "str", "length": 500}, {"name": "target_chapter_no", "title": "对应章节号", "type": "str", "length": 32}, {"name": "order_no", "title": "排序", "type": "int", "default": "0"}, {"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_scoring_items', 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"
}
}

View File

@ -0,0 +1,41 @@
ns = params_kw.copy()
for k,v in ns.items():
if v == 'NaN' or v == 'null':
ns[k] = None
id = params_kw.id
if not id or len(id) > 32:
id = uuid()
ns['id'] = id
db = DBPools()
dbname = get_module_dbname('pipeline_bidding')
async with db.sqlorContext(dbname) as sor:
r = await sor.C('bid_tender_files', ns.copy())
_new_rows = await sor.sqlExe("SELECT * FROM bid_tender_files WHERE id = ${id}$", {'id': id})
_user_data = _new_rows[0] if _new_rows else ns
return {
"widgettype":"Message",
"options":{
"cwidth":16,
"cheight":9,
"title":"Add Success",
"timeout":3,
"message":"ok",
"user_data": _user_data
}
}
return {
"widgettype":"Error",
"options":{
"title":"Add Error",
"cwidth":16,
"cheight":9,
"timeout":3,
"message":"failed"
}
}

Some files were not shown because too many files have changed in this diff Show More