fix(投标): 招标书在工作空间还反复催上传根治+同一事项待办只发一次
- 对账器0b: 无bid_tender_files记录时先扫工作空间,发现招标书自动import_tender_file登记启动解析(与上传入口等价),不再发上传待办 - request_tender_file硬门禁: 已有文件记录拒绝发待办; pending存在返回已有id - create_human_task机制层去重: 同项目+同类型pending存在→返回已有id不重复创建 - prompt/工具描述同步: 文件已在工作空间/已登记时禁止发上传待办
This commit is contained in:
parent
ceeec055a3
commit
b7bbee6563
@ -33,7 +33,7 @@ BID_TOOLS = [
|
||||
parameters={"name": "项目名称", "description": "项目描述(可选)",
|
||||
"tender_content": "招标文件正文(用户已提供时必填,直接导入跳过上传步骤)",
|
||||
"tender_file_name": "招标文件名(可选)"}, category="project"),
|
||||
ToolDefinition(name="request_tender_file", description="发布/重发「等待上传招标文件」人类任务",
|
||||
ToolDefinition(name="request_tender_file", description="仅当项目确实没有任何招标文件且需要人工上传时才发「等待上传」人类待办(同一事项只发一次)。若文件已在工作空间/已登记,禁止调用——用 import_tender_file 登记",
|
||||
parameters={"note": "说明(可选)"}, category="project"),
|
||||
ToolDefinition(name="list_tender_files", description="查看已上传的招标文件及抽取状态", parameters={}, category="project"),
|
||||
|
||||
@ -122,7 +122,10 @@ BID_PROMPT = """你是「投标产线」的驾驶舱 agent,负责从招标文
|
||||
抽取不到就如实说"文件未载明",必要时 ask_user。
|
||||
- 标书内容的案例与业绩必须来自公司投标知识库的真实合同(bid_kb_docs 里 doc_type=case),
|
||||
禁止虚构客户名、合同金额、历史成果数字。
|
||||
- 章节没全部评审通过,不允许合成标书;评分没过门限,不允许当作可交付。"""
|
||||
- 章节没全部评审通过,不允许合成标书;评分没过门限,不允许当作可交付。
|
||||
- **招标文件已在工作空间/已登记(list_tender_files 非空)时,禁止发上传待办**——
|
||||
直接 import_tender_file(file_path=工作空间路径) 登记启动解析;系统对账器也会
|
||||
自动登记工作空间里的招标书。同一事项的人类待办只发一次,禁止重复催。"""
|
||||
|
||||
# ══════════════════ 角色集 ══════════════════
|
||||
|
||||
|
||||
@ -284,7 +284,17 @@ async def has_pending_blocking_human_task(sor, project_id):
|
||||
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 留空 → 走项目级门禁。"""
|
||||
"""创建人类任务(pipeline_human_tasks)。iteration_id 留空 → 走项目级门禁。
|
||||
|
||||
同一事项只发一次待办(2026-09-01 机制层不变量):同项目+同类型已有
|
||||
pending 任务 → 直接返回已有 id,不重复创建。防止对账器/agent 每轮
|
||||
重复发同类待办把用户淹没。
|
||||
"""
|
||||
exist = await find_human_task(sor, project_id, task_type, status="pending")
|
||||
if exist:
|
||||
logger.info("bid create_human_task dedup: type=%s existing=%s project=%s",
|
||||
task_type, exist.get("id"), project_id)
|
||||
return exist.get("id")
|
||||
hid = new_id()
|
||||
await sor.C("pipeline_human_tasks", {
|
||||
"id": hid,
|
||||
|
||||
@ -15,6 +15,7 @@ RoleSpec.next_role 全部留空:投标产线不用「任务链跳下一角色
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
from .bid_common import (
|
||||
get_db, rows_to_dicts, rec_to_dict, to_int, to_float, json_loads,
|
||||
@ -204,6 +205,48 @@ async def _qc_collect_improvements(sor, project_id):
|
||||
return items
|
||||
|
||||
|
||||
async def _auto_import_workspace_tender(sor, project_id):
|
||||
"""工作空间已有招标书文件但 bid_tender_files 无记录 → 自动注册并启动解析。
|
||||
|
||||
根治(2026-09-01):用户把招标书直接放进项目工作空间(或 agent 落盘)时,
|
||||
旧逻辑只认 bid_tender_files 表记录,对账器每轮都发「上传招标文件」人类待办,
|
||||
用户怎么答「文件就在目录里」都没用——死循环。现在对账器主动扫描工作空间,
|
||||
发现招标书即 import_tender_file 注册(落库+自动关闭上传待办+收敛器派解析),
|
||||
与上传入口完全等价,不再催人。
|
||||
"""
|
||||
try:
|
||||
from pipeline_service.workspace import get_project_dir_by_id
|
||||
pdir, _base = await get_project_dir_by_id(sor, project_id)
|
||||
if not pdir or not os.path.isdir(pdir):
|
||||
return ""
|
||||
cands = []
|
||||
for fn in sorted(os.listdir(pdir)):
|
||||
ext = os.path.splitext(fn)[1].lower()
|
||||
if ext not in (".docx", ".doc", ".pdf", ".txt", ".md"):
|
||||
continue
|
||||
fp = os.path.join(pdir, fn)
|
||||
if os.path.isfile(fp) and os.path.getsize(fp) > 0:
|
||||
cands.append((os.path.getsize(fp), fp, fn))
|
||||
if not cands:
|
||||
return ""
|
||||
cands.sort(reverse=True) # 最大文件优先(招标书通常是项目里最大的文档)
|
||||
_sz, fp, fn = cands[0]
|
||||
from .bid_tender_capability import import_tender_file
|
||||
ok, msg = await import_tender_file(
|
||||
project_id, file_path=fp, file_name=fn,
|
||||
who="system.reconciler", agent_id="bid_flow")
|
||||
if ok:
|
||||
logger.info("bid_flow: auto-imported workspace tender %s project=%s",
|
||||
fn, project_id)
|
||||
return "auto_imported: %s(%s)" % (fn, msg)
|
||||
logger.warning("bid_flow: auto-import tender failed project=%s: %s",
|
||||
project_id, str(msg)[:160])
|
||||
except Exception as e:
|
||||
logger.warning("bid_flow: auto-import workspace tender error project=%s: %s",
|
||||
project_id, str(e)[:160])
|
||||
return ""
|
||||
|
||||
|
||||
async def reconcile_project(sor, project_id, project_name=""):
|
||||
"""对单个投标项目做一次状态对账。返回动作说明列表(幂等,可反复调用)。"""
|
||||
acts = []
|
||||
@ -235,21 +278,27 @@ async def reconcile_project(sor, project_id, project_name=""):
|
||||
|
||||
# ── 0b. 招标文件到位则自动关闭上传任务,再判阻塞门禁 ──
|
||||
await _auto_close_file_upload_task(sor, project_id)
|
||||
# 产线起点兜底:项目无任何招标文件且无待办上传任务 → 自动发「上传招标文件」任务
|
||||
# (无论项目经 create_bid_project 还是通用 create_project 创建,都从上传文件起步)
|
||||
# 产线起点兜底:项目无招标文件记录时——
|
||||
# ① 先扫工作空间:招标书已在项目目录里就直接自动注册并启动解析(根治
|
||||
# 「文件在目录里还反复催上传」死循环,2026-09-01),不发待办;
|
||||
# ② 真没有且无 pending 上传待办才发一次(同一事项 pending 期间不重发)。
|
||||
_n_files_pre = await _count(sor, "SELECT COUNT(*) AS c FROM bid_tender_files "
|
||||
"WHERE project_id=${p}$", {"p": project_id})
|
||||
if _n_files_pre <= 0:
|
||||
_up_ht = await find_human_task(sor, project_id, HT_TENDER_FILE, status="pending")
|
||||
if not _up_ht:
|
||||
p = await get_project(sor, project_id)
|
||||
hid = await create_human_task(
|
||||
sor, project_id, HT_TENDER_FILE,
|
||||
"上传招标文件:%s" % (p.get("name", "") or project_id),
|
||||
("请上传招标文件(含答疑/补遗文件)。\n"
|
||||
"上传后系统自动进入招标文件解析(抽取评分项、得分规则、所需资质、投标文件要求、章节骨架)。"))
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
return acts + ["created: 上传招标文件任务 %s" % hid]
|
||||
_imp = await _auto_import_workspace_tender(sor, project_id)
|
||||
if _imp:
|
||||
acts.append(_imp)
|
||||
else:
|
||||
_up_ht = await find_human_task(sor, project_id, HT_TENDER_FILE, status="pending")
|
||||
if not _up_ht:
|
||||
p = await get_project(sor, project_id)
|
||||
hid = await create_human_task(
|
||||
sor, project_id, HT_TENDER_FILE,
|
||||
"上传招标文件:%s" % (p.get("name", "") or project_id),
|
||||
("请上传招标文件(含答疑/补遗文件)。\n"
|
||||
"上传后系统自动进入招标文件解析(抽取评分项、得分规则、所需资质、投标文件要求、章节骨架)。"))
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
return acts + ["created: 上传招标文件任务 %s" % hid]
|
||||
if await has_pending_blocking_human_task(sor, project_id):
|
||||
return ["blocked: 有待办人类任务(等招标文件/等人类资料/等QC人工介入),本轮不派发"]
|
||||
|
||||
|
||||
@ -238,12 +238,22 @@ async def list_members(project_id):
|
||||
|
||||
|
||||
async def request_tender_file(project_id, note="", who=None, agent_id=None):
|
||||
"""(重新)发布「等待上传招标文件」人类任务。"""
|
||||
"""(重新)发布「等待上传招标文件」人类任务。
|
||||
|
||||
硬门禁(2026-09-01,同一事项只发一次待办):
|
||||
- 项目已有招标文件记录(bid_tender_files)→ 文件已到位,**拒绝发待办**,
|
||||
提示用 list_tender_files 查看/解析会自动进行(防「文件已在还催人上传」);
|
||||
- 已有 pending 上传待办 → 返回已有任务 id,不重复创建。
|
||||
"""
|
||||
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
|
||||
n = await _count_files(sor, project_id)
|
||||
if n > 0:
|
||||
return False, ("项目已有 %d 个招标文件记录,无需再发上传待办"
|
||||
"(list_tender_files 查看,解析自动进行)" % n)
|
||||
exist = await find_human_task(sor, project_id, HT_TENDER_FILE, status="pending")
|
||||
if exist:
|
||||
return True, "已有待办上传任务: %s" % exist.get("id")
|
||||
@ -255,6 +265,14 @@ async def request_tender_file(project_id, note="", who=None, agent_id=None):
|
||||
return True, "已发布上传招标文件任务: %s" % hid
|
||||
|
||||
|
||||
async def _count_files(sor, project_id):
|
||||
recs = await sor.sqlExe(
|
||||
"SELECT COUNT(*) AS c FROM bid_tender_files WHERE project_id=${p}$",
|
||||
{"p": project_id})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
return to_int(getattr(recs[0], "c", 0) if recs else 0)
|
||||
|
||||
|
||||
async def list_tender_files(project_id):
|
||||
db, dbname = get_db()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user