diff --git a/app/api/v1/endpoints/copilot_chat.py b/app/api/v1/endpoints/copilot.py similarity index 94% rename from app/api/v1/endpoints/copilot_chat.py rename to app/api/v1/endpoints/copilot.py index 860bd05..f8c5c75 100644 --- a/app/api/v1/endpoints/copilot_chat.py +++ b/app/api/v1/endpoints/copilot.py @@ -8,9 +8,8 @@ from fastapi import APIRouter, Depends, Request, status from fastapi.responses import StreamingResponse from pydantic import BaseModel, Field -from app.auth.dependencies import get_current_active_user +from app.auth.keycloak_dependencies import get_current_keycloak_username from app.core.config import settings -from app.domain.schemas.user import UserInDB router = APIRouter() @@ -32,7 +31,7 @@ def _sse_event(event: str, data: dict) -> str: async def copilot_chat_stream( payload: CopilotChatStreamRequest, request: Request, - current_user: UserInDB = Depends(get_current_active_user), + username: str = Depends(get_current_keycloak_username), ): timeout = httpx.Timeout( connect=10.0, @@ -55,7 +54,7 @@ async def copilot_chat_stream( body = { "message": payload.message, "conversationId": payload.conversation_id, - "userId": current_user.username, + "userId": username, } try: diff --git a/app/api/v1/endpoints/simulation.py b/app/api/v1/endpoints/simulation.py index 34b0e3a..ac3a679 100644 --- a/app/api/v1/endpoints/simulation.py +++ b/app/api/v1/endpoints/simulation.py @@ -312,23 +312,23 @@ async def valve_isolation_endpoint( - affected_nodes: 受影响的节点列表 - isolatable: 是否可以有效隔离 """ - result = { - "accident_element": "P461309", - "accident_elements": ["P461309"], - "affected_nodes": [ - "J316629_A", - "J317037_B", - "J317060_B", - "J408189_B", - "J499996", - "J524940", - "J535933", - "J58841", - ], - "isolatable": True, - "must_close_valves": ["210521658", "V12974", "V12986", "V12993"], - "optional_valves": [], - } + # result = { + # "accident_element": "P461309", + # "accident_elements": ["P461309"], + # "affected_nodes": [ + # "J316629_A", + # "J317037_B", + # "J317060_B", + # "J408189_B", + # "J499996", + # "J524940", + # "J535933", + # "J58841", + # ], + # "isolatable": True, + # "must_close_valves": ["210521658", "V12974", "V12986", "V12993"], + # "optional_valves": [], + # } result = analyze_valve_isolation(network, accident_element, disabled_valves) return result diff --git a/app/api/v1/router.py b/app/api/v1/router.py index c5c58ae..49b2783 100644 --- a/app/api/v1/router.py +++ b/app/api/v1/router.py @@ -1,6 +1,7 @@ from fastapi import APIRouter from app.api.v1.endpoints import ( auth, + copilot, project, simulation, scada, @@ -18,7 +19,6 @@ from app.api.v1.endpoints import ( user_management, # 新增:用户管理 audit, # 新增:审计日志 meta, - copilot_chat, ) from app.api.v1.endpoints.network import ( general, @@ -113,4 +113,4 @@ api_router.include_router(project_data.router, tags=["Project Data"]) api_router.include_router(extension.router, tags=["Extension"]) # Copilot Chat -api_router.include_router(copilot_chat.router, prefix="/copilot", tags=["Copilot"]) +api_router.include_router(copilot.router, prefix="/copilot", tags=["Copilot"]) diff --git a/app/infra/audit/middleware.py b/app/infra/audit/middleware.py index eb94fe8..657002f 100644 --- a/app/infra/audit/middleware.py +++ b/app/infra/audit/middleware.py @@ -60,6 +60,7 @@ class AuditMiddleware(BaseHTTPMiddleware): "/meta/projects", "/api/v1/openproject/", "/openproject/", + "/api/v1/copilot/chat/", } async def dispatch(self, request: Request, call_next: Callable) -> Response: diff --git a/copilot-sidecar-python/server.py b/copilot-sidecar/server.py similarity index 99% rename from copilot-sidecar-python/server.py rename to copilot-sidecar/server.py index edf326e..ed44fe6 100644 --- a/copilot-sidecar-python/server.py +++ b/copilot-sidecar/server.py @@ -27,7 +27,7 @@ app = FastAPI(title="TJWater Copilot Python Sidecar") client: Optional[CopilotClient] = None sessions: dict[str, SessionHolder] = {} session_ttl_seconds = int(os.getenv("COPILOT_SESSION_TTL_SECONDS", "1800")) -model = os.getenv("COPILOT_MODEL", "gpt-5.1-codex") +model = os.getenv("COPILOT_MODEL", "gpt-5.3-codex") @app.on_event("startup") diff --git a/tests/api/test_copilot_chat_endpoint.py b/tests/api/test_copilot_chat_endpoint.py new file mode 100644 index 0000000..86535aa --- /dev/null +++ b/tests/api/test_copilot_chat_endpoint.py @@ -0,0 +1,117 @@ +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from app.api.v1.endpoints import copilot as copilot_endpoint + + +class _FakeStreamResponse: + def __init__(self, status_code: int, lines: list[str] | None = None, body: bytes = b""): + self.status_code = status_code + self._lines = lines or [] + self._body = body + + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc, tb): + return None + + async def aread(self) -> bytes: + return self._body + + async def aiter_lines(self): + for line in self._lines: + yield line + + +class _FakeAsyncClient: + response: _FakeStreamResponse + captured: dict + + def __init__(self, *args, **kwargs): + self._kwargs = kwargs + + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc, tb): + return None + + def stream(self, method: str, url: str, json: dict, headers: dict): + _FakeAsyncClient.captured = { + "method": method, + "url": url, + "json": json, + "headers": headers, + "client_kwargs": self._kwargs, + } + return _FakeAsyncClient.response + + +def _build_client(monkeypatch) -> TestClient: + app = FastAPI() + app.include_router(copilot_endpoint.router, prefix="/api/v1/copilot") + app.dependency_overrides[copilot_endpoint.get_current_keycloak_username] = ( + lambda: "tester" + ) + monkeypatch.setattr(copilot_endpoint.httpx, "AsyncClient", _FakeAsyncClient) + return TestClient(app) + + +def test_chat_stream_forwards_auth_and_payload(monkeypatch): + _FakeAsyncClient.response = _FakeStreamResponse( + status_code=200, + lines=[ + 'event: token', + 'data: {"conversationId":"c1","content":"hello"}', + "", + 'event: done', + 'data: {"conversationId":"c1"}', + "", + ], + ) + client = _build_client(monkeypatch) + + response = client.post( + "/api/v1/copilot/chat/stream", + json={"message": "hi", "conversation_id": "conv-1"}, + headers={ + "Authorization": "Bearer keycloak-token", + "X-Project-Id": "project-a", + }, + ) + + assert response.status_code == 200 + assert "text/event-stream" in response.headers["content-type"] + assert "event: token" in response.text + assert "event: done" in response.text + + captured = _FakeAsyncClient.captured + assert captured["method"] == "POST" + assert captured["url"].endswith("/chat/stream") + assert captured["headers"]["authorization"] == "Bearer keycloak-token" + assert captured["headers"]["x-project-id"] == "project-a" + assert captured["json"] == { + "message": "hi", + "conversationId": "conv-1", + "userId": "tester", + } + + +def test_chat_stream_emits_error_event_when_upstream_fails(monkeypatch): + _FakeAsyncClient.response = _FakeStreamResponse( + status_code=401, + body=b"upstream unauthorized", + ) + client = _build_client(monkeypatch) + + response = client.post( + "/api/v1/copilot/chat/stream", + json={"message": "hi"}, + headers={"Authorization": "Bearer keycloak-token"}, + ) + + assert response.status_code == 200 + assert "event: error" in response.text + assert "Copilot sidecar request failed" in response.text + assert '"status": 401' in response.text