fix(orchestration): 环境信息只卡部署,不卡需求/设计/开发
用户 2026-08-26 定调纠正了我原方案的错误。我原本提议把「env/*.json 无占位符」做成 requirement 阶段的结束条件(把缺陷堵在最便宜的阶段)——这是错的,会把死循环制度化: 主机/SSH账号/密码/部署路径是**外部输入**,requirement agent 根本产不出来。若要求它 填全才能通过,agent 就卡在无法解决的事情上无限重试。正是 2026-08-25 hrs7 的死循环成因: deploy_test 因 env 占位符失败 → PM 判「根因在 requirement」→ 回退整链并作废 owner 已 人工确认的三模块设计 → 重做需求后 env 依然是占位符 → 再次失败。 实际上 hrs7 的 requirement agent 做得对:填了能确定的(port 9187/dbname hrs7),未知项 标「待明确」并列入 pending 清单 —— 这是正确履职,被回退是冤枉的。 实现: 1. _check_deploy_env_ready 只对 agent.deploy_test/deploy_prod 生效,其余角色一律放行。 检查 ssh.host/user、deploy.path、db.host/user/dbname + ssh 凭据(password 或 key_file 二者其一) + requirement 自列的 pending 清单;domain/status 等非必需项不阻断。 2. 认领路径加此进入条件:不齐备 → 不进 running,任务置 waiting(界面可见,不在 submitted 静默打转) + 冒泡 deploy_env_required 人工任务给 owner,写清缺哪些字段、文件在哪, 并说明「补齐后自动继续,无需重跑上游阶段」。按 task_id 去重。 3. PM prompt 加硬约束:外部输入缺失禁止回退上游。回退前先自问「这个缺陷是上游 agent 能做对却做错了,还是本来就产不出?」——前者才回退,后者一律冒泡等人补。 验证:11 例离线用例全过(用 hrs7 线上真实 test.json 结构),覆盖 requirement/design/develop/test 四阶段占位符必须放行、部署阶段必须阻塞并列缺失字段、 补齐后放行、密钥文件替代密码、空密码无密钥阻塞、domain 缺失不阻断。
This commit is contained in:
parent
31eab89234
commit
ccba6f470e
@ -555,7 +555,9 @@ __ROLE_SKILLS__
|
||||
|
||||
## 回退规则(test 阶段)
|
||||
- 测试提交 Bug 后,零散 Bug → 走 Bug 闭环(report_bug→fix_bug→verify_bug),不打断任务链。
|
||||
- 系统性缺陷(Bug 多/严重、设计有缺陷、需求理解错、部署有问题)→ review_rollback,rollback_role 指定回退目标阶段(develop/design/requirement/deploy_test),回退点之后的关联任务将全部作废。"""
|
||||
- 系统性缺陷(Bug 多/严重、设计有缺陷、需求理解错、部署有问题)→ review_rollback,rollback_role 指定回退目标阶段(develop/design/requirement/deploy_test),回退点之后的关联任务将全部作废。
|
||||
- ⚠️ **外部输入缺失禁止回退上游**:部署主机/SSH账号/密码/部署路径/域名这类信息是**外部输入**,requirement/design/develop 三个阶段的 agent 根本产不出来(它们只能诚实标注「待明确」并列入 env/*.json 的 pending 清单,这是正确履职、不是缺陷)。这类阻塞由系统在部署任务的进入条件处自动冒泡人工任务给 owner 补齐,**你不要因此 review_rollback 到 requirement/design** —— 那会作废下游全部成果(含 owner 已人工确认的设计),而重做一遍后环境信息依然不会有,形成死循环(2026-08-25 hrs7 实际发生过)。正确处置:如实记录阻塞原因、raise_problem 冒泡,任务等 owner 补齐后自动继续。
|
||||
- 回退前先自问:**这个缺陷是上游 agent「能做对却做错了」,还是「本来就产不出」?** 前者才回退;后者一律冒泡等人补。"""
|
||||
|
||||
|
||||
QC_SYSTEM_PROMPT = """你是质量控制工程师(QC)。对交付件做合规检查和质量检查,不合规直接退回重做。
|
||||
@ -658,6 +660,118 @@ async def _task_deps_satisfied(sor, depends_on_raw, task_id=''):
|
||||
return True, ''
|
||||
|
||||
|
||||
PLACEHOLDER_MARKS = ('待明确', '待确认', '待补充', 'TODO', 'todo', 'xxx', 'XXX', '占位', '<', '未知')
|
||||
|
||||
|
||||
async def _check_deploy_env_ready(sor, project_id, role):
|
||||
"""部署类任务的进入条件:env/<环境>.json 的真实环境信息是否齐备。
|
||||
|
||||
⚠️ 设计定调(用户 2026-08-26 明确纠正,很重要):
|
||||
**环境信息只影响部署任务,不该阻断需求和开发。**
|
||||
|
||||
理由:主机/SSH账号/密码/部署路径是**外部输入**,requirement agent 根本产不出来。
|
||||
若把「env 必须填全」做成 requirement 的结束条件,agent 就会卡在它无法解决的事情上
|
||||
无限重试 —— 正是 2026-08-25 hrs7 的死循环成因(deploy_test 因 env 占位符失败 → PM
|
||||
判「根因在 requirement」→ 回退整链并作废 owner 已确认的设计 → 重做需求后 env 依然
|
||||
是占位符 → 再次失败)。把门禁前移会把这个死循环制度化。
|
||||
|
||||
正确处置:
|
||||
· requirement/design/develop 一律不检查 env 完整性,占位符可正常通过;
|
||||
requirement 只需诚实标注(hrs7 的 requirement agent 做对了:填了能确定的
|
||||
port/dbname,未知项标「待明确」并列入 pending 清单)。
|
||||
· 只有 deploy_test / deploy_prod 进入前检查;不齐备 → 不进 running,
|
||||
冒泡人工任务给能提供信息的人(owner/运维),任务置 waiting。
|
||||
· PM **不得**因外部输入缺失而回退上游阶段 —— 那不是任何 agent 阶段的产出缺陷。
|
||||
|
||||
返回 (ready: bool, missing: [字段名], env_file: str)
|
||||
"""
|
||||
if role not in ('agent.deploy_test', 'agent.deploy_prod'):
|
||||
return True, [], ''
|
||||
env_name = 'test' if role == 'agent.deploy_test' else 'prod'
|
||||
pdir = await _get_project_dir(sor, project_id)
|
||||
if not pdir:
|
||||
return True, [], '' # 取不到项目目录不阻断(宁放过不误杀)
|
||||
import os as _os
|
||||
env_file = _os.path.join(pdir, 'env', f'{env_name}.json')
|
||||
if not _os.path.isfile(env_file):
|
||||
return False, [f'env/{env_name}.json 文件不存在'], env_file
|
||||
try:
|
||||
with open(env_file, encoding='utf-8') as f:
|
||||
cfg = json.load(f)
|
||||
except Exception as e:
|
||||
return False, [f'env/{env_name}.json 解析失败:{e}'], env_file
|
||||
|
||||
missing = []
|
||||
|
||||
def _bad(v):
|
||||
if v is None:
|
||||
return False # null 合法(如 key_file=null 表示用密码登录)
|
||||
if isinstance(v, str):
|
||||
s = v.strip()
|
||||
if not s:
|
||||
return True
|
||||
return any(m in s for m in PLACEHOLDER_MARKS)
|
||||
return False
|
||||
|
||||
# 部署真正必需的字段(其余如 domain/status 缺失不阻断部署)
|
||||
REQUIRED = (('ssh', 'host'), ('ssh', 'user'), ('deploy', 'path'),
|
||||
('db', 'host'), ('db', 'user'), ('db', 'dbname'))
|
||||
for path in REQUIRED:
|
||||
cur = cfg
|
||||
for k in path:
|
||||
cur = cur.get(k) if isinstance(cur, dict) else None
|
||||
if cur is None or _bad(cur):
|
||||
missing.append('.'.join(path))
|
||||
# SSH 凭据:password 与 key_file 二者其一即可(key_file=null 表示用密码登录)
|
||||
_ssh = cfg.get('ssh') or {}
|
||||
_pwd_ok = not _bad(_ssh.get('password')) and bool(str(_ssh.get('password') or '').strip())
|
||||
_key = _ssh.get('key_file')
|
||||
_key_ok = bool(str(_key).strip()) and not _bad(_key) if isinstance(_key, str) else False
|
||||
if not (_pwd_ok or _key_ok):
|
||||
missing.append('ssh.password 或 ssh.key_file(二者其一)')
|
||||
# requirement 自己列的 pending 清单也纳入(它最清楚哪些没定)
|
||||
for p in (cfg.get('pending') or []):
|
||||
if isinstance(p, str) and p.strip() and p.strip() not in missing:
|
||||
_k = p.strip()
|
||||
if any(_k.startswith(x) for x in ('ssh.', 'deploy.', 'db.')):
|
||||
if _k not in missing:
|
||||
missing.append(_k)
|
||||
return (not missing), missing, env_file
|
||||
|
||||
|
||||
async def _bubble_deploy_env_missing(sor, project_id, task_id, task_title, missing, env_file):
|
||||
"""部署环境信息缺失 → 冒泡给 owner 补齐(去重)。
|
||||
|
||||
这是「逃逸阀」而非「门禁」:任务不会静默死等,owner 待办里会出现一条明确的
|
||||
「补齐部署环境信息」任务,写清缺哪些字段、文件在哪。
|
||||
"""
|
||||
from .human_task_capability import create_human_task
|
||||
exists = await sor.sqlExe(
|
||||
"SELECT id FROM pipeline_human_tasks WHERE project_id=${pid}$ AND task_id=${tid}$ "
|
||||
"AND task_type='deploy_env_required' AND status='pending' LIMIT 1",
|
||||
{"pid": project_id, "tid": task_id})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
if exists:
|
||||
return
|
||||
_own = await sor.sqlExe("SELECT created_by FROM sd_projects WHERE id=${pid}$", {"pid": project_id})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
owner_id = (getattr(_own[0], 'created_by', '') if _own else '') or 'user-01'
|
||||
await create_human_task(
|
||||
project_id,
|
||||
f"补齐部署环境信息:{task_title or task_id[:8]}",
|
||||
"部署任务无法开始:真实部署环境信息尚未提供(这属于外部输入,agent 无法自行产出,"
|
||||
"因此不回退需求/设计/开发阶段,只等这里补齐)。\n\n"
|
||||
f"配置文件:{env_file}\n\n"
|
||||
"缺失字段:\n" + "\n".join(f" · {m}" for m in missing) +
|
||||
"\n\n补齐后本任务会自动继续(无需重跑上游阶段)。",
|
||||
task_type='deploy_env_required',
|
||||
assignee_id=owner_id,
|
||||
created_by='system.orchestrator',
|
||||
task_id=task_id,
|
||||
)
|
||||
logger.warning(f"部署环境信息缺失冒泡: task={task_id} missing={missing}")
|
||||
|
||||
|
||||
async def _bubble_blocked_dependency(sor, project_id, task_id, task_title, reason):
|
||||
"""逃逸阀:任务依赖「永不可达」时冒泡人工任务,避免静默死锁。
|
||||
|
||||
@ -713,10 +827,28 @@ async def _claim_task(sor, tenant_id, role, state='submitted', match_role=True,
|
||||
_tid = getattr(rec, 'id', '')
|
||||
ok_dep, why = await _task_deps_satisfied(
|
||||
sor, getattr(rec, 'depends_on', '') or '', task_id=_tid)
|
||||
if ok_dep:
|
||||
task = rec
|
||||
break
|
||||
blocked.append((_tid, getattr(rec, 'title', '') or '', why))
|
||||
if not ok_dep:
|
||||
blocked.append((_tid, getattr(rec, 'title', '') or '', why))
|
||||
continue
|
||||
# 进入条件(部署类):真实部署环境信息必须齐备。
|
||||
# 环境信息属外部输入,只卡部署、不卡需求/设计/开发(用户 2026-08-26 定调)。
|
||||
_rrole = getattr(rec, 'role', '') or role
|
||||
env_ok, env_missing, env_file = await _check_deploy_env_ready(sor, tenant_id, _rrole)
|
||||
if not env_ok:
|
||||
_rtitle = getattr(rec, 'title', '') or ''
|
||||
try:
|
||||
await _bubble_deploy_env_missing(
|
||||
sor, tenant_id, _tid, _rtitle, env_missing, env_file)
|
||||
# 置 waiting:让「卡住」在界面上可见,不在 submitted 里静默打转
|
||||
await sor.sqlExe(
|
||||
"UPDATE pipeline_tasks SET state='waiting', updated_at=NOW() "
|
||||
"WHERE id=${i}$ AND state='submitted'", {"i": _tid})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
except Exception as e:
|
||||
logger.warning(f"_bubble_deploy_env_missing failed task={_tid}: {e}")
|
||||
continue
|
||||
task = rec
|
||||
break
|
||||
# 逃逸阀:依赖「永不可达」(不存在/已作废/自依赖)的任务会永久 submitted 且无人知晓,
|
||||
# 与「自动 pause 无出口」是同一类活性缺陷。这里冒泡人工任务,让 owner 能看到并修正。
|
||||
for _tid, _ti, _why in blocked:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user