Fundamental_Analysis/frontend/src/hooks/use-row-config.ts
Lv, Qi a1e4b265ba feat(config): Implement database-centric LLM provider architecture
本次提交完成了一次全面的架构重构,实现了以数据库为中心的、支持多供应商的LLM配置体系。

**核心变更**:
1.  **数据库驱动配置**: 废弃了所有基于本地文件的配置方案 (`analysis-config.json`),将LLM Provider和分析模块的配置作为结构化数据存入数据库的`system_config`表中,由`data-persistence-service`统一管理。
2.  **Schema-in-Code**: 在`common-contracts`中定义了所有配置的Rust Structs,作为整个系统的“单一事实源”,确保了端到端的类型安全。
3.  **服务职责重构**:
    *   `data-persistence-service`吸收了配置管理功能,成为配置的“守门人”。
    *   `config-service-rs`服务已被彻底移除。
    *   `report-generator-service`重构为可以为每个任务动态创建使用不同Provider配置的LLM客户端。
4.  **前端功能增强**:
    *   新增了独立的`/llm-config`页面,用于对LLM Providers及其可用模型进行完整的CRUD管理,并支持模型自动发现。
    *   重构了旧的`/config`页面,为分析模块提供了级联选择器来精确指定所需的Provider和Model。

此次重构极大地提升了系统的灵活性和可扩展性,完全对齐了“配置即数据”的现代化设计原则。
2025-11-17 04:41:36 +08:00

522 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.

"use client";
import { useState, useEffect, useCallback, useMemo } from "react";
import { TableRowConfig } from "@/components/ui/row-settings";
// 配置存储的键前缀
const CONFIG_STORAGE_PREFIX = "table_row_config_";
// 默认配置生成函数
function createDefaultConfig(rowIds: string[]): Record<string, TableRowConfig> {
const config: Record<string, TableRowConfig> = {};
rowIds.forEach((rowId, index) => {
config[rowId] = {
rowId,
isVisible: true,
displayOrder: index
};
});
return config;
}
// 检查localStorage是否可用
function isLocalStorageAvailable(): boolean {
try {
const testKey = '__localStorage_test__';
localStorage.setItem(testKey, 'test');
localStorage.removeItem(testKey);
return true;
} catch {
return false;
}
}
// 从localStorage加载配置
function loadConfigFromStorage(companyCode: string, rowIds: string[]): {
config: Record<string, TableRowConfig>;
customRows: Record<string, { displayText: string; customRowType: 'separator' }>;
} {
// 检查localStorage是否可用
if (!isLocalStorageAvailable()) {
console.warn('localStorage is not available, using default config');
return {
config: createDefaultConfig(rowIds),
customRows: {}
};
}
try {
const storageKey = `${CONFIG_STORAGE_PREFIX}${companyCode}`;
const stored = localStorage.getItem(storageKey);
if (!stored) {
return {
config: createDefaultConfig(rowIds),
customRows: {}
};
}
const parsed = JSON.parse(stored);
// 检查配置版本兼容性
if (parsed._version && parsed._version !== '1.0') {
console.warn('Incompatible config version, resetting to default');
return {
config: createDefaultConfig(rowIds),
customRows: {}
};
}
// 验证配置完整性确保所有rowId都存在
const config: Record<string, TableRowConfig> = {};
let hasInvalidConfig = false;
rowIds.forEach((rowId, index) => {
if (parsed[rowId] && typeof parsed[rowId].isVisible === 'boolean') {
config[rowId] = {
rowId,
isVisible: parsed[rowId].isVisible,
displayOrder: parsed[rowId].displayOrder ?? index
};
} else {
// 如果配置不存在或无效,使用默认值
config[rowId] = {
rowId,
isVisible: true,
displayOrder: index
};
hasInvalidConfig = true;
}
});
// 加载自定义行数据
const customRows = parsed._customRows || {};
// 将自定义行也添加到配置中
Object.entries(customRows).forEach(([rowId, customRow]) => {
if (customRow && typeof customRow === 'object' && 'customRowType' in customRow && customRow.customRowType === 'separator') {
config[rowId] = {
rowId,
isVisible: parsed[rowId]?.isVisible ?? true,
displayOrder: parsed[rowId]?.displayOrder ?? Object.keys(config).length,
isCustomRow: true,
customRowType: 'separator' as const
};
}
});
// 如果有无效配置,保存修复后的配置
if (hasInvalidConfig) {
saveConfigToStorage(companyCode, config, customRows);
}
return { config, customRows };
} catch (error) {
console.warn('Failed to load row config from localStorage:', error);
// 尝试清除损坏的配置
try {
const storageKey = `${CONFIG_STORAGE_PREFIX}${companyCode}`;
localStorage.removeItem(storageKey);
} catch {
// 忽略清除失败
}
return {
config: createDefaultConfig(rowIds),
customRows: {}
};
}
}
// 保存配置到localStorage
function saveConfigToStorage(
companyCode: string,
config: Record<string, TableRowConfig>,
customRows?: Record<string, { displayText: string; customRowType: 'separator' }>
): { success: boolean; error?: string } {
if (!isLocalStorageAvailable()) {
const error = 'localStorage不可用配置无法持久化保存';
console.warn(error);
return { success: false, error };
}
try {
const storageKey = `${CONFIG_STORAGE_PREFIX}${companyCode}`;
const configWithVersion = {
...config,
_customRows: customRows || {},
_version: '1.0',
_timestamp: Date.now()
};
localStorage.setItem(storageKey, JSON.stringify(configWithVersion));
return { success: true };
} catch (error) {
console.warn('Failed to save row config to localStorage:', error);
// 如果是存储空间不足,尝试清理旧配置
if (error instanceof Error && error.name === 'QuotaExceededError') {
try {
cleanupOldConfigs();
// 重试保存
const configWithVersion = {
...config,
_version: '1.0',
_timestamp: Date.now()
};
const storageKey = `${CONFIG_STORAGE_PREFIX}${companyCode}`;
localStorage.setItem(storageKey, JSON.stringify(configWithVersion));
return { success: true };
} catch (retryError) {
const errorMsg = '存储空间不足,清理后仍无法保存配置';
console.warn(errorMsg, retryError);
return { success: false, error: errorMsg };
}
}
const errorMsg = error instanceof Error ? error.message : '配置保存失败';
return { success: false, error: errorMsg };
}
}
// 清理旧的配置数据
function cleanupOldConfigs(): void {
try {
const keysToRemove: string[] = [];
const cutoffTime = Date.now() - (30 * 24 * 60 * 60 * 1000); // 30天前
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && key.startsWith(CONFIG_STORAGE_PREFIX)) {
try {
const stored = localStorage.getItem(key);
if (stored) {
const parsed = JSON.parse(stored);
if (parsed._timestamp && parsed._timestamp < cutoffTime) {
keysToRemove.push(key);
}
}
} catch {
// 如果解析失败,也标记为删除
keysToRemove.push(key);
}
}
}
keysToRemove.forEach(key => {
localStorage.removeItem(key);
});
console.log(`Cleaned up ${keysToRemove.length} old config entries`);
} catch (error) {
console.warn('Failed to cleanup old configs:', error);
}
}
// 配置保存状态接口
export interface ConfigSaveStatus {
status: 'idle' | 'saving' | 'success' | 'error';
message?: string;
timestamp?: number;
}
// 行配置管理Hook
export function useRowConfig(companyCode: string | null, rowIds: string[]) {
// Create a stable reference for rowIds to prevent unnecessary re-renders
const stableRowIds = useMemo(() => rowIds, [rowIds]);
const [rowConfigs, setRowConfigs] = useState<Record<string, TableRowConfig>>(() => {
// 初始化时如果有公司代码,尝试加载配置
if (companyCode && stableRowIds.length > 0) {
const { config } = loadConfigFromStorage(companyCode, stableRowIds);
return config;
}
return createDefaultConfig(stableRowIds);
});
// 自定义行数据存储
const [customRows, setCustomRows] = useState<Record<string, { displayText: string; customRowType: 'separator' }>>(() => {
// 初始化时如果有公司代码,尝试加载自定义行数据
if (companyCode && stableRowIds.length > 0) {
const { customRows } = loadConfigFromStorage(companyCode, stableRowIds);
return customRows;
}
return {};
});
// 配置保存状态
const [saveStatus, setSaveStatus] = useState<ConfigSaveStatus>({
status: 'idle'
});
// 当公司代码或行ID变化时重新加载配置
useEffect(() => {
if (companyCode && stableRowIds.length > 0) {
const { config, customRows: loadedCustomRows } = loadConfigFromStorage(companyCode, stableRowIds);
setRowConfigs(config);
setCustomRows(loadedCustomRows);
} else if (stableRowIds.length > 0) {
// 没有公司代码时使用默认配置
setRowConfigs(createDefaultConfig(stableRowIds));
setCustomRows({});
}
}, [companyCode, stableRowIds]);
// 安全保存配置的内部函数
const saveConfigSafely = useCallback((config: Record<string, TableRowConfig>, customRowsData?: Record<string, { displayText: string; customRowType: 'separator' }>) => {
if (!companyCode) return;
setSaveStatus({ status: 'saving' });
// 使用 setTimeout 来模拟异步保存避免阻塞UI
setTimeout(() => {
const result = saveConfigToStorage(companyCode, config, customRowsData || customRows);
if (result.success) {
setSaveStatus({
status: 'success',
message: '配置已保存',
timestamp: Date.now()
});
// 成功状态2秒后自动清除
setTimeout(() => {
setSaveStatus(prev => prev.status === 'success' ? { status: 'idle' } : prev);
}, 2000);
} else {
setSaveStatus({
status: 'error',
message: result.error || '配置保存失败',
timestamp: Date.now()
});
// 错误状态5秒后自动清除
setTimeout(() => {
setSaveStatus(prev => prev.status === 'error' ? { status: 'idle' } : prev);
}, 5000);
}
}, 0);
}, [companyCode, customRows]);
// 更新配置
const updateRowConfig = useCallback((newConfig: Record<string, TableRowConfig>) => {
setRowConfigs(newConfig);
// 如果有公司代码保存到localStorage
if (companyCode) {
saveConfigSafely(newConfig, customRows);
}
}, [companyCode, saveConfigSafely, customRows]);
// 切换单个行的可见性
const toggleRowVisibility = useCallback((rowId: string) => {
setRowConfigs(prev => {
const newConfig = {
...prev,
[rowId]: {
...prev[rowId],
isVisible: !prev[rowId]?.isVisible
}
};
if (companyCode) {
saveConfigSafely(newConfig, customRows);
}
return newConfig;
});
}, [companyCode, saveConfigSafely, customRows]);
// 重置配置为默认值
const resetConfig = useCallback(() => {
const defaultConfig = createDefaultConfig(stableRowIds);
const emptyCustomRows = {};
setRowConfigs(defaultConfig);
setCustomRows(emptyCustomRows);
if (companyCode) {
saveConfigSafely(defaultConfig, emptyCustomRows);
}
}, [companyCode, stableRowIds, saveConfigSafely]);
// 获取可见的行ID列表
const visibleRowIds = Object.entries(rowConfigs)
.filter(([, config]) => config.isVisible)
.sort((a, b) => (a[1].displayOrder ?? 0) - (b[1].displayOrder ?? 0))
.map(([rowId]) => rowId);
// 导出配置
const exportConfig = useCallback(() => {
if (!companyCode) return null;
return {
companyCode,
config: rowConfigs,
exportedAt: new Date().toISOString(),
version: '1.0'
};
}, [companyCode, rowConfigs]);
// 导入配置
const importConfig = useCallback((importedData: unknown) => {
try {
if (!importedData || typeof importedData !== 'object') {
throw new Error('Invalid import data format');
}
const data = importedData as Record<string, unknown>;
if (!data.config || !data.companyCode) {
throw new Error('Invalid import data format');
}
if (data.companyCode !== companyCode) {
console.warn('Imported config is for a different company');
return false;
}
// 验证导入的配置
const importedConfig = data.config as Record<string, unknown>;
const validatedConfig: Record<string, TableRowConfig> = {};
stableRowIds.forEach((rowId, index) => {
const rowConfig = importedConfig[rowId] as Record<string, unknown> | undefined;
if (rowConfig && typeof rowConfig.isVisible === 'boolean') {
validatedConfig[rowId] = {
rowId,
isVisible: rowConfig.isVisible,
displayOrder: typeof rowConfig.displayOrder === 'number' ? rowConfig.displayOrder : index
};
} else {
validatedConfig[rowId] = {
rowId,
isVisible: true,
displayOrder: index
};
}
});
setRowConfigs(validatedConfig);
if (companyCode) {
saveConfigSafely(validatedConfig);
}
return true;
} catch (error) {
console.warn('Failed to import config:', error);
return false;
}
}, [companyCode, stableRowIds, saveConfigSafely]);
// 获取配置统计信息
const getConfigStats = useCallback(() => {
const total = Object.keys(rowConfigs).length;
const visible = Object.values(rowConfigs).filter(config => config.isVisible).length;
const hidden = total - visible;
return {
total,
visible,
hidden,
visibilityRate: total > 0 ? Math.round((visible / total) * 100) : 0
};
}, [rowConfigs]);
// 添加自定义行(仅支持分隔线)
const addCustomRow = useCallback((rowType: 'separator', displayText?: string) => {
// 强制只支持分隔线类型
const actualRowType = 'separator';
const rowId = `separator_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const text = displayText || '分组标题';
// 添加到自定义行数据
setCustomRows(prev => ({
...prev,
[rowId]: { displayText: text, customRowType: actualRowType }
}));
// 添加到行配置
const newConfig: Record<string, TableRowConfig> = {
...rowConfigs,
[rowId]: {
rowId,
isVisible: true,
displayOrder: Object.keys(rowConfigs).length,
isCustomRow: true,
customRowType: actualRowType
}
};
setRowConfigs(newConfig);
if (companyCode) {
// 获取更新后的自定义行数据
const updatedCustomRows: Record<string, { displayText: string; customRowType: 'separator' }> = {
...customRows,
[rowId]: { displayText: text, customRowType: 'separator' }
};
saveConfigSafely(newConfig, updatedCustomRows);
}
return rowId;
}, [rowConfigs, customRows, companyCode, saveConfigSafely]);
// 删除自定义行
const deleteCustomRow = useCallback((rowId: string) => {
if (!rowConfigs[rowId]?.isCustomRow) {
console.warn('Cannot delete non-custom row:', rowId);
return;
}
// 从自定义行数据中删除
const updatedCustomRows = { ...customRows };
delete updatedCustomRows[rowId];
setCustomRows(updatedCustomRows);
// 从行配置中删除
const newConfig = { ...rowConfigs };
delete newConfig[rowId];
setRowConfigs(newConfig);
if (companyCode) {
saveConfigSafely(newConfig, updatedCustomRows);
}
}, [rowConfigs, customRows, companyCode, saveConfigSafely]);
// 更新行顺序
const updateRowOrder = useCallback((newOrder: string[]) => {
const newConfig = { ...rowConfigs };
newOrder.forEach((rowId, index) => {
if (newConfig[rowId]) {
newConfig[rowId] = { ...newConfig[rowId], displayOrder: index };
}
});
setRowConfigs(newConfig);
if (companyCode) {
saveConfigSafely(newConfig, customRows);
}
}, [rowConfigs, customRows, companyCode, saveConfigSafely]);
// 清除保存状态
const clearSaveStatus = useCallback(() => {
setSaveStatus({ status: 'idle' });
}, []);
return {
rowConfigs,
customRows,
updateRowConfig,
toggleRowVisibility,
resetConfig,
visibleRowIds,
exportConfig,
importConfig,
getConfigStats,
saveStatus,
clearSaveStatus,
addCustomRow,
deleteCustomRow,
updateRowOrder
};
}