yumoqing 19a3d67c68 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
2026-08-26 23:45:03 +08:00

612 lines
30 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""投标产线能力包(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()