Files
SenMeshNet/application/state/useAISettingsState.ts
zhaolei 4a02370006
Some checks failed
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
feat: add monitoring settings tab, workbench view, and inspection alert improvements
2026-10-30 12:12:04 +08:00

399 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useCallback, useEffect, useMemo, useState } from 'react';
import { localStorageAdapter } from '../../infrastructure/persistence/localStorageAdapter';
import {
STORAGE_KEY_AI_PROVIDERS,
STORAGE_KEY_AI_ACTIVE_PROVIDER,
STORAGE_KEY_AI_ACTIVE_MODEL,
STORAGE_KEY_AI_PERMISSION_MODE,
STORAGE_KEY_AI_TOOL_INTEGRATION_MODE,
STORAGE_KEY_AI_EXTERNAL_AGENTS,
STORAGE_KEY_AI_DEFAULT_AGENT,
STORAGE_KEY_AI_COMMAND_BLOCKLIST,
STORAGE_KEY_AI_COMMAND_TIMEOUT,
STORAGE_KEY_AI_RESPONSE_IDLE_TIMEOUT,
STORAGE_KEY_AI_MAX_ITERATIONS,
STORAGE_KEY_AI_AGENT_MODEL_MAP,
STORAGE_KEY_AI_AGENT_PROVIDER_MAP,
STORAGE_KEY_AI_WEB_SEARCH,
STORAGE_KEY_AI_QUICK_MESSAGES,
STORAGE_KEY_AI_SHOW_TERMINAL_SELECTION_ACTION,
} from '../../infrastructure/config/storageKeys';
import type { AIQuickMessage } from '../../infrastructure/ai/quickMessages';
import { sanitizeQuickMessages } from '../../infrastructure/ai/quickMessages';
import type {
AIPermissionMode,
AIToolIntegrationMode,
ExternalAgentConfig,
ProviderConfig,
WebSearchConfig,
} from '../../infrastructure/ai/types';
import {
DEFAULT_COMMAND_TIMEOUT_SECONDS,
DEFAULT_RESPONSE_IDLE_TIMEOUT_SECONDS,
normalizeCommandTimeoutSeconds,
normalizeResponseIdleTimeoutSeconds,
} from '../../infrastructure/ai/types';
import { removeProviderReferences } from './aiProviderCleanup';
import { AI_STATE_CHANGED_EVENT, emitAIStateChanged } from './aiStateEvents';
import { getAIBridge } from './aiStateSnapshots';
import { useStoredBoolean } from './useStoredBoolean';
import {
persistCommandBlocklistSetting,
readCommandBlocklistSetting,
} from './commandBlocklistSettings';
function readPermissionMode(): AIPermissionMode {
const stored = localStorageAdapter.readString(STORAGE_KEY_AI_PERMISSION_MODE);
if (stored === 'observer' || stored === 'confirm' || stored === 'auto') return stored;
return 'confirm';
}
function readToolIntegrationMode(): AIToolIntegrationMode {
return localStorageAdapter.readString(STORAGE_KEY_AI_TOOL_INTEGRATION_MODE) === 'skills'
? 'skills'
: 'mcp';
}
export function useAISettingsState() {
const [providers, setProvidersRaw] = useState<ProviderConfig[]>(() =>
localStorageAdapter.read<ProviderConfig[]>(STORAGE_KEY_AI_PROVIDERS) ?? []
);
const [activeProviderId, setActiveProviderIdRaw] = useState<string>(() =>
localStorageAdapter.readString(STORAGE_KEY_AI_ACTIVE_PROVIDER) ?? ''
);
const [activeModelId, setActiveModelIdRaw] = useState<string>(() =>
localStorageAdapter.readString(STORAGE_KEY_AI_ACTIVE_MODEL) ?? ''
);
const [globalPermissionMode, setGlobalPermissionModeRaw] = useState<AIPermissionMode>(readPermissionMode);
const [toolIntegrationMode, setToolIntegrationModeRaw] = useState<AIToolIntegrationMode>(readToolIntegrationMode);
const [externalAgents, setExternalAgentsRaw] = useState<ExternalAgentConfig[]>(() =>
localStorageAdapter.read<ExternalAgentConfig[]>(STORAGE_KEY_AI_EXTERNAL_AGENTS) ?? []
);
const [defaultAgentId, setDefaultAgentIdRaw] = useState<string>(() =>
localStorageAdapter.readString(STORAGE_KEY_AI_DEFAULT_AGENT) ?? 'catty'
);
const [commandBlocklist, setCommandBlocklistRaw] = useState<string[]>(readCommandBlocklistSetting);
const [commandTimeout, setCommandTimeoutRaw] = useState<number>(() =>
normalizeCommandTimeoutSeconds(
localStorageAdapter.readNumber(STORAGE_KEY_AI_COMMAND_TIMEOUT) ?? DEFAULT_COMMAND_TIMEOUT_SECONDS,
)
);
const [responseIdleTimeout, setResponseIdleTimeoutRaw] = useState<number>(() =>
normalizeResponseIdleTimeoutSeconds(
localStorageAdapter.readNumber(STORAGE_KEY_AI_RESPONSE_IDLE_TIMEOUT)
?? DEFAULT_RESPONSE_IDLE_TIMEOUT_SECONDS,
)
);
const [maxIterations, setMaxIterationsRaw] = useState<number>(() =>
localStorageAdapter.readNumber(STORAGE_KEY_AI_MAX_ITERATIONS) ?? 20
);
const [webSearchConfig, setWebSearchConfigRaw] = useState<WebSearchConfig | null>(() =>
localStorageAdapter.read<WebSearchConfig>(STORAGE_KEY_AI_WEB_SEARCH) ?? null
);
const [quickMessages, setQuickMessagesRaw] = useState<AIQuickMessage[]>(() =>
sanitizeQuickMessages(localStorageAdapter.read<unknown>(STORAGE_KEY_AI_QUICK_MESSAGES)),
);
const [showTerminalSelectionAIAction, setShowTerminalSelectionAIAction] = useStoredBoolean(
STORAGE_KEY_AI_SHOW_TERMINAL_SELECTION_ACTION,
true,
);
const setProviders = useCallback((value: ProviderConfig[] | ((prev: ProviderConfig[]) => ProviderConfig[])) => {
setProvidersRaw((prev) => {
const next = typeof value === 'function' ? value(prev) : value;
localStorageAdapter.write(STORAGE_KEY_AI_PROVIDERS, next);
return next;
});
}, []);
const addProvider = useCallback((provider: ProviderConfig) => {
setProviders((prev) => [...prev, provider]);
}, [setProviders]);
const updateProvider = useCallback((id: string, updates: Partial<ProviderConfig>) => {
setProviders((prev) => prev.map((provider) => (
provider.id === id ? { ...provider, ...updates } : provider
)));
}, [setProviders]);
const removeProvider = useCallback((id: string) => {
setProviders((prev) => prev.filter((provider) => provider.id !== id));
setActiveProviderIdRaw((prevId) => {
if (prevId !== id) return prevId;
localStorageAdapter.writeString(STORAGE_KEY_AI_ACTIVE_PROVIDER, '');
return '';
});
const agentProviderMap =
localStorageAdapter.read<Record<string, string>>(STORAGE_KEY_AI_AGENT_PROVIDER_MAP) ?? {};
const agentModelMap =
localStorageAdapter.read<Record<string, string>>(STORAGE_KEY_AI_AGENT_MODEL_MAP) ?? {};
const cleanup = removeProviderReferences(id, agentProviderMap, agentModelMap);
if (cleanup.providerMapChanged) {
localStorageAdapter.write(STORAGE_KEY_AI_AGENT_PROVIDER_MAP, cleanup.agentProviderMap);
}
if (cleanup.modelMapChanged) {
localStorageAdapter.write(STORAGE_KEY_AI_AGENT_MODEL_MAP, cleanup.agentModelMap);
}
}, [setProviders]);
const setActiveProviderId = useCallback((id: string) => {
setActiveProviderIdRaw(id);
localStorageAdapter.writeString(STORAGE_KEY_AI_ACTIVE_PROVIDER, id);
}, []);
const setActiveModelId = useCallback((id: string) => {
setActiveModelIdRaw(id);
localStorageAdapter.writeString(STORAGE_KEY_AI_ACTIVE_MODEL, id);
}, []);
const setGlobalPermissionMode = useCallback((mode: AIPermissionMode) => {
setGlobalPermissionModeRaw(mode);
localStorageAdapter.writeString(STORAGE_KEY_AI_PERMISSION_MODE, mode);
getAIBridge()?.aiMcpSetPermissionMode?.(mode);
}, []);
const setToolIntegrationMode = useCallback((mode: AIToolIntegrationMode) => {
setToolIntegrationModeRaw(mode);
localStorageAdapter.writeString(STORAGE_KEY_AI_TOOL_INTEGRATION_MODE, mode);
getAIBridge()?.aiMcpSetToolIntegrationMode?.(mode);
}, []);
const setExternalAgents = useCallback((value: ExternalAgentConfig[] | ((prev: ExternalAgentConfig[]) => ExternalAgentConfig[])) => {
setExternalAgentsRaw((prev) => {
const next = typeof value === 'function' ? value(prev) : value;
localStorageAdapter.write(STORAGE_KEY_AI_EXTERNAL_AGENTS, next);
return next;
});
}, []);
const setDefaultAgentId = useCallback((id: string) => {
setDefaultAgentIdRaw(id);
localStorageAdapter.writeString(STORAGE_KEY_AI_DEFAULT_AGENT, id);
}, []);
const setCommandBlocklist = useCallback((value: string[]) => {
setCommandBlocklistRaw(value);
persistCommandBlocklistSetting(value);
getAIBridge()?.aiMcpSetCommandBlocklist?.(value);
}, []);
const setCommandTimeout = useCallback((value: number) => {
const normalizedValue = normalizeCommandTimeoutSeconds(value);
setCommandTimeoutRaw(normalizedValue);
localStorageAdapter.writeNumber(STORAGE_KEY_AI_COMMAND_TIMEOUT, normalizedValue);
getAIBridge()?.aiMcpSetCommandTimeout?.(normalizedValue);
}, []);
const setResponseIdleTimeout = useCallback((value: number) => {
const normalizedValue = normalizeResponseIdleTimeoutSeconds(value);
setResponseIdleTimeoutRaw(normalizedValue);
localStorageAdapter.writeNumber(STORAGE_KEY_AI_RESPONSE_IDLE_TIMEOUT, normalizedValue);
}, []);
const setMaxIterations = useCallback((value: number) => {
setMaxIterationsRaw(value);
localStorageAdapter.writeNumber(STORAGE_KEY_AI_MAX_ITERATIONS, value);
getAIBridge()?.aiMcpSetMaxIterations?.(value);
}, []);
const setWebSearchConfig = useCallback((config: WebSearchConfig | null) => {
setWebSearchConfigRaw(config);
if (config) {
localStorageAdapter.write(STORAGE_KEY_AI_WEB_SEARCH, config);
} else {
localStorageAdapter.remove(STORAGE_KEY_AI_WEB_SEARCH);
}
}, []);
const setQuickMessages = useCallback((value: AIQuickMessage[] | ((prev: AIQuickMessage[]) => AIQuickMessage[])) => {
setQuickMessagesRaw((prev) => {
const nextRaw = typeof value === 'function' ? value(prev) : value;
const next = sanitizeQuickMessages(nextRaw);
localStorageAdapter.write(STORAGE_KEY_AI_QUICK_MESSAGES, next);
emitAIStateChanged(STORAGE_KEY_AI_QUICK_MESSAGES);
return next;
});
}, []);
useEffect(() => {
const syncFromStorageKey = (key: string | null) => {
try {
switch (key) {
case STORAGE_KEY_AI_PROVIDERS: {
const parsed = localStorageAdapter.read<ProviderConfig[]>(STORAGE_KEY_AI_PROVIDERS);
if (parsed != null && !Array.isArray(parsed)) break;
setProvidersRaw(parsed ?? []);
break;
}
case STORAGE_KEY_AI_ACTIVE_PROVIDER:
setActiveProviderIdRaw(localStorageAdapter.readString(STORAGE_KEY_AI_ACTIVE_PROVIDER) ?? '');
break;
case STORAGE_KEY_AI_ACTIVE_MODEL:
setActiveModelIdRaw(localStorageAdapter.readString(STORAGE_KEY_AI_ACTIVE_MODEL) ?? '');
break;
case STORAGE_KEY_AI_PERMISSION_MODE:
setGlobalPermissionModeRaw(readPermissionMode());
getAIBridge()?.aiMcpSetPermissionMode?.(readPermissionMode());
break;
case STORAGE_KEY_AI_TOOL_INTEGRATION_MODE:
setToolIntegrationModeRaw(readToolIntegrationMode());
getAIBridge()?.aiMcpSetToolIntegrationMode?.(readToolIntegrationMode());
break;
case STORAGE_KEY_AI_EXTERNAL_AGENTS: {
const agents = localStorageAdapter.read<ExternalAgentConfig[]>(STORAGE_KEY_AI_EXTERNAL_AGENTS);
if (agents != null && !Array.isArray(agents)) break;
setExternalAgentsRaw(agents ?? []);
break;
}
case STORAGE_KEY_AI_DEFAULT_AGENT:
setDefaultAgentIdRaw(localStorageAdapter.readString(STORAGE_KEY_AI_DEFAULT_AGENT) ?? 'catty');
break;
case STORAGE_KEY_AI_COMMAND_BLOCKLIST: {
const blocklist = readCommandBlocklistSetting();
setCommandBlocklistRaw(blocklist);
getAIBridge()?.aiMcpSetCommandBlocklist?.(blocklist);
break;
}
case STORAGE_KEY_AI_COMMAND_TIMEOUT: {
const timeout = localStorageAdapter.readNumber(STORAGE_KEY_AI_COMMAND_TIMEOUT) ?? DEFAULT_COMMAND_TIMEOUT_SECONDS;
if (!Number.isFinite(timeout)) break;
const normalizedTimeout = normalizeCommandTimeoutSeconds(timeout);
setCommandTimeoutRaw(normalizedTimeout);
getAIBridge()?.aiMcpSetCommandTimeout?.(normalizedTimeout);
break;
}
case STORAGE_KEY_AI_RESPONSE_IDLE_TIMEOUT: {
const timeout = localStorageAdapter.readNumber(STORAGE_KEY_AI_RESPONSE_IDLE_TIMEOUT)
?? DEFAULT_RESPONSE_IDLE_TIMEOUT_SECONDS;
if (!Number.isFinite(timeout)) break;
setResponseIdleTimeoutRaw(normalizeResponseIdleTimeoutSeconds(timeout));
break;
}
case STORAGE_KEY_AI_MAX_ITERATIONS: {
const iters = localStorageAdapter.readNumber(STORAGE_KEY_AI_MAX_ITERATIONS) ?? 20;
if (!Number.isFinite(iters)) break;
setMaxIterationsRaw(iters);
getAIBridge()?.aiMcpSetMaxIterations?.(iters);
break;
}
case STORAGE_KEY_AI_WEB_SEARCH:
setWebSearchConfigRaw(localStorageAdapter.read<WebSearchConfig>(STORAGE_KEY_AI_WEB_SEARCH) ?? null);
break;
case STORAGE_KEY_AI_QUICK_MESSAGES:
setQuickMessagesRaw(sanitizeQuickMessages(localStorageAdapter.read<unknown>(STORAGE_KEY_AI_QUICK_MESSAGES)));
break;
}
} catch (err) {
console.warn('[useAISettingsState] Failed to process AI settings storage change', key, err);
}
};
const handleStorage = (event: StorageEvent) => syncFromStorageKey(event.key);
const handleLocalStateChanged = (event: Event) => {
syncFromStorageKey((event as CustomEvent<{ key?: string }>).detail?.key ?? null);
};
// 远程浏览器改 AI 设置 → 主进程 broadcast → preload 更新 localStorage 并 dispatch 此事件
// 这里遍历 changedEntries 中所有 AI_ 开头的 key,逐个刷新 React state
const handleBatchSync = (event: Event) => {
const detail = (event as CustomEvent).detail;
if (!detail || typeof detail !== 'object') return;
const entries = detail.changedEntries as Record<string, unknown> | undefined;
if (!entries) return;
for (const key of Object.keys(entries)) {
if (key.startsWith('netcatty_ai_') || key.startsWith('netcatty.ai')) {
syncFromStorageKey(key);
}
}
};
window.addEventListener('storage', handleStorage);
window.addEventListener(AI_STATE_CHANGED_EVENT, handleLocalStateChanged);
window.addEventListener('netcatty-settings-batch', handleBatchSync);
return () => {
window.removeEventListener('storage', handleStorage);
window.removeEventListener(AI_STATE_CHANGED_EVENT, handleLocalStateChanged);
window.removeEventListener('netcatty-settings-batch', handleBatchSync);
};
}, []);
useEffect(() => {
const bridge = getAIBridge();
bridge?.aiMcpSetCommandBlocklist?.(commandBlocklist);
bridge?.aiMcpSetCommandTimeout?.(commandTimeout);
bridge?.aiMcpSetMaxIterations?.(maxIterations);
bridge?.aiMcpSetPermissionMode?.(globalPermissionMode);
bridge?.aiMcpSetToolIntegrationMode?.(toolIntegrationMode);
}, [commandBlocklist, commandTimeout, globalPermissionMode, maxIterations, toolIntegrationMode]);
const activeProvider = providers.find((provider) => provider.id === activeProviderId) ?? null;
return useMemo(() => ({
providers,
setProviders,
addProvider,
updateProvider,
removeProvider,
activeProviderId,
setActiveProviderId,
activeModelId,
setActiveModelId,
activeProvider,
globalPermissionMode,
setGlobalPermissionMode,
toolIntegrationMode,
setToolIntegrationMode,
externalAgents,
setExternalAgents,
defaultAgentId,
setDefaultAgentId,
commandBlocklist,
setCommandBlocklist,
commandTimeout,
setCommandTimeout,
responseIdleTimeout,
setResponseIdleTimeout,
maxIterations,
setMaxIterations,
webSearchConfig,
setWebSearchConfig,
quickMessages,
setQuickMessages,
showTerminalSelectionAIAction,
setShowTerminalSelectionAIAction,
}), [
providers,
setProviders,
addProvider,
updateProvider,
removeProvider,
activeProviderId,
setActiveProviderId,
activeModelId,
setActiveModelId,
activeProvider,
globalPermissionMode,
setGlobalPermissionMode,
toolIntegrationMode,
setToolIntegrationMode,
externalAgents,
setExternalAgents,
defaultAgentId,
setDefaultAgentId,
commandBlocklist,
setCommandBlocklist,
commandTimeout,
setCommandTimeout,
responseIdleTimeout,
setResponseIdleTimeout,
maxIterations,
setMaxIterations,
webSearchConfig,
setWebSearchConfig,
quickMessages,
setQuickMessages,
showTerminalSelectionAIAction,
setShowTerminalSelectionAIAction,
]);
}