Files
MyGoNavi/frontend/src/components/ai/aiProviderInsights.ts
tianqijiuyun-latiao 0ba984b277 Merge remote-tracking branch 'origin/dev' into feature/20260602_connection_driver_i18n
# Conflicts:
#	frontend/src/App.tsx
#	frontend/src/components/AISettingsModal.tsx
#	frontend/src/components/ConnectionModal.edit-password.test.tsx
#	frontend/src/components/ConnectionModal.tsx
#	frontend/src/components/DataSyncModal.i18n.test.ts
#	frontend/src/components/DataSyncModal.tsx
#	frontend/src/components/QueryEditor.external-sql-save.test.tsx
#	frontend/src/components/QueryEditor.tsx
#	frontend/src/components/Sidebar.locate-toolbar.test.tsx
#	frontend/src/components/Sidebar.tsx
#	frontend/src/components/SnippetSettingsModal.tsx
#	frontend/src/components/TableOverview.tsx
#	frontend/src/components/ai/AIChatHeader.test.tsx
#	frontend/src/components/ai/AISettingsProvidersSection.tsx
#	frontend/src/components/ai/aiChatPayloadDispatch.ts
#	frontend/src/components/ai/aiChatReadiness.ts
#	frontend/src/components/ai/aiSettingsModalConfig.tsx
#	frontend/src/components/ai/messageBubble/AIMessageCodeBlock.tsx
#	frontend/src/components/sidebarV2Utils.ts
#	frontend/src/i18n/catalog.test.ts
#	frontend/src/utils/connectionTypeCatalog.test.ts
#	frontend/src/utils/connectionTypeCatalog.ts
#	frontend/src/utils/tabDisplay.ts
#	internal/ai/provider/custom.go
#	internal/ai/service/service.go
#	internal/app/methods_driver.go
#	internal/app/methods_file.go
#	internal/db/custom_impl.go
#	internal/db/iris_impl.go
#	internal/db/mariadb_impl.go
#	internal/db/sqlserver_impl.go
#	shared/i18n/de-DE.json
#	shared/i18n/en-US.json
#	shared/i18n/ja-JP.json
#	shared/i18n/ru-RU.json
#	shared/i18n/zh-CN.json
#	shared/i18n/zh-TW.json
2026-06-23 12:41:27 +08:00

170 lines
6.3 KiB
TypeScript

import type { AIProviderConfig } from '../../types';
import type { AIInspectionTranslator } from './aiInspectionI18n';
import { translateInspectionCopy } from './aiInspectionI18n';
const DECLARED_MODEL_PREVIEW_LIMIT = 12;
const DYNAMIC_MODEL_PREVIEW_LIMIT = 20;
const sliceList = <T,>(items: T[], limit: number) => {
const list = Array.isArray(items) ? items : [];
return {
items: list.slice(0, limit),
truncated: list.length > limit,
total: list.length,
};
};
const trimText = (value: unknown): string => String(value || '').trim();
const hasProviderSecret = (provider: AIProviderConfig): boolean =>
provider.hasSecret ?? Boolean(provider.secretRef || provider.apiKey);
const isBaseURLOptionalProvider = (provider: AIProviderConfig): boolean =>
provider.type === 'custom' && trimText(provider.apiFormat) === 'codebuddy-cli';
const isModelOptionalProvider = (provider: AIProviderConfig): boolean =>
provider.type === 'custom' && ['codebuddy-cli', 'cursor-agent'].includes(trimText(provider.apiFormat));
const getProviderHost = (baseUrl: string): string => {
const normalized = trimText(baseUrl);
if (!normalized) {
return '';
}
try {
return new URL(normalized).host;
} catch {
return '';
}
};
const buildProviderIssues = (provider: AIProviderConfig): string[] => {
const issues: string[] = [];
const hasSecret = hasProviderSecret(provider);
const baseUrl = trimText(provider.baseUrl);
const model = trimText(provider.model);
const declaredModels = Array.isArray(provider.models)
? provider.models.map((item) => trimText(item)).filter(Boolean)
: [];
if (!hasSecret) {
issues.push('missing_secret');
}
if (!isBaseURLOptionalProvider(provider) && !baseUrl) {
issues.push('missing_base_url');
}
if (!isModelOptionalProvider(provider) && !model) {
issues.push('missing_selected_model');
}
if (declaredModels.length === 0) {
issues.push('missing_declared_models');
}
return issues;
};
export const buildAIProviderSnapshot = (params: {
providers?: AIProviderConfig[];
activeProviderId?: string | null;
dynamicModels?: string[];
translate?: AIInspectionTranslator;
}) => {
const translate = params.translate;
const providers = Array.isArray(params.providers) ? params.providers : [];
const activeProviderId = trimText(params.activeProviderId);
const dynamicModelPreview = sliceList(
(Array.isArray(params.dynamicModels) ? params.dynamicModels : [])
.map((item) => trimText(item))
.filter(Boolean),
DYNAMIC_MODEL_PREVIEW_LIMIT,
);
const providerSummaries = providers.map((provider) => {
const declaredModelPreview = sliceList(
(Array.isArray(provider.models) ? provider.models : [])
.map((item) => trimText(item))
.filter(Boolean),
DECLARED_MODEL_PREVIEW_LIMIT,
);
const headerKeys = Object.keys(provider.headers || {})
.map((item) => trimText(item))
.filter(Boolean)
.sort((left, right) => left.localeCompare(right));
const issues = buildProviderIssues(provider);
return {
id: provider.id,
name: trimText(provider.name),
type: provider.type,
apiFormat: trimText(provider.apiFormat) || 'openai',
active: provider.id === activeProviderId,
baseUrl: trimText(provider.baseUrl),
baseUrlHost: getProviderHost(provider.baseUrl),
model: trimText(provider.model),
declaredModelCount: declaredModelPreview.total,
declaredModels: declaredModelPreview.items,
declaredModelsTruncated: declaredModelPreview.truncated,
hasSecret: hasProviderSecret(provider),
hasHeaders: headerKeys.length > 0,
headerKeys,
maxTokens: Number(provider.maxTokens) || 0,
temperature: Number(provider.temperature) || 0,
issues,
issueCount: issues.length,
status: issues.length === 0 ? 'ready' : 'needs_attention',
};
});
const activeProvider = providerSummaries.find((provider) => provider.active) || null;
const providersNeedingAttention = providerSummaries.filter((provider) => provider.issueCount > 0);
const providerHosts = Array.from(
new Set(
providerSummaries
.map((provider) => provider.baseUrlHost)
.filter(Boolean),
),
).sort((left, right) => left.localeCompare(right));
return {
hasActiveProvider: Boolean(activeProvider),
activeProviderId,
activeProvider,
providerCount: providerSummaries.length,
readyProviderCount: providerSummaries.length - providersNeedingAttention.length,
providersNeedingAttentionCount: providersNeedingAttention.length,
missingSecretCount: providerSummaries.filter((provider) => provider.issues.includes('missing_secret')).length,
missingSelectedModelCount: providerSummaries.filter((provider) => provider.issues.includes('missing_selected_model')).length,
missingBaseUrlCount: providerSummaries.filter((provider) => provider.issues.includes('missing_base_url')).length,
providers: providerSummaries,
providerHosts,
dynamicModelCount: dynamicModelPreview.total,
dynamicModels: dynamicModelPreview.items,
dynamicModelsTruncated: dynamicModelPreview.truncated,
message: providerSummaries.length === 0
? translateInspectionCopy(
translate,
'ai_chat.inspection.provider.message.empty',
'No AI providers are configured',
)
: activeProvider
? activeProvider.issueCount > 0
? translateInspectionCopy(
translate,
'ai_chat.inspection.provider.message.active_needs_attention',
`Using ${activeProvider.name || activeProvider.id}, but ${activeProvider.issueCount} items still need checking`,
{ provider: activeProvider.name || activeProvider.id, issueCount: activeProvider.issueCount },
)
: translateInspectionCopy(
translate,
'ai_chat.inspection.provider.message.active_ready',
`${providerSummaries.length} providers are configured; using ${activeProvider.name || activeProvider.id}`,
{ count: providerSummaries.length, provider: activeProvider.name || activeProvider.id },
)
: translateInspectionCopy(
translate,
'ai_chat.inspection.provider.message.unselected',
`${providerSummaries.length} providers are configured, but no active provider is selected`,
{ count: providerSummaries.length },
),
};
};