refactor: 移除前端 Mock API,新增共享组件,优化认证流程

- 删除 mock_api_client、mock_calendar_service、mock_history_service
- 新增 fixed_length_code_input、link_button、message_composer 共享组件
- 优化登录/注册/密码重置页面使用新组件
- 简化 injection.dart 移除 mock 分支
- 更新 env.dart 配置(BACKEND_URL 替换 API_URL)
- 后端 agentscope 工具和测试更新
- 重构 AGENTS.md 文档结构
- 新增 deploy/ 目录和 protocol 文档
This commit is contained in:
qzl
2026-03-12 16:41:45 +08:00
parent d7fbb74bf8
commit 01c36eb32e
70 changed files with 5138 additions and 5829 deletions
+7 -11
View File
@@ -783,7 +783,7 @@ class TestInviteCodeSignup:
"username": "demo",
"email": "user@example.com",
"password": "secret123",
"invite_code": "A2B3C4D5",
"invite_code": "A2B3",
},
)
assert response.status_code == 202
@@ -791,7 +791,7 @@ class TestInviteCodeSignup:
finally:
app.dependency_overrides = {}
def test_signup_with_invalid_invite_code_length_returns_422(self) -> None:
def test_signup_with_invalid_invite_code_length_returns_202(self) -> None:
user = AuthUser(id="user-1", email="user@example.com")
token_response = SessionResponse(
access_token="access",
@@ -815,14 +815,12 @@ class TestInviteCodeSignup:
"invite_code": "ABC123",
},
)
assert response.status_code == 422
assert response.headers["content-type"].startswith(
"application/problem+json"
)
assert response.status_code == 202
assert response.json() == {"email": "user@example.com"}
finally:
app.dependency_overrides = {}
def test_signup_with_invalid_invite_code_chars_returns_422(self) -> None:
def test_signup_with_invalid_invite_code_chars_returns_202(self) -> None:
user = AuthUser(id="user-1", email="user@example.com")
token_response = SessionResponse(
access_token="access",
@@ -846,9 +844,7 @@ class TestInviteCodeSignup:
"invite_code": "ABCD1234",
},
)
assert response.status_code == 422
assert response.headers["content-type"].startswith(
"application/problem+json"
)
assert response.status_code == 202
assert response.json() == {"email": "user@example.com"}
finally:
app.dependency_overrides = {}
@@ -5,6 +5,7 @@ from types import SimpleNamespace
from uuid import uuid4
from ag_ui.core import RunAgentInput
from fastapi import HTTPException
from fastapi.testclient import TestClient
from app import app
@@ -150,6 +151,7 @@ def test_run_requires_auth_and_returns_202_task_id() -> None:
app.dependency_overrides[get_agent_service] = lambda: _FakeAgentService()
client = TestClient(app)
original_allow_run = agent_router._allow_run_request
original_verify_token = agent_router._verified_access_token_for_user
async def _allow_run(*, user_id: str) -> bool:
del user_id
@@ -157,6 +159,13 @@ def test_run_requires_auth_and_returns_202_task_id() -> None:
agent_router._allow_run_request = _allow_run # type: ignore[assignment]
def _verify_token(**kwargs: object) -> str:
if kwargs.get("authorization"):
return "token-ok"
raise HTTPException(status_code=401, detail="Unauthorized")
agent_router._verified_access_token_for_user = _verify_token # type: ignore[assignment]
try:
unauthorized = client.post(
"/api/v1/agent/runs",
@@ -177,6 +186,7 @@ def test_run_requires_auth_and_returns_202_task_id() -> None:
)
authorized = client.post(
"/api/v1/agent/runs",
headers={"Authorization": "Bearer token-ok"},
json={
"threadId": "00000000-0000-0000-0000-000000000001",
"runId": "run-1",
@@ -192,8 +202,23 @@ def test_run_requires_auth_and_returns_202_task_id() -> None:
assert authorized.json()["threadId"] == "00000000-0000-0000-0000-000000000001"
assert authorized.json()["runId"] == "run-1"
assert authorized.json()["created"] is False
missing_header = client.post(
"/api/v1/agent/runs",
json={
"threadId": "00000000-0000-0000-0000-000000000001",
"runId": "run-2",
"state": {},
"messages": [{"id": "u2", "role": "user", "content": "hello"}],
"tools": [],
"context": [],
"forwardedProps": {},
},
)
assert missing_header.status_code == 401
finally:
agent_router._allow_run_request = original_allow_run # type: ignore[assignment]
agent_router._verified_access_token_for_user = original_verify_token # type: ignore[assignment]
app.dependency_overrides = {}
@@ -390,10 +415,19 @@ def test_resume_accepts_tool_message_without_user_message() -> None:
id=uuid4(), email="user@example.com"
)
client = TestClient(app)
original_verify_token = agent_router._verified_access_token_for_user
def _verify_token(**kwargs: object) -> str:
if kwargs.get("authorization"):
return "token-ok"
raise HTTPException(status_code=401, detail="Unauthorized")
agent_router._verified_access_token_for_user = _verify_token # type: ignore[assignment]
try:
response = client.post(
"/api/v1/agent/runs/00000000-0000-0000-0000-000000000001/resume",
headers={"Authorization": "Bearer token-ok"},
json={
"threadId": "00000000-0000-0000-0000-000000000001",
"runId": "run-resume-1",
@@ -413,7 +447,29 @@ def test_resume_accepts_tool_message_without_user_message() -> None:
)
assert response.status_code == 202
assert response.json()["taskId"] == "task-resume-1"
missing_header = client.post(
"/api/v1/agent/runs/00000000-0000-0000-0000-000000000001/resume",
json={
"threadId": "00000000-0000-0000-0000-000000000001",
"runId": "run-resume-2",
"state": {},
"messages": [
{
"id": "tool-2",
"role": "tool",
"toolCallId": "call-2",
"content": '{"toolName":"navigate_to_route","toolArgs":{"target":"/calendar/dayweek"},"nonce":"n2","result":{"ok":true}}',
}
],
"tools": [],
"context": [],
"forwardedProps": {},
},
)
assert missing_header.status_code == 401
finally:
agent_router._verified_access_token_for_user = original_verify_token # type: ignore[assignment]
app.dependency_overrides = {}
@@ -16,13 +16,9 @@ async def test_calendar_read_returns_list_payload(
) -> None:
async def _fake_execute(**kwargs: Any) -> dict[str, object]:
del kwargs
return {"type": "calendar_event_list.v1", "version": "v1", "data": {}}
return {"type": "calendar_event_list.v1", "version": "v1", "data": {"ok": True}}
monkeypatch.setattr(
calendar_module,
"_execute_list_calendar_events",
_fake_execute,
)
monkeypatch.setattr(calendar_module, "_execute_list_calendar_events", _fake_execute)
monkeypatch.setattr(calendar_module, "_verify_user_token", lambda **_: True)
monkeypatch.setattr(calendar_module, "build_tool_response", lambda payload: payload)
@@ -62,9 +58,7 @@ async def test_calendar_write_maps_event_id_for_update(
return {"type": "calendar_card.v1", "version": "v1", "data": {"ok": True}}
monkeypatch.setattr(
calendar_module,
"_execute_mutate_calendar_event",
_fake_execute,
calendar_module, "_execute_mutate_calendar_event", _fake_execute
)
monkeypatch.setattr(calendar_module, "_verify_user_token", lambda **_: True)
monkeypatch.setattr(calendar_module, "build_tool_response", lambda payload: payload)
@@ -82,57 +76,6 @@ async def test_calendar_write_maps_event_id_for_update(
assert "eventId" in captured
@pytest.mark.asyncio
async def test_calendar_write_requires_preset_user_token(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(calendar_module, "build_tool_response", lambda payload: payload)
monkeypatch.setattr(calendar_module, "_verify_user_token", lambda **_: False)
result = await calendar_module.calendar_write(
session=cast(AsyncSession, SimpleNamespace()),
owner_id=uuid4(),
user_token="bad-token",
operation="create",
)
assert result["data"]["ok"] is False
assert result["data"]["code"] == "UNAUTHORIZED"
@pytest.mark.asyncio
async def test_calendar_write_rejects_missing_event_id_for_update(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(calendar_module, "build_tool_response", lambda payload: payload)
result = await calendar_module.calendar_write(
session=cast(AsyncSession, SimpleNamespace()),
owner_id=uuid4(),
user_token="token-abc",
operation="update",
)
assert result["data"]["ok"] is False
assert result["data"]["code"] == "INVALID_ARGUMENT"
@pytest.mark.asyncio
async def test_calendar_write_rejects_event_id_for_create(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(calendar_module, "build_tool_response", lambda payload: payload)
result = await calendar_module.calendar_write(
session=cast(AsyncSession, SimpleNamespace()),
owner_id=uuid4(),
user_token="token-abc",
operation="create",
event_id=str(uuid4()),
)
assert result["data"]["ok"] is False
assert result["data"]["code"] == "INVALID_ARGUMENT"
@pytest.mark.asyncio
async def test_calendar_write_maps_reminder_minutes(
monkeypatch: pytest.MonkeyPatch,
@@ -144,9 +87,7 @@ async def test_calendar_write_maps_reminder_minutes(
return {"type": "calendar_card.v1", "version": "v1", "data": {"ok": True}}
monkeypatch.setattr(
calendar_module,
"_execute_mutate_calendar_event",
_fake_execute,
calendar_module, "_execute_mutate_calendar_event", _fake_execute
)
monkeypatch.setattr(calendar_module, "_verify_user_token", lambda **_: True)
monkeypatch.setattr(calendar_module, "build_tool_response", lambda payload: payload)
@@ -163,46 +104,54 @@ async def test_calendar_write_maps_reminder_minutes(
@pytest.mark.asyncio
async def test_calendar_write_rejects_invalid_reminder_minutes(
async def test_calendar_write_returns_failed_tool_response_on_error(
monkeypatch: pytest.MonkeyPatch,
) -> None:
async def _fake_execute(**kwargs: Any) -> dict[str, object]:
del kwargs
raise ValueError("eventId is required")
monkeypatch.setattr(
calendar_module, "_execute_mutate_calendar_event", _fake_execute
)
monkeypatch.setattr(calendar_module, "_verify_user_token", lambda **_: True)
monkeypatch.setattr(calendar_module, "build_tool_response", lambda payload: payload)
result = await calendar_module.calendar_write(
session=cast(AsyncSession, SimpleNamespace()),
owner_id=uuid4(),
user_token="token-abc",
operation="create",
reminder_minutes=10081,
operation="update",
)
assert result["type"] == "calendar_operation.v1"
assert result["data"]["ok"] is False
assert result["data"]["code"] == "INVALID_ARGUMENT"
@pytest.mark.asyncio
async def test_calendar_write_maps_invite_arguments(
async def test_calendar_share_maps_arguments(
monkeypatch: pytest.MonkeyPatch,
) -> None:
captured: dict[str, object] = {}
async def _fake_execute(**kwargs: Any) -> dict[str, object]:
captured.update(cast(dict[str, object], kwargs["tool_args"]))
return {"type": "calendar_card.v1", "version": "v1", "data": {"ok": True}}
return {
"type": "calendar_operation.v1",
"version": "v1",
"data": {"operation": "share", "ok": True},
}
monkeypatch.setattr(
calendar_module,
"_execute_mutate_calendar_event",
_fake_execute,
)
monkeypatch.setattr(calendar_module, "_execute_share_calendar_event", _fake_execute)
monkeypatch.setattr(calendar_module, "_verify_user_token", lambda **_: True)
monkeypatch.setattr(calendar_module, "build_tool_response", lambda payload: payload)
await calendar_module.calendar_write(
result = await calendar_module.calendar_share(
session=cast(AsyncSession, SimpleNamespace()),
owner_id=uuid4(),
user_token="token-abc",
operation="create",
event_id=str(uuid4()),
invite_user_emails=["a@example.com"],
invite_user_names=["alice"],
invite_user_ids=[str(uuid4())],
@@ -211,6 +160,8 @@ async def test_calendar_write_maps_invite_arguments(
invite_permission_invite=True,
)
assert result["type"] == "calendar_operation.v1"
assert captured["eventId"]
assert captured["inviteUserEmails"] == ["a@example.com"]
assert captured["inviteUserNames"] == ["alice"]
assert isinstance(captured["inviteUserIds"], list)
@@ -220,46 +171,18 @@ async def test_calendar_write_maps_invite_arguments(
@pytest.mark.asyncio
async def test_user_resolve_maps_identity_arguments(
monkeypatch: pytest.MonkeyPatch,
) -> None:
captured: dict[str, object] = {}
async def _fake_execute(**kwargs: Any) -> dict[str, object]:
captured.update(cast(dict[str, object], kwargs["tool_args"]))
return {"type": "user_lookup.v1", "version": "v1", "data": {"ok": True}}
monkeypatch.setattr(
calendar_module,
"_execute_resolve_user_identity",
_fake_execute,
)
monkeypatch.setattr(calendar_module, "_verify_user_token", lambda **_: True)
monkeypatch.setattr(calendar_module, "build_tool_response", lambda payload: payload)
result = await calendar_module.user_resolve(
session=cast(AsyncSession, SimpleNamespace()),
owner_id=uuid4(),
user_token="token-abc",
user_email="a@example.com",
)
assert result["type"] == "user_lookup.v1"
assert captured == {"userEmail": "a@example.com", "userName": None}
@pytest.mark.asyncio
async def test_user_resolve_requires_valid_user_token(
async def test_calendar_share_requires_valid_user_token(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(calendar_module, "_verify_user_token", lambda **_: False)
monkeypatch.setattr(calendar_module, "build_tool_response", lambda payload: payload)
result = await calendar_module.user_resolve(
result = await calendar_module.calendar_share(
session=cast(AsyncSession, SimpleNamespace()),
owner_id=uuid4(),
user_token="bad-token",
user_name="alice",
event_id=str(uuid4()),
invite_user_emails=["a@example.com"],
)
assert result["data"]["ok"] is False
@@ -22,7 +22,7 @@ async def test_build_toolkit_registers_calendar_tools() -> None:
names = {item["function"]["name"] for item in schemas}
assert "calendar_read" in names
assert "calendar_write" in names
assert "user_resolve" in names
assert "calendar_share" in names
write_schema = next(
item for item in schemas if item["function"]["name"] == "calendar_write"
@@ -28,6 +28,17 @@ def test_signup_requires_username() -> None:
)
def test_signup_allows_any_invite_code_input() -> None:
request = VerificationCreateRequest(
username="demo",
email="user@example.com",
password="secret123",
invite_code="abc123",
)
assert request.invite_code == "abc123"
def test_signup_verify_requires_six_digit_token() -> None:
with pytest.raises(ValidationError):
VerificationVerifyRequest(email="user@example.com", token="abc123")
@@ -22,10 +22,12 @@ from v1.auth.service import AuthService, AuthServiceGateway
class FakeGateway(AuthServiceGateway):
def __init__(self, response: SessionResponse) -> None:
self._response = response
self.last_create_verification_request: VerificationCreateRequest | None = None
async def create_verification(
self, request: VerificationCreateRequest
) -> VerificationCreateResponse:
self.last_create_verification_request = request
return VerificationCreateResponse(email=request.email)
async def verify_verification(
@@ -121,6 +123,58 @@ async def test_signup_resend_returns_none() -> None:
assert result is None
@pytest.mark.asyncio
async def test_create_verification_ignores_invalid_invite_code() -> None:
user = AuthUser(id="user-1", email="user@example.com")
token_response = SessionResponse(
access_token="access",
refresh_token="refresh",
expires_in=3600,
token_type="bearer",
user=user,
)
gateway = FakeGateway(token_response)
service = AuthService(gateway=gateway)
await service.create_verification(
VerificationCreateRequest(
username="demo",
email="user@example.com",
password="secret123",
invite_code="bad-code",
)
)
assert gateway.last_create_verification_request is not None
assert gateway.last_create_verification_request.invite_code is None
@pytest.mark.asyncio
async def test_create_verification_normalizes_valid_invite_code() -> None:
user = AuthUser(id="user-1", email="user@example.com")
token_response = SessionResponse(
access_token="access",
refresh_token="refresh",
expires_in=3600,
token_type="bearer",
user=user,
)
gateway = FakeGateway(token_response)
service = AuthService(gateway=gateway)
await service.create_verification(
VerificationCreateRequest(
username="demo",
email="user@example.com",
password="secret123",
invite_code="a2b3",
)
)
assert gateway.last_create_verification_request is not None
assert gateway.last_create_verification_request.invite_code == "A2B3"
@pytest.mark.asyncio
async def test_supabase_signup_passes_username_in_metadata(
monkeypatch: pytest.MonkeyPatch,