feat: TypeScript + Electron v2 重构 - 纯桌面版
- 全面迁移到 TypeScript,严格类型定义 - 放弃 Web 版,专注 Electron 桌面应用 - 主进程模块化:main.ts, preload.ts, menu.ts, tray.ts, ipc.ts, utils.ts - 渲染进程完整迁移所有功能组件 - 删除 PWA 相关文件 (sw.js, manifest.json) - 删除 Web 版降级逻辑 - 保留所有核心功能:流式对话、多模型、Think推理、多模态、RAG知识库、Agent预设、历史管理 - 保留 Windows 11 Fluent Design 暗色主题样式
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
/**
|
||||
* State - 轻量级响应式状态管理
|
||||
*/
|
||||
|
||||
import type { StateKey, ChatSession, ChatDB, OllamaAPI, Preset } from '../types.js';
|
||||
|
||||
function shallowEqual(a: unknown, b: unknown): boolean {
|
||||
if (a === b) return true;
|
||||
if (a == null || b == null) return a === b;
|
||||
if (typeof a !== 'object' || typeof b !== 'object') return false;
|
||||
if (Array.isArray(a) && Array.isArray(b)) {
|
||||
if (a.length !== b.length) return false;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (a[i] !== b[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
||||
const keysA = Object.keys(a as object);
|
||||
const keysB = Object.keys(b as object);
|
||||
if (keysA.length !== keysB.length) return false;
|
||||
for (const key of keysA) {
|
||||
if (!Object.prototype.hasOwnProperty.call(b, key) || (a as Record<string, unknown>)[key] !== (b as Record<string, unknown>)[key]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
type Listener = (value: unknown, old: unknown) => void;
|
||||
|
||||
class AppState {
|
||||
private _state: Record<string, unknown> = {};
|
||||
private _listeners = new Map<string, Set<Listener>>();
|
||||
private _batching = false;
|
||||
private _batchQueue: Set<string> | null = null;
|
||||
|
||||
set(key: string, value: unknown): void {
|
||||
const old = this._state[key];
|
||||
this._state[key] = value;
|
||||
if (!shallowEqual(old, value)) {
|
||||
if (this._batching) {
|
||||
this._batchQueue!.add(key);
|
||||
} else {
|
||||
this._notify(key, value, old);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update(key: string, updater: (old: unknown) => unknown): void {
|
||||
const old = this._state[key];
|
||||
const value = updater(old);
|
||||
this._state[key] = value;
|
||||
if (!shallowEqual(old, value)) {
|
||||
if (this._batching) {
|
||||
this._batchQueue!.add(key);
|
||||
} else {
|
||||
this._notify(key, value, old);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setIn(key: string, path: (string | number)[], value: unknown): void {
|
||||
if (!path || path.length === 0) {
|
||||
this.set(key, value);
|
||||
return;
|
||||
}
|
||||
const old = this._state[key];
|
||||
if (old == null || typeof old !== 'object') {
|
||||
console.warn(`[State] setIn 失败: ${key} 不是对象`);
|
||||
return;
|
||||
}
|
||||
const newRoot = Array.isArray(old) ? [...(old as unknown[])] : { ...(old as Record<string, unknown>) };
|
||||
let current: Record<string, unknown> = newRoot as Record<string, unknown>;
|
||||
for (let i = 0; i < path.length - 1; i++) {
|
||||
const segment = path[i];
|
||||
const next = current[segment as string];
|
||||
if (next == null || typeof next !== 'object') {
|
||||
const isIndex = typeof path[i + 1] === 'number';
|
||||
current[segment as string] = isIndex ? [] : {};
|
||||
} else {
|
||||
current[segment as string] = Array.isArray(next) ? [...next] : { ...(next as Record<string, unknown>) };
|
||||
}
|
||||
current = current[segment as string] as Record<string, unknown>;
|
||||
}
|
||||
current[path[path.length - 1] as string] = value;
|
||||
this._state[key] = newRoot;
|
||||
if (!shallowEqual(old, newRoot)) {
|
||||
if (this._batching) {
|
||||
this._batchQueue!.add(key);
|
||||
} else {
|
||||
this._notify(key, newRoot, old);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
batch(fn: () => void): void {
|
||||
this._batching = true;
|
||||
this._batchQueue = new Set();
|
||||
try {
|
||||
fn();
|
||||
} finally {
|
||||
const keys = this._batchQueue;
|
||||
this._batching = false;
|
||||
this._batchQueue = null;
|
||||
for (const k of keys) {
|
||||
this._notify(k, this._state[k], undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _notify(key: string, value: unknown, old: unknown): void {
|
||||
if (!this._listeners.has(key)) return;
|
||||
for (const cb of this._listeners.get(key)!) {
|
||||
try { cb(value, old); } catch (e) {
|
||||
console.error(`[State] 监听器错误 (${key}):`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get<T = unknown>(key: string, defaultValue?: T): T {
|
||||
return (key in this._state ? this._state[key] : defaultValue) as T;
|
||||
}
|
||||
|
||||
on(key: string, callback: Listener): () => void {
|
||||
if (!this._listeners.has(key)) {
|
||||
this._listeners.set(key, new Set());
|
||||
}
|
||||
this._listeners.get(key)!.add(callback);
|
||||
return () => this._listeners.get(key)?.delete(callback);
|
||||
}
|
||||
|
||||
init(initial: Record<string, unknown>): void {
|
||||
Object.assign(this._state, initial);
|
||||
}
|
||||
}
|
||||
|
||||
export const state = new AppState();
|
||||
|
||||
export const KEYS = {
|
||||
DB: 'db',
|
||||
API: 'api',
|
||||
CURRENT_SESSION: 'currentSession',
|
||||
IS_STREAMING: 'isStreaming',
|
||||
IS_HISTORY_VIEW: 'isHistoryView',
|
||||
PENDING_IMAGES: 'pendingImages',
|
||||
ABORT_CONTROLLER: 'abortController',
|
||||
SYSTEM_PROMPT: 'systemPrompt',
|
||||
SYSTEM_PROMPT_ENABLED: 'systemPromptEnabled',
|
||||
NUM_CTX: 'numCtx',
|
||||
HISTORY_PAGE: 'historyPage',
|
||||
HISTORY_SEARCH: 'historySearchQuery',
|
||||
} as const;
|
||||
Reference in New Issue
Block a user