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:
LoganandClaude committed 2026-06-28 22:37:46 +08:00
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>
+18 -35
View File
@@ -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');
}