refactor(backend): update API routes and service layer

- Update agent router/service/repository with new endpoints
- Update auth routes with phone-based authentication
- Update users service with new phone lookup
- Update schedule_items with new schemas
- Update message schemas with visibility support
- Update settings with new automation scheduler config
- Update CLI with new commands
- Update tests to match new API contracts
This commit is contained in:
qzl
2026-03-19 18:42:59 +08:00
parent 641d847008
commit f0af44d840
36 changed files with 1083 additions and 1853 deletions
@@ -10,7 +10,7 @@ from v1.agent.service import ensure_session_owner
def test_owner_guard_denies_non_owner() -> None:
user = CurrentUser(id=uuid4(), email="self@example.com")
user = CurrentUser(id=uuid4(), phone="self@example.com")
with pytest.raises(HTTPException):
ensure_session_owner(owner_id="other-user", current_user=user)
@@ -7,6 +7,8 @@ from uuid import uuid4
import pytest
from models.agent_chat_message import AgentChatMessageRole
from sqlalchemy import select
from models.agent_chat_message import AgentChatMessage
from v1.agent.repository import AgentRepository
@@ -79,6 +81,7 @@ async def test_persist_user_message_sets_session_title_when_empty() -> None:
session_id=session_id,
content=" 请帮我安排明天下午开会 ",
metadata=None,
visibility_mask=1,
)
assert session_row.title == "请帮我安排明天下午开会"
@@ -101,6 +104,7 @@ async def test_persist_user_message_keeps_existing_session_title() -> None:
session_id=session_id,
content="新的消息内容",
metadata=None,
visibility_mask=1,
)
assert session_row.title == "已有标题"
@@ -164,3 +168,13 @@ async def test_get_history_day_uses_target_day_queries_only() -> None:
messages = payload["messages"]
assert isinstance(messages, list)
assert len(messages) == 1
def test_apply_visibility_filter_adds_bitwise_expression() -> None:
repository = AgentRepository(session=SimpleNamespace()) # type: ignore[arg-type]
stmt = select(AgentChatMessage)
filtered = repository._apply_visibility_filter(stmt=stmt, visibility_mask=1)
assert "visibility_mask" in str(filtered)
assert "&" in str(filtered)
+106 -7
View File
@@ -20,6 +20,7 @@ class _FakeRepository:
def __init__(self) -> None:
self.committed = False
self.persisted_user_messages: list[dict[str, object]] = []
self.created_session_calls = 0
async def get_session_owner(self, *, session_id: str) -> str:
if session_id == "00000000-0000-0000-0000-000000000001":
@@ -30,6 +31,7 @@ class _FakeRepository:
self, *, user_id: str, session_id: str | None = None
) -> str:
del user_id
self.created_session_calls += 1
return session_id or "00000000-0000-0000-0000-000000000999"
async def commit(self) -> None:
@@ -39,9 +41,13 @@ class _FakeRepository:
return None
async def get_history_day(
self, *, session_id: str, before: date | None
self,
*,
session_id: str,
before: date | None,
visibility_mask: int | None = None,
) -> dict[str, object] | None:
del session_id, before
del session_id, before, visibility_mask
return None
async def get_latest_session_id_for_user(self, *, user_id: str) -> str | None:
@@ -54,15 +60,42 @@ class _FakeRepository:
session_id: str,
content: str,
metadata: AgentChatMessageMetadata | None,
visibility_mask: int,
) -> None:
self.persisted_user_messages.append(
{
"session_id": session_id,
"content": content,
"metadata": metadata,
"visibility_mask": visibility_mask,
}
)
async def get_system_agent_config(
self, *, agent_type: str
) -> dict[str, object] | None:
normalized = agent_type.strip().lower()
mapping = {
"router": 16,
"worker": 17,
"memory": 18,
}
bit = mapping.get(normalized)
if bit is None:
return None
return {
"agent_type": normalized,
"status": "active",
"config": {
"temperature": 0.7,
"max_tokens": None,
"timeout_seconds": 30,
"visibility_consumer_bit": bit,
"context_messages": {"mode": "number", "count": 20},
"enabled_tools": [],
},
}
class _FakeQueue:
def __init__(self) -> None:
@@ -122,11 +155,11 @@ class _FakeAttachmentStorage:
def _user() -> CurrentUser:
return CurrentUser(
id=UUID("00000000-0000-0000-0000-000000000001"),
email="user@example.com",
phone="+8613812345678",
)
def _build_run_input(*, urls: list[str]) -> RunAgentInput:
def _build_run_input(*, urls: list[str], agent_type: str = "worker") -> RunAgentInput:
content: list[dict[str, str]] = [{"type": "text", "text": "hello"}]
for url in urls:
content.append({"type": "binary", "mimeType": "image/png", "url": url})
@@ -144,7 +177,7 @@ def _build_run_input(*, urls: list[str]) -> RunAgentInput:
],
"tools": [],
"context": [],
"forwardedProps": {},
"forwardedProps": {"agent_type": agent_type},
}
)
@@ -222,6 +255,68 @@ async def test_enqueue_run_persists_attachment_and_queue_without_user_token(
assert run_input["runId"] == "run-1"
@pytest.mark.asyncio
async def test_enqueue_run_rejects_unknown_agent_type(monkeypatch) -> None:
monkeypatch.setattr(
agent_service_module.config.storage, "bucket", "agent-test-bucket"
)
service = AgentService(
repository=_FakeRepository(),
queue=_FakeQueue(),
stream=_FakeStream(),
attachment_storage=_FakeAttachmentStorage(),
)
base_url = str(config.supabase.url).rstrip("/")
safe_path = quote(
"agent-inputs/00000000-0000-0000-0000-000000000001/"
"00000000-0000-0000-0000-000000000001/uploads/a.png"
)
run_input = _build_run_input(
urls=[
f"{base_url}/storage/v1/object/sign/agent-test-bucket/{safe_path}?token=1"
],
agent_type="planner",
)
with pytest.raises(HTTPException) as exc_info:
await service.enqueue_run(run_input=run_input, current_user=_user())
assert exc_info.value.status_code == 422
@pytest.mark.asyncio
async def test_enqueue_run_rejects_memory_mode_for_api(monkeypatch) -> None:
monkeypatch.setattr(
agent_service_module.config.storage, "bucket", "agent-test-bucket"
)
repository = _FakeRepository()
service = AgentService(
repository=repository,
queue=_FakeQueue(),
stream=_FakeStream(),
attachment_storage=_FakeAttachmentStorage(),
)
base_url = str(config.supabase.url).rstrip("/")
safe_path = quote(
"agent-inputs/00000000-0000-0000-0000-000000000001/"
"00000000-0000-0000-0000-000000000001/uploads/a.png"
)
run_input = _build_run_input(
urls=[
f"{base_url}/storage/v1/object/sign/agent-test-bucket/{safe_path}?token=1"
],
agent_type="memory",
)
with pytest.raises(HTTPException) as exc_info:
await service.enqueue_run(run_input=run_input, current_user=_user())
assert exc_info.value.status_code == 422
assert exc_info.value.detail == "memory mode is automation-only"
assert repository.created_session_calls == 0
assert repository.persisted_user_messages == []
@pytest.mark.asyncio
async def test_create_attachment_signed_url_returns_url(monkeypatch) -> None:
monkeypatch.setattr(
@@ -317,9 +412,13 @@ async def test_enqueue_run_rejects_too_many_attachments(monkeypatch) -> None:
async def test_get_history_snapshot_filters_out_tool_messages() -> None:
class _HistoryRepository(_FakeRepository):
async def get_history_day(
self, *, session_id: str, before: date | None
self,
*,
session_id: str,
before: date | None,
visibility_mask: int | None = None,
) -> dict[str, object] | None:
del session_id, before
del session_id, before, visibility_mask
return {
"day": "2026-03-17",
"hasMore": False,