feat: implement task management store with IPC integration

- Added a new task store in `src-react/stores/task.ts` to manage tasks and their statuses.
- Implemented functions for creating, executing, and retrying tasks, along with handling task progress and completion.
- Introduced persistence for tasks using IPC.
- Created utility functions for normalizing room types and building subtasks.
- Added a new CSS file for global styles in `src-react/styles.css`.
- Created runtime types in `src-react/types/runtime.ts` and exported them.
- Updated the main entry points for Vue and React applications to support dynamic framework loading.
- Refactored chat model interfaces and utility functions into `src/shared/chat-model.ts`.
- Updated TypeScript configuration to include paths for React components and types.
- Enhanced Vite configuration to support both Vue and React frameworks.
This commit is contained in:
duanshuwen
2026-04-17 07:09:56 +08:00
parent d233b94b2a
commit b1dea9a5c2
68 changed files with 5910 additions and 397 deletions

142
src-react/lib/host-api.ts Normal file
View File

@@ -0,0 +1,142 @@
import { IPC_EVENTS } from './constants';
import type { HostApiResult } from '../types/runtime';
type RequestInitLike = Pick<RequestInit, 'method' | 'headers' | 'body'>;
type LooseIpcBridge = {
invoke<T = unknown>(channel: string, ...args: any[]): Promise<T>;
on?(channel: string, callback: (...args: any[]) => void): () => void;
};
function normalizeHeaders(headers?: HeadersInit): Headers {
return new Headers(headers ?? {});
}
function readCookie(name: string): string | null {
if (typeof document === 'undefined' || !document.cookie) return null;
const match = document.cookie.match(new RegExp(`(?:^|; )${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}=([^;]*)`));
return match ? decodeURIComponent(match[1]) : null;
}
function readToken(): string | null {
if (typeof window === 'undefined') return null;
const storageCandidates = [window.sessionStorage, window.localStorage];
for (const storage of storageCandidates) {
for (const key of ['token', 'access_token', 'refresh_token']) {
const value = storage.getItem(key);
if (value) return value;
}
}
return readCookie('token') ?? readCookie('access_token') ?? readCookie('refresh_token');
}
function normalizeBody(body: BodyInit | null | undefined): BodyInit | null | undefined {
if (body == null) return body;
if (
typeof body === 'string' ||
body instanceof Blob ||
body instanceof FormData ||
body instanceof URLSearchParams ||
body instanceof ArrayBuffer
) {
return body;
}
if (ArrayBuffer.isView(body)) {
return body;
}
return JSON.stringify(body);
}
function extractResult<T>(response: unknown): T {
if (response && typeof response === 'object') {
const result = response as HostApiResult<T>;
if (result.success === false || result.ok === false) {
throw new Error(result.error || result.text || 'Request failed');
}
if (typeof result.json !== 'undefined') return result.json;
if (typeof result.data !== 'undefined') {
const data = result.data as { json?: T } | T;
return (data && typeof data === 'object' && 'json' in data ? data.json : data) as T;
}
}
return response as T;
}
export function hasHostApiBridge(): boolean {
return typeof window !== 'undefined' && Boolean(window.api?.invoke);
}
export async function invokeIpc<T = unknown>(channel: string, ...args: any[]): Promise<T> {
if (!hasHostApiBridge()) {
throw new Error(`IPC bridge is unavailable for ${channel}`);
}
const bridge = window.api as unknown as LooseIpcBridge;
return bridge.invoke<T>(channel, ...args);
}
export function onIpc(channel: string, callback: (...args: any[]) => void): () => void {
if (!hasHostApiBridge() || !window.api?.on) {
return () => {};
}
const bridge = window.api as unknown as LooseIpcBridge;
return bridge.on ? bridge.on(channel, callback) : () => {};
}
export async function hostApiFetch<T>(path: string, init?: RequestInitLike): Promise<T> {
const method = init?.method ?? 'GET';
const headers = normalizeHeaders(init?.headers);
const token = readToken();
if (token && !headers.has('Authorization')) {
headers.set('Authorization', `Bearer ${token}`);
}
const request = {
path,
method,
headers: (() => {
const headerObject: Record<string, string> = {};
headers.forEach((value, key) => {
headerObject[key] = value;
});
return headerObject;
})(),
body: init?.body ?? null,
};
if (hasHostApiBridge()) {
const response = await invokeIpc(IPC_EVENTS.HOST_API_FETCH, request);
return extractResult<T>(response);
}
if (typeof fetch === 'function') {
const response = await fetch(path, {
method,
headers,
body: normalizeBody(init?.body),
});
if (!response.ok) {
const text = await response.text();
throw new Error(text || response.statusText || `Request failed with ${response.status}`);
}
const contentType = response.headers.get('content-type') ?? '';
if (contentType.includes('application/json')) {
return (await response.json()) as T;
}
return (await response.text()) as unknown as T;
}
throw new Error(`No HTTP bridge available for ${path}`);
}