feat/ internal eval平台现支持实机联调。 1. 显示当前本地数据库里登录用户的历史聊天,已生成的回忆录。支持在网页直接对话,不依赖手机app。
This commit is contained in:
@@ -183,6 +183,8 @@ MEMORY_COMPACTION_ENABLED=true
|
||||
SECRET_KEY=replace_with_a_strong_random_secret
|
||||
ALGORITHM=HS256
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES=120
|
||||
# 内网评测:开启后可用 POST /api/auth/mock/sms-login(跳过短信);APP_ENV=production 时该路由仍返回 404
|
||||
# MOCK_SMS_LOGIN_ENABLED=1
|
||||
|
||||
# =============================================================================
|
||||
# Tencent Cloud — 短信
|
||||
|
||||
@@ -50,6 +50,8 @@ class Settings(BaseSettings):
|
||||
algorithm: str = "HS256"
|
||||
access_token_expire_minutes: int = 120
|
||||
refresh_token_expire_days: int = 30
|
||||
# 本地/内网评测:允许 POST /api/auth/mock/sms-login 跳过短信(须显式开启;production 下路由仍拒绝)
|
||||
mock_sms_login_enabled: bool = False
|
||||
|
||||
# ── LLM / DeepSeek ───────────────────────────────────────
|
||||
deepseek_api_key: str = ""
|
||||
@@ -209,6 +211,15 @@ class Settings(BaseSettings):
|
||||
return False
|
||||
return str(v).strip().lower() in ("1", "true", "yes", "on")
|
||||
|
||||
@field_validator("mock_sms_login_enabled", mode="before")
|
||||
@classmethod
|
||||
def _coerce_mock_sms_login_enabled(cls, v: object) -> bool:
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
if v is None:
|
||||
return False
|
||||
return str(v).strip().lower() in ("1", "true", "yes", "on")
|
||||
|
||||
@field_validator("log_agent_verbose", mode="before")
|
||||
@classmethod
|
||||
def _coerce_log_agent_verbose(cls, v: object) -> bool:
|
||||
|
||||
@@ -5,6 +5,7 @@ from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status
|
||||
from fastapi.responses import FileResponse
|
||||
from PIL import Image
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.dependencies import get_current_user
|
||||
from app.core.logging import get_logger
|
||||
from app.features.auth.deps import get_auth_service
|
||||
@@ -12,6 +13,7 @@ from app.features.auth.schemas import (
|
||||
ChangePasswordRequest,
|
||||
ChangePhoneRequest,
|
||||
LoginRequest,
|
||||
MockSmsLoginRequest,
|
||||
RefreshTokenRequest,
|
||||
RegisterRequest,
|
||||
ResetPasswordRequest,
|
||||
@@ -73,6 +75,13 @@ def _check_terms(agreed: bool) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _mock_sms_login_route_enabled() -> bool:
|
||||
env = (settings.app_environment or "").lower().strip()
|
||||
if env == "production":
|
||||
return False
|
||||
return bool(settings.mock_sms_login_enabled)
|
||||
|
||||
|
||||
# ── registration & login ─────────────────────────────────────
|
||||
|
||||
|
||||
@@ -410,6 +419,36 @@ async def login_with_sms(
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/mock/sms-login",
|
||||
response_model=TokenResponse,
|
||||
summary="[评测] Mock 短信登录(跳过验证码)",
|
||||
description=(
|
||||
"需 MOCK_SMS_LOGIN_ENABLED=1 且 APP_ENV 非 production。"
|
||||
"供 Eval Web 等内网工具联调,勿在生产环境开启。"
|
||||
),
|
||||
responses={404: {"description": "未启用或生产环境已禁用"}},
|
||||
)
|
||||
async def mock_sms_login_route(
|
||||
request: MockSmsLoginRequest,
|
||||
service: AuthService = Depends(get_auth_service),
|
||||
):
|
||||
if not _mock_sms_login_route_enabled():
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not Found")
|
||||
_check_terms(request.agreed_to_terms)
|
||||
try:
|
||||
result = await service.mock_sms_login(
|
||||
phone=request.phone,
|
||||
nickname=request.nickname,
|
||||
)
|
||||
except AuthError as e:
|
||||
raise _map_auth_error(e)
|
||||
return TokenResponse(
|
||||
access_token=result["access_token"],
|
||||
refresh_token=result["refresh_token"],
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/register/sms",
|
||||
response_model=TokenResponse,
|
||||
|
||||
@@ -59,6 +59,16 @@ class SmsLoginRequest(BaseModel):
|
||||
)
|
||||
|
||||
|
||||
class MockSmsLoginRequest(BaseModel):
|
||||
"""开发/评测专用:与 MOCK_SMS_LOGIN_ENABLED 联用,跳过短信校验。"""
|
||||
|
||||
phone: str = Field(..., min_length=11, max_length=11, description="手机号(11位)")
|
||||
agreed_to_terms: bool = Field(..., description="是否同意用户协议和隐私政策")
|
||||
nickname: Optional[str] = Field(
|
||||
None, max_length=50, description="新用户昵称(可选)"
|
||||
)
|
||||
|
||||
|
||||
class SmsRegisterRequest(BaseModel):
|
||||
phone: str = Field(..., min_length=11, max_length=11, description="手机号(11位)")
|
||||
code: str = Field(..., min_length=6, max_length=6, description="验证码(6位)")
|
||||
|
||||
@@ -218,6 +218,18 @@ class AuthService:
|
||||
if not success:
|
||||
raise AuthError(message, "INVALID_SMS_CODE")
|
||||
|
||||
return await self._sms_login_after_code_verified(
|
||||
phone, device_info=device_info, nickname=nickname
|
||||
)
|
||||
|
||||
async def _sms_login_after_code_verified(
|
||||
self,
|
||||
phone: str,
|
||||
*,
|
||||
device_info: str = "",
|
||||
nickname: str | None = None,
|
||||
) -> dict:
|
||||
"""SMS 已校验通过后:查找或创建用户并签发令牌。"""
|
||||
user = await repo.get_user_by_phone(phone, self._db)
|
||||
is_new_user = user is None
|
||||
|
||||
@@ -240,6 +252,17 @@ class AuthService:
|
||||
|
||||
return {"user": user, "is_new_user": is_new_user, **tokens}
|
||||
|
||||
async def mock_sms_login(
|
||||
self,
|
||||
phone: str,
|
||||
device_info: str = "",
|
||||
nickname: str | None = None,
|
||||
) -> dict:
|
||||
"""跳过短信校验的登录/自动注册(仅由 mock 路由在配置允许时调用)。"""
|
||||
return await self._sms_login_after_code_verified(
|
||||
phone, device_info=device_info, nickname=nickname
|
||||
)
|
||||
|
||||
async def register_with_sms(
|
||||
self,
|
||||
phone: str,
|
||||
|
||||
@@ -62,6 +62,8 @@ VITE_EVAL_API_BASE=http://127.0.0.1:8001 VITE_EVAL_API_KEY=与上同 npm run dev
|
||||
|
||||
或使用仓库根目录 `npm run eval-web`(需本地已 `npm install` 在 `app-eval-web`)。
|
||||
|
||||
**部署约定**:`app-eval-web` **不提供**生产/预发 Docker 镜像,也 **不**纳入 Staging/Production 的 `api/docker-compose.yml` 部署。评测 UI 仅在开发阶段于本机通过 Vite `npm run dev`(或 `./internal-eval.sh` 拉起)使用;不要在预发/生产环境编译 `dist/` 或挂载独立容器对外提供服务。
|
||||
|
||||
## 流式评审
|
||||
|
||||
`POST /internal/api/evaluation/judge/conversation-stream` 使用 **fetch 读取 SSE**(chunk),请求头携带 `X-Internal-Eval-Key` 即可;不要求浏览器 `EventSource`。Body 可选 **`judge_provider`**:`zhipu`(默认)| `deepseek`,以及 **`judge_model`**(空则用该供应商环境默认)。首轮 `meta` 事件会回显 `judge_provider` / `judge_model`。
|
||||
@@ -76,6 +78,11 @@ VITE_EVAL_API_BASE=http://127.0.0.1:8001 VITE_EVAL_API_KEY=与上同 npm run dev
|
||||
- **Playground · 分步测评**:选用户导出 MD 为基线 → `eval-sandbox` + 逐轮 `replay/conversation`(**`skip_memoir: true`** 时只做对话)→ **`memoir-submit`** 再可选轮询 **`memoir-phase1-ready`** → 跳转 **Memoir / Stories** 看成稿;支持 **智谱 / DeepSeek R1** 对话流式评分(工具栏「评审模型」)。
|
||||
- **Memoir**:按 `user_id` 拉库中章节快照与基线对照评审。
|
||||
- **Stories**:故事列表与评审。
|
||||
- **实机联调**(侧栏「实机联调」,哈希路由 `#live`):用与 **消费者主站**相同的 REST / WebSocket 测核心聊天与回忆录。需主站 `main:app` 已启动(默认 **:8000**),并在主站环境启用 Mock 登录(见下)。开发时 `app-eval-web` 将 **`/api`** 与 **`/ws`** 代理到主站(`VITE_MAIN_API_PROXY_TARGET`,默认 `http://127.0.0.1:8000`);也可设 `VITE_MAIN_API_BASE` 直连完整主站 URL。
|
||||
|
||||
### Mock 登录(仅非 production)
|
||||
|
||||
在主站 `.env` / `.env.development` 中设置 **`MOCK_SMS_LOGIN_ENABLED=1`**(或 `true`)。`APP_ENV=production` 时 **`POST /api/auth/mock/sms-login` 始终返回 404**。请求体:`phone`(11 位)、`agreed_to_terms: true`,可选 `nickname`(新用户);响应与正式短信登录相同(`access_token` + `refresh_token`)。**切勿在生产环境开启。**
|
||||
|
||||
## 真实链路透传回放(与 App 一致)
|
||||
|
||||
|
||||
101
api/tests/test_mock_sms_login_http.py
Normal file
101
api/tests/test_mock_sms_login_http.py
Normal file
@@ -0,0 +1,101 @@
|
||||
"""Mock 短信登录路由:配置门禁与返回的 JWT 可解析性。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from httpx import ASGITransport, AsyncClient
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.security import create_access_token, verify_token
|
||||
from app.features.auth.deps import get_auth_service
|
||||
from app.features.auth.router import router as auth_router
|
||||
from app.features.auth.service import AuthService
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_sms_app() -> FastAPI:
|
||||
app = FastAPI()
|
||||
app.include_router(auth_router)
|
||||
mock_service = MagicMock(spec=AuthService)
|
||||
uid = str(uuid.uuid4())
|
||||
mock_service.mock_sms_login = AsyncMock(
|
||||
return_value={
|
||||
"access_token": create_access_token(data={"sub": uid}),
|
||||
"refresh_token": "rt-mock",
|
||||
}
|
||||
)
|
||||
app.dependency_overrides[get_auth_service] = lambda: mock_service
|
||||
app.state._mock_service = mock_service
|
||||
app.state._mock_uid = uid
|
||||
return app
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mock_sms_login_disabled_returns_404(mock_sms_app, monkeypatch) -> None:
|
||||
monkeypatch.setattr(settings, "mock_sms_login_enabled", False)
|
||||
monkeypatch.setattr(settings, "app_environment", "development")
|
||||
transport = ASGITransport(app=mock_sms_app)
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as ac:
|
||||
r = await ac.post(
|
||||
"/api/auth/mock/sms-login",
|
||||
json={"phone": "13800138000", "agreed_to_terms": True},
|
||||
)
|
||||
assert r.status_code == 404
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mock_sms_login_production_returns_404(mock_sms_app, monkeypatch) -> None:
|
||||
monkeypatch.setattr(settings, "mock_sms_login_enabled", True)
|
||||
monkeypatch.setattr(settings, "app_environment", "production")
|
||||
transport = ASGITransport(app=mock_sms_app)
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as ac:
|
||||
r = await ac.post(
|
||||
"/api/auth/mock/sms-login",
|
||||
json={"phone": "13800138000", "agreed_to_terms": True},
|
||||
)
|
||||
assert r.status_code == 404
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mock_sms_login_enabled_returns_valid_access_jwt(
|
||||
mock_sms_app, monkeypatch
|
||||
) -> None:
|
||||
monkeypatch.setattr(settings, "mock_sms_login_enabled", True)
|
||||
monkeypatch.setattr(settings, "app_environment", "development")
|
||||
transport = ASGITransport(app=mock_sms_app)
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as ac:
|
||||
r = await ac.post(
|
||||
"/api/auth/mock/sms-login",
|
||||
json={"phone": "13800138000", "agreed_to_terms": True},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
body = r.json()
|
||||
assert body.get("token_type") == "bearer"
|
||||
access = body["access_token"]
|
||||
payload = verify_token(access)
|
||||
assert payload is not None
|
||||
assert payload.get("type") == "access"
|
||||
assert payload.get("sub") == mock_sms_app.state._mock_uid
|
||||
|
||||
svc: MagicMock = mock_sms_app.state._mock_service
|
||||
svc.mock_sms_login.assert_awaited_once_with(
|
||||
phone="13800138000",
|
||||
nickname=None,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mock_sms_login_requires_terms(mock_sms_app, monkeypatch) -> None:
|
||||
monkeypatch.setattr(settings, "mock_sms_login_enabled", True)
|
||||
monkeypatch.setattr(settings, "app_environment", "development")
|
||||
transport = ASGITransport(app=mock_sms_app)
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as ac:
|
||||
r = await ac.post(
|
||||
"/api/auth/mock/sms-login",
|
||||
json={"phone": "13800138000", "agreed_to_terms": False},
|
||||
)
|
||||
assert r.status_code == 400
|
||||
Reference in New Issue
Block a user