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:
142
src-react/lib/host-api.ts
Normal file
142
src-react/lib/host-api.ts
Normal 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}`);
|
||||
}
|
||||
Reference in New Issue
Block a user