style: 一些复杂而综合的细节修正

This commit is contained in:
2026-05-25 21:11:29 +08:00
parent 8efb55827c
commit ff2074b400
8 changed files with 97 additions and 133 deletions
+44 -80
View File
@@ -38,17 +38,21 @@ import inspect
import logging import logging
from typing import Any, Dict, Optional, Set 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 import FastAPI
from fastapi.openapi.utils import get_openapi from fastapi.openapi.utils import get_openapi
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def _parse_docstring( def _parse_docstring(func: Any, style: DocstringStyle = DocstringStyle.AUTO) -> Optional[Docstring]: # noqa ANN401 有意为之
func: Any, style: DocstringStyle = DocstringStyle.AUTO """
) -> Optional[Any]: 解析函数的 Google Style docstring,失败时返回 None。
"""解析函数的 Google Style docstring,失败时返回 None。"""
Returns:
成功解析时返回 docstring_parser.Docstring。
失败时返回 None。
"""
doc = inspect.getdoc(func) doc = inspect.getdoc(func)
if not doc: if not doc:
return None return None
@@ -67,8 +71,13 @@ def _parse_docstring(
return None return None
def _build_param_lookup(parsed_doc: Any) -> Dict[str, str]: def _build_param_lookup(parsed_doc: Docstring) -> Dict[str, str]:
"""从解析后的 docstring 构建 {参数名: 描述} 映射。""" """
从解析后的 docstring 构建 {参数名: 描述} 映射。
Returns:
{参数名: 描述}
"""
lookup: Dict[str, str] = {} lookup: Dict[str, str] = {}
for param in getattr(parsed_doc, "params", []) or []: for param in getattr(parsed_doc, "params", []) or []:
name = getattr(param, "arg_name", None) name = getattr(param, "arg_name", None)
@@ -79,8 +88,13 @@ def _build_param_lookup(parsed_doc: Any) -> Dict[str, str]:
return lookup return lookup
def _build_returns_description(parsed_doc: Any) -> Optional[str]: def _build_returns_description(parsed_doc: Docstring) -> Optional[str]:
"""从解析后的 docstring 构建返回值描述。""" """
从解析后的 docstring 构建返回值描述。
Returns:
可能的字符串为返回值描述。
"""
ret = getattr(parsed_doc, "returns", None) ret = getattr(parsed_doc, "returns", None)
if not ret: if not ret:
return None return None
@@ -91,8 +105,13 @@ def _build_returns_description(parsed_doc: Any) -> Optional[str]:
return desc or None return desc or None
def _build_raises_description(parsed_doc: Any) -> Optional[str]: def _build_raises_description(parsed_doc: Docstring) -> Optional[str]:
"""从解析后的 docstring 构建异常描述(Markdown 列表)。""" """
从解析后的 docstring 构建异常描述(Markdown 列表)。
Returns:
可能的字符串为异常描述。
"""
raises = getattr(parsed_doc, "raises", None) raises = getattr(parsed_doc, "raises", None)
if not raises: if not raises:
return None return None
@@ -110,7 +129,12 @@ def _build_raises_description(parsed_doc: Any) -> Optional[str]:
def _dedent_description(text: str) -> str: def _dedent_description(text: str) -> str:
"""清理 docstring 描述中的多余缩进,保留段落结构。""" """
清理 docstring 描述中的多余缩进,保留段落结构。
Returns:
docstring 字符串
"""
if not text: if not text:
return "" return ""
lines = text.splitlines() lines = text.splitlines()
@@ -123,10 +147,11 @@ def _dedent_description(text: str) -> str:
return "\n".join(cleaned).strip() 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。 生成干净的 operation description。
Returns:
策略:保留 summary + long_description,去掉 Args/Returns/Raises 等机器块。 策略:保留 summary + long_description,去掉 Args/Returns/Raises 等机器块。
""" """
parts: list[str] = [] parts: list[str] = []
@@ -174,7 +199,7 @@ def _inject_schema_properties(
properties = schema.get("properties") properties = schema.get("properties")
if isinstance(properties, dict): if isinstance(properties, dict):
for prop_name, prop_schema in properties.items(): for prop_name, prop_schema in properties.items():
if prop_name in param_lookup: if prop_name in param_lookup: # noqa SIM102
# 只有当 docstring 描述非空时才写入 # 只有当 docstring 描述非空时才写入
if param_lookup[prop_name]: if param_lookup[prop_name]:
prop_schema["description"] = param_lookup[prop_name] prop_schema["description"] = param_lookup[prop_name]
@@ -220,8 +245,8 @@ def enrich_openapi_from_docstrings(
解析风格。团队统一用 Google 时可显式传入 ``DocstringStyle.GOOGLE``。 解析风格。团队统一用 Google 时可显式传入 ``DocstringStyle.GOOGLE``。
""" """
# 保存原始的 openapi 函数(如果有) # 可选择保存原始的 openapi 函数(如果有)
original_openapi = app.openapi # original_openapi = app.openapi
def custom_openapi() -> Dict[str, Any]: def custom_openapi() -> Dict[str, Any]:
# 如果已经有缓存,直接返回 # 如果已经有缓存,直接返回
@@ -284,9 +309,7 @@ def enrich_openapi_from_docstrings(
for media_obj in content.values(): for media_obj in content.values():
schema = media_obj.get("schema", {}) schema = media_obj.get("schema", {})
if schema: if schema:
_inject_schema_properties( _inject_schema_properties(schema, param_lookup, openapi_schema=openapi_schema)
schema, param_lookup, openapi_schema=openapi_schema
)
# ---------- 4. 注入返回值描述 ---------- # ---------- 4. 注入返回值描述 ----------
if returns_desc: if returns_desc:
@@ -300,9 +323,7 @@ def enrich_openapi_from_docstrings(
if schema and not schema.get("description"): if schema and not schema.get("description"):
schema["description"] = returns_desc schema["description"] = returns_desc
# 递归注入 schema 内部字段 # 递归注入 schema 内部字段
_inject_schema_properties( _inject_schema_properties(schema, param_lookup, openapi_schema=openapi_schema)
schema, param_lookup, openapi_schema=openapi_schema
)
# ---------- 5. 异常描述追加 ---------- # ---------- 5. 异常描述追加 ----------
if append_raises_to_description and raises_desc: if append_raises_to_description and raises_desc:
@@ -315,61 +336,4 @@ def enrich_openapi_from_docstrings(
app.openapi_schema = openapi_schema app.openapi_schema = openapi_schema
return app.openapi_schema return app.openapi_schema
app.openapi = custom_openapi app.openapi = custom_openapi # type: ignore[method-assign]
# =============================================================================
# 完整使用示例
# =============================================================================
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))
+1 -1
View File
@@ -19,7 +19,7 @@ from .response_model import ReturnDto
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
admin_router = APIRouter(tags=["admin"], prefix="/admin") admin_router = APIRouter(tags=["Admin"], prefix="/admin")
class UserLogin(BaseModel): class UserLogin(BaseModel):
+2
View File
@@ -15,6 +15,7 @@ declare module 'vue' {
AiiModelAddModal: typeof import('./src/components/chatroom/AiiModelAddModal.vue')['default'] AiiModelAddModal: typeof import('./src/components/chatroom/AiiModelAddModal.vue')['default']
AiiProviderAddModal: typeof import('./src/components/chatroom/AiiProviderAddModal.vue')['default'] AiiProviderAddModal: typeof import('./src/components/chatroom/AiiProviderAddModal.vue')['default']
ChangeEmailModal: typeof import('./src/components/admin/ChangeEmailModal.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'] ChatControlPanel: typeof import('./src/components/chatroom/ChatControlPanel.vue')['default']
ChatMessage: typeof import('./src/components/chatroom/ChatMessage.vue')['default'] ChatMessage: typeof import('./src/components/chatroom/ChatMessage.vue')['default']
ChatPromptQuicker: typeof import('./src/components/chatroom/ChatPromptQuicker.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 AiiModelAddModal: typeof import('./src/components/chatroom/AiiModelAddModal.vue')['default']
const AiiProviderAddModal: typeof import('./src/components/chatroom/AiiProviderAddModal.vue')['default'] const AiiProviderAddModal: typeof import('./src/components/chatroom/AiiProviderAddModal.vue')['default']
const ChangeEmailModal: typeof import('./src/components/admin/ChangeEmailModal.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 ChatControlPanel: typeof import('./src/components/chatroom/ChatControlPanel.vue')['default']
const ChatMessage: typeof import('./src/components/chatroom/ChatMessage.vue')['default'] const ChatMessage: typeof import('./src/components/chatroom/ChatMessage.vue')['default']
const ChatPromptQuicker: typeof import('./src/components/chatroom/ChatPromptQuicker.vue')['default'] const ChatPromptQuicker: typeof import('./src/components/chatroom/ChatPromptQuicker.vue')['default']
+4
View File
@@ -6,6 +6,10 @@ body {
overflow: hidden; overflow: hidden;
} }
* {
box-sizing: border-box;
}
div#app { div#app {
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
+11 -9
View File
@@ -1,10 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import UserAction from '@/components/admin/UserAction.vue' import UserAction from '@/components/admin/UserAction.vue'
import type { MenuOption } from 'naive-ui' import type {MenuOption} from 'naive-ui'
import { computed, onMounted, ref, useTemplateRef } from 'vue' import {computed, onMounted, ref, useTemplateRef} from 'vue'
import { useRouter } from 'vue-router' import {useRouter} from 'vue-router'
import { useNowUser } from '@/stores/now-user.js' import {useNowUser} from '@/stores/now-user.js'
import { useHead } from '@unhead/vue' import {useHead} from '@unhead/vue'
useHead({ useHead({
titleTemplate: '%s | 管理面板 | NayHome', titleTemplate: '%s | 管理面板 | NayHome',
@@ -62,7 +62,9 @@ function handleMenuClick(key: string) {
onMounted(() => { onMounted(() => {
const key = ROUTER.currentRoute.value.fullPath.replace('/admin/', '') const key = ROUTER.currentRoute.value.fullPath.replace('/admin/', '')
if (key) { if (key.endsWith('/admin')) {
selectOption.value = ''
} else if (key) {
selectOption.value = key selectOption.value = key
menu.value?.showOption(key) menu.value?.showOption(key)
} else { } else {
@@ -74,16 +76,16 @@ onMounted(() => {
<template> <template>
<div id="user-page"> <div id="user-page">
<div id="user-page-sidebar"> <div id="user-page-sidebar">
<user-action /> <user-action/>
<div class="nyahome-card"> <div class="nyahome-card">
<n-menu ref="menu" v-model:value="selectOption" :options @update:value="handleMenuClick" /> <n-menu ref="menu" v-model:value="selectOption" :options @update:value="handleMenuClick"/>
</div> </div>
</div> </div>
<router-view v-slot="{ Component }"> <router-view v-slot="{ Component }">
<div id="user-page-content"> <div id="user-page-content">
<keep-alive> <keep-alive>
<component :is="Component" /> <component :is="Component"/>
</keep-alive> </keep-alive>
</div> </div>
</router-view> </router-view>
+12 -12
View File
@@ -1,10 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { useNowUser } from '@/stores/now-user.js' import {useNowUser} from '@/stores/now-user.js'
import { ref, watch } from 'vue' import {ref, watch} from 'vue'
import SelectFileModal from '@/components/file/SelectFileModal.vue' import SelectFileModal from '@/components/file/SelectFileModal.vue'
import { api } from '@/tools/web.js' import {api} from '@/tools/web.js'
import type { UploadFileDto, UserDto } from '@/types/user.js' import type {UploadFileDto, UserDto} from '@/types/user.js'
import { useHead } from '@unhead/vue' import {useHead} from '@unhead/vue'
import ChangeEmailModal from '@/components/admin/ChangeEmailModal.vue' import ChangeEmailModal from '@/components/admin/ChangeEmailModal.vue'
import ChangePhoneModal from '@/components/admin/ChangePhoneModal.vue' import ChangePhoneModal from '@/components/admin/ChangePhoneModal.vue'
@@ -69,7 +69,7 @@ watch(
() => { () => {
reInitForm() reInitForm()
}, },
{ immediate: true }, {immediate: true},
) )
async function save() { async function save() {
@@ -91,14 +91,14 @@ async function save() {
<div class="ui-content"> <div class="ui-content">
<n-form style="width: 450px" label-width="auto" label-placement="left" label-align="right"> <n-form style="width: 450px" label-width="auto" label-placement="left" label-align="right">
<n-form-item label="用户名"> <n-form-item label="用户名">
<n-input v-model:value="infoForm.name" /> <n-input v-model:value="infoForm.name"/>
</n-form-item> </n-form-item>
<n-form-item label="展示名称"> <n-form-item label="展示名称">
<n-input v-model:value="infoForm.display_name" /> <n-input v-model:value="infoForm.display_name"/>
</n-form-item> </n-form-item>
<n-form-item label="头像"> <n-form-item label="头像">
<n-flex> <n-flex>
<n-avatar v-model:src="infoForm.avatar_url" :size="96" circle /> <n-avatar v-model:src="infoForm.avatar_url" :size="96" circle/>
<n-flex vertical> <n-flex vertical>
<n-tag type="info">需在内容-上传中提前上传图像</n-tag> <n-tag type="info">需在内容-上传中提前上传图像</n-tag>
<n-tag type="warning">使用方形图像以获得最佳效果</n-tag> <n-tag type="warning">使用方形图像以获得最佳效果</n-tag>
@@ -116,7 +116,7 @@ async function save() {
</n-form-item> </n-form-item>
<n-form-item label="个人背景"> <n-form-item label="个人背景">
<n-flex> <n-flex>
<n-avatar v-model:src="infoForm.background_url" :size="96" object-fit="cover" /> <n-avatar v-model:src="infoForm.background_url" :size="96" object-fit="cover"/>
<n-flex vertical> <n-flex vertical>
<n-tag type="info">需在内容-上传中提前上传图像</n-tag> <n-tag type="info">需在内容-上传中提前上传图像</n-tag>
<n-flex> <n-flex>
@@ -139,10 +139,10 @@ async function save() {
/> />
</n-form-item> </n-form-item>
<n-form-item label="邮箱"> <n-form-item label="邮箱">
<n-input v-model:value="NOWUSER.email" disabled /> <n-input v-model:value="NOWUSER.email" disabled/>
</n-form-item> </n-form-item>
<n-form-item label="手机号"> <n-form-item label="手机号">
<n-input v-model:value="NOWUSER.phone" disabled /> <n-input v-model:value="NOWUSER.phone" disabled/>
</n-form-item> </n-form-item>
</n-form> </n-form>
<n-flex> <n-flex>
-3
View File
@@ -13,13 +13,10 @@
// Bundler mode provides a smoother developer experience. // Bundler mode provides a smoother developer experience.
"module": "preserve", "module": "preserve",
"moduleResolution": "bundler", "moduleResolution": "bundler",
// Include Node.js types and avoid accidentally including other `@types/*` packages. // Include Node.js types and avoid accidentally including other `@types/*` packages.
"types": ["node"], "types": ["node"],
// Disable emitting output during `vue-tsc --build`, which is used for type-checking only. // Disable emitting output during `vue-tsc --build`, which is used for type-checking only.
"noEmit": true, "noEmit": true,
// `vue-tsc --build` produces a .tsbuildinfo file for incremental type-checking. // `vue-tsc --build` produces a .tsbuildinfo file for incremental type-checking.
// Specified here to keep it out of the root directory. // Specified here to keep it out of the root directory.
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo" "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo"
+11 -16
View File
@@ -4,15 +4,15 @@ import {defineConfig} from 'vite'
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx' import vueJsx from '@vitejs/plugin-vue-jsx'
import vueDevTools from 'vite-plugin-vue-devtools' import vueDevTools from 'vite-plugin-vue-devtools'
import {readFileSync} from "node:fs"; import {readFileSync} from 'node:fs'
import {resolve} from "path"; import {resolve} from 'path'
import AutoImport from 'unplugin-auto-import/vite' import AutoImport from 'unplugin-auto-import/vite'
import {NaiveUiResolver} from 'unplugin-vue-components/resolvers' import {NaiveUiResolver} from 'unplugin-vue-components/resolvers'
import Components from 'unplugin-vue-components/vite' import Components from 'unplugin-vue-components/vite'
import {unheadVueComposablesImports} from "@unhead/vue"; import {unheadVueComposablesImports} from '@unhead/vue'
// 从 package.json 里搞到 WebUI 版本号 // 从 package.json 里搞到 WebUI 版本号
const pkg = JSON.parse(readFileSync(resolve(__dirname, 'package.json'), 'utf-8')); const pkg = JSON.parse(readFileSync(resolve(__dirname, 'package.json'), 'utf-8'))
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
@@ -24,22 +24,17 @@ export default defineConfig({
imports: [ imports: [
'vue', 'vue',
{ {
'naive-ui': [ 'naive-ui': ['useDialog', 'useMessage', 'useNotification', 'useLoadingBar'],
'useDialog',
'useMessage',
'useNotification',
'useLoadingBar'
]
}, },
unheadVueComposablesImports, unheadVueComposablesImports,
] ],
}), }),
Components({ Components({
resolvers: [NaiveUiResolver()] resolvers: [NaiveUiResolver()],
}) }),
], ],
define: { define: {
__VERSION__: JSON.stringify(pkg.version) __VERSION__: JSON.stringify(pkg.version),
}, },
build: { build: {
rolldownOptions: { rolldownOptions: {
@@ -47,11 +42,11 @@ export default defineConfig({
index: resolve(__dirname, 'index.html'), index: resolve(__dirname, 'index.html'),
}, },
}, },
outDir: "../src/nyahome/static" outDir: '../src/nyahome/static',
}, },
resolve: { resolve: {
alias: { alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)) '@': fileURLToPath(new URL('./src', import.meta.url)),
}, },
}, },
server: { server: {