refactor: 代码审查修复 — 错误边界、DRY抽取、测试修复、合规性改进
- P1: server.ts 统一错误边界 (withErrorHandler + AppError),修复 3 个失败/卡死测试 - P2: db 层 wrap/paginateQuery 抽取,前端 handleResponse 抽取,parseIdFromUrl 抽取 - P3: middleware 验证消息中文化,Flex→Space 替换 - P0: docs/development/README.md 新增已知设计决策章节 - P3-11 setup 拆分已尝试回退(@testing-library/react preload 依赖无法拆分) - P3-13 config 层测试从本次变更移除
This commit is contained in:
@@ -11,6 +11,8 @@ import type {
|
||||
UpdateModelRequest,
|
||||
} from "../../shared/api";
|
||||
|
||||
import { handleResponse, handleVoidResponse } from "../utils/api";
|
||||
|
||||
const MODELS_KEY = ["models"] as const;
|
||||
|
||||
export async function createModel(data: CreateModelRequest): Promise<Model> {
|
||||
@@ -19,20 +21,17 @@ export async function createModel(data: CreateModelRequest): Promise<Model> {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "POST",
|
||||
});
|
||||
return handleResponse(response);
|
||||
return handleResponse(response, (data) => (data as ModelResponse).model);
|
||||
}
|
||||
|
||||
export async function deleteModel(id: string): Promise<void> {
|
||||
const response = await fetch(`/api/models/${id}`, { method: "DELETE" });
|
||||
if (!response.ok) {
|
||||
const body = (await response.json().catch(() => null)) as null | { error?: string };
|
||||
throw new Error(body?.error ?? `HTTP ${response.status}`);
|
||||
}
|
||||
return handleVoidResponse(response);
|
||||
}
|
||||
|
||||
export async function fetchModel(id: string): Promise<Model> {
|
||||
const response = await fetch(`/api/models/${id}`);
|
||||
return handleResponse(response);
|
||||
return handleResponse(response, (data) => (data as ModelResponse).model);
|
||||
}
|
||||
|
||||
export async function fetchModelList(params: {
|
||||
@@ -76,7 +75,7 @@ export async function updateModel(id: string, data: UpdateModelRequest): Promise
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "PATCH",
|
||||
});
|
||||
return handleResponse(response);
|
||||
return handleResponse(response, (data) => (data as ModelResponse).model);
|
||||
}
|
||||
|
||||
export function useCreateModel() {
|
||||
@@ -129,12 +128,3 @@ export function useUpdateModel() {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function handleResponse(response: Response): Promise<Model> {
|
||||
if (!response.ok) {
|
||||
const body = (await response.json().catch(() => null)) as null | { error?: string };
|
||||
throw new Error(body?.error ?? `HTTP ${response.status}`);
|
||||
}
|
||||
const data = (await response.json()) as ModelResponse;
|
||||
return data.model;
|
||||
}
|
||||
|
||||
@@ -9,16 +9,13 @@ import type {
|
||||
UpdateProjectRequest,
|
||||
} from "../../shared/api";
|
||||
|
||||
import { handleResponse, handleVoidResponse } from "../utils/api";
|
||||
|
||||
const PROJECTS_KEY = ["projects"] as const;
|
||||
|
||||
export async function archiveProject(id: string): Promise<Project> {
|
||||
const response = await fetch(`/api/projects/${id}/archive`, { method: "POST" });
|
||||
if (!response.ok) {
|
||||
const body = (await response.json().catch(() => null)) as null | { error?: string };
|
||||
throw new Error(body?.error ?? `HTTP ${response.status}`);
|
||||
}
|
||||
const data = (await response.json()) as ProjectResponse;
|
||||
return data.project;
|
||||
return handleResponse(response, (data) => (data as ProjectResponse).project);
|
||||
}
|
||||
|
||||
export async function createProject(data: CreateProjectRequest): Promise<Project> {
|
||||
@@ -27,30 +24,17 @@ export async function createProject(data: CreateProjectRequest): Promise<Project
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "POST",
|
||||
});
|
||||
if (!response.ok) {
|
||||
const body = (await response.json().catch(() => null)) as null | { error?: string };
|
||||
throw new Error(body?.error ?? `HTTP ${response.status}`);
|
||||
}
|
||||
const result = (await response.json()) as ProjectResponse;
|
||||
return result.project;
|
||||
return handleResponse(response, (data) => (data as ProjectResponse).project);
|
||||
}
|
||||
|
||||
export async function deleteProject(id: string): Promise<void> {
|
||||
const response = await fetch(`/api/projects/${id}`, { method: "DELETE" });
|
||||
if (!response.ok) {
|
||||
const body = (await response.json().catch(() => null)) as null | { error?: string };
|
||||
throw new Error(body?.error ?? `HTTP ${response.status}`);
|
||||
}
|
||||
return handleVoidResponse(response);
|
||||
}
|
||||
|
||||
export async function fetchProject(id: string): Promise<Project> {
|
||||
const response = await fetch(`/api/projects/${id}`);
|
||||
if (!response.ok) {
|
||||
const body = (await response.json().catch(() => null)) as null | { error?: string };
|
||||
throw new Error(body?.error ?? `HTTP ${response.status}`);
|
||||
}
|
||||
const data = (await response.json()) as ProjectResponse;
|
||||
return data.project;
|
||||
return handleResponse(response, (data) => (data as ProjectResponse).project);
|
||||
}
|
||||
|
||||
export async function fetchProjectList(params: {
|
||||
@@ -76,12 +60,7 @@ export async function fetchProjectList(params: {
|
||||
|
||||
export async function restoreProject(id: string): Promise<Project> {
|
||||
const response = await fetch(`/api/projects/${id}/restore`, { method: "POST" });
|
||||
if (!response.ok) {
|
||||
const body = (await response.json().catch(() => null)) as null | { error?: string };
|
||||
throw new Error(body?.error ?? `HTTP ${response.status}`);
|
||||
}
|
||||
const data = (await response.json()) as ProjectResponse;
|
||||
return data.project;
|
||||
return handleResponse(response, (data) => (data as ProjectResponse).project);
|
||||
}
|
||||
|
||||
export async function updateProject(id: string, data: UpdateProjectRequest): Promise<Project> {
|
||||
@@ -90,12 +69,7 @@ export async function updateProject(id: string, data: UpdateProjectRequest): Pro
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "PATCH",
|
||||
});
|
||||
if (!response.ok) {
|
||||
const body = (await response.json().catch(() => null)) as null | { error?: string };
|
||||
throw new Error(body?.error ?? `HTTP ${response.status}`);
|
||||
}
|
||||
const result = (await response.json()) as ProjectResponse;
|
||||
return result.project;
|
||||
return handleResponse(response, (data) => (data as ProjectResponse).project);
|
||||
}
|
||||
|
||||
export function useArchiveProject() {
|
||||
|
||||
@@ -11,6 +11,8 @@ import type {
|
||||
UpdateProviderRequest,
|
||||
} from "../../shared/api";
|
||||
|
||||
import { handleResponse, handleVoidResponse } from "../utils/api";
|
||||
|
||||
const PROVIDERS_KEY = ["providers"] as const;
|
||||
const MODELS_KEY = ["models"] as const;
|
||||
|
||||
@@ -20,20 +22,17 @@ export async function createProvider(data: CreateProviderRequest): Promise<Provi
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "POST",
|
||||
});
|
||||
return handleResponse(response);
|
||||
return handleResponse(response, (data) => (data as ProviderResponse).provider);
|
||||
}
|
||||
|
||||
export async function deleteProvider(id: string): Promise<void> {
|
||||
const response = await fetch(`/api/providers/${id}`, { method: "DELETE" });
|
||||
if (!response.ok) {
|
||||
const body = (await response.json().catch(() => null)) as null | { error?: string };
|
||||
throw new Error(body?.error ?? `HTTP ${response.status}`);
|
||||
}
|
||||
return handleVoidResponse(response);
|
||||
}
|
||||
|
||||
export async function fetchProvider(id: string): Promise<Provider> {
|
||||
const response = await fetch(`/api/providers/${id}`);
|
||||
return handleResponse(response);
|
||||
return handleResponse(response, (data) => (data as ProviderResponse).provider);
|
||||
}
|
||||
|
||||
export async function fetchProviderList(params: {
|
||||
@@ -84,7 +83,7 @@ export async function updateProvider(id: string, data: UpdateProviderRequest): P
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "PATCH",
|
||||
});
|
||||
return handleResponse(response);
|
||||
return handleResponse(response, (data) => (data as ProviderResponse).provider);
|
||||
}
|
||||
|
||||
export function useCreateProvider() {
|
||||
@@ -145,12 +144,3 @@ export function useUpdateProvider() {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function handleResponse(response: Response): Promise<Provider> {
|
||||
if (!response.ok) {
|
||||
const body = (await response.json().catch(() => null)) as null | { error?: string };
|
||||
throw new Error(body?.error ?? `HTTP ${response.status}`);
|
||||
}
|
||||
const data = (await response.json()) as ProviderResponse;
|
||||
return data.provider;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PlusOutlined } from "@ant-design/icons";
|
||||
import { Button, Flex, Input, Tabs } from "antd";
|
||||
import { Button, Flex, Input, Space, Tabs } from "antd";
|
||||
import { useState } from "react";
|
||||
|
||||
interface ModelsToolbarProps {
|
||||
@@ -29,9 +29,9 @@ export function ModelsToolbar({
|
||||
const createLabel = activeTab === "providers" ? "新建供应商" : "新建模型";
|
||||
|
||||
return (
|
||||
<Flex align="center" gap="var(--ant-margin-lg)" justify="space-between" wrap="wrap">
|
||||
<Flex align="center" gap="large" justify="space-between" wrap="wrap">
|
||||
<Tabs activeKey={activeTab} items={TAB_ITEMS} onChange={onTabChange} />
|
||||
<Flex align="center" gap="small">
|
||||
<Space size="small">
|
||||
<Input.Search
|
||||
allowClear
|
||||
enterButton="搜索"
|
||||
@@ -47,7 +47,7 @@ export function ModelsToolbar({
|
||||
<Button icon={<PlusOutlined />} onClick={openCreateDialog} type="primary">
|
||||
{createLabel}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Space>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Flex } from "antd";
|
||||
import { Space } from "antd";
|
||||
import { useState } from "react";
|
||||
|
||||
import type { Model, Provider, TestModelRequest } from "../../../shared/api";
|
||||
@@ -111,7 +111,7 @@ export function ModelsPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex flex={1} gap="var(--ant-margin-lg)" vertical>
|
||||
<Space orientation="vertical" size="large" style={{ flex: 1 }}>
|
||||
<ModelsToolbar
|
||||
activeTab={activeTab}
|
||||
key={activeTab}
|
||||
@@ -185,6 +185,6 @@ export function ModelsPage() {
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PlusOutlined } from "@ant-design/icons";
|
||||
import { Button, Flex, Input, Tabs } from "antd";
|
||||
import { Button, Flex, Input, Space, Tabs } from "antd";
|
||||
import { useState } from "react";
|
||||
|
||||
import type { ProjectStatus } from "../../../../shared/api";
|
||||
@@ -29,9 +29,9 @@ export function ProjectToolbar({
|
||||
const [draftKeyword, setDraftKeyword] = useState(keyword);
|
||||
|
||||
return (
|
||||
<Flex align="center" gap="var(--ant-margin-lg)" justify="space-between" wrap="wrap">
|
||||
<Flex align="center" gap="large" justify="space-between" wrap="wrap">
|
||||
<Tabs activeKey={activeTab} items={STATUS_TAB_ITEMS} onChange={onTabChange} />
|
||||
<Flex align="center" gap="small">
|
||||
<Space size="small">
|
||||
<Input.Search
|
||||
allowClear
|
||||
enterButton="搜索"
|
||||
@@ -49,7 +49,7 @@ export function ProjectToolbar({
|
||||
新建项目
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
</Space>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Flex } from "antd";
|
||||
import { Space } from "antd";
|
||||
import { useState } from "react";
|
||||
|
||||
import type { Project, ProjectStatus } from "../../../shared/api";
|
||||
@@ -35,7 +35,7 @@ export function ProjectsPage() {
|
||||
const isRowActionPending = archiveMutation.isPending || restoreMutation.isPending || deleteMutation.isPending;
|
||||
|
||||
return (
|
||||
<Flex flex={1} gap="var(--ant-margin-lg)" vertical>
|
||||
<Space orientation="vertical" size="large" style={{ flex: 1 }}>
|
||||
<ProjectToolbar
|
||||
activeTab={tabValue}
|
||||
keyword={keyword}
|
||||
@@ -84,6 +84,6 @@ export function ProjectsPage() {
|
||||
open={dialogOpen}
|
||||
submitting={isSubmitting}
|
||||
/>
|
||||
</Flex>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
15
src/web/utils/api.ts
Normal file
15
src/web/utils/api.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export async function handleResponse<T>(response: Response, extract: (data: unknown) => T): Promise<T> {
|
||||
if (!response.ok) {
|
||||
const body = (await response.json().catch(() => null)) as null | { error?: string };
|
||||
throw new Error(body?.error ?? `HTTP ${response.status}`);
|
||||
}
|
||||
const data: unknown = await response.json();
|
||||
return extract(data);
|
||||
}
|
||||
|
||||
export async function handleVoidResponse(response: Response): Promise<void> {
|
||||
if (!response.ok) {
|
||||
const body = (await response.json().catch(() => null)) as null | { error?: string };
|
||||
throw new Error(body?.error ?? `HTTP ${response.status}`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user