refactor: update knowledge document types and API client interfaces

- Refactored types in `Knowledge/types.ts` to introduce new interfaces for document handling.
- Added `KnowledgeDocItem`, `KnowledgeDocsListResponse`, `KnowledgeDocsUploadInput`, `KnowledgeDocsUploadResponse`, and `KnowledgeDocsDeleteResponse` for better structure and clarity.
- Updated `KnowledgeDocsApiClient` interface to include methods for listing, uploading, and deleting documents.

fix: replace deprecated icons in AccountSettingsPanel and SettingMenu

- Replaced `CheckCircleIcon` with `CheckCircle` from `lucide-react` in `AccountSettingsPanel.tsx`.
- Updated `SettingMenu.tsx` to use `Settings` and `User` from `lucide-react` instead of custom icons.

test: add tests for knowledge docs routes and KnowledgePage

- Created `knowledge-docs-routes.test.ts` to test API routes for listing, uploading, and deleting knowledge documents.
- Added `knowledge-page.test.tsx` to test the rendering and functionality of the KnowledgePage component, including document loading and deletion.
This commit is contained in:
duanshuwen
2026-04-19 09:40:07 +08:00
parent 92ec3189bc
commit 5cc9b86e1f
20 changed files with 1752 additions and 1159 deletions

View File

@@ -0,0 +1,132 @@
import { hostApiFetch } from './host-api';
import type {
KnowledgeDocItem,
KnowledgeDocsDeleteResponse,
KnowledgeDocsListResponse,
KnowledgeDocsUploadInput,
KnowledgeDocsUploadResponse,
} from '../pages/Knowledge/types';
type KnowledgeDocsListPayload = KnowledgeDocsListResponse | KnowledgeDocItem[] | { success?: boolean; files?: unknown; error?: string };
type KnowledgeDocsUploadPayload = KnowledgeDocsUploadResponse | { success?: boolean; file?: unknown; files?: unknown; error?: string };
type KnowledgeDocsDeletePayload = KnowledgeDocsDeleteResponse | { success?: boolean; error?: string };
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
}
function normalizeName(value: unknown): string {
return typeof value === 'string' ? value.trim() : '';
}
function normalizeSize(value: unknown): number {
if (typeof value === 'number' && Number.isFinite(value) && value >= 0) {
return Math.floor(value);
}
if (typeof value === 'string') {
const parsed = Number(value);
if (Number.isFinite(parsed) && parsed >= 0) {
return Math.floor(parsed);
}
}
return 0;
}
function normalizeModifiedAt(value: unknown): string {
if (typeof value === 'string' && value.trim()) {
return value.trim();
}
if (typeof value === 'number' && Number.isFinite(value)) {
return new Date(value).toISOString();
}
return new Date(0).toISOString();
}
function normalizeType(value: unknown, name: string): string {
const rawType = typeof value === 'string' ? value.trim() : '';
if (rawType) return rawType;
const dotIndex = name.lastIndexOf('.');
if (dotIndex > 0 && dotIndex < name.length - 1) {
return name.slice(dotIndex + 1).toLowerCase();
}
return 'unknown';
}
function normalizeDocItem(value: unknown): KnowledgeDocItem | null {
if (!isRecord(value)) return null;
const name = normalizeName(value.name ?? value.fileName ?? value.file ?? value.path);
if (!name) return null;
return {
name,
size: normalizeSize(value.size ?? value.bytes ?? value.length),
modifiedAt: normalizeModifiedAt(value.modifiedAt ?? value.updatedAt ?? value.mtime ?? value.lastModified),
type: normalizeType(value.type ?? value.mimeType, name),
};
}
function extractListItems(payload: KnowledgeDocsListPayload): KnowledgeDocItem[] {
if (Array.isArray(payload)) {
return payload.map(normalizeDocItem).filter((item): item is KnowledgeDocItem => Boolean(item));
}
if (isRecord(payload)) {
const files = Array.isArray(payload.files) ? payload.files : [];
return files.map(normalizeDocItem).filter((item): item is KnowledgeDocItem => Boolean(item));
}
return [];
}
function extractSingleItem(payload: KnowledgeDocsUploadPayload): KnowledgeDocItem {
if (isRecord(payload)) {
const candidate = normalizeDocItem(payload.file ?? payload);
if (candidate) return candidate;
const files = Array.isArray(payload.files) ? payload.files : [];
for (const item of files) {
const normalized = normalizeDocItem(item);
if (normalized) return normalized;
}
}
throw new Error('Invalid knowledge docs upload response');
}
function ensureSuccess(payload: { success?: boolean; error?: string } | null | undefined, fallbackError: string): void {
if (payload && payload.success === false) {
throw new Error(payload.error || fallbackError);
}
}
export const knowledgeDocsApi = {
async list(): Promise<KnowledgeDocItem[]> {
const response = await hostApiFetch<KnowledgeDocsListPayload>('/api/knowledge/docs');
ensureSuccess(isRecord(response) ? response : undefined, 'Failed to load knowledge docs');
return extractListItems(response).sort((left, right) => right.modifiedAt.localeCompare(left.modifiedAt));
},
async upload(input: KnowledgeDocsUploadInput): Promise<KnowledgeDocItem> {
const response = await hostApiFetch<KnowledgeDocsUploadPayload>('/api/knowledge/docs', {
method: 'POST',
body: JSON.stringify(input),
});
ensureSuccess(isRecord(response) ? response : undefined, 'Failed to upload knowledge doc');
return extractSingleItem(response);
},
async delete(name: string): Promise<void> {
const trimmedName = String(name ?? '').trim();
if (!trimmedName) {
throw new Error('Document name is required');
}
const response = await hostApiFetch<KnowledgeDocsDeletePayload>(`/api/knowledge/docs/${encodeURIComponent(trimmedName)}`, {
method: 'DELETE',
});
ensureSuccess(isRecord(response) ? response : undefined, 'Failed to delete knowledge doc');
},
};