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:
parent
493732e062
commit
7bb18d8839
@ -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 看不到刚问的
|
||||
# 问题,按更旧的上下文答非所问(用户答 C,agent 复读 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 = []
|
||||
|
||||
@ -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.*')时
|
||||
# 没有任何人类是字面 owner,owner 门禁会永久拒绝,导致用户无法管理自己产线里
|
||||
# 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,两者都可暂停。
|
||||
暂停前的原状态记入审计 detail(prev_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_progress(2026-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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user