diff --git a/pipeline_bidding/bid_analysis_capability.py b/pipeline_bidding/bid_analysis_capability.py index 5f8077c..c4d8912 100644 --- a/pipeline_bidding/bid_analysis_capability.py +++ b/pipeline_bidding/bid_analysis_capability.py @@ -806,7 +806,9 @@ async def list_scoring_items(project_id, section=""): try: project_id = await resolve_project_id(project_id) except ValueError as e: - return [] + # 2026-09-03:静默返回 [] 会让 LLM 误判「无数据」(实测 C-12 绑定阻塞根因), + # 改为明确报错,提示补 project_id。 + return "ERROR: 项目上下文缺失(%s)。请显式传 project_id 后重试。" % e db, dbname = get_db() async with db.sqlorContext(dbname) as sor: sql = ("SELECT id, section, item_no, item_name, max_score, is_veto, " @@ -828,7 +830,7 @@ async def list_doc_requirements(project_id, req_type=""): try: project_id = await resolve_project_id(project_id) except ValueError as e: - return [] + return "ERROR: 项目上下文缺失(%s)。请显式传 project_id 后重试。" % e db, dbname = get_db() async with db.sqlorContext(dbname) as sor: sql = ("SELECT id, req_type, chapter_no, chapter_title, requirement, page_limit " @@ -938,3 +940,98 @@ async def create_chapter_outline(project_id, chapters="", who=None, agent_id=Non who=who, agent_id=agent_id, detail="新增 %d 章" % created) return True, ("章节骨架已生成:新增 %d 章,已存在跳过 %d 章。" "系统将自动为每个章节派发编写任务。" % (created, skipped)) + + +async def patch_chapters(project_id, patches="", who=None, agent_id=None): + """按 chapter_no 增量更新已有章节(2026-09-03,根治 create_chapter_outline 只增不改)。 + + patches: JSON 数组,每项 {chapter_no, title?, outline?, section?, + scoring_item_ids?, scoring_items_by_name?, max_score?}。 + - scoring_item_ids 直接覆盖(传真实 id 数组); + - scoring_items_by_name: [{item_name, max_score?}] 按名称在 bid_scoring_items + 反查 id 绑定(item_name 为「商务N|名称|出处」格式时按包含匹配); + - 白名单外的章节不动;不存在的 chapter_no 报错列出。 + """ + try: + project_id = await resolve_project_id(project_id) + except ValueError as e: + return False, str(e) + items = json_loads(patches, []) if isinstance(patches, str) else (patches or []) + if not items: + return False, "patches 为空:传 JSON 数组 [{chapter_no, ...}]" + db, dbname = get_db() + async with db.sqlorContext(dbname) as sor: + sitems = await sor.sqlExe( + "SELECT id, item_name, max_score FROM bid_scoring_items WHERE project_id=${pid}$", + {"pid": project_id}) + await sor.sqlExe("COMMIT", {}) + sc = rows_to_dicts(sitems) + + def _bind_by_name(names): + ids, miss = [], [] + for n in (names or []): + nm = (n.get("item_name") or "").strip() if isinstance(n, dict) else str(n or "").strip() + hit = None + for s in sc: + sn = s.get("item_name") or "" + if nm and (nm == sn or nm in sn or sn in nm): + hit = s + break + if hit: + ids.append(hit["id"]) + else: + miss.append(nm) + return ids, miss + + updated, missing, bound, unbound = [], [], 0, [] + for it in items: + if not isinstance(it, dict): + continue + cno = str(it.get("chapter_no") or "").strip() + if not cno: + continue + recs = await sor.sqlExe( + "SELECT id, scoring_item_ids FROM bid_chapters " + "WHERE project_id=${pid}$ AND chapter_no=${cno}$ LIMIT 1", + {"pid": project_id, "cno": cno}) + await sor.sqlExe("COMMIT", {}) + if not recs: + missing.append(cno) + continue + sets, p = [], {"pid": project_id, "cno": cno} + if it.get("title"): + sets.append("title=${t}$"); p["t"] = str(it["title"])[:250] + if it.get("outline") is not None and it.get("outline") != "": + sets.append("outline=${o}$"); p["o"] = str(it["outline"])[:20000] + if it.get("section"): + sets.append("section=${s}$"); p["s"] = str(it["section"])[:30] + ids = list(it.get("scoring_item_ids") or []) + if it.get("scoring_items_by_name"): + b, m = _bind_by_name(it["scoring_items_by_name"]) + ids = b + bound += len(b) + unbound += m + if it.get("scoring_item_ids") is not None or it.get("scoring_items_by_name"): + sets.append("scoring_item_ids=${si}$") + p["si"] = json.dumps(ids, ensure_ascii=False) + if it.get("max_score") is not None: + sets.append("max_score=${ms}$"); p["ms"] = to_float(it["max_score"], 0) + if not sets: + continue + sets.append("updated_at=NOW()") + sets.append("version=version+1") + await sor.sqlExe( + "UPDATE bid_chapters SET " + ", ".join(sets) + + " WHERE project_id=${pid}$ AND chapter_no=${cno}$", p) + updated.append(cno) + await sor.sqlExe("COMMIT", {}) + await record(sor, project_id, "bid_chapters", project_id, "patch_outline", + who=who, agent_id=agent_id, + detail="更新 %d 章:%s" % (len(updated), ",".join(updated)[:200])) + msg = "章节增量更新:成功 %d 章(%s),评分项按名绑定 %d 条" % ( + len(updated), ",".join(updated)[:200], bound) + if missing: + msg += ";不存在的章节:%s" % ",".join(missing) + if unbound: + msg += ";按名未绑定的评分项:%s" % ",".join(unbound)[:200] + return True, msg diff --git a/pipeline_bidding/bid_kb_capability.py b/pipeline_bidding/bid_kb_capability.py index 85706e2..2be4117 100644 --- a/pipeline_bidding/bid_kb_capability.py +++ b/pipeline_bidding/bid_kb_capability.py @@ -96,7 +96,7 @@ async def list_qualifications(project_id, match_status=""): try: project_id = await resolve_project_id(project_id) except ValueError as e: - return [] + return "ERROR: 项目上下文缺失(%s)。请显式传 project_id 后重试。" % e db, dbname = get_db() async with db.sqlorContext(dbname) as sor: sql = ("SELECT id, qual_name, is_mandatory, owner, match_status, kb_doc_id, " diff --git a/pipeline_bidding/role_tool_schemas.py b/pipeline_bidding/role_tool_schemas.py index 5260e73..9ffb92b 100644 --- a/pipeline_bidding/role_tool_schemas.py +++ b/pipeline_bidding/role_tool_schemas.py @@ -103,6 +103,13 @@ BID_ROLE_TOOL_SCHEMAS = { "params": {"project_id": "项目ID(可选,默认当前)", "chapters": "章节JSON数组(可选,默认自动生成)"}, "required": [], }, + "patch_chapters": { + "module": f"{_M}.bid_analysis_capability", + "description": "按 chapter_no 增量更新已有章节(改标题/outline/挂载评分项/满分)。create_chapter_outline 只增不改,修正既有骨架必须用它。scoring_items_by_name 按名称反查绑定真实 id", + "params": {"project_id": "项目ID(可选,默认当前)", + "patches": "JSON数组[{chapter_no,title?,outline?,section?,scoring_item_ids?,scoring_items_by_name?,max_score?}]"}, + "required": ["patches"], + }, "list_scoring_items": { "module": f"{_M}.bid_analysis_capability", "description": "列出评分项与得分规则",