diff --git a/src/nyahome/cli/openapi_docstring.py b/src/nyahome/cli/openapi_docstring.py index 467b655..e5cb9c9 100644 --- a/src/nyahome/cli/openapi_docstring.py +++ b/src/nyahome/cli/openapi_docstring.py @@ -38,17 +38,21 @@ import inspect import logging from typing import Any, Dict, Optional, Set -from docstring_parser import DocstringStyle, ParseError, parse +from docstring_parser import Docstring, DocstringStyle, ParseError, parse from fastapi import FastAPI from fastapi.openapi.utils import get_openapi logger = logging.getLogger(__name__) -def _parse_docstring( - func: Any, style: DocstringStyle = DocstringStyle.AUTO -) -> Optional[Any]: - """解析函数的 Google Style docstring,失败时返回 None。""" +def _parse_docstring(func: Any, style: DocstringStyle = DocstringStyle.AUTO) -> Optional[Docstring]: # noqa ANN401 有意为之 + """ + 解析函数的 Google Style docstring,失败时返回 None。 + + Returns: + 成功解析时返回 docstring_parser.Docstring。 + 失败时返回 None。 + """ doc = inspect.getdoc(func) if not doc: return None @@ -67,8 +71,13 @@ def _parse_docstring( return None -def _build_param_lookup(parsed_doc: Any) -> Dict[str, str]: - """从解析后的 docstring 构建 {参数名: 描述} 映射。""" +def _build_param_lookup(parsed_doc: Docstring) -> Dict[str, str]: + """ + 从解析后的 docstring 构建 {参数名: 描述} 映射。 + + Returns: + {参数名: 描述} + """ lookup: Dict[str, str] = {} for param in getattr(parsed_doc, "params", []) or []: name = getattr(param, "arg_name", None) @@ -79,8 +88,13 @@ def _build_param_lookup(parsed_doc: Any) -> Dict[str, str]: return lookup -def _build_returns_description(parsed_doc: Any) -> Optional[str]: - """从解析后的 docstring 构建返回值描述。""" +def _build_returns_description(parsed_doc: Docstring) -> Optional[str]: + """ + 从解析后的 docstring 构建返回值描述。 + + Returns: + 可能的字符串为返回值描述。 + """ ret = getattr(parsed_doc, "returns", None) if not ret: return None @@ -91,8 +105,13 @@ def _build_returns_description(parsed_doc: Any) -> Optional[str]: return desc or None -def _build_raises_description(parsed_doc: Any) -> Optional[str]: - """从解析后的 docstring 构建异常描述(Markdown 列表)。""" +def _build_raises_description(parsed_doc: Docstring) -> Optional[str]: + """ + 从解析后的 docstring 构建异常描述(Markdown 列表)。 + + Returns: + 可能的字符串为异常描述。 + """ raises = getattr(parsed_doc, "raises", None) if not raises: return None @@ -110,7 +129,12 @@ def _build_raises_description(parsed_doc: Any) -> Optional[str]: def _dedent_description(text: str) -> str: - """清理 docstring 描述中的多余缩进,保留段落结构。""" + """ + 清理 docstring 描述中的多余缩进,保留段落结构。 + + Returns: + docstring 字符串 + """ if not text: return "" lines = text.splitlines() @@ -123,11 +147,12 @@ def _dedent_description(text: str) -> str: return "\n".join(cleaned).strip() -def _get_clean_operation_description(parsed_doc: Any) -> str: +def _get_clean_operation_description(parsed_doc: Docstring) -> str: """ 生成干净的 operation description。 - 策略:保留 summary + long_description,去掉 Args/Returns/Raises 等机器块。 + Returns: + 策略:保留 summary + long_description,去掉 Args/Returns/Raises 等机器块。 """ parts: list[str] = [] short = getattr(parsed_doc, "short_description", None) @@ -142,10 +167,10 @@ def _get_clean_operation_description(parsed_doc: Any) -> str: def _inject_schema_properties( - schema: Dict[str, Any], - param_lookup: Dict[str, str], - visited_refs: Optional[Set[str]] = None, - openapi_schema: Optional[Dict[str, Any]] = None, + schema: Dict[str, Any], + param_lookup: Dict[str, str], + visited_refs: Optional[Set[str]] = None, + openapi_schema: Optional[Dict[str, Any]] = None, ) -> None: """ 递归注入 schema properties 的描述。 @@ -174,7 +199,7 @@ def _inject_schema_properties( properties = schema.get("properties") if isinstance(properties, dict): for prop_name, prop_schema in properties.items(): - if prop_name in param_lookup: + if prop_name in param_lookup: # noqa SIM102 # 只有当 docstring 描述非空时才写入 if param_lookup[prop_name]: prop_schema["description"] = param_lookup[prop_name] @@ -198,11 +223,11 @@ def _inject_schema_properties( def enrich_openapi_from_docstrings( - app: FastAPI, - *, - prefer_docstring_over_field: bool = True, - append_raises_to_description: bool = True, - docstring_style: DocstringStyle = DocstringStyle.AUTO, + app: FastAPI, + *, + prefer_docstring_over_field: bool = True, + append_raises_to_description: bool = True, + docstring_style: DocstringStyle = DocstringStyle.AUTO, ) -> None: """ 为 FastAPI 应用启用 docstring 驱动的 OpenAPI 增强。 @@ -220,8 +245,8 @@ def enrich_openapi_from_docstrings( 解析风格。团队统一用 Google 时可显式传入 ``DocstringStyle.GOOGLE``。 """ - # 保存原始的 openapi 函数(如果有) - original_openapi = app.openapi + # 可选择保存原始的 openapi 函数(如果有) + # original_openapi = app.openapi def custom_openapi() -> Dict[str, Any]: # 如果已经有缓存,直接返回 @@ -284,9 +309,7 @@ def enrich_openapi_from_docstrings( for media_obj in content.values(): schema = media_obj.get("schema", {}) if schema: - _inject_schema_properties( - schema, param_lookup, openapi_schema=openapi_schema - ) + _inject_schema_properties(schema, param_lookup, openapi_schema=openapi_schema) # ---------- 4. 注入返回值描述 ---------- if returns_desc: @@ -300,9 +323,7 @@ def enrich_openapi_from_docstrings( if schema and not schema.get("description"): schema["description"] = returns_desc # 递归注入 schema 内部字段 - _inject_schema_properties( - schema, param_lookup, openapi_schema=openapi_schema - ) + _inject_schema_properties(schema, param_lookup, openapi_schema=openapi_schema) # ---------- 5. 异常描述追加 ---------- if append_raises_to_description and raises_desc: @@ -315,61 +336,4 @@ def enrich_openapi_from_docstrings( app.openapi_schema = openapi_schema return app.openapi_schema - app.openapi = custom_openapi - - -# ============================================================================= -# 完整使用示例 -# ============================================================================= -if __name__ == "__main__": - import json - from typing import Annotated, Optional - - from fastapi import FastAPI, Path, Query - from pydantic import BaseModel, Field - - # ---------- 定义 DTO ---------- - class EditChatDto(BaseModel): - old_message: str = Field(..., description="原始消息内容") - new_message: str = Field(..., description="修改后的消息内容") - change: str = Field(..., description="变更类型") - - class ReturnDto(BaseModel): - result: str = Field(..., description="操作结果") - content: str = Field(..., description="最新聊天记录内容") - - # ---------- 创建应用 ---------- - app = FastAPI(title="Chatroom API", version="1.0.0") - - # 启用 docstring 增强(必须在注册路由之前或之后都可以,但要在生成 schema 之前) - enrich_openapi_from_docstrings(app) - - # ---------- 注册路由 ---------- - @app.post("/api/chatroom/{id_}/chat/edit/", response_model=ReturnDto) - async def edit_chatroom_chat( - id_: Annotated[str, Path(description="聊天室 ID")], - body: EditChatDto, - force: Annotated[Optional[bool], Query(description="是否强制覆盖")] = None, - ) -> ReturnDto: - """编辑聊天室消息。 - - 此端点不负责调用 AI 生成输出,而是用于修改一条已经保存在聊天记录中的消息。 - 前端调用后应使用返回的 content 刷新当前聊天室界面。 - - Args: - id_: 聊天室唯一标识符,UUID 格式。 - body: 编辑请求体,包含旧消息、新消息和变更类型。 - force: 是否跳过冲突检测直接覆盖。 - - Returns: - ReturnDto: 操作结果,其中 result 字段表示状态,content 字段为最新聊天记录。 - - Raises: - HTTPException: 404 表明未找到聊天室。 - HTTPException: 400 表明聊天记录匹配失败,未更新。 - """ - return ReturnDto(result="ok", content="new content") - - # ---------- 输出生成的 OpenAPI schema ---------- - schema = app.openapi() - print(json.dumps(schema, indent=2, ensure_ascii=False)) \ No newline at end of file + app.openapi = custom_openapi # type: ignore[method-assign] diff --git a/src/nyahome/router/admin_router.py b/src/nyahome/router/admin_router.py index e870a63..598c1eb 100644 --- a/src/nyahome/router/admin_router.py +++ b/src/nyahome/router/admin_router.py @@ -19,7 +19,7 @@ from .response_model import ReturnDto logger = logging.getLogger(__name__) -admin_router = APIRouter(tags=["admin"], prefix="/admin") +admin_router = APIRouter(tags=["Admin"], prefix="/admin") class UserLogin(BaseModel): diff --git a/webui/components.d.ts b/webui/components.d.ts index 28257c7..2ad6b39 100644 --- a/webui/components.d.ts +++ b/webui/components.d.ts @@ -15,6 +15,7 @@ declare module 'vue' { AiiModelAddModal: typeof import('./src/components/chatroom/AiiModelAddModal.vue')['default'] AiiProviderAddModal: typeof import('./src/components/chatroom/AiiProviderAddModal.vue')['default'] ChangeEmailModal: typeof import('./src/components/admin/ChangeEmailModal.vue')['default'] + ChangePhoneModal: typeof import('./src/components/admin/ChangePhoneModal.vue')['default'] ChatControlPanel: typeof import('./src/components/chatroom/ChatControlPanel.vue')['default'] ChatMessage: typeof import('./src/components/chatroom/ChatMessage.vue')['default'] ChatPromptQuicker: typeof import('./src/components/chatroom/ChatPromptQuicker.vue')['default'] @@ -84,6 +85,7 @@ declare global { const AiiModelAddModal: typeof import('./src/components/chatroom/AiiModelAddModal.vue')['default'] const AiiProviderAddModal: typeof import('./src/components/chatroom/AiiProviderAddModal.vue')['default'] const ChangeEmailModal: typeof import('./src/components/admin/ChangeEmailModal.vue')['default'] + const ChangePhoneModal: typeof import('./src/components/admin/ChangePhoneModal.vue')['default'] const ChatControlPanel: typeof import('./src/components/chatroom/ChatControlPanel.vue')['default'] const ChatMessage: typeof import('./src/components/chatroom/ChatMessage.vue')['default'] const ChatPromptQuicker: typeof import('./src/components/chatroom/ChatPromptQuicker.vue')['default'] diff --git a/webui/src/assets/main.scss b/webui/src/assets/main.scss index 430eb00..244dabc 100644 --- a/webui/src/assets/main.scss +++ b/webui/src/assets/main.scss @@ -6,6 +6,10 @@ body { overflow: hidden; } +* { + box-sizing: border-box; +} + div#app { height: 100%; overflow: hidden; diff --git a/webui/src/pages/AdminPage.vue b/webui/src/pages/AdminPage.vue index 9896d7f..e28ff56 100644 --- a/webui/src/pages/AdminPage.vue +++ b/webui/src/pages/AdminPage.vue @@ -1,10 +1,10 @@