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:
2026-06-17 13:10:31 +00:00
parent 6d658b4f4d
commit 005ce566a8
49 changed files with 1093 additions and 30 deletions
@@ -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>
);
+11 -1
View File
@@ -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",
+11 -1
View File
@@ -251,7 +251,11 @@
"providerAdvanced": "参数设置",
"providerCustomModels": "自定义模型列表",
"providerCustomModelsPlaceholder": "用逗号分隔,如:gpt-4o, gpt-4o-mini",
"providerCustomModelsHint": "可选,留空则自动从供应商拉取模型清单"
"providerCustomModelsHint": "可选,留空则自动从供应商拉取模型清单",
"providerModelSettings": "模型调用参数",
"providerModelSettingsHint": "JSON 对象,键是 model_id 或 __default__,值是 ModelSettingstemperature/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 服务",
+2
View File
@@ -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 {