feat: Provider model_settings 全链路 + 监管节点工具集 + 重型插件注入 + 前端打磨
- Provider model_settings (Provider+Model 级别参数配置): DB JSONB → API → GSM → AgentFactory.resolve → 三节点 agent.run 注入 - 新增 data/toolset/regulatory_toolset/: 监管节点专属工具(query_workflow_status / query_task_list / send_file) - send_file 从 interactive_toolset 迁移至 regulatory_toolset,interactive 仅保留 approval - mcp_helper 合入 GlobalPluginManager dispatch tools - 前端 Provider 弹窗参数设置区加 JSON 编辑器(model_settings) - 前端 Plugin 页面新增"重型插件"Tab(HeavyPluginList 占位) - .gitignore 精简:去除系统默认项,修复 data/ 子目录追踪 - data/toolset/ 与 data/plugin/ 首次纳入版本控制 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -37,13 +37,14 @@ export function ProvidersSettings() {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [editingProvider, setEditingProvider] = useState<string | null>(null);
|
||||
const [selectedTypeId, setSelectedTypeId] = useState<string>('openai');
|
||||
const [formData, setFormData] = useState({ provider_title: '', provider_url: '', provider_apikey: '', custom_models: '' });
|
||||
const [formData, setFormData] = useState({ provider_title: '', provider_url: '', provider_apikey: '', custom_models: '', model_settings: '' });
|
||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||
const [submitLoading, setSubmitLoading] = useState(false);
|
||||
const [testLoading, setTestLoading] = useState(false);
|
||||
const [testResult, setTestResult] = useState<{ success: boolean; error?: string; model_count?: number } | null>(null);
|
||||
const [error, setError] = useState('');
|
||||
const [expandedProvider, setExpandedProvider] = useState<string | null>(null);
|
||||
const [modelSettingsError, setModelSettingsError] = useState('');
|
||||
|
||||
const selectedType = PROVIDER_TYPES.find((p) => p.id === selectedTypeId) || PROVIDER_TYPES[0];
|
||||
|
||||
@@ -65,8 +66,9 @@ export function ProvidersSettings() {
|
||||
const openAddModal = () => {
|
||||
setEditingProvider(null);
|
||||
setSelectedTypeId('openai');
|
||||
setFormData({ provider_title: '', provider_url: PROVIDER_TYPES[0].defaultUrl, provider_apikey: '', custom_models: '' });
|
||||
setFormData({ provider_title: '', provider_url: PROVIDER_TYPES[0].defaultUrl, provider_apikey: '', custom_models: '', model_settings: '' });
|
||||
setError('');
|
||||
setModelSettingsError('');
|
||||
setTestResult(null);
|
||||
setShowAdvanced(false);
|
||||
setIsModalOpen(true);
|
||||
@@ -81,8 +83,12 @@ export function ProvidersSettings() {
|
||||
provider_url: provider.provider_url || '',
|
||||
provider_apikey: '',
|
||||
custom_models: '',
|
||||
model_settings: provider.model_settings && Object.keys(provider.model_settings).length > 0
|
||||
? JSON.stringify(provider.model_settings, null, 2)
|
||||
: '',
|
||||
});
|
||||
setError('');
|
||||
setModelSettingsError('');
|
||||
setTestResult(null);
|
||||
setShowAdvanced(false);
|
||||
setIsModalOpen(true);
|
||||
@@ -97,6 +103,29 @@ export function ProvidersSettings() {
|
||||
setTestResult(null);
|
||||
};
|
||||
|
||||
const parseModelSettings = (): { ok: true; value: Record<string, Record<string, unknown>> | undefined } | { ok: false } => {
|
||||
const raw = formData.model_settings.trim();
|
||||
if (!raw) return { ok: true, value: undefined };
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
||||
setModelSettingsError(t('agent.providerModelSettingsInvalid'));
|
||||
return { ok: false };
|
||||
}
|
||||
for (const v of Object.values(parsed)) {
|
||||
if (typeof v !== 'object' || v === null || Array.isArray(v)) {
|
||||
setModelSettingsError(t('agent.providerModelSettingsInvalid'));
|
||||
return { ok: false };
|
||||
}
|
||||
}
|
||||
setModelSettingsError('');
|
||||
return { ok: true, value: parsed as Record<string, Record<string, unknown>> };
|
||||
} catch {
|
||||
setModelSettingsError(t('agent.providerModelSettingsInvalid'));
|
||||
return { ok: false };
|
||||
}
|
||||
};
|
||||
|
||||
const buildPayload = () => {
|
||||
const customModels = formData.custom_models
|
||||
.split(',').map((s) => s.trim()).filter(Boolean);
|
||||
@@ -107,6 +136,8 @@ export function ProvidersSettings() {
|
||||
provider_apikey: formData.provider_apikey,
|
||||
};
|
||||
if (customModels.length > 0) payload.custom_models = customModels;
|
||||
const ms = parseModelSettings();
|
||||
if (ms.ok && ms.value) payload.model_settings = ms.value;
|
||||
return payload;
|
||||
};
|
||||
|
||||
@@ -134,6 +165,8 @@ export function ProvidersSettings() {
|
||||
setError(t('agent.providerFillAll'));
|
||||
return;
|
||||
}
|
||||
const ms = parseModelSettings();
|
||||
if (!ms.ok) return;
|
||||
setSubmitLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
@@ -361,6 +394,31 @@ export function ProvidersSettings() {
|
||||
/>
|
||||
<p className="text-[10px] text-text-muted mt-1">{t('agent.providerCustomModelsHint')}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">
|
||||
{t('agent.providerModelSettings')}
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.model_settings}
|
||||
onChange={(e) => {
|
||||
setFormData({ ...formData, model_settings: e.target.value });
|
||||
if (modelSettingsError) setModelSettingsError('');
|
||||
}}
|
||||
placeholder={t('agent.providerModelSettingsPlaceholder')}
|
||||
rows={8}
|
||||
className={`w-full bg-bg-input border text-sm rounded-xl px-3.5 py-2.5 focus:outline-none focus:ring-2 text-text-primary placeholder:text-text-muted/50 font-mono resize-none ${
|
||||
modelSettingsError
|
||||
? 'border-danger/50 focus:ring-danger/20 focus:border-danger'
|
||||
: 'border-border-primary focus:ring-accent/20 focus:border-accent'
|
||||
}`}
|
||||
/>
|
||||
{modelSettingsError ? (
|
||||
<p className="text-[10px] text-danger mt-1">{modelSettingsError}</p>
|
||||
) : (
|
||||
<p className="text-[10px] text-text-muted mt-1">{t('agent.providerModelSettingsHint')}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Boxes, Loader2, Package } from 'lucide-react';
|
||||
import apiClient from '../../api/client';
|
||||
|
||||
interface HeavyPlugin {
|
||||
name: string;
|
||||
display_name: string;
|
||||
description: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export function HeavyPluginList() {
|
||||
const { t } = useTranslation();
|
||||
const [plugins, setPlugins] = useState<HeavyPlugin[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPlugins = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const resp = await apiClient.get('/api/v1/plugin/list');
|
||||
setPlugins(resp.data.plugins || []);
|
||||
setError('');
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch heavy plugins:', err);
|
||||
setError(t('plugin.heavyPluginLoadFailed'));
|
||||
setPlugins([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchPlugins();
|
||||
}, [t]);
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-text-primary">{t('plugin.heavyPluginManagement')}</h3>
|
||||
<p className="text-sm text-text-muted mt-0.5">{t('plugin.heavyPluginDesc')}</p>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-text-muted">
|
||||
<Loader2 size={24} className="animate-spin mb-3" />
|
||||
<span className="text-sm">{t('common.loading')}</span>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="p-3 bg-danger-bg text-danger text-sm rounded-xl border border-danger/20">{error}</div>
|
||||
) : plugins.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 bg-bg-card rounded-2xl border border-border-primary border-dashed text-text-muted">
|
||||
<Boxes size={32} className="mb-3 opacity-40" />
|
||||
<span className="text-sm">{t('plugin.heavyPluginEmpty')}</span>
|
||||
<span className="text-[11px] mt-1.5 opacity-70">{t('plugin.heavyPluginEmptyHint')}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{plugins.map((p) => (
|
||||
<div key={p.name} className="bg-bg-card border border-border-primary rounded-2xl p-5 card-hover">
|
||||
<div className="flex items-start gap-3 mb-3">
|
||||
<div className="w-9 h-9 rounded-xl bg-bg-secondary border border-border-secondary flex items-center justify-center text-accent shrink-0">
|
||||
<Package size={18} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="font-semibold text-sm text-text-primary truncate">{p.display_name || p.name}</h4>
|
||||
<span className="text-[10px] text-text-muted font-mono">{p.name}</span>
|
||||
</div>
|
||||
<span className={`flex items-center gap-1 text-[10px] font-medium px-2 py-1 rounded-lg border ${
|
||||
p.status === 'running'
|
||||
? 'bg-success-bg text-success border-success/20'
|
||||
: 'bg-bg-secondary text-text-muted border-border-primary'
|
||||
}`}>
|
||||
{p.status === 'running' && <span className="w-1 h-1 rounded-full bg-success" />}
|
||||
{p.status}
|
||||
</span>
|
||||
</div>
|
||||
{p.description && (
|
||||
<p className="text-xs text-text-secondary leading-relaxed">{p.description}</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Sparkles, Plug } from 'lucide-react';
|
||||
import { Sparkles, Plug, Boxes } from 'lucide-react';
|
||||
import { SkillSettings } from './SkillSettings';
|
||||
import { MCPSettings } from './MCPSettings';
|
||||
import { HeavyPluginList } from './HeavyPluginList';
|
||||
|
||||
type PluginTab = 'skill' | 'mcp';
|
||||
type PluginTab = 'skill' | 'mcp' | 'heavy';
|
||||
|
||||
export function PluginLayout() {
|
||||
const { t } = useTranslation();
|
||||
@@ -13,6 +14,7 @@ export function PluginLayout() {
|
||||
const tabs: { key: PluginTab; label: string; icon: typeof Sparkles }[] = [
|
||||
{ key: 'skill', label: t('plugin.skillTab'), icon: Sparkles },
|
||||
{ key: 'mcp', label: t('plugin.mcpTab'), icon: Plug },
|
||||
{ key: 'heavy', label: t('plugin.heavyTab'), icon: Boxes },
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -39,6 +41,7 @@ export function PluginLayout() {
|
||||
<div className="flex-1 overflow-y-auto p-8">
|
||||
{tab === 'skill' && <SkillSettings />}
|
||||
{tab === 'mcp' && <MCPSettings />}
|
||||
{tab === 'heavy' && <HeavyPluginList />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -251,7 +251,11 @@
|
||||
"providerAdvanced": "Parameter Settings",
|
||||
"providerCustomModels": "Custom Model List",
|
||||
"providerCustomModelsPlaceholder": "Comma separated, e.g. gpt-4o, gpt-4o-mini",
|
||||
"providerCustomModelsHint": "Optional. Leave empty to auto-fetch model list from provider."
|
||||
"providerCustomModelsHint": "Optional. Leave empty to auto-fetch model list from provider.",
|
||||
"providerModelSettings": "Model Call Parameters",
|
||||
"providerModelSettingsHint": "JSON object. Keys are model_id or __default__, values are ModelSettings (temperature/max_tokens/thinking etc.). __default__ is the fallback; specific model_id overrides it.",
|
||||
"providerModelSettingsPlaceholder": "{\n \"__default__\": {\"temperature\": 0.7, \"max_tokens\": 4096},\n \"o1\": {\"thinking\": \"high\"}\n}",
|
||||
"providerModelSettingsInvalid": "Invalid JSON. Please check the syntax."
|
||||
},
|
||||
"plugin": {
|
||||
"toolManagement": "Toolset Center",
|
||||
@@ -284,6 +288,12 @@
|
||||
"install": "Install",
|
||||
"skillTab": "Skills",
|
||||
"mcpTab": "MCP Servers",
|
||||
"heavyTab": "Heavy Plugins",
|
||||
"heavyPluginManagement": "Heavy Plugin Management",
|
||||
"heavyPluginDesc": "Heavy plugins are extension modules with UI and multi-agent collaboration. Loaded from data/plugin/ directory.",
|
||||
"heavyPluginEmpty": "No heavy plugins loaded",
|
||||
"heavyPluginEmptyHint": "Place plugin directories in data/plugin/ and they will load on startup",
|
||||
"heavyPluginLoadFailed": "Failed to load heavy plugin list",
|
||||
"mcpManagement": "MCP Server Management",
|
||||
"mcpDesc": "Manage Model Context Protocol servers to extend agent tools",
|
||||
"mcpAdd": "Add MCP Server",
|
||||
|
||||
@@ -251,7 +251,11 @@
|
||||
"providerAdvanced": "参数设置",
|
||||
"providerCustomModels": "自定义模型列表",
|
||||
"providerCustomModelsPlaceholder": "用逗号分隔,如:gpt-4o, gpt-4o-mini",
|
||||
"providerCustomModelsHint": "可选,留空则自动从供应商拉取模型清单"
|
||||
"providerCustomModelsHint": "可选,留空则自动从供应商拉取模型清单",
|
||||
"providerModelSettings": "模型调用参数",
|
||||
"providerModelSettingsHint": "JSON 对象,键是 model_id 或 __default__,值是 ModelSettings(temperature/max_tokens/thinking 等)。__default__ 是兜底参数,具体 model_id 会覆盖之。",
|
||||
"providerModelSettingsPlaceholder": "{\n \"__default__\": {\"temperature\": 0.7, \"max_tokens\": 4096},\n \"o1\": {\"thinking\": \"high\"}\n}",
|
||||
"providerModelSettingsInvalid": "JSON 格式错误,请检查"
|
||||
},
|
||||
"plugin": {
|
||||
"toolManagement": "工具集中心",
|
||||
@@ -284,6 +288,12 @@
|
||||
"install": "安装",
|
||||
"skillTab": "技能",
|
||||
"mcpTab": "MCP 服务",
|
||||
"heavyTab": "重型插件",
|
||||
"heavyPluginManagement": "重型插件管理",
|
||||
"heavyPluginDesc": "重型插件是带 UI 与多 Agent 协作能力的扩展模块,从 data/plugin/ 目录加载",
|
||||
"heavyPluginEmpty": "暂无已加载的重型插件",
|
||||
"heavyPluginEmptyHint": "把插件目录放进 data/plugin/,启动时会自动加载",
|
||||
"heavyPluginLoadFailed": "加载重型插件列表失败",
|
||||
"mcpManagement": "MCP 服务管理",
|
||||
"mcpDesc": "管理 Model Context Protocol 服务器,扩展 agent 工具能力",
|
||||
"mcpAdd": "添加 MCP 服务",
|
||||
|
||||
@@ -22,6 +22,7 @@ export interface Provider {
|
||||
provider_status?: string;
|
||||
status?: string;
|
||||
model?: string;
|
||||
model_settings?: Record<string, Record<string, unknown>>;
|
||||
}
|
||||
|
||||
export interface ProviderRegisterRequest {
|
||||
@@ -29,6 +30,7 @@ export interface ProviderRegisterRequest {
|
||||
provider_title: string;
|
||||
provider_url: string;
|
||||
provider_apikey: string;
|
||||
model_settings?: Record<string, Record<string, unknown>>;
|
||||
}
|
||||
|
||||
export interface ProviderListResponse {
|
||||
|
||||
Reference in New Issue
Block a user