feat(llm): 大模型双协议(OpenAI+Anthropic)接入 + SSH凭证修复 + 容量预测AI解读
后端双协议: 新增 anthropic.go(Complete/Stream/工具转换) + client.go 工厂按 Protocol 路由; llm.Client 加 Stream; llm_chat 三方法收敛复用 llm.Client; resolveProtocol 按 provider 路由; Agent CLI 注入协议。编码套餐(GLM-5.2/Kimi/MiMo/Anthropic)走 Anthropic /v1/messages。 SSH凭证修复: applyCredential 正确解析凭证 JSON 的 private_key(旧实现把整个 JSON 当私钥 → ssh.ParsePrivateKey 失败 → ssh-key 登录不通); AssignCredential/批量导入/单台 CRUD 三处统一; 单台补齐 credential_id 字段(原被忽略)。 容量预测: 新增 AI 深度解读(buildCapacityInsightPrompt + PredictiveCapacityTab 按钮, 复用 aiApi.chat)。 其他: DeepSeek 模型名更新为 deepseek-v4-flash/pro; 智能助手去掉重复模型配置按钮; LLMConfig 精简生成参数(Claude Code 式)。 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
1 parent
c0ee8e4ddd
commit
0b2e43ea9e
16 files changed
+689
-296
No files matched your search
@@ -27,16 +27,6 @@ import {
|
||||
} from 'lucide-react';
|
||||
import type { JsonData } from '@/types/common';
|
||||
|
||||
// LLM 提供商列表(OpenAI 兼容接口) // Logan.Li
|
||||
const LLM_PROVIDERS: { id: string; name: string; defaultEndpoint: string; models: string[] }[] = [
|
||||
{ id: 'openai', name: 'OpenAI', defaultEndpoint: 'https://api.openai.com/v1', models: ['gpt-4o-mini', 'gpt-4o', 'gpt-4-turbo'] },
|
||||
{ id: 'deepseek', name: 'DeepSeek', defaultEndpoint: 'https://api.deepseek.com/v1', models: ['deepseek-chat', 'deepseek-reasoner'] },
|
||||
{ id: 'qwen', name: '通义千问 (DashScope)', defaultEndpoint: 'https://dashscope.aliyuncs.com/compatible-mode/v1', models: ['qwen-plus', 'qwen-turbo', 'qwen-max'] },
|
||||
{ id: 'moonshot', name: 'Moonshot 月之暗面', defaultEndpoint: 'https://api.moonshot.cn/v1', models: ['moonshot-v1-8k', 'moonshot-v1-32k'] },
|
||||
{ id: 'ollama', name: 'Ollama 本地', defaultEndpoint: 'http://localhost:11434/v1', models: ['qwen2.5:7b', 'llama3.2', 'deepseek-r1:7b'] },
|
||||
{ id: 'custom', name: '自定义 (OpenAI 兼容)', defaultEndpoint: '', models: [] },
|
||||
];
|
||||
|
||||
const defaultConfig: LLMConfigData = {
|
||||
provider: 'openai',
|
||||
model: 'gpt-4o-mini',
|
||||
@@ -323,15 +313,6 @@ export function AIOpsAssistant() {
|
||||
{config.model}
|
||||
</Badge>
|
||||
) : null}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border-slate-700 text-slate-300 hover:text-purple-400"
|
||||
onClick={() => navigate('/ai-llm-config')}
|
||||
>
|
||||
<Settings className="h-4 w-4 mr-1" />
|
||||
模型配置
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -13,19 +13,27 @@ import { toast } from 'sonner';
|
||||
import { Loader2, Save, Wifi, KeyRound, Brain } from 'lucide-react';
|
||||
import { aiApi, type LLMConfigData } from '@/api';
|
||||
|
||||
// OpenAI 兼容提供商列表 // Logan.Li
|
||||
// 提供商列表(后端按 provider 自动路由协议) // Logan.Li
|
||||
// Anthropic 协议(glm-coding / anthropic):端点不含 /v1,后端拼 /v1/messages。
|
||||
// OpenAI 协议(其余):端点含 /v1,后端拼 /chat/completions。
|
||||
const PROVIDERS = [
|
||||
{ id: 'openai', name: 'OpenAI', endpoint: 'https://api.openai.com/v1', models: 'gpt-4o-mini, gpt-4o' },
|
||||
{ id: 'deepseek', name: 'DeepSeek', endpoint: 'https://api.deepseek.com/v1', models: 'deepseek-chat, deepseek-reasoner' },
|
||||
{ id: 'openai', name: 'OpenAI', endpoint: 'https://api.openai.com/v1', models: 'gpt-4o-mini, gpt-4o, gpt-4.1' },
|
||||
{ id: 'deepseek', name: 'DeepSeek', endpoint: 'https://api.deepseek.com/v1', models: 'deepseek-v4-flash, deepseek-v4-pro' },
|
||||
{ id: 'glm', name: '智谱 GLM (通用)', endpoint: 'https://open.bigmodel.cn/api/paas/v4', models: 'glm-4.6, glm-4.5, glm-4-flash' },
|
||||
{ id: 'glm-coding', name: '智谱 GLM (编码套餐)', endpoint: 'https://open.bigmodel.cn/api/anthropic', models: 'GLM-5.2' },
|
||||
{ id: 'qwen', name: '通义千问 (DashScope)', endpoint: 'https://dashscope.aliyuncs.com/compatible-mode/v1', models: 'qwen-plus, qwen-turbo, qwen-max' },
|
||||
{ id: 'moonshot', name: 'Moonshot 月之暗面', endpoint: 'https://api.moonshot.cn/v1', models: 'moonshot-v1-8k, moonshot-v1-32k' },
|
||||
{ id: 'moonshot', name: 'Moonshot (Kimi)', endpoint: 'https://api.moonshot.cn/v1', models: 'kimi-latest, moonshot-v1-8k, moonshot-v1-32k' },
|
||||
{ id: 'kimi', name: 'Kimi 编码套餐', endpoint: 'https://api.kimi.com/coding/v1', models: 'kimi-for-coding' },
|
||||
{ id: 'mimo', name: '小米 MiMo', endpoint: 'https://api.mimo.mi.com/v1', models: 'mimo-v2-flash, mimo-v2' },
|
||||
{ id: 'anthropic', name: 'Anthropic Claude', endpoint: 'https://api.anthropic.com', models: 'claude-sonnet-4-6, claude-opus-4-8, claude-haiku-4-5' },
|
||||
{ id: 'ollama', name: 'Ollama 本地', endpoint: 'http://localhost:11434/v1', models: 'qwen2.5:7b, llama3.2' },
|
||||
{ id: 'custom', name: '自定义 (OpenAI 兼容)', endpoint: '', models: '' },
|
||||
];
|
||||
|
||||
// 生成参数(温度/max_tokens/系统提示词)已按 Claude Code 模式精简:UI 不再暴露,走内置默认。 // Logan.Li
|
||||
const defaultCfg: LLMConfigData = {
|
||||
provider: 'deepseek', model: 'deepseek-chat', api_key: '',
|
||||
endpoint: 'https://api.deepseek.com/v1', temperature: 0.3, max_tokens: 4096,
|
||||
provider: 'deepseek', model: 'deepseek-v4-flash', api_key: '',
|
||||
endpoint: 'https://api.deepseek.com/v1', temperature: 0.7, max_tokens: 8192,
|
||||
system_prompt: '', enabled: false,
|
||||
};
|
||||
|
||||
@@ -105,36 +113,11 @@ export default function LLMConfigPage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-slate-900/60 border-slate-800">
|
||||
<CardHeader><CardTitle className="text-slate-200 text-base">生成参数</CardTitle></CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-slate-300">温度(Temperature)</Label>
|
||||
<Input type="number" step="0.1" min="0" max="2" value={cfg.temperature}
|
||||
onChange={(e) => set('temperature', Number(e.target.value))}
|
||||
className="bg-slate-800 border-slate-700 text-slate-200" />
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-slate-300">最大输出 Token</Label>
|
||||
<Input type="number" min="256" value={cfg.max_tokens}
|
||||
onChange={(e) => set('max_tokens', Number(e.target.value))}
|
||||
className="bg-slate-800 border-slate-700 text-slate-200" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-slate-300">系统提示词(可选,留空使用内置运维助手角色)</Label>
|
||||
<textarea value={cfg.system_prompt} onChange={(e) => set('system_prompt', e.target.value)} rows={3}
|
||||
className="w-full bg-slate-800 border border-slate-700 rounded-md px-3 py-2 text-sm text-slate-200 resize-none" />
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-slate-300">启用 LLM 服务</Label>
|
||||
<Switch checked={cfg.enabled} onCheckedChange={(v) => set('enabled', v)} />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 text-sm text-slate-300">
|
||||
<Switch checked={cfg.enabled} onCheckedChange={(v) => set('enabled', v)} />
|
||||
<span>启用 LLM 服务</span>
|
||||
</div>
|
||||
<Button onClick={save} disabled={saving} className="bg-blue-600 hover:bg-blue-700">
|
||||
{saving ? <Loader2 className="w-4 h-4 mr-1 animate-spin" /> : <Save className="w-4 h-4 mr-1" />} 保存配置
|
||||
</Button>
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { HardDrive, Database, Zap, Loader2 } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { HardDrive, Database, Zap, Loader2, Sparkles } from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import type { CapacityForecast } from './predictUtils';
|
||||
import { toast } from 'sonner';
|
||||
import { aiApi } from '@/api';
|
||||
import { buildCapacityInsightPrompt, type CapacityForecast } from './predictUtils';
|
||||
|
||||
export function PredictiveCapacityTab({
|
||||
forecasts,
|
||||
@@ -10,6 +14,25 @@ export function PredictiveCapacityTab({
|
||||
forecasts: CapacityForecast[];
|
||||
loading: boolean;
|
||||
}) {
|
||||
const [insight, setInsight] = useState('');
|
||||
const [analyzing, setAnalyzing] = useState(false);
|
||||
|
||||
// AI 深度解读:把容量预测硬数据喂给 LLM,生成综合风险评估与运维建议 // Logan.Li
|
||||
// 复用 aiApi.chat(双协议后自动支持 GLM-5.2 等);失败仅 toast,下方逐条模板建议仍可读。
|
||||
const analyze = async () => {
|
||||
const prompt = buildCapacityInsightPrompt(forecasts);
|
||||
if (!prompt) return;
|
||||
setAnalyzing(true);
|
||||
try {
|
||||
const res = await aiApi.chat([{ role: 'user', content: prompt }]);
|
||||
setInsight(res?.content || '');
|
||||
} catch {
|
||||
toast.error('AI 解读失败,请检查大模型配置或稍后重试');
|
||||
} finally {
|
||||
setAnalyzing(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="bg-slate-800 border-slate-700">
|
||||
<CardHeader className="pb-3">
|
||||
@@ -18,7 +41,22 @@ export function PredictiveCapacityTab({
|
||||
<HardDrive className="h-5 w-5 text-amber-400" />
|
||||
容量规划预警
|
||||
</CardTitle>
|
||||
<span className="text-xs text-slate-500">基于日均增长率推算阈值触及时间</span>
|
||||
{!loading && forecasts.length > 0 && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={analyze}
|
||||
disabled={analyzing}
|
||||
className="border-purple-500/30 text-purple-300 hover:text-purple-200 h-7"
|
||||
>
|
||||
{analyzing ? (
|
||||
<Loader2 className="h-3.5 w-3.5 mr-1 animate-spin" />
|
||||
) : (
|
||||
<Sparkles className="h-3.5 w-3.5 mr-1" />
|
||||
)}
|
||||
AI 深度解读
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -30,6 +68,16 @@ export function PredictiveCapacityTab({
|
||||
<div className="text-center py-12 text-slate-500">暂无容量数据</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{/* AI 深度解读结果(点击按钮后展示) */}
|
||||
{insight && (
|
||||
<div className="p-4 rounded-lg border border-purple-500/30 bg-purple-500/5">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Sparkles className="h-4 w-4 text-purple-400" />
|
||||
<span className="text-sm font-medium text-purple-300">AI 深度解读</span>
|
||||
</div>
|
||||
<p className="text-sm text-slate-300 whitespace-pre-wrap break-words">{insight}</p>
|
||||
</div>
|
||||
)}
|
||||
{forecasts.map((forecast, idx) => (
|
||||
<div key={idx} className="p-4 rounded-lg border border-slate-700 bg-slate-800/50">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
|
||||
@@ -319,3 +319,26 @@ export function trendColorClass(trend: string): string {
|
||||
return 'text-slate-400';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建容量预测「AI 深度解读」的 prompt —— 把硬数据序列化为结构化上下文喂给 LLM。 // Logan.Li
|
||||
* 纯函数,供 PredictiveCapacityTab 调 aiApi.chat 使用。无数据返回空串(调用方据此禁用按钮)。
|
||||
*/
|
||||
export function buildCapacityInsightPrompt(forecasts: CapacityForecast[]): string {
|
||||
if (forecasts.length === 0) return '';
|
||||
const lines = forecasts.map((f) => {
|
||||
const risk = f.daysUntilThreshold >= 999 ? '增长平缓,暂无触顶风险' : `${f.daysUntilThreshold} 天后触及阈值`;
|
||||
return `- ${f.resource}:当前 ${f.currentUsage}% → 7天后预测 ${f.predictedUsage}%(阈值 ${f.threshold}%),${risk}`;
|
||||
});
|
||||
return [
|
||||
'你是 AIOps 容量规划专家。基于以下服务器资源容量预测数据,给出综合风险评估与可执行的运维建议:',
|
||||
'',
|
||||
'【容量预测数据】',
|
||||
...lines,
|
||||
'',
|
||||
'要求:',
|
||||
'1. 指出最紧急的风险资源(触及阈值最快者优先)',
|
||||
'2. 给出针对性处置建议(扩容 / 清理 / 优化,具体到操作命令或步骤)',
|
||||
'3. 简洁中文,Markdown 无序列表,不超过 200 字',
|
||||
].join('\n');
|
||||
}
|
||||
Reference in new issue
Block a user