mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 23:47:41 +08:00
fix: preserve legacy model query ABI
This commit is contained in:
@@ -31,6 +31,18 @@ _R = TypeVar("_R")
|
|||||||
# 接管、返回值原样透传」。否则调用方传 None 或传异步会话都会被判成类型不符,而这恰恰是
|
# 接管、返回值原样透传」。否则调用方传 None 或传异步会话都会被判成类型不符,而这恰恰是
|
||||||
# 装饰器存在的理由(各 Oper 的 self._db 常态就是 None)。
|
# 装饰器存在的理由(各 Oper 的 self._db 常态就是 None)。
|
||||||
|
|
||||||
|
|
||||||
|
def run_legacy_sync_query(operation: Callable[[Session], _R]) -> _R:
|
||||||
|
"""为已移除查询装饰器的旧 Model ABI 提供一次性同步会话。"""
|
||||||
|
db = ScopedSession()
|
||||||
|
try:
|
||||||
|
return operation(db)
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
db.close()
|
||||||
|
except Exception as close_err: # noqa: BLE001 兼容查询释放失败不改变返回语义
|
||||||
|
logger.error(f"释放数据库会话失败:{close_err}")
|
||||||
|
|
||||||
def _get_args_db(
|
def _get_args_db(
|
||||||
args: tuple[Any, ...],
|
args: tuple[Any, ...],
|
||||||
kwargs: dict[str, Any],
|
kwargs: dict[str, Any],
|
||||||
|
|||||||
+29
-11
@@ -4,6 +4,7 @@ from sqlalchemy import Boolean, Index, Integer, String, Text, select, update
|
|||||||
from sqlalchemy.orm import Mapped, Session, mapped_column
|
from sqlalchemy.orm import Mapped, Session, mapped_column
|
||||||
|
|
||||||
from app.db.base import Base, execute_dml, get_id_column
|
from app.db.base import Base, execute_dml, get_id_column
|
||||||
|
from app.db.decorators import run_legacy_sync_query
|
||||||
|
|
||||||
|
|
||||||
def _get_for_user_statement(
|
def _get_for_user_statement(
|
||||||
@@ -86,30 +87,47 @@ class AgentTask(Base):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def get_for_user(
|
def get_for_user(
|
||||||
cls,
|
cls,
|
||||||
db: Session,
|
db: Session | int | None = None,
|
||||||
task_id: int,
|
task_id: int | None = None,
|
||||||
user_id: Optional[str] = None,
|
user_id: Optional[str] = None,
|
||||||
) -> Optional["AgentTask"]:
|
) -> Optional["AgentTask"]:
|
||||||
"""
|
"""
|
||||||
按任务 ID 和可选用户 ID 查询 Agent 定时任务。
|
按任务 ID 和可选用户 ID 查询,并保留无 Session 的旧插件调用方式。
|
||||||
"""
|
"""
|
||||||
return db.execute(
|
if task_id is None and isinstance(db, int):
|
||||||
_get_for_user_statement(cls, task_id=task_id, user_id=user_id)
|
task_id, db = db, None
|
||||||
).scalars().first()
|
if task_id is None:
|
||||||
|
raise TypeError("task_id is required")
|
||||||
|
|
||||||
|
def query(session: Session) -> Optional["AgentTask"]:
|
||||||
|
"""在给定会话中读取单个 Agent 任务。"""
|
||||||
|
return session.execute(
|
||||||
|
_get_for_user_statement(cls, task_id=task_id, user_id=user_id)
|
||||||
|
).scalars().first()
|
||||||
|
|
||||||
|
if isinstance(db, Session):
|
||||||
|
return query(db)
|
||||||
|
return run_legacy_sync_query(query)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def list_for_user(
|
def list_for_user(
|
||||||
cls,
|
cls,
|
||||||
db: Session,
|
db: Session | None = None,
|
||||||
user_id: Optional[str] = None,
|
user_id: Optional[str] = None,
|
||||||
enabled: Optional[bool] = None,
|
enabled: Optional[bool] = None,
|
||||||
) -> list["AgentTask"]:
|
) -> list["AgentTask"]:
|
||||||
"""
|
"""
|
||||||
按用户和启用状态查询 Agent 定时任务。
|
按用户和启用状态查询,并保留无 Session 的旧插件调用方式。
|
||||||
"""
|
"""
|
||||||
return list(db.execute(
|
def query(session: Session) -> list["AgentTask"]:
|
||||||
_list_for_user_statement(cls, user_id=user_id, enabled=enabled)
|
"""在给定会话中读取 Agent 任务列表。"""
|
||||||
).scalars().all())
|
return list(session.execute(
|
||||||
|
_list_for_user_statement(cls, user_id=user_id, enabled=enabled)
|
||||||
|
).scalars().all())
|
||||||
|
|
||||||
|
if isinstance(db, Session):
|
||||||
|
return query(db)
|
||||||
|
return run_legacy_sync_query(query)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def update_task(
|
def update_task(
|
||||||
|
|||||||
+41
-11
@@ -5,7 +5,7 @@ from sqlalchemy.orm import Mapped, Session, mapped_column
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from app.db.base import Base, get_id_column
|
from app.db.base import Base, get_id_column
|
||||||
from app.db.decorators import db_query, async_db_query
|
from app.db.decorators import async_db_query, db_query, run_legacy_sync_query
|
||||||
|
|
||||||
|
|
||||||
def _get_by_user_id_statement(model: type["PassKey"], user_id: int):
|
def _get_by_user_id_statement(model: type["PassKey"], user_id: int):
|
||||||
@@ -51,11 +51,26 @@ class PassKey(Base):
|
|||||||
transports: Mapped[Optional[str]] = mapped_column(String, nullable=True)
|
transports: Mapped[Optional[str]] = mapped_column(String, nullable=True)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_by_user_id(cls, db: Session, user_id: int):
|
def get_by_user_id(
|
||||||
"""获取用户的所有PassKey"""
|
cls,
|
||||||
return list(db.execute(
|
db: Session | int | None = None,
|
||||||
_get_by_user_id_statement(cls, user_id)
|
user_id: int | None = None,
|
||||||
).scalars().all())
|
):
|
||||||
|
"""获取用户的所有 PassKey,并保留无 Session 的旧插件调用方式。"""
|
||||||
|
if user_id is None and isinstance(db, int):
|
||||||
|
user_id, db = db, None
|
||||||
|
if user_id is None:
|
||||||
|
raise TypeError("user_id is required")
|
||||||
|
|
||||||
|
def query(session: Session):
|
||||||
|
"""在给定会话中执行启用凭证查询。"""
|
||||||
|
return list(session.execute(
|
||||||
|
_get_by_user_id_statement(cls, user_id)
|
||||||
|
).scalars().all())
|
||||||
|
|
||||||
|
if isinstance(db, Session):
|
||||||
|
return query(db)
|
||||||
|
return run_legacy_sync_query(query)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@async_db_query
|
@async_db_query
|
||||||
@@ -67,11 +82,26 @@ class PassKey(Base):
|
|||||||
return list(result.scalars().all())
|
return list(result.scalars().all())
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_by_credential_id(cls, db: Session, credential_id: str):
|
def get_by_credential_id(
|
||||||
"""根据凭证ID获取PassKey"""
|
cls,
|
||||||
return db.execute(
|
db: Session | str | None = None,
|
||||||
_get_by_credential_id_statement(cls, credential_id)
|
credential_id: str | None = None,
|
||||||
).scalars().first()
|
):
|
||||||
|
"""按凭证 ID 获取 PassKey,并保留无 Session 的旧插件调用方式。"""
|
||||||
|
if credential_id is None and isinstance(db, str):
|
||||||
|
credential_id, db = db, None
|
||||||
|
if credential_id is None:
|
||||||
|
raise TypeError("credential_id is required")
|
||||||
|
|
||||||
|
def query(session: Session):
|
||||||
|
"""在给定会话中执行启用凭证查询。"""
|
||||||
|
return session.execute(
|
||||||
|
_get_by_credential_id_statement(cls, credential_id)
|
||||||
|
).scalars().first()
|
||||||
|
|
||||||
|
if isinstance(db, Session):
|
||||||
|
return query(db)
|
||||||
|
return run_legacy_sync_query(query)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@async_db_query
|
@async_db_query
|
||||||
|
|||||||
@@ -433,7 +433,9 @@ flowchart TB
|
|||||||
无 Session 的旧 Oper 入口继续由组合根兼容事务执行器承接。随后 PassKey 的宿主同步查询迁移到
|
无 Session 的旧 Oper 入口继续由组合根兼容事务执行器承接。随后 PassKey 的宿主同步查询迁移到
|
||||||
`PassKeyOper`,其按用户/凭证的启用状态过滤由显式 Session 测试覆盖;异步 Model 查询保留旧 ABI。
|
`PassKeyOper`,其按用户/凭证的启用状态过滤由显式 Session 测试覆盖;异步 Model 查询保留旧 ABI。
|
||||||
查询装饰器低水位由 123 降至 119,归属过滤、启用状态过滤和创建时间/主键稳定排序由 canonical
|
查询装饰器低水位由 123 降至 119,归属过滤、启用状态过滤和创建时间/主键稳定排序由 canonical
|
||||||
Oper 测试覆盖。
|
Oper 测试覆盖。`PassKey.get_by_user_id/get_by_credential_id` 与
|
||||||
|
`AgentTask.get_for_user/list_for_user` 同时保留旧插件省略 Session 的同步调用方式;该路径显式委托
|
||||||
|
一次性兼容查询会话,不重新增加 Model 查询装饰器,也不影响宿主显式 Session 的事务所有权。
|
||||||
|
|
||||||
#### ARCH-222:按风险迁移其余写用例
|
#### ARCH-222:按风险迁移其余写用例
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -13,8 +13,8 @@
|
|||||||
"runtime_to_db": [],
|
"runtime_to_db": [],
|
||||||
"workflow_to_db": []
|
"workflow_to_db": []
|
||||||
},
|
},
|
||||||
"edge_count": 6413,
|
"edge_count": 6414,
|
||||||
"edge_sha256": "02b7d1b32347dbd9be15ab2634c196dd36806fa9db89b0f8989ac6cc152c2b73",
|
"edge_sha256": "aea2c4a5f65a9800bc69a83990efd368ba0ec7a9b19e40f38f8d0df6c222ae26",
|
||||||
"edges": [
|
"edges": [
|
||||||
"app -> app.runtime",
|
"app -> app.runtime",
|
||||||
"app -> app.runtime.compat",
|
"app -> app.runtime.compat",
|
||||||
@@ -3473,6 +3473,7 @@
|
|||||||
"app.db.models.agentchat -> app.db.decorators",
|
"app.db.models.agentchat -> app.db.decorators",
|
||||||
"app.db.models.agenttask -> app.db",
|
"app.db.models.agenttask -> app.db",
|
||||||
"app.db.models.agenttask -> app.db.base",
|
"app.db.models.agenttask -> app.db.base",
|
||||||
|
"app.db.models.agenttask -> app.db.decorators",
|
||||||
"app.db.models.agenttaskrun -> app.db",
|
"app.db.models.agenttaskrun -> app.db",
|
||||||
"app.db.models.agenttaskrun -> app.db.base",
|
"app.db.models.agenttaskrun -> app.db.base",
|
||||||
"app.db.models.agenttaskrun -> app.db.decorators",
|
"app.db.models.agenttaskrun -> app.db.decorators",
|
||||||
|
|||||||
@@ -238,6 +238,20 @@ def test_passkey_lookup_by_credential_id_skips_inactive(db):
|
|||||||
assert asyncio.run(PassKey.async_get_by_credential_id(credential_id="cred-dead")) is None
|
assert asyncio.run(PassKey.async_get_by_credential_id(credential_id="cred-dead")) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_passkey_model_sync_queries_keep_no_session_plugin_abi(db, monkeypatch):
|
||||||
|
"""旧插件不传 Session 时仍应获得短会话查询,而不恢复 Model 装饰器。"""
|
||||||
|
db.add(_passkey(9004, "cred-legacy"))
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"app.db.models.passkey.run_legacy_sync_query",
|
||||||
|
lambda operation: operation(db.session),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert [item.credential_id for item in PassKey.get_by_user_id(user_id=9004)] == [
|
||||||
|
"cred-legacy"
|
||||||
|
]
|
||||||
|
assert PassKey.get_by_credential_id("cred-legacy").user_id == 9004
|
||||||
|
|
||||||
|
|
||||||
def test_passkey_get_by_id_ignores_active_flag(db):
|
def test_passkey_get_by_id_ignores_active_flag(db):
|
||||||
"""
|
"""
|
||||||
按主键取记录是管理用途,不应过滤停用状态——否则管理端看不到自己刚停用的凭据。
|
按主键取记录是管理用途,不应过滤停用状态——否则管理端看不到自己刚停用的凭据。
|
||||||
|
|||||||
@@ -308,6 +308,21 @@ def test_agenttask_get_for_user_enforces_ownership(db):
|
|||||||
assert AgentTask.get_for_user(db.session, task_id, user_id="bob") is None
|
assert AgentTask.get_for_user(db.session, task_id, user_id="bob") is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_agenttask_model_queries_keep_no_session_plugin_abi(db, monkeypatch):
|
||||||
|
"""旧插件省略 Session 时仍可按原关键字参数查询 Agent 任务。"""
|
||||||
|
task_id = AgentTask.add_task(db.session, **_task("legacy", user_id="legacy-user"))
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"app.db.models.agenttask.run_legacy_sync_query",
|
||||||
|
lambda operation: operation(db.session),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert AgentTask.get_for_user(
|
||||||
|
task_id=task_id,
|
||||||
|
user_id="legacy-user",
|
||||||
|
).id == task_id
|
||||||
|
assert [task.id for task in AgentTask.list_for_user(user_id="legacy-user")] == [task_id]
|
||||||
|
|
||||||
|
|
||||||
def test_agenttask_oper_reads_with_explicit_session(db, monkeypatch):
|
def test_agenttask_oper_reads_with_explicit_session(db, monkeypatch):
|
||||||
"""AgentTaskOper 的宿主查询使用调用方 Session,不再经过旧事务兼容执行器。"""
|
"""AgentTaskOper 的宿主查询使用调用方 Session,不再经过旧事务兼容执行器。"""
|
||||||
task_id = AgentTask.add_task(db.session, **_task("canonical", user_id="alice"))
|
task_id = AgentTask.add_task(db.session, **_task("canonical", user_id="alice"))
|
||||||
|
|||||||
Reference in New Issue
Block a user