fix(workspace): 悬空项目引用防护——解析层校验+自愈清理+删除时清指针
根因:删除项目后不清理 pipeline_session_settings/pipeline_agent_settings 里的 当前项目指针;解析逻辑(会话级优先)又不校验项目是否存在,悬空引用遮蔽有效 的全局设置,导致工作空间报「请先在会话中切换项目」(实测复现:孤儿项目 0x0EISBKAOsvHzW09dIOS 卡死会话级解析)。 修复(根治,三层): 1. get_session_project_id 加存在性校验:悬空记录自愈清理并回退全局, 查询异常保守放行不误伤 2. get_session_context 收敛为复用前者,消除重复实现的语义漂移 3. delete_project 删项目时清空两张设置表的指针;孤儿清扫清单纳入这两表
This commit is contained in:
parent
58d773b6ad
commit
98b90d4f21
@ -444,6 +444,11 @@ async def delete_project(project_id, who=None, agent_id=None, confirm=False):
|
||||
"DELETE FROM pipeline_tasks WHERE tenant_id=${pid}$",
|
||||
"DELETE FROM audit_log WHERE tenant_id COLLATE utf8mb4_unicode_ci=${pid}$",
|
||||
"DELETE FROM sd_iterations WHERE project_id=${pid}$",
|
||||
# 会话/全局「当前项目」指针:清空而非删行(行里还有 iteration/llm 等其他设置)。
|
||||
# 不清理会产生悬空引用:下次打开工作空间/菜单时按会话解析仍命中已删项目,
|
||||
# 报「请先在会话中切换项目」——这正是本 bug 的源头。
|
||||
"UPDATE pipeline_session_settings SET current_project_id='' WHERE current_project_id=${pid}$",
|
||||
"UPDATE pipeline_agent_settings SET current_project_id='' WHERE current_project_id=${pid}$",
|
||||
"DELETE FROM sd_projects WHERE id=${pid}$",
|
||||
]
|
||||
for sql in direct:
|
||||
@ -495,6 +500,10 @@ _ORPHAN_CLEANUPS = [
|
||||
("sd_features(无任务)", "sd_features", "task_id", "pipeline_tasks", False),
|
||||
("sd_iterations(无任务)", "sd_iterations", "task_id", "pipeline_tasks", False),
|
||||
("sd_conversations(无任务)", "sd_conversations", "task_id", "pipeline_tasks", True),
|
||||
# 「当前项目」指针:指向已删项目的会话/全局设置记录。解析层已能自愈回退,
|
||||
# 这里清扫是存量脏数据的批量兜底(历史 CRUD 删除不清理指针留下的)。
|
||||
("pipeline_session_settings(无项目)", "pipeline_session_settings", "current_project_id", "sd_projects", True),
|
||||
("pipeline_agent_settings(无项目)", "pipeline_agent_settings", "current_project_id", "sd_projects", True),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@ -105,7 +105,23 @@ async def get_session_project_id(sor, uid, session_id=''):
|
||||
|
||||
session_id 非空时优先读 pipeline_session_settings(web 多 tab 各自项目上下文),
|
||||
无记录或表不存在时回退全局 pipeline_agent_settings.current_project_id。
|
||||
|
||||
存在性校验(悬空引用防护):记录指向的项目若已从 sd_projects 删除,
|
||||
视为无效——顺手清掉死记录(自愈,避免每次请求重复命中),继续回退全局;
|
||||
全局也无效则返回 ''。若不清理,删除项目后该会话的所有项目相关入口
|
||||
(工作空间/任务/菜单)都会被死引用遮蔽,报「请先在会话中切换项目」。
|
||||
"""
|
||||
async def _valid(pid):
|
||||
"""pid 对应项目存在则原样返回;不存在返回 '';查询异常保守放行(不因校验误伤功能)。"""
|
||||
if not pid:
|
||||
return ''
|
||||
try:
|
||||
recs = await sor.sqlExe(
|
||||
"SELECT 1 FROM sd_projects WHERE id=${p}$ LIMIT 1", {"p": pid})
|
||||
return pid if recs else ''
|
||||
except Exception:
|
||||
return pid
|
||||
|
||||
if session_id:
|
||||
try:
|
||||
recs = await sor.sqlExe(
|
||||
@ -115,14 +131,37 @@ async def get_session_project_id(sor, uid, session_id=''):
|
||||
if recs:
|
||||
pid = getattr(recs[0], 'current_project_id', '') or ''
|
||||
if pid:
|
||||
return pid
|
||||
vpid = await _valid(pid)
|
||||
if vpid:
|
||||
return vpid
|
||||
# 悬空引用:清掉死记录,回退全局
|
||||
try:
|
||||
await sor.sqlExe(
|
||||
"DELETE FROM pipeline_session_settings "
|
||||
"WHERE user_id=${u}$ AND session_id=${s}$",
|
||||
{"u": uid, "s": session_id})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
recs = await sor.sqlExe(
|
||||
"SELECT current_project_id FROM pipeline_agent_settings WHERE user_id=${u}$",
|
||||
{"u": uid})
|
||||
return getattr(recs[0], 'current_project_id', '') if recs else ''
|
||||
pid = getattr(recs[0], 'current_project_id', '') if recs else ''
|
||||
if pid:
|
||||
vpid = await _valid(pid)
|
||||
if vpid:
|
||||
return vpid
|
||||
try:
|
||||
await sor.sqlExe(
|
||||
"UPDATE pipeline_agent_settings SET current_project_id='' "
|
||||
"WHERE user_id=${u}$", {"u": uid})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
except Exception:
|
||||
pass
|
||||
return ''
|
||||
except Exception:
|
||||
return ''
|
||||
|
||||
@ -130,33 +169,37 @@ async def get_session_project_id(sor, uid, session_id=''):
|
||||
async def get_session_context(sor, uid, session_id=''):
|
||||
"""按会话解析 (project_id, iteration_id)。per-session 优先,回退 per-user 全局。
|
||||
|
||||
多 tab 隔离:session_id 非空时优先读 pipeline_session_settings(每个 user+session 一条),
|
||||
无记录回退 pipeline_agent_settings.current_project_id/current_iteration_id。
|
||||
项目 id 统一走 get_session_project_id(含悬空引用防护:指向已删除项目的
|
||||
记录视为无效并自愈清理)。禁止在本函数里重复实现解析逻辑——两份实现
|
||||
语义漂移正是「会话有项目、工作空间说没有」这类 bug 的根因。
|
||||
iteration_id 只从提供有效 pid 的同一条记录取,避免 pid 与 iid 来自不同
|
||||
记录造成错配。
|
||||
"""
|
||||
pid = await get_session_project_id(sor, uid, session_id)
|
||||
if not pid:
|
||||
return '', ''
|
||||
iid = ''
|
||||
if session_id:
|
||||
try:
|
||||
recs = await sor.sqlExe(
|
||||
"SELECT current_project_id, current_iteration_id FROM pipeline_session_settings "
|
||||
"WHERE user_id=${u}$ AND session_id=${s}$",
|
||||
{"u": uid, "s": session_id})
|
||||
if recs:
|
||||
pid = getattr(recs[0], 'current_project_id', '') or ''
|
||||
if recs and (getattr(recs[0], 'current_project_id', '') or '') == pid:
|
||||
iid = getattr(recs[0], 'current_iteration_id', '') or ''
|
||||
if pid or iid:
|
||||
return pid, iid
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
recs = await sor.sqlExe(
|
||||
"SELECT current_project_id, current_iteration_id FROM pipeline_agent_settings "
|
||||
"WHERE user_id=${u}$",
|
||||
{"u": uid})
|
||||
if recs:
|
||||
return (getattr(recs[0], 'current_project_id', '') or '',
|
||||
getattr(recs[0], 'current_iteration_id', '') or '')
|
||||
except Exception:
|
||||
pass
|
||||
return '', ''
|
||||
if not iid:
|
||||
try:
|
||||
recs = await sor.sqlExe(
|
||||
"SELECT current_project_id, current_iteration_id FROM pipeline_agent_settings "
|
||||
"WHERE user_id=${u}$",
|
||||
{"u": uid})
|
||||
if recs and (getattr(recs[0], 'current_project_id', '') or '') == pid:
|
||||
iid = getattr(recs[0], 'current_iteration_id', '') or ''
|
||||
except Exception:
|
||||
pass
|
||||
return pid, iid
|
||||
|
||||
|
||||
async def get_workspace_dir(sor, uid, session_id=''):
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user