chore: commit pending local changes (2026-09-17 github sync)

This commit is contained in:
yumoqing 2026-09-17 13:45:37 +08:00
parent beca611f29
commit 6ef74388c1
2 changed files with 69 additions and 9 deletions

View File

@ -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_ESCALATIONQC 轮次用尽)降级为章节级依赖——只阻塞依赖该产出类型的章节,其余章节照常流转。
# 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" # 评分项 + 得分规则

View File

@ -142,6 +142,54 @@ def _clean_md_cell(text):
return t.strip()
# ── 内联 markdown → Word run2026-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.322.1.33封顶 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: