Initial commit

This commit is contained in:
shiyu
2025-08-24 18:49:00 +08:00
parent 99866befe1
commit 6b0f2bd4fa
129 changed files with 11587 additions and 0 deletions

34
web/src/api/adapters.ts Normal file
View File

@@ -0,0 +1,34 @@
import request from './client';
export interface AdapterItem {
id: number;
name: string;
type: string;
config: any;
enabled: boolean;
mount_path?: string | null;
sub_path?: string | null;
}
export interface AdapterTypeField {
key: string;
label: string;
type: 'string' | 'password' | 'number';
required?: boolean;
placeholder?: string;
default?: any;
}
export interface AdapterTypeMeta {
type: string;
name: string;
config_schema: AdapterTypeField[];
}
export const adaptersApi = {
list: () => request<AdapterItem[]>('/adapters'),
create: (payload: Omit<AdapterItem, 'id'>) => request<AdapterItem>('/adapters', { method: 'POST', json: payload }),
update: (id: number, payload: Omit<AdapterItem, 'id'>) => request<AdapterItem>(`/adapters/${id}`, { method: 'PUT', json: payload }),
remove: (id: number) => request<void>(`/adapters/${id}`, { method: 'DELETE' }),
available: () => request<AdapterTypeMeta[]>('/adapters/available'),
};

45
web/src/api/auth.ts Normal file
View File

@@ -0,0 +1,45 @@
import request from './client';
export interface LoginPayload {
username: string;
password: string;
}
export interface RegisterPayload {
username: string;
password: string;
email?: string;
full_name?: string;
}
export interface AuthResponse {
access_token: string;
token_type: string;
}
export const authApi = {
register: async (username: string, password: string, email?: string, full_name?: string): Promise<any> => {
return request('/auth/register', {
method: 'POST',
json: { username, password, email, full_name },
});
},
login: async (payload: LoginPayload) => {
const form = new URLSearchParams();
form.append('username', payload.username);
form.append('password', payload.password);
try {
return await request<AuthResponse>('/auth/login', {
method: 'POST',
body: form,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
});
} catch (e) {
console.error('[authApi.login] error:', e);
throw e;
}
},
logout: () => {
localStorage.removeItem('token');
},
};

38
web/src/api/backup.ts Normal file
View File

@@ -0,0 +1,38 @@
import request from './client';
export const backupApi = {
export: async () => {
const response = await request('/backup/export', {
method: 'GET',
rawResponse: true,
}) as Response;
const contentDisposition = response.headers.get('content-disposition');
let filename = 'backup.json';
if (contentDisposition) {
const filenameMatch = contentDisposition.match(/filename="?(.+)"?/);
if (filenameMatch && filenameMatch.length > 1) {
filename = filenameMatch[1];
}
}
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', filename);
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(url);
},
import: async (file: File) => {
const formData = new FormData();
formData.append('file', file);
return request('/backup/import', {
method: 'POST',
body: formData,
});
},
};

76
web/src/api/client.ts Normal file
View File

@@ -0,0 +1,76 @@
export interface RequestOptions extends RequestInit {
json?: any;
formData?: FormData;
text?: string;
rawResponse?: boolean;
}
const BASE_URL = import.meta.env.PROD ? '/api' : 'http://127.0.0.1:8000/api';
export const API_BASE_URL = BASE_URL;
async function request<T = any>(url: string, options: RequestOptions = {}): Promise<T> {
const { json, formData, text, rawResponse, headers, ...rest } = options;
const finalHeaders: Record<string, string> = {
...(headers as Record<string, string> || {})
};
const token = localStorage.getItem('token');
if (token) {
finalHeaders['Authorization'] = `Bearer ${token}`;
}
let body: BodyInit | undefined;
if (json !== undefined) {
body = JSON.stringify(json);
finalHeaders['Content-Type'] = 'application/json';
} else if (formData) {
body = formData;
} else if (text !== undefined) {
body = text;
finalHeaders['Content-Type'] = 'text/plain;charset=utf-8';
} else if ((rest as any).body !== undefined) {
body = (rest as any).body;
delete (rest as any).body;
}
const resp = await fetch(BASE_URL + url, {
...rest,
headers: finalHeaders,
body,
});
if (rawResponse) return resp as any;
if (!resp.ok) {
let errMsg = resp.statusText;
try {
const data = await resp.json();
if (Array.isArray(data?.detail)) {
errMsg = data.detail.map((e: any) => e.msg || JSON.stringify(e)).join('; ');
} else {
errMsg = (typeof data?.detail === 'string') ? data.detail : (data.detail ? JSON.stringify(data.detail) : JSON.stringify(data));
}
} catch (_) { }
throw new Error(errMsg || `Request failed: ${resp.status}`);
}
const contentType = resp.headers.get('content-type') || '';
if (contentType.includes('application/json')) {
const json = await resp.json();
if (json && typeof json === 'object' && 'code' in json && ('msg' in json || 'message' in json)) {
if (json.code !== 0) {
throw new Error(json.msg || json.message || 'Error');
}
return json.data as T;
}
return json;
}
if (contentType.startsWith('text/')) {
return await resp.text() as any;
}
return await resp.arrayBuffer() as any;
}
export { vfsApi, type VfsEntry, type DirListing } from './vfs';
export { adaptersApi, type AdapterItem, type AdapterTypeField, type AdapterTypeMeta } from './adapters';
export { shareApi } from './share';
export default request;

27
web/src/api/config.ts Normal file
View File

@@ -0,0 +1,27 @@
import request from './client';
export async function getConfig(key: string) {
return request<{ key: string; value: string }>('/config?key=' + encodeURIComponent(key));
}
export async function setConfig(key: string, value: string) {
const form = new FormData();
form.append('key', key);
form.append('value', value);
return request('/config', { method: 'POST', formData: form });
}
export async function getAllConfig() {
return request<Record<string, string>>('/config/all');
}
export interface SystemStatus {
version: string;
title: string;
logo: string;
is_initialized: boolean;
}
export async function status() {
return request<SystemStatus>('/config/status');
}

54
web/src/api/logs.ts Normal file
View File

@@ -0,0 +1,54 @@
import request from './client';
export interface LogItem {
id: number;
timestamp: string;
level: string;
source: string;
message: string;
details: Record<string, any>;
user_id?: number;
}
export interface PaginatedLogs {
items: LogItem[];
total: number;
page: number;
page_size: number;
pages: number;
}
export interface GetLogsParams {
page?: number;
page_size?: number;
level?: string;
source?: string;
start_time?: string;
end_time?: string;
}
export interface ClearLogsParams {
start_time?: string;
end_time?: string;
}
export const logsApi = {
list: (params: GetLogsParams = {}) => {
const query = new URLSearchParams();
if (params.page) query.append('page', params.page.toString());
if (params.page_size) query.append('page_size', params.page_size.toString());
if (params.level) query.append('level', params.level);
if (params.source) query.append('source', params.source);
if (params.start_time) query.append('start_time', params.start_time);
if (params.end_time) query.append('end_time', params.end_time);
return request<PaginatedLogs>(`/logs?${query.toString()}`);
},
clear: (params: ClearLogsParams = {}) => {
const query = new URLSearchParams();
if (params.start_time) query.append('start_time', params.start_time);
if (params.end_time) query.append('end_time', params.end_time);
return request<{ deleted_count: number }>(`/logs?${query.toString()}`, {
method: 'DELETE',
});
},
};

39
web/src/api/processors.ts Normal file
View File

@@ -0,0 +1,39 @@
import request from './client';
export interface ProcessorTypeField {
key: string;
label: string;
type: 'string' | 'password' | 'number' | 'select';
required?: boolean;
placeholder?: string;
default?: any;
options?: { label: string; value: string | number }[];
}
export interface ProcessorTypeMeta {
type: string;
name: string;
supported_exts: string[];
config_schema: ProcessorTypeField[];
produces_file:boolean;
}
export const processorsApi = {
list: () => request<ProcessorTypeMeta[]>('/processors', {
method: 'GET'
}),
process: (params: {
path: string;
processor_type: string;
config: any;
save_to?: string;
overwrite?: boolean;
}) =>
request<any>('/processors/process', {
method: 'POST',
body: JSON.stringify(params),
headers: {
'Content-Type': 'application/json'
}
}),
};

39
web/src/api/share.ts Normal file
View File

@@ -0,0 +1,39 @@
import request, { API_BASE_URL } from './client';
import type { DirListing } from './vfs';
export interface ShareInfo {
id: number;
token: string;
name: string;
paths: string[];
created_at: string;
expires_at?: string;
access_type: 'public' | 'password';
}
export interface ShareCreatePayload {
name: string;
paths: string[];
expires_in_days?: number;
access_type: 'public' | 'password';
password?: string;
}
export const shareApi = {
create: (payload: ShareCreatePayload) => request<ShareInfo>('/shares', { method: 'POST', json: payload }),
list: () => request<ShareInfo[]>('/shares'),
remove: (shareId: number) => request<void>(`/shares/${shareId}`, { method: 'DELETE' }),
get: (token: string) => request<ShareInfo>(`/s/${token}`),
verifyPassword: (token: string, password: string) => request<void>(`/s/${token}/verify`, { method: 'POST', json: { password } }),
listDir: (token: string, path: string = '/', password?: string) => {
const params: Record<string, string> = { path };
if (password) {
params.password = password;
}
return request<DirListing>(`/s/${token}/ls?${new URLSearchParams(params)}`);
},
downloadUrl: (token: string, path: string, password?: string) => {
const url = `${API_BASE_URL}/s/${token}/download?path=${encodeURIComponent(path)}`;
return password ? `${url}&password=${encodeURIComponent(password)}` : url;
},
};

22
web/src/api/tasks.ts Normal file
View File

@@ -0,0 +1,22 @@
import request from './client';
export interface AutomationTask {
id: number;
name: string;
event: string;
path_pattern?: string;
filename_regex?: string;
processor_type: string;
processor_config: Record<string, any>;
enabled: boolean;
}
export type AutomationTaskCreate = Omit<AutomationTask, 'id'>;
export type AutomationTaskUpdate = Partial<AutomationTaskCreate>;
export const tasksApi = {
list: () => request<AutomationTask[]>('/tasks/'),
create: (payload: AutomationTaskCreate) => request<AutomationTask>('/tasks', { method: 'POST', json: payload }),
update: (id: number, payload: AutomationTaskUpdate) => request<AutomationTask>(`/tasks/${id}`, { method: 'PUT', json: payload }),
remove: (id: number) => request<void>(`/tasks/${id}`, { method: 'DELETE' }),
};

92
web/src/api/vfs.ts Normal file
View File

@@ -0,0 +1,92 @@
import request, { API_BASE_URL } from './client';
export interface VfsEntry {
name: string;
is_dir: boolean;
size: number;
mtime: number;
type?: string;
is_image?: boolean;
}
export interface DirListing {
path: string;
entries: VfsEntry[];
pagination?: {
total: number;
page: number;
page_size: number;
pages: number;
};
}
export interface SearchResultItem {
id: number;
path: string;
score: number;
}
export const vfsApi = {
list: (path: string, page: number = 1, pageSize: number = 50) => {
const cleaned = path.replace(/\\/g, '/');
const trimmed = cleaned === '/' ? '' : cleaned.replace(/^\/+/, '');
const params = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString()
});
return request<DirListing>(`/fs/${encodeURI(trimmed)}?${params}`);
},
readFile: (path: string) => request<ArrayBuffer>(`/fs/file/${encodeURI(path.replace(/^\/+/, ''))}`),
uploadFile: (fullPath: string, file: File | Blob) => {
const fd = new FormData();
fd.append('file', file);
return request(`/fs/file/${encodeURI(fullPath.replace(/^\/+/, ''))}`, { method: 'POST', formData: fd });
},
mkdir: (path: string) => request('/fs/mkdir', { method: 'POST', json: { path } }),
deletePath: (path: string) => request(`/fs/${encodeURI(path.replace(/^\/+/, ''))}`, { method: 'DELETE' }),
move: (src: string, dst: string) => request('/fs/move', { method: 'POST', json: { src, dst } }),
rename: (src: string, dst: string) => request('/fs/rename', { method: 'POST', json: { src, dst } }),
thumb: (path: string, w=256, h=256, fit='cover') =>
request<ArrayBuffer>(`/fs/thumb/${encodeURI(path.replace(/^\/+/, ''))}?w=${w}&h=${h}&fit=${fit}`),
streamUrl: (path: string) => `${API_BASE_URL}/fs/stream/${encodeURI(path.replace(/^\/+/, ''))}`,
stat: (path: string) => request(`/fs/stat/${encodeURI(path.replace(/^\/+/, ''))}`),
getTempLinkToken: (path: string) => request<{token: string}>(`/fs/temp-link/${encodeURI(path.replace(/^\/+/, ''))}`),
getTempPublicUrl: (token: string) => `${API_BASE_URL}/fs/public/${token}`,
uploadStream: (fullPath: string, file: File, overwrite: boolean = true, onProgress?: (loaded: number, total: number) => void) => {
const enc = encodeURI(fullPath.replace(/^\/+/, ''));
return new Promise<any>((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('POST', `${API_BASE_URL}/fs/upload/${enc}?overwrite=${overwrite}`);
const token = localStorage.getItem('token');
if (token) xhr.setRequestHeader('Authorization', `Bearer ${token}`);
xhr.upload.onprogress = (ev) => {
if (ev.lengthComputable && onProgress) onProgress(ev.loaded, ev.total);
};
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
try {
const json = JSON.parse(xhr.responseText);
if (json.code === 0) return resolve(json.data);
return reject(new Error(json.msg || json.message || 'Upload failed'));
} catch (e) {
return reject(new Error('Invalid response'));
}
} else {
let err = 'Upload failed';
try {
const json = JSON.parse(xhr.responseText);
err = json.detail || json.msg || json.message || err;
} catch (_) {}
reject(new Error(err));
}
}
};
const fd = new FormData();
fd.append('file', file);
xhr.send(fd);
});
},
searchFiles: (q: string, top_k: number = 10, mode: 'vector' | 'filename' = 'vector') =>
request<{ items: SearchResultItem[]; query: string }>(`/search?q=${encodeURIComponent(q)}&top_k=${top_k}&mode=${mode}`),
};