diff --git a/pipeline_bidding/bid_common.py b/pipeline_bidding/bid_common.py index be035a8..4cb3b79 100644 --- a/pipeline_bidding/bid_common.py +++ b/pipeline_bidding/bid_common.py @@ -40,14 +40,18 @@ HT_DELIVERY_CONFIRM = "bid_delivery_confirm" HT_FLOW_CONFIRM = "flow_plan_confirm" # 流程裁剪确认(引擎通用类型,pipeline_flow_plans 机制) HT_TEMPLATE_CONFIRM = "tech_template_confirm" # 技术方案模版/骨架确认(纯技术方案流程,2026-09-14 批2) -BLOCKING_HT_TYPES = (HT_TENDER_FILE, HT_FLOW_CONFIRM, HT_TEMPLATE_CONFIRM) +BLOCKING_HT_TYPES = (HT_TENDER_FILE, HT_FLOW_CONFIRM) # 2026-09-03 起只保留「等招标文件」为全局阻塞: # - HT_HUMAN_DOCS(等人类资质文件)降级为章节级依赖——只阻塞商务章(等资料准备),技术/报价章不受影响; # - HT_QC_ESCALATION(QC 轮次用尽)降级为章节级依赖——只阻塞依赖该产出类型的章节,其余章节照常流转。 # 2026-09-11 增 HT_FLOW_CONFIRM:裁剪计划待用户确认期间全局阻塞(确认后才按计划派发, # 防「用户在确认、对账器按旧流程抢跑」)。驳回后的暂停由 bid_flow 查 pipeline_flow_plans 状态兜住。 -# 2026-09-14 增 HT_TEMPLATE_CONFIRM:技术方案模版骨架待用户确认期间全局阻塞(用户明确要 -# 「模版由用户确认后」才开写章节——骨架没确认就编写 = 方向错了全白写)。 +# 2026-09-17 阻塞语义分流:HT_TEMPLATE_CONFIRM 移出全局阻塞—— +# - 纯技术方案流程:骨架确认卡的是全流程,但 T3 段本就显式 waiting(确认前不派任何任务), +# 全局阻塞是重复保险,移除后行为不变; +# - 标准投标流程(模版裁剪链挂入后):裁剪确认只关技术章的事,商务/报价章与其他维度 +# 照常流转——若全局阻塞,用户晚确认一天模版,整个标书停摆(过度阻塞)。 +# 技术章的门禁在 C 段按章节卡(tech_tpl_confirmed 才派技术章编写)。 # ── QC 契合度审核类型(解析产出逐类审核)── QC_TYPE_SCORING = "scoring_items" # 评分项 + 得分规则 diff --git a/pipeline_bidding/bid_compose_capability.py b/pipeline_bidding/bid_compose_capability.py index 7fdfe83..74b727a 100644 --- a/pipeline_bidding/bid_compose_capability.py +++ b/pipeline_bidding/bid_compose_capability.py @@ -142,6 +142,54 @@ def _clean_md_cell(text): return t.strip() +# ── 内联 markdown → Word run(2026-09-17 用户报障:**粗体** 原样带星号落 docx)── +_MD_BOLD_RE = re.compile(r'\*\*([^*]+)\*\*') + + +def _strip_inline_code(s): + """去掉行内代码反引号(docx 无等宽语义,保留文字)。""" + return re.sub(r'`([^`]*)`', r'\1', s or '') + + +def _add_runs_with_inline_md(para, text): + """把一段文本按内联 markdown 拆成 runs 写进段落:**粗体**→加粗 run, + `code`→去反引号,制表符→空格,其余原样。无标记时等价 add_paragraph(text)。""" + t = (text or '').replace('\t', ' ') + pos = 0 + for m in _MD_BOLD_RE.finditer(t): + if m.start() > pos: + para.add_run(_strip_inline_code(t[pos:m.start()])) + r = para.add_run(_strip_inline_code(m.group(1))) + r.bold = True + pos = m.end() + if pos < len(t): + para.add_run(_strip_inline_code(t[pos:])) + if not t: + para.add_run('') + + +# 项目符号(半角 - * 必须跟空白;全角 •/·/●/◦/- 可不跟空白,用户实测 "•\t**xx**") +_MD_BULLET_RE = re.compile(r'^\s*(?:[-*]\s+|(?:[•·●◦]|-)\s*)(?=\S)') +# 小节号标题:"1.3 xxx" / "2.1.3 xxx"(点分编号开头) +_MD_NUMHEAD_RE = re.compile(r'^\s*(\d+(?:\.\d+)+)[\s、.\.]*(\S.*)$') + + +def _numhead_heading(line): + """识别小节号标题行,返回 (level, text) 或 None。 + + 保守判定防误伤正文:编号后标题 ≤40 字且不含句号("1.3 倍冗余……" 这类 + 以小数开头的正文句子不当标题)。level = 点分段数(1.3→2,2.1.3→3),封顶 4。 + """ + m = _MD_NUMHEAD_RE.match(line or '') + if not m: + return None + no, rest = m.group(1), m.group(2).strip() + if not rest or len(rest) > 40 or '。' in rest: + return None + level = min(4, no.count('.') + 1) + return level, "%s %s" % (no, rest) + + def _write_docx(path, title, subtitle, chapters): """用 python-docx 写标书。返回 (ok, page_hint_or_err)。 @@ -247,14 +295,20 @@ def _write_docx(path, title, subtitle, chapters): doc.add_paragraph("[配图缺失(下载失败):%s %s]" % (alt or "", url[:80])) img_fail += 1 continue + _nh = _numhead_heading(t) if t.startswith("### "): - doc.add_heading(t[4:], level=3) + doc.add_heading(_strip_inline_code(t[4:].replace('**', '')), level=3) elif t.startswith("## "): - doc.add_heading(t[3:], level=2) + doc.add_heading(_strip_inline_code(t[3:].replace('**', '')), 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") + doc.add_heading(_strip_inline_code(t[2:].replace('**', '')), level=2) + elif _nh is not None: + # 小节号标题("1.3 xxx")→ 真 Word 标题层级(目录/导航可用) + doc.add_heading(_strip_inline_code(_nh[1].replace('**', '')), level=_nh[0]) + elif _MD_BULLET_RE.match(t) and not t.lstrip().startswith('**'): + # 项目符号行(- * • · ● ◦ -,可跟制表符)→ List Bullet + 内联粗体 + _bp = doc.add_paragraph(style="List Bullet") + _add_runs_with_inline_md(_bp, _MD_BULLET_RE.sub('', t)) elif _is_table_sep(t): continue # 游离分隔行(无表头)跳过 elif t.strip().startswith('|') and t.strip().endswith('|'): @@ -269,7 +323,9 @@ def _write_docx(path, title, subtitle, chapters): except Exception: doc.add_paragraph(t) else: - doc.add_paragraph(t) + # 普通段落/编号行:**粗体** 拆 run 加粗,`code` 去反引号,\t→空格 + _pp = doc.add_paragraph() + _add_runs_with_inline_md(_pp, t) doc.add_page_break() doc.save(path) if img_ok or img_fail or tbl_ok: