本次提交完成了一次全面的架构重构,实现了以数据库为中心的、支持多供应商的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。
此次重构极大地提升了系统的灵活性和可扩展性,完全对齐了“配置即数据”的现代化设计原则。
522 lines
16 KiB
TypeScript
522 lines
16 KiB
TypeScript
"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
|
||
};
|
||
} |