feat: patch_chapters工具(根治create_chapter_outline只增不改)+list_*项目上下文缺失改报错不静默空

This commit is contained in:
yumoqing 2026-09-03 19:13:49 +08:00
parent 6c87cd12b6
commit fdd98266f8
3 changed files with 107 additions and 3 deletions

View File

@ -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

View File

@ -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, "

View File

@ -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": "列出评分项与得分规则",