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:
parent
73d19bbd49
commit
19a3d67c68
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.egg-info/
|
||||
build/
|
||||
dist/
|
||||
113
build.sh
Normal file
113
build.sh
Normal 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
227
docs/DESIGN.md
Normal 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
30
i18n/en/msg.txt
Normal 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
30
i18n/zh/msg.txt
Normal file
@ -0,0 +1,30 @@
|
||||
招标信息: 招标信息
|
||||
招标信息台: 招标信息台
|
||||
邮箱配置: 邮箱配置
|
||||
投标知识库: 投标知识库
|
||||
评分项: 评分项
|
||||
标书章节: 标书章节
|
||||
合成标书: 合成标书
|
||||
采集招标信息: 采集招标信息
|
||||
提交审批: 提交审批
|
||||
审批通过: 审批通过
|
||||
审批不通过: 审批不通过
|
||||
存档作废: 存档作废
|
||||
立项: 立项
|
||||
参与人员: 参与人员
|
||||
上传招标文件: 上传招标文件
|
||||
资质要求: 资质要求
|
||||
投标文件要求: 投标文件要求
|
||||
章节评审: 章节评审
|
||||
整书评分: 整书评分
|
||||
改进意见: 改进意见
|
||||
得分规则: 得分规则
|
||||
评审通过: 评审通过
|
||||
退回重写: 退回重写
|
||||
交付确认: 交付确认
|
||||
投标开始时间: 投标开始时间
|
||||
投标截止时间: 投标截止时间
|
||||
项目周期: 项目周期
|
||||
招标金额: 招标金额
|
||||
投标保证金: 投标保证金
|
||||
内容摘要: 内容摘要
|
||||
136
init/data.json
Normal file
136
init/data.json
Normal 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
50
json/bid_chapters.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
38
json/bid_doc_requirements.json
Normal file
38
json/bid_doc_requirements.json
Normal 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
39
json/bid_documents.json
Normal 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
45
json/bid_kb_docs.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
42
json/bid_mail_accounts.json
Normal file
42
json/bid_mail_accounts.json
Normal 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
37
json/bid_members.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
38
json/bid_qualifications.json
Normal file
38
json/bid_qualifications.json
Normal 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
48
json/bid_reviews.json
Normal 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
37
json/bid_scores.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
43
json/bid_scoring_items.json
Normal file
43
json/bid_scoring_items.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
40
json/bid_tender_files.json
Normal file
40
json/bid_tender_files.json
Normal 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
52
json/bid_tenders.json
Normal 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
4
json/build.sh
Normal 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
193
models/bid_chapters.json
Normal 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'"
|
||||
}
|
||||
]
|
||||
}
|
||||
106
models/bid_doc_requirements.json
Normal file
106
models/bid_doc_requirements.json
Normal 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
142
models/bid_documents.json
Normal 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
152
models/bid_kb_docs.json
Normal 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'"
|
||||
}
|
||||
]
|
||||
}
|
||||
159
models/bid_mail_accounts.json
Normal file
159
models/bid_mail_accounts.json
Normal 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
103
models/bid_members.json
Normal 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'"
|
||||
}
|
||||
]
|
||||
}
|
||||
131
models/bid_qualifications.json
Normal file
131
models/bid_qualifications.json
Normal 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
156
models/bid_reviews.json
Normal 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
111
models/bid_scores.json
Normal 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": []
|
||||
}
|
||||
132
models/bid_scoring_items.json
Normal file
132
models/bid_scoring_items.json
Normal 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'"
|
||||
}
|
||||
]
|
||||
}
|
||||
129
models/bid_tender_files.json
Normal file
129
models/bid_tender_files.json
Normal 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
246
models/bid_tenders.json
Normal 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
526
mysql.ddl.sql
Normal 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);
|
||||
9
pipeline_bidding/__init__.py
Normal file
9
pipeline_bidding/__init__.py
Normal 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"
|
||||
611
pipeline_bidding/bid_ability.py
Normal file
611
pipeline_bidding/bid_ability.py
Normal 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()
|
||||
444
pipeline_bidding/bid_analysis_capability.py
Normal file
444
pipeline_bidding/bid_analysis_capability.py
Normal 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))
|
||||
333
pipeline_bidding/bid_common.py
Normal file
333
pipeline_bidding/bid_common.py
Normal 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])
|
||||
221
pipeline_bidding/bid_compose_capability.py
Normal file
221
pipeline_bidding/bid_compose_capability.py
Normal 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]))
|
||||
355
pipeline_bidding/bid_flow.py
Normal file
355
pipeline_bidding/bid_flow.py
Normal 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
|
||||
263
pipeline_bidding/bid_kb_capability.py
Normal file
263
pipeline_bidding/bid_kb_capability.py
Normal 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))
|
||||
307
pipeline_bidding/bid_mail.py
Normal file
307
pipeline_bidding/bid_mail.py
Normal 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" ?", " ", 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
|
||||
182
pipeline_bidding/bid_review_capability.py
Normal file
182
pipeline_bidding/bid_review_capability.py
Normal 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)
|
||||
334
pipeline_bidding/bid_score_capability.py
Normal file
334
pipeline_bidding/bid_score_capability.py
Normal 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)
|
||||
465
pipeline_bidding/bid_tender_capability.py
Normal file
465
pipeline_bidding/bid_tender_capability.py
Normal 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)
|
||||
140
pipeline_bidding/bid_write_capability.py
Normal file
140
pipeline_bidding/bid_write_capability.py
Normal 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
58
pipeline_bidding/init.py
Normal 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
|
||||
305
pipeline_bidding/role_tool_schemas.py
Normal file
305
pipeline_bidding/role_tool_schemas.py
Normal 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
9
pyproject.toml
Normal 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
6
scripts/gen_ddl.sh
Normal 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
64
scripts/load_path.py
Normal 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()
|
||||
41
wwwroot/bid_chapters/add_bid_chapters.dspy
Normal file
41
wwwroot/bid_chapters/add_bid_chapters.dspy
Normal 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"
|
||||
}
|
||||
}
|
||||
33
wwwroot/bid_chapters/delete_bid_chapters.dspy
Normal file
33
wwwroot/bid_chapters/delete_bid_chapters.dspy
Normal 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"
|
||||
}
|
||||
}
|
||||
50
wwwroot/bid_chapters/get_bid_chapters.dspy
Normal file
50
wwwroot/bid_chapters/get_bid_chapters.dspy
Normal 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":[]
|
||||
}
|
||||
370
wwwroot/bid_chapters/index.ui
Normal file
370
wwwroot/bid_chapters/index.ui
Normal 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":[]
|
||||
|
||||
}]
|
||||
}
|
||||
44
wwwroot/bid_chapters/update_bid_chapters.dspy
Normal file
44
wwwroot/bid_chapters/update_bid_chapters.dspy
Normal 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"
|
||||
}
|
||||
}
|
||||
41
wwwroot/bid_doc_requirements/add_bid_doc_requirements.dspy
Normal file
41
wwwroot/bid_doc_requirements/add_bid_doc_requirements.dspy
Normal 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"
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
49
wwwroot/bid_doc_requirements/get_bid_doc_requirements.dspy
Normal file
49
wwwroot/bid_doc_requirements/get_bid_doc_requirements.dspy
Normal 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":[]
|
||||
}
|
||||
231
wwwroot/bid_doc_requirements/index.ui
Normal file
231
wwwroot/bid_doc_requirements/index.ui
Normal 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":[]
|
||||
|
||||
}]
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
41
wwwroot/bid_documents/add_bid_documents.dspy
Normal file
41
wwwroot/bid_documents/add_bid_documents.dspy
Normal 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"
|
||||
}
|
||||
}
|
||||
33
wwwroot/bid_documents/delete_bid_documents.dspy
Normal file
33
wwwroot/bid_documents/delete_bid_documents.dspy
Normal 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"
|
||||
}
|
||||
}
|
||||
49
wwwroot/bid_documents/get_bid_documents.dspy
Normal file
49
wwwroot/bid_documents/get_bid_documents.dspy
Normal 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":[]
|
||||
}
|
||||
289
wwwroot/bid_documents/index.ui
Normal file
289
wwwroot/bid_documents/index.ui
Normal 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":[]
|
||||
|
||||
}]
|
||||
}
|
||||
44
wwwroot/bid_documents/update_bid_documents.dspy
Normal file
44
wwwroot/bid_documents/update_bid_documents.dspy
Normal 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"
|
||||
}
|
||||
}
|
||||
41
wwwroot/bid_kb_docs/add_bid_kb_docs.dspy
Normal file
41
wwwroot/bid_kb_docs/add_bid_kb_docs.dspy
Normal 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"
|
||||
}
|
||||
}
|
||||
33
wwwroot/bid_kb_docs/delete_bid_kb_docs.dspy
Normal file
33
wwwroot/bid_kb_docs/delete_bid_kb_docs.dspy
Normal 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"
|
||||
}
|
||||
}
|
||||
49
wwwroot/bid_kb_docs/get_bid_kb_docs.dspy
Normal file
49
wwwroot/bid_kb_docs/get_bid_kb_docs.dspy
Normal 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":[]
|
||||
}
|
||||
302
wwwroot/bid_kb_docs/index.ui
Normal file
302
wwwroot/bid_kb_docs/index.ui
Normal 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":[]
|
||||
|
||||
}]
|
||||
}
|
||||
44
wwwroot/bid_kb_docs/update_bid_kb_docs.dspy
Normal file
44
wwwroot/bid_kb_docs/update_bid_kb_docs.dspy
Normal 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"
|
||||
}
|
||||
}
|
||||
41
wwwroot/bid_mail_accounts/add_bid_mail_accounts.dspy
Normal file
41
wwwroot/bid_mail_accounts/add_bid_mail_accounts.dspy
Normal 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"
|
||||
}
|
||||
}
|
||||
33
wwwroot/bid_mail_accounts/delete_bid_mail_accounts.dspy
Normal file
33
wwwroot/bid_mail_accounts/delete_bid_mail_accounts.dspy
Normal 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"
|
||||
}
|
||||
}
|
||||
47
wwwroot/bid_mail_accounts/get_bid_mail_accounts.dspy
Normal file
47
wwwroot/bid_mail_accounts/get_bid_mail_accounts.dspy
Normal 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":[]
|
||||
}
|
||||
307
wwwroot/bid_mail_accounts/index.ui
Normal file
307
wwwroot/bid_mail_accounts/index.ui
Normal 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":[]
|
||||
|
||||
}]
|
||||
}
|
||||
44
wwwroot/bid_mail_accounts/update_bid_mail_accounts.dspy
Normal file
44
wwwroot/bid_mail_accounts/update_bid_mail_accounts.dspy
Normal 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"
|
||||
}
|
||||
}
|
||||
41
wwwroot/bid_members/add_bid_members.dspy
Normal file
41
wwwroot/bid_members/add_bid_members.dspy
Normal 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"
|
||||
}
|
||||
}
|
||||
33
wwwroot/bid_members/delete_bid_members.dspy
Normal file
33
wwwroot/bid_members/delete_bid_members.dspy
Normal 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"
|
||||
}
|
||||
}
|
||||
49
wwwroot/bid_members/get_bid_members.dspy
Normal file
49
wwwroot/bid_members/get_bid_members.dspy
Normal 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":[]
|
||||
}
|
||||
212
wwwroot/bid_members/index.ui
Normal file
212
wwwroot/bid_members/index.ui
Normal 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":[]
|
||||
|
||||
}]
|
||||
}
|
||||
44
wwwroot/bid_members/update_bid_members.dspy
Normal file
44
wwwroot/bid_members/update_bid_members.dspy
Normal 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"
|
||||
}
|
||||
}
|
||||
41
wwwroot/bid_qualifications/add_bid_qualifications.dspy
Normal file
41
wwwroot/bid_qualifications/add_bid_qualifications.dspy
Normal 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"
|
||||
}
|
||||
}
|
||||
33
wwwroot/bid_qualifications/delete_bid_qualifications.dspy
Normal file
33
wwwroot/bid_qualifications/delete_bid_qualifications.dspy
Normal 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"
|
||||
}
|
||||
}
|
||||
50
wwwroot/bid_qualifications/get_bid_qualifications.dspy
Normal file
50
wwwroot/bid_qualifications/get_bid_qualifications.dspy
Normal 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":[]
|
||||
}
|
||||
268
wwwroot/bid_qualifications/index.ui
Normal file
268
wwwroot/bid_qualifications/index.ui
Normal 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":[]
|
||||
|
||||
}]
|
||||
}
|
||||
44
wwwroot/bid_qualifications/update_bid_qualifications.dspy
Normal file
44
wwwroot/bid_qualifications/update_bid_qualifications.dspy
Normal 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"
|
||||
}
|
||||
}
|
||||
41
wwwroot/bid_reviews/add_bid_reviews.dspy
Normal file
41
wwwroot/bid_reviews/add_bid_reviews.dspy
Normal 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"
|
||||
}
|
||||
}
|
||||
33
wwwroot/bid_reviews/delete_bid_reviews.dspy
Normal file
33
wwwroot/bid_reviews/delete_bid_reviews.dspy
Normal 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"
|
||||
}
|
||||
}
|
||||
49
wwwroot/bid_reviews/get_bid_reviews.dspy
Normal file
49
wwwroot/bid_reviews/get_bid_reviews.dspy
Normal 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":[]
|
||||
}
|
||||
338
wwwroot/bid_reviews/index.ui
Normal file
338
wwwroot/bid_reviews/index.ui
Normal 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}')}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
}]
|
||||
}
|
||||
44
wwwroot/bid_reviews/update_bid_reviews.dspy
Normal file
44
wwwroot/bid_reviews/update_bid_reviews.dspy
Normal 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"
|
||||
}
|
||||
}
|
||||
41
wwwroot/bid_scores/add_bid_scores.dspy
Normal file
41
wwwroot/bid_scores/add_bid_scores.dspy
Normal 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"
|
||||
}
|
||||
}
|
||||
33
wwwroot/bid_scores/delete_bid_scores.dspy
Normal file
33
wwwroot/bid_scores/delete_bid_scores.dspy
Normal 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"
|
||||
}
|
||||
}
|
||||
47
wwwroot/bid_scores/get_bid_scores.dspy
Normal file
47
wwwroot/bid_scores/get_bid_scores.dspy
Normal 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
219
wwwroot/bid_scores/index.ui
Normal 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":[]
|
||||
|
||||
}]
|
||||
}
|
||||
44
wwwroot/bid_scores/update_bid_scores.dspy
Normal file
44
wwwroot/bid_scores/update_bid_scores.dspy
Normal 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"
|
||||
}
|
||||
}
|
||||
41
wwwroot/bid_scoring_items/add_bid_scoring_items.dspy
Normal file
41
wwwroot/bid_scoring_items/add_bid_scoring_items.dspy
Normal 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"
|
||||
}
|
||||
}
|
||||
33
wwwroot/bid_scoring_items/delete_bid_scoring_items.dspy
Normal file
33
wwwroot/bid_scoring_items/delete_bid_scoring_items.dspy
Normal 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"
|
||||
}
|
||||
}
|
||||
49
wwwroot/bid_scoring_items/get_bid_scoring_items.dspy
Normal file
49
wwwroot/bid_scoring_items/get_bid_scoring_items.dspy
Normal 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":[]
|
||||
}
|
||||
275
wwwroot/bid_scoring_items/index.ui
Normal file
275
wwwroot/bid_scoring_items/index.ui
Normal 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":[]
|
||||
|
||||
}]
|
||||
}
|
||||
44
wwwroot/bid_scoring_items/update_bid_scoring_items.dspy
Normal file
44
wwwroot/bid_scoring_items/update_bid_scoring_items.dspy
Normal 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"
|
||||
}
|
||||
}
|
||||
41
wwwroot/bid_tender_files/add_bid_tender_files.dspy
Normal file
41
wwwroot/bid_tender_files/add_bid_tender_files.dspy
Normal 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
Loading…
x
Reference in New Issue
Block a user