Files
zn-ai/src-react/stores/channel.ts
duanshuwen b1dea9a5c2 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.
2026-04-17 07:09:56 +08:00

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);
}