From 7bb18d88390ac6e2f6c0c2a8a2337f62737b69c3 Mon Sep 17 00:00:00 2001 From: ymq Date: Tue, 1 Sep 2026 15:29:59 +0800 Subject: [PATCH] =?UTF-8?q?fix(=E9=A1=B9=E7=9B=AE=E7=AE=A1=E7=90=86):=20?= =?UTF-8?q?=E7=A1=AE=E8=AE=A4=E6=AD=BB=E5=BE=AA=E7=8E=AF=E6=A0=B9=E6=B2=BB?= =?UTF-8?q?=E5=9B=9B=E4=BB=B6=E5=A5=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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→降级同机构人类可操作(否则永久拒绝) --- pipeline_service/agent_loop_v2.py | 15 ++-- pipeline_service/project_capability.py | 103 +++++++++++++++++++++++-- 2 files changed, 105 insertions(+), 13 deletions(-) diff --git a/pipeline_service/agent_loop_v2.py b/pipeline_service/agent_loop_v2.py index acc84a6..c1639bc 100644 --- a/pipeline_service/agent_loop_v2.py +++ b/pipeline_service/agent_loop_v2.py @@ -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 = [] diff --git a/pipeline_service/project_capability.py b/pipeline_service/project_capability.py index 029c296..ff7fde7 100644 --- a/pipeline_service/project_capability.py +++ b/pipeline_service/project_capability.py @@ -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,