- 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.
203 lines
5.2 KiB
TypeScript
203 lines
5.2 KiB
TypeScript
import { useSyncExternalStore } from 'react';
|
|
import type { AutomationScript } from '@lib/script-types';
|
|
import { scriptApi } from '@lib/script-api';
|
|
import { resolveChannel } from '@constant/channel';
|
|
import { CONFIG_KEYS, IPC_EVENTS } from '../lib/constants';
|
|
import { invokeIpc } from '../lib/host-api';
|
|
|
|
export interface ChannelItem {
|
|
id: string;
|
|
channelName: string;
|
|
channelUrl: string;
|
|
}
|
|
|
|
export interface ChannelStoreState {
|
|
initialized: boolean;
|
|
loading: boolean;
|
|
selectedChannels: ChannelItem[];
|
|
availableChannels: ChannelItem[];
|
|
error: string | null;
|
|
}
|
|
|
|
const listeners = new Set<() => void>();
|
|
let initPromise: Promise<void> | null = null;
|
|
let state: ChannelStoreState = {
|
|
initialized: false,
|
|
loading: false,
|
|
selectedChannels: [],
|
|
availableChannels: [],
|
|
error: null,
|
|
};
|
|
|
|
function emit(): void {
|
|
for (const listener of listeners) {
|
|
listener();
|
|
}
|
|
}
|
|
|
|
function patchState(patch: Partial<ChannelStoreState>): ChannelStoreState {
|
|
state = { ...state, ...patch };
|
|
emit();
|
|
return state;
|
|
}
|
|
|
|
function normalizeChannelItem(item: Partial<ChannelItem> | null | undefined): ChannelItem | null {
|
|
const channelUrl = String(item?.channelUrl ?? '').trim();
|
|
if (!channelUrl) return null;
|
|
|
|
const channelName = String(item?.channelName ?? channelUrl).trim() || channelUrl;
|
|
const id = String(item?.id ?? channelUrl).trim() || channelUrl;
|
|
|
|
return {
|
|
id,
|
|
channelName,
|
|
channelUrl,
|
|
};
|
|
}
|
|
|
|
function dedupeChannels(items: Array<Partial<ChannelItem> | null | undefined>): ChannelItem[] {
|
|
const channelMap = new Map<string, ChannelItem>();
|
|
|
|
for (const item of items) {
|
|
const normalized = normalizeChannelItem(item);
|
|
if (!normalized || channelMap.has(normalized.channelUrl)) continue;
|
|
channelMap.set(normalized.channelUrl, normalized);
|
|
}
|
|
|
|
return Array.from(channelMap.values());
|
|
}
|
|
|
|
function mapScriptsToChannels(scripts: AutomationScript[]): ChannelItem[] {
|
|
const items: ChannelItem[] = [];
|
|
|
|
for (const script of scripts) {
|
|
if (!script.channel) continue;
|
|
|
|
const resolved = resolveChannel(script.channel);
|
|
const channelUrl = String(resolved.url ?? '').trim();
|
|
if (!channelUrl) continue;
|
|
|
|
items.push({
|
|
id: channelUrl,
|
|
channelName: String(resolved.name ?? channelUrl).trim() || channelUrl,
|
|
channelUrl,
|
|
});
|
|
}
|
|
|
|
return dedupeChannels(items);
|
|
}
|
|
|
|
async function loadSelectedChannels(): Promise<ChannelItem[]> {
|
|
const saved = await invokeIpc<ChannelItem[]>(IPC_EVENTS.GET_CONFIG, CONFIG_KEYS.SELECTED_CHANNELS);
|
|
return Array.isArray(saved) ? dedupeChannels(saved) : [];
|
|
}
|
|
|
|
async function loadAvailableChannels(): Promise<ChannelItem[]> {
|
|
const scripts = await scriptApi.list();
|
|
return mapScriptsToChannels(Array.isArray(scripts) ? scripts : []);
|
|
}
|
|
|
|
async function hydrate(): Promise<void> {
|
|
patchState({ loading: true, error: null });
|
|
|
|
try {
|
|
const [selectedChannels, availableChannels] = await Promise.all([
|
|
loadSelectedChannels(),
|
|
loadAvailableChannels(),
|
|
]);
|
|
|
|
patchState({
|
|
initialized: true,
|
|
loading: false,
|
|
selectedChannels,
|
|
availableChannels,
|
|
error: null,
|
|
});
|
|
} catch (error) {
|
|
patchState({
|
|
initialized: true,
|
|
loading: false,
|
|
selectedChannels: [],
|
|
availableChannels: [],
|
|
error: error instanceof Error ? error.message : String(error),
|
|
});
|
|
}
|
|
}
|
|
|
|
async function refreshAvailableChannels(): Promise<ChannelItem[]> {
|
|
patchState({ loading: true, error: null });
|
|
|
|
try {
|
|
const availableChannels = await loadAvailableChannels();
|
|
patchState({
|
|
loading: false,
|
|
availableChannels,
|
|
error: null,
|
|
});
|
|
return availableChannels;
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
patchState({
|
|
loading: false,
|
|
error: message,
|
|
});
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async function saveSelectedChannels(items: ChannelItem[]): Promise<ChannelItem[]> {
|
|
const nextItems = dedupeChannels(items);
|
|
patchState({ selectedChannels: nextItems, error: null });
|
|
|
|
await invokeIpc(IPC_EVENTS.SET_CONFIG, CONFIG_KEYS.SELECTED_CHANNELS, nextItems);
|
|
return nextItems;
|
|
}
|
|
|
|
function setSelectedChannels(items: ChannelItem[]): ChannelItem[] {
|
|
const nextItems = dedupeChannels(items);
|
|
patchState({ selectedChannels: nextItems });
|
|
return nextItems;
|
|
}
|
|
|
|
function addSelectedChannel(item: ChannelItem): ChannelItem[] {
|
|
return setSelectedChannels([...state.selectedChannels, item]);
|
|
}
|
|
|
|
function removeSelectedChannel(id: string): ChannelItem[] {
|
|
return setSelectedChannels(state.selectedChannels.filter((item) => item.id !== id));
|
|
}
|
|
|
|
function subscribe(listener: () => void): () => void {
|
|
listeners.add(listener);
|
|
return () => listeners.delete(listener);
|
|
}
|
|
|
|
function getSnapshot(): ChannelStoreState {
|
|
return state;
|
|
}
|
|
|
|
async function initChannelStore(): Promise<void> {
|
|
if (!initPromise) {
|
|
initPromise = hydrate();
|
|
}
|
|
|
|
await initPromise;
|
|
}
|
|
|
|
export const channelStore = {
|
|
subscribe,
|
|
getSnapshot,
|
|
getState: () => state,
|
|
init: initChannelStore,
|
|
loadSelectedChannels,
|
|
refreshAvailableChannels,
|
|
saveSelectedChannels,
|
|
setSelectedChannels,
|
|
addSelectedChannel,
|
|
removeSelectedChannel,
|
|
};
|
|
|
|
export function useChannelStore(): ChannelStoreState {
|
|
return useSyncExternalStore(channelStore.subscribe, channelStore.getSnapshot, channelStore.getSnapshot);
|
|
}
|