fix(项目管理): 确认死循环根治四件套

- fix1 _load_history: ASC LIMIT取最旧N条→DESC LIMIT取最新N条再倒回正序(会话超历史上限后LLM看不到用户刚答的确认,复读旧回复)
- fix2 delete_project描述: 去掉'先复述确认再调用'指令(平台确认门已弹窗给用户,LLM再问=死循环),改为直接confirm=true调用
- fix3 pause/resume跨产线状态兼容: 进行中不硬编码active(S_ACTIVE_LIKE含in_progress);暂停前原状态记审计detail,恢复时还原原状态(投标项目不会被改成状态机外的active)
- fix4 owner门禁降级: created_by='agent.*'的项目无人类字面owner→降级同机构人类可操作(否则永久拒绝)
This commit is contained in:
ymq 2026-09-01 15:29:59 +08:00
parent 493732e062
commit 7bb18d8839
2 changed files with 105 additions and 13 deletions

View File

@ -1077,8 +1077,8 @@ class AgentExecutor:
async def _t_delete_project(self, sor, p, pid):
confirm = p.get("confirm")
if str(confirm).lower() not in ("true", "1", "yes"):
return ("FAIL: 删除项目不可撤销。请先向用户复述项目名与后果"
"取得明确确认后再带 confirm=true 调用本工具")
return ("FAIL: 参数错误,请直接带 confirm=true 调用(平台确认门会弹窗向用户最终确认"
"无需你先向用户复述确认)")
tpid, pname, err = await self._resolve_owned_project(sor, p)
if err:
return f"FAIL: {err}"
@ -1453,6 +1453,10 @@ class AgentExecutor:
db = DBPools()
async with db.sqlorContext("pipeline") as sor:
# 会话级隔离web 多 tab 独立会话)
# 注意2026-09-01 修复):必须 ORDER BY created_at DESC LIMIT N 取「最新」N 条
# 再倒序回时间正序。历史用 ASC LIMIT 会取「最旧」N 条——会话消息超过
# history_limit 后,最近的提问(如 A/B/C 选项被截掉LLM 看不到刚问的
# 问题,按更旧的上下文答非所问(用户答 Cagent 复读 4 小时前的旧回复)。
if self.session_id:
# 产线隔离2026-08-31 修复跨产线串扰):各产线页面默认 tab 共用
# session_id='default',历史若不按产线过滤,投标页会加载开发页的
@ -1464,7 +1468,7 @@ class AgentExecutor:
if self.pipeline_id and not self.generic:
sql += "AND pipeline_id=${pl}$ "
params["pl"] = self.pipeline_id
sql += "ORDER BY created_at ASC LIMIT ${lim}$"
sql += "ORDER BY created_at DESC LIMIT ${lim}$"
recs = await sor.sqlExe(sql, params)
else:
# 项目隔离
@ -1473,19 +1477,20 @@ class AgentExecutor:
recs = await sor.sqlExe(
"SELECT role, content FROM pipeline_conversations "
"WHERE iteration_id=${pid}$ AND created_by=${uid}$ "
"ORDER BY created_at ASC LIMIT ${lim}$",
"ORDER BY created_at DESC LIMIT ${lim}$",
{"pid": pid, "uid": self.user_id or "", "lim": self.config.history_limit},
)
else:
recs = await sor.sqlExe(
"SELECT role, content FROM pipeline_conversations "
"WHERE created_by=${uid}$ "
"ORDER BY created_at ASC LIMIT ${lim}$",
"ORDER BY created_at DESC LIMIT ${lim}$",
{"uid": self.user_id or "", "lim": self.config.history_limit},
)
if not recs:
return []
recs = list(reversed(recs)) # DESC 取出 → 倒回时间正序
# 过滤 tool_call JSON 污染(关键!)
msgs = []

View File

@ -35,6 +35,11 @@ S_PAUSED = "paused" # 已暂停暂停推进PM 不再推进项目
S_COMPLETED = "completed" # 已完成
S_ARCHIVED = "archived" # 已归档
# 「进行中」状态集合跨产线状态兼容2026-09-01
# SDLC 产线用 active投标/商机等产线用 in_progress。
# pause/resume 必须对两者都生效,否则非 SDLC 产线项目无法暂停。
S_ACTIVE_LIKE = {S_ACTIVE, "in_progress"}
def _get_db():
db = DBPools()
@ -125,7 +130,7 @@ async def check_project_owner(project_id, user_id, sor=None):
async def _check(_sor):
recs = await _sor.sqlExe(
"SELECT created_by, name FROM sd_projects WHERE id=${pid}$",
"SELECT created_by, name, org_id FROM sd_projects WHERE id=${pid}$",
{"pid": project_id})
await _sor.sqlExe("COMMIT", {})
if not recs:
@ -133,6 +138,19 @@ async def check_project_owner(project_id, user_id, sor=None):
owner = getattr(recs[0], 'created_by', '') or ''
if owner == user_id:
return True, ''
# owner 降级2026-09-01 修复):项目由 agent 创建created_by='agent.*')时
# 没有任何人类是字面 ownerowner 门禁会永久拒绝,导致用户无法管理自己产线里
# agent 建的项目。此时降级为「同机构人类」:项目 org_id == 请求用户的 orgid 即放行。
if owner.startswith("agent."):
proj_org = str(getattr(recs[0], 'org_id', '') or '')
u = await _sor.sqlExe(
"SELECT orgid FROM users WHERE id=${uid}$", {"uid": user_id})
await _sor.sqlExe("COMMIT", {})
if u:
user_org = str(getattr(u[0], 'orgid', '') or '')
if user_org and user_org == proj_org:
return True, ''
return False, "项目由 agent 创建,仅同机构成员可操作"
return False, "仅项目 owner 可执行此操作"
if sor is not None:
@ -221,16 +239,85 @@ async def reopen_project(project_id, who=None, agent_id=None):
async def pause_project(project_id, who=None, agent_id=None):
"""暂停推进active → paused。暂停后 PM 不再推进项目(不认领审核任务、不创建后续任务),
直到 resume_project 恢复仅用户明确指令暂停推进时才调用默认必须推进无需指令"""
return await _transition(project_id, S_ACTIVE, S_PAUSED, 'pause',
who=who, agent_id=agent_id)
"""暂停推进:进行中 → paused。暂停后 PM 不再推进项目(不认领审核任务、不创建后续任务),
直到 resume_project 恢复仅用户明确指令暂停推进时才调用默认必须推进无需指令
跨产线状态兼容2026-09-01 修复进行中状态不硬编码 active
SDLC 产线是 active投标/商机等产线是 in_progress两者都可暂停
暂停前的原状态记入审计 detailprev_state=xxx恢复时据此还原
"""
if not project_id:
return False, "缺少 project_id"
db, dbname = _get_db()
async with db.sqlorContext(dbname) as sor:
recs = await sor.R(TABLE, {'id': project_id})
if not recs:
await sor.sqlExe("COMMIT", {})
return False, "项目不存在"
cur = getattr(recs[0], 'status', '') or ''
if cur not in S_ACTIVE_LIKE:
await sor.sqlExe("COMMIT", {})
return False, f"无法暂停:项目状态 {cur}(仅 {sorted(S_ACTIVE_LIKE)} 可暂停)"
await sor.sqlExe(
f"UPDATE {TABLE} SET status=${{to}}$, updated_at=NOW() "
"WHERE id=${pid}$ AND status=${from}$",
{"to": S_PAUSED, "pid": project_id, "from": cur})
recs2 = await sor.R(TABLE, {'id': project_id})
await sor.sqlExe("COMMIT", {})
if getattr(recs2[0], 'status', '') != S_PAUSED:
return False, f"状态迁移失败(CAS): 期望 to={S_PAUSED} 实际 status={getattr(recs2[0], 'status', '')}"
# 原状态记入审计(恢复时还原的依据)
await record_audit(project_id, TABLE, project_id, 'pause',
from_state=cur, to_state=S_PAUSED,
who=_normalize_role(who), agent_id=agent_id,
detail=f"prev_state={cur}", sor=sor)
return True, S_PAUSED
async def resume_project(project_id, who=None, agent_id=None):
"""恢复推进paused → active。"""
return await _transition(project_id, S_PAUSED, S_ACTIVE, 'resume',
who=who, agent_id=agent_id)
"""恢复推进paused → 原状态(优先还原暂停前的状态,跨产线兼容)。
SDLC 项目还原为 active投标/商机项目若暂停前是 in_progress 则还原为
in_progress2026-09-01原实现硬编码还原 active会把投标项目
改成产线状态机里不存在的状态查不到暂停审计时兜底还原 active
"""
if not project_id:
return False, "缺少 project_id"
db, dbname = _get_db()
async with db.sqlorContext(dbname) as sor:
recs = await sor.R(TABLE, {'id': project_id})
if not recs:
await sor.sqlExe("COMMIT", {})
return False, "项目不存在"
cur = getattr(recs[0], 'status', '') or ''
if cur != S_PAUSED:
await sor.sqlExe("COMMIT", {})
return False, f"无法恢复:项目状态 {cur}(仅 {S_PAUSED} 可恢复)"
# 查最近一次暂停审计,还原暂停前状态
restore_to = S_ACTIVE
audits = await sor.sqlExe(
"SELECT detail FROM audit_log WHERE tenant_id=${pid}$ AND entity=${e}$ "
"AND action='pause' ORDER BY created_at DESC LIMIT 1",
{"pid": project_id, "e": TABLE})
await sor.sqlExe("COMMIT", {})
if audits:
d = getattr(audits[0], 'detail', '') or ''
if d.startswith("prev_state="):
prev = d[len("prev_state="):].strip()
if prev in S_ACTIVE_LIKE:
restore_to = prev
await sor.sqlExe(
f"UPDATE {TABLE} SET status=${{to}}$, updated_at=NOW() "
"WHERE id=${pid}$ AND status=${from}$",
{"to": restore_to, "pid": project_id, "from": S_PAUSED})
recs2 = await sor.R(TABLE, {'id': project_id})
await sor.sqlExe("COMMIT", {})
if getattr(recs2[0], 'status', '') != restore_to:
return False, f"状态迁移失败(CAS): 期望 to={restore_to} 实际 status={getattr(recs2[0], 'status', '')}"
await record_audit(project_id, TABLE, project_id, 'resume',
from_state=S_PAUSED, to_state=restore_to,
who=_normalize_role(who), agent_id=agent_id, sor=sor)
return True, restore_to
async def set_project_state(project_id, from_state, to_state,