mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-08 23:33:16 +08:00
fix(chat): allow administrator to delete channel-only sessions
Align administrator session deletion with the administrator chat view so sessions surfaced through channel_session_v2 can be removed without a ui_session_owner row. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
4f36f4a36f
commit
e3828418d4
5 changed files with 93 additions and 4 deletions
|
|
@ -984,8 +984,10 @@ def include_chat_routes(router: APIRouter, *, resolve_auth: Callable[[SqliteStor
|
||||||
if not sess:
|
if not sess:
|
||||||
raise HTTPException(status_code=404, detail="session_not_found")
|
raise HTTPException(status_code=404, detail="session_not_found")
|
||||||
if _is_administrator_chat_viewer(ctx):
|
if _is_administrator_chat_viewer(ctx):
|
||||||
deleted = store.delete_session_for_administrator_username(
|
deleted = store.delete_session_for_administrator_chat_view(
|
||||||
session_id=session_id, username=_chat_username(ctx)
|
session_id=session_id,
|
||||||
|
username=_chat_username(ctx),
|
||||||
|
tenant_id=tenant_id,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
deleted = store.delete_session_for_user(
|
deleted = store.delete_session_for_user(
|
||||||
|
|
@ -994,8 +996,11 @@ def include_chat_routes(router: APIRouter, *, resolve_auth: Callable[[SqliteStor
|
||||||
if not deleted:
|
if not deleted:
|
||||||
raise HTTPException(status_code=404, detail="session_not_found")
|
raise HTTPException(status_code=404, detail="session_not_found")
|
||||||
if _is_administrator_chat_viewer(ctx):
|
if _is_administrator_chat_viewer(ctx):
|
||||||
remaining = store.list_sessions_for_administrator_username(
|
remaining = store.list_sessions_for_administrator_chat_view(
|
||||||
username=_chat_username(ctx), limit=1, offset=0
|
username=_chat_username(ctx),
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
limit=1,
|
||||||
|
offset=0,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
remaining = store.list_sessions_for_user(
|
remaining = store.list_sessions_for_user(
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,7 @@ class AssistantStoreProtocol(Protocol):
|
||||||
def delete_memory_item(self, *, memory_id: 'str') -> 'int': ...
|
def delete_memory_item(self, *, memory_id: 'str') -> 'int': ...
|
||||||
def delete_message(self, *, session_id: 'str', message_id: 'int') -> 'bool': ...
|
def delete_message(self, *, session_id: 'str', message_id: 'int') -> 'bool': ...
|
||||||
def delete_session(self, session_id: 'str') -> 'None': ...
|
def delete_session(self, session_id: 'str') -> 'None': ...
|
||||||
|
def delete_session_for_administrator_chat_view(self, *, session_id: 'str', username: 'str', tenant_id: 'str') -> 'bool': ...
|
||||||
def delete_session_for_user(self, *, session_id: 'str', tenant_id: 'str', user_id: 'str') -> 'bool': ...
|
def delete_session_for_user(self, *, session_id: 'str', tenant_id: 'str', user_id: 'str') -> 'bool': ...
|
||||||
def delete_session_in_tenant(self, *, session_id: 'str', tenant_id: 'str') -> 'bool': ...
|
def delete_session_in_tenant(self, *, session_id: 'str', tenant_id: 'str') -> 'bool': ...
|
||||||
def delete_setting(self, key: 'str') -> 'None': ...
|
def delete_setting(self, key: 'str') -> 'None': ...
|
||||||
|
|
|
||||||
|
|
@ -614,6 +614,31 @@ class ChatSessionsSaRepository:
|
||||||
conn.execute(delete(chat_session).where(chat_session.c.id == sid))
|
conn.execute(delete(chat_session).where(chat_session.c.id == sid))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def try_delete_chat_session_for_administrator_chat_view(
|
||||||
|
self, *, session_id: str, username: str, tenant_id: str
|
||||||
|
) -> bool:
|
||||||
|
sid = str(session_id or "").strip()
|
||||||
|
if not sid:
|
||||||
|
return False
|
||||||
|
ids = self._administrator_chat_session_ids_subquery(
|
||||||
|
username=username,
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
)
|
||||||
|
with self._engine.begin() as conn:
|
||||||
|
chk = conn.execute(
|
||||||
|
select(1)
|
||||||
|
.select_from(chat_session)
|
||||||
|
.where(
|
||||||
|
chat_session.c.id == sid,
|
||||||
|
chat_session.c.id.in_(select(ids.c.sid)),
|
||||||
|
)
|
||||||
|
.limit(1)
|
||||||
|
).first()
|
||||||
|
if not chk:
|
||||||
|
return False
|
||||||
|
conn.execute(delete(chat_session).where(chat_session.c.id == sid))
|
||||||
|
return True
|
||||||
|
|
||||||
def try_delete_chat_session_for_tenant(self, *, session_id: str, tenant_id: str) -> bool:
|
def try_delete_chat_session_for_tenant(self, *, session_id: str, tenant_id: str) -> bool:
|
||||||
sid, tid = str(session_id or "").strip(), str(tenant_id)
|
sid, tid = str(session_id or "").strip(), str(tenant_id)
|
||||||
if not sid:
|
if not sid:
|
||||||
|
|
|
||||||
|
|
@ -1666,6 +1666,15 @@ class SqliteStore(ScheduledJobStoreMixin):
|
||||||
session_id=session_id, username=username, tenant_id=tenant_id
|
session_id=session_id, username=username, tenant_id=tenant_id
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def delete_session_for_administrator_chat_view(
|
||||||
|
self, *, session_id: str, username: str, tenant_id: str
|
||||||
|
) -> bool:
|
||||||
|
return self._chat_sessions_repo().try_delete_chat_session_for_administrator_chat_view(
|
||||||
|
session_id=session_id,
|
||||||
|
username=username,
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
)
|
||||||
|
|
||||||
def get_session_for_administrator_username(
|
def get_session_for_administrator_username(
|
||||||
self, *, session_id: str, username: str
|
self, *, session_id: str, username: str
|
||||||
) -> Optional[ChatSession]:
|
) -> Optional[ChatSession]:
|
||||||
|
|
|
||||||
|
|
@ -76,3 +76,52 @@ def test_delete_session_for_administrator_username_cross_tenant(
|
||||||
assert s.get_session_for_administrator_username(session_id=str(other.id), username="administrator") is None
|
assert s.get_session_for_administrator_username(session_id=str(other.id), username="administrator") is None
|
||||||
meta = s.get_sessions_list_meta_for_administrator_username(username="administrator")
|
meta = s.get_sessions_list_meta_for_administrator_username(username="administrator")
|
||||||
assert int(meta.session_count or 0) == 1
|
assert int(meta.session_count or 0) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_channel_only_session_for_administrator_chat_view(
|
||||||
|
store_two_tenants: SqliteStore,
|
||||||
|
) -> None:
|
||||||
|
s = store_two_tenants
|
||||||
|
team = next(
|
||||||
|
x for x in s.list_sessions_for_administrator_username(username="administrator", limit=50)
|
||||||
|
if x.title == "team-sess"
|
||||||
|
)
|
||||||
|
team_owner = s.get_ui_session_owner(session_id=str(team.id))
|
||||||
|
login_tid = str((team_owner or {}).get("tenant_id") or "")
|
||||||
|
assert login_tid
|
||||||
|
|
||||||
|
channel_sid = s.get_or_create_channel_session_v2(
|
||||||
|
tenant_id=login_tid,
|
||||||
|
channel="scheduled_job",
|
||||||
|
account_id="job",
|
||||||
|
external_chat_id="job-123",
|
||||||
|
external_user_id="job-123",
|
||||||
|
session_title="Scheduled · job-123",
|
||||||
|
)
|
||||||
|
assert s.get_ui_session_owner(session_id=channel_sid) is None
|
||||||
|
assert (
|
||||||
|
s.get_session_for_administrator_chat_view(
|
||||||
|
session_id=channel_sid,
|
||||||
|
username="administrator",
|
||||||
|
tenant_id=login_tid,
|
||||||
|
)
|
||||||
|
is not None
|
||||||
|
)
|
||||||
|
|
||||||
|
assert (
|
||||||
|
s.delete_session_for_administrator_chat_view(
|
||||||
|
session_id=channel_sid,
|
||||||
|
username="administrator",
|
||||||
|
tenant_id=login_tid,
|
||||||
|
)
|
||||||
|
is True
|
||||||
|
)
|
||||||
|
assert s.get_session(channel_sid) is None
|
||||||
|
assert (
|
||||||
|
s.get_session_for_administrator_chat_view(
|
||||||
|
session_id=channel_sid,
|
||||||
|
username="administrator",
|
||||||
|
tenant_id=login_tid,
|
||||||
|
)
|
||||||
|
is None
|
||||||
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue