fix(agent): G2缺口指纹超长静默丢失+fallback交付件跨仓库污染+setup_repos空目录崩溃
- _check_orchestration_gaps: G1环/G2指纹改短哈希(_short_fingerprint)——原实现拼接全部未依赖任务ID(15任务≈400字符)超pipeline_pm_notices.fingerprint VARCHAR(255)→INSERT DataError(1406)被catch记debug→PM告警静默丢失,脚手架先批deploy_test提前启动(pbls 09-15实测)
- _build_fallback_deliverable: git扫描只报本任务write_file写入过的仓库——原实现扫全工作空间apps/modules所有git仓库,16天前元景遗留的apps/scense脏工作区被算进M1a交付件→QC误判越权改动构成退回理由(pbls实测)
- _setup_repos: project_dir空串守卫——os.makedirs('')抛Errno 2且中断clone,日志高频刷屏
This commit is contained in:
parent
5fe5c238ec
commit
e774cb238b
@ -1418,7 +1418,12 @@ async def _commit_repos_after_approve(space_dir, project_name='', title=''):
|
|||||||
async def _setup_repos(sor, space_dir, project_id):
|
async def _setup_repos(sor, space_dir, project_id):
|
||||||
"""clone 项目关联仓库到机构工作空间:应用→apps/、模块→modules/;项目目录→projects/{项目名}/。"""
|
"""clone 项目关联仓库到机构工作空间:应用→apps/、模块→modules/;项目目录→projects/{项目名}/。"""
|
||||||
project_dir = await _get_project_dir(sor, project_id)
|
project_dir = await _get_project_dir(sor, project_id)
|
||||||
await _ensure_project_repo(project_dir)
|
# 守卫:取不到项目目录(sd_projects 记录缺 directory_name/name,或 space_dir 解析失败)时
|
||||||
|
# 原实现把空串传进 _ensure_project_repo → os.makedirs('') 抛
|
||||||
|
# FileNotFoundError [Errno 2] No such file or directory: ''(日志高频刷屏,
|
||||||
|
# 且整个 setup_repos 中断,后续 clone 也不执行)。空目录直接跳过项目仓库步骤。
|
||||||
|
if project_dir:
|
||||||
|
await _ensure_project_repo(project_dir)
|
||||||
repos = await _get_project_repos(sor, project_id)
|
repos = await _get_project_repos(sor, project_id)
|
||||||
results = []
|
results = []
|
||||||
for repo in repos:
|
for repo in repos:
|
||||||
@ -1587,7 +1592,7 @@ async def _check_orchestration_gaps(sor, project_id, task, next_role='', next_ta
|
|||||||
continue
|
continue
|
||||||
if color[m] == GRAY:
|
if color[m] == GRAY:
|
||||||
cyc = path[path.index(m):] if m in path else [m]
|
cyc = path[path.index(m):] if m in path else [m]
|
||||||
fp = "G1:cycle:" + ",".join(sorted(cyc))
|
fp = _short_fingerprint("G1:cycle", *sorted(cyc))
|
||||||
if fp in _cyc_seen:
|
if fp in _cyc_seen:
|
||||||
continue
|
continue
|
||||||
_cyc_seen.add(fp)
|
_cyc_seen.add(fp)
|
||||||
@ -1617,7 +1622,7 @@ async def _check_orchestration_gaps(sor, project_id, task, next_role='', next_ta
|
|||||||
if miss_tasks:
|
if miss_tasks:
|
||||||
miss_ids = [m['id'] for m in miss_tasks]
|
miss_ids = [m['id'] for m in miss_tasks]
|
||||||
gaps.append({
|
gaps.append({
|
||||||
'kind': 'G2', 'fingerprint': "G2:" + a['id'] + ":" + ",".join(sorted(miss_ids)),
|
'kind': 'G2', 'fingerprint': _short_fingerprint("G2", a['id'], *sorted(miss_ids)),
|
||||||
'task_id': a['id'], 'fix_ids': miss_ids,
|
'task_id': a['id'], 'fix_ids': miss_ids,
|
||||||
'text': (f"G2 应用级 develop「{a['title']}」({a['id']}) 未依赖以下未完成的模块任务:"
|
'text': (f"G2 应用级 develop「{a['title']}」({a['id']}) 未依赖以下未完成的模块任务:"
|
||||||
+ "、".join(f"{m['title']}({m['id']})" for m in miss_tasks)
|
+ "、".join(f"{m['title']}({m['id']})" for m in miss_tasks)
|
||||||
@ -1654,6 +1659,21 @@ async def _check_orchestration_gaps(sor, project_id, task, next_role='', next_ta
|
|||||||
return gaps
|
return gaps
|
||||||
|
|
||||||
|
|
||||||
|
def _short_fingerprint(prefix, *parts):
|
||||||
|
"""缺口指纹定长化:prefix + 内容 md5 前 16 位。
|
||||||
|
|
||||||
|
2026-09-15 pbls 实测:G2 指纹把全部未依赖任务 ID 拼进字符串(15 个任务 ≈ 400 字符),
|
||||||
|
超出 pipeline_pm_notices.fingerprint VARCHAR(255) → INSERT DataError(1406),
|
||||||
|
而 _save_gap_notices 只 catch 记 debug → 告警静默丢失,PM 从未看到
|
||||||
|
「应用级 develop 未依赖模块任务」→ 脚手架先批、deploy_test 在编码未完成时启动
|
||||||
|
(正是 G2 要防的事故)。指纹只服务 (tenant_id, fingerprint) 去重,不需要可读,
|
||||||
|
哈希后长度恒定,与依赖数量无关。
|
||||||
|
"""
|
||||||
|
import hashlib
|
||||||
|
body = ":".join(str(p) for p in parts if p)
|
||||||
|
return f"{prefix}:{hashlib.md5(body.encode('utf-8')).hexdigest()[:16]}"
|
||||||
|
|
||||||
|
|
||||||
def _gap_texts(gaps):
|
def _gap_texts(gaps):
|
||||||
"""结构化缺口 → 纯文本列表(日志用)。"""
|
"""结构化缺口 → 纯文本列表(日志用)。"""
|
||||||
return [g['text'] for g in (gaps or []) if isinstance(g, dict) and g.get('text')]
|
return [g['text'] for g in (gaps or []) if isinstance(g, dict) and g.get('text')]
|
||||||
@ -2263,32 +2283,41 @@ def _validate_deliverable_type(role, deliverable_type, allowed_types):
|
|||||||
f"(如 {allowed_types[0]})。")
|
f"(如 {allowed_types[0]})。")
|
||||||
|
|
||||||
|
|
||||||
async def _build_fallback_deliverable(space_dir, written_files, role):
|
async def _build_fallback_deliverable(space_dir, written_files, role, task_id=''):
|
||||||
"""循环结束未 deliver 时,检测 write_file/git 实际产出,构造真实交付件给 QC(而非占位符「Agent未产出交付件」)。"""
|
"""循环结束未 deliver 时,检测 write_file/git 实际产出,构造真实交付件给 QC(而非占位符「Agent未产出交付件」)。
|
||||||
|
|
||||||
|
2026-09-15 pbls 修复:git 扫描只报告「本任务 write_file 实际写入过的仓库」。
|
||||||
|
原实现把机构工作空间 apps/ + modules/ 下所有 git 仓库的脏状态都塞进交付件,
|
||||||
|
历史遗留的脏工作区(实测 apps/scense 的 12 项变更全是 16 天前别的任务留下的)
|
||||||
|
被当成本任务产出 → QC 误判「越权改动其它应用仓库」构成独立退回理由。
|
||||||
|
本任务没写过文件的仓库与本任务无关,不进交付件。
|
||||||
|
"""
|
||||||
lines = []
|
lines = []
|
||||||
if written_files:
|
if written_files:
|
||||||
lines.append("本任务执行期间 write_file 实际写入的文件:")
|
lines.append("本任务执行期间 write_file 实际写入的文件:")
|
||||||
for f in sorted(set(written_files)):
|
for f in sorted(written_files):
|
||||||
rel = os.path.relpath(f, space_dir) if f.startswith(space_dir) else f
|
rel = os.path.relpath(f, space_dir) if f.startswith(space_dir) else f
|
||||||
lines.append("- " + rel)
|
lines.append("- " + rel)
|
||||||
# 扫描 apps/ + modules/ + projects/ 下的 git 仓库工作区变更
|
# 只扫描本任务实际写入过的仓库(write_file 落点所属),不扫全工作空间
|
||||||
for sub in ('apps', 'modules', 'projects'):
|
touched_repos = set()
|
||||||
base = os.path.join(space_dir, sub)
|
for f in written_files or []:
|
||||||
if not os.path.isdir(base):
|
rp = f if os.path.isabs(f) else os.path.join(space_dir, f)
|
||||||
|
rel = os.path.relpath(rp, space_dir)
|
||||||
|
parts = rel.split(os.sep)
|
||||||
|
if len(parts) >= 2 and parts[0] in ('apps', 'modules', 'projects'):
|
||||||
|
touched_repos.add(os.path.join(space_dir, parts[0], parts[1]))
|
||||||
|
for rp in sorted(touched_repos):
|
||||||
|
if not os.path.isdir(os.path.join(rp, '.git')):
|
||||||
continue
|
continue
|
||||||
entries = ([(p, p) for p in sorted(os.listdir(base))] if sub == 'projects'
|
try:
|
||||||
else [(f"{sub}/{n}", n) for n in sorted(os.listdir(base))])
|
r = await _run_shell('git status --short', rp, 10)
|
||||||
for label, name in entries:
|
out = (r.get('stdout') or '').strip()
|
||||||
rp = os.path.join(base, name)
|
if out:
|
||||||
if os.path.isdir(os.path.join(rp, '.git')):
|
label = os.path.relpath(rp, space_dir)
|
||||||
try:
|
lines.append(f"git 仓库 [{label}] 工作区变更(仅本任务写入过的仓库):")
|
||||||
r = await _run_shell('git status --short', rp, 10)
|
lines.append(out[:2000])
|
||||||
out = (r.get('stdout') or '').strip()
|
except Exception:
|
||||||
if out:
|
pass
|
||||||
lines.append(f"git 仓库 [{label}] 工作区变更:")
|
|
||||||
lines.append(out[:2000])
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
if not lines:
|
if not lines:
|
||||||
lines.append("(本轮未检测到 write_file 或 git 变更——agent 确实未产出)")
|
lines.append("(本轮未检测到 write_file 或 git 变更——agent 确实未产出)")
|
||||||
return {
|
return {
|
||||||
@ -2718,7 +2747,7 @@ async def role_agent_run(project_id, role, agent_id=None, model_name=None):
|
|||||||
|
|
||||||
if not deliverable:
|
if not deliverable:
|
||||||
# 兜底:循环结束未 deliver,检测 write_file/git 实际产出构造真实交付件(而非占位符)
|
# 兜底:循环结束未 deliver,检测 write_file/git 实际产出构造真实交付件(而非占位符)
|
||||||
deliverable = await _build_fallback_deliverable(space_dir, written_files, role)
|
deliverable = await _build_fallback_deliverable(space_dir, written_files, role, task_id=task_id)
|
||||||
|
|
||||||
# ── 处理产出 ──
|
# ── 处理产出 ──
|
||||||
from appPublic.uniqueID import getID
|
from appPublic.uniqueID import getID
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user