feat(mcp): 完整落地 MCP 服务管理
- Dockerfile 后端 stage 加 nodejs/npm,支持 stdio MCP server - 新增前端 MCP 服务管理 UI(MCPSettings.tsx),支持 stdio/sse/http 三种 transport - PluginLayout 改双 tab(技能 / MCP 服务) - i18n 补 MCP 相关 zh/en 文案 - send_file 工具的 trace_id 改可选,聊天场景退化为返回内容 - system_workflow 工具集纳入 send_file Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -13,10 +13,13 @@ FROM python:3.13-slim
|
||||
WORKDIR /app
|
||||
|
||||
# Install system dependencies (for building PostgreSQL drivers and other native extensions)
|
||||
# nodejs/npm are needed for stdio-mode MCP servers (e.g. npx -y @modelcontextprotocol/...)
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
libpq-dev \
|
||||
git \
|
||||
nodejs \
|
||||
npm \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install uv package manager
|
||||
|
||||
@@ -0,0 +1,378 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Plus, Trash2, Server, Loader2, Plug, ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import apiClient from '../../api/client';
|
||||
|
||||
type Transport = 'stdio' | 'sse' | 'http';
|
||||
|
||||
interface MCPServer {
|
||||
server_id: string;
|
||||
name: string;
|
||||
transport: Transport;
|
||||
command?: string;
|
||||
args?: string[];
|
||||
url?: string;
|
||||
tool_prefix?: string;
|
||||
env?: Record<string, string>;
|
||||
}
|
||||
|
||||
interface MCPToolInfo {
|
||||
server_id: string;
|
||||
name: string;
|
||||
transport: string;
|
||||
tools: string[];
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export function MCPSettings() {
|
||||
const { t } = useTranslation();
|
||||
const [servers, setServers] = useState<MCPServer[]>([]);
|
||||
const [toolsByServer, setToolsByServer] = useState<Record<string, MCPToolInfo>>({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [expanded, setExpanded] = useState<string | null>(null);
|
||||
|
||||
// Form state
|
||||
const [name, setName] = useState('');
|
||||
const [transport, setTransport] = useState<Transport>('stdio');
|
||||
const [command, setCommand] = useState('');
|
||||
const [argsText, setArgsText] = useState('');
|
||||
const [url, setUrl] = useState('');
|
||||
const [toolPrefix, setToolPrefix] = useState('');
|
||||
const [envText, setEnvText] = useState('');
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [message, setMessage] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const fetchServers = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await apiClient.get('/api/v1/resource/mcp');
|
||||
setServers(response.data.servers || []);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch MCP servers:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchToolsForAll = async () => {
|
||||
try {
|
||||
const response = await apiClient.get('/api/v1/resource/tool');
|
||||
const mcpTools: MCPToolInfo[] = response.data.mcp_servers || [];
|
||||
const map: Record<string, MCPToolInfo> = {};
|
||||
mcpTools.forEach((t) => {
|
||||
if (t.server_id) map[t.server_id] = t;
|
||||
});
|
||||
setToolsByServer(map);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch MCP tools:', err);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchServers();
|
||||
fetchToolsForAll();
|
||||
}, []);
|
||||
|
||||
const resetForm = () => {
|
||||
setName('');
|
||||
setCommand('');
|
||||
setArgsText('');
|
||||
setUrl('');
|
||||
setToolPrefix('');
|
||||
setEnvText('');
|
||||
};
|
||||
|
||||
const parseEnv = (text: string): Record<string, string> => {
|
||||
const env: Record<string, string> = {};
|
||||
text.split('\n').forEach((line) => {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) return;
|
||||
const eq = trimmed.indexOf('=');
|
||||
if (eq > 0) {
|
||||
env[trimmed.slice(0, eq).trim()] = trimmed.slice(eq + 1).trim();
|
||||
}
|
||||
});
|
||||
return env;
|
||||
};
|
||||
|
||||
const handleAdd = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!name) return;
|
||||
setSubmitting(true);
|
||||
setMessage('');
|
||||
setError('');
|
||||
|
||||
const payload: any = { name, transport, tool_prefix: toolPrefix || null };
|
||||
if (transport === 'stdio') {
|
||||
payload.command = command;
|
||||
payload.args = argsText
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
} else {
|
||||
payload.url = url;
|
||||
}
|
||||
const env = parseEnv(envText);
|
||||
if (Object.keys(env).length > 0) payload.env = env;
|
||||
|
||||
try {
|
||||
await apiClient.post('/api/v1/resource/mcp', payload);
|
||||
setMessage(t('plugin.mcpAddSuccess'));
|
||||
resetForm();
|
||||
await fetchServers();
|
||||
await fetchToolsForAll();
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.detail || t('plugin.mcpAddFailed'));
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (server: MCPServer) => {
|
||||
if (!confirm(t('plugin.mcpDeleteConfirm', { name: server.name }))) return;
|
||||
try {
|
||||
await apiClient.delete(`/api/v1/resource/mcp/${server.server_id}`);
|
||||
await fetchServers();
|
||||
await fetchToolsForAll();
|
||||
} catch {
|
||||
alert(t('plugin.mcpDeleteFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl space-y-6">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Plug size={16} className="text-accent" />
|
||||
<h1 className="text-lg font-bold text-text-primary">{t('plugin.mcpManagement')}</h1>
|
||||
</div>
|
||||
<p className="text-sm text-text-muted">{t('plugin.mcpDesc')}</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-bg-card rounded-2xl border border-border-primary shadow-sm overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-border-primary flex items-center gap-3">
|
||||
<div className="w-9 h-9 rounded-xl bg-accent-light flex items-center justify-center">
|
||||
<Plus size={16} className="text-accent" />
|
||||
</div>
|
||||
<h2 className="text-sm font-bold text-text-primary">{t('plugin.mcpAdd')}</h2>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<form onSubmit={handleAdd} className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">
|
||||
{t('plugin.mcpName')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder={t('plugin.mcpNamePlaceholder')}
|
||||
className="w-full px-3.5 py-2.5 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent placeholder:text-text-muted/50"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">
|
||||
{t('plugin.mcpTransport')}
|
||||
</label>
|
||||
<select
|
||||
value={transport}
|
||||
onChange={(e) => setTransport(e.target.value as Transport)}
|
||||
className="w-full px-3.5 py-2.5 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent"
|
||||
>
|
||||
<option value="stdio">stdio</option>
|
||||
<option value="sse">sse</option>
|
||||
<option value="http">http</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{transport === 'stdio' ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">
|
||||
{t('plugin.mcpCommand')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={command}
|
||||
onChange={(e) => setCommand(e.target.value)}
|
||||
placeholder={t('plugin.mcpCommandPlaceholder')}
|
||||
className="w-full px-3.5 py-2.5 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent placeholder:text-text-muted/50"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">
|
||||
{t('plugin.mcpArgs')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={argsText}
|
||||
onChange={(e) => setArgsText(e.target.value)}
|
||||
placeholder={t('plugin.mcpArgsPlaceholder')}
|
||||
className="w-full px-3.5 py-2.5 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent placeholder:text-text-muted/50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">
|
||||
{t('plugin.mcpUrl')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={url}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
placeholder={t('plugin.mcpUrlPlaceholder')}
|
||||
className="w-full px-3.5 py-2.5 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent placeholder:text-text-muted/50"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">
|
||||
{t('plugin.mcpToolPrefix')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={toolPrefix}
|
||||
onChange={(e) => setToolPrefix(e.target.value)}
|
||||
placeholder={t('plugin.mcpToolPrefixPlaceholder')}
|
||||
className="w-full px-3.5 py-2.5 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent placeholder:text-text-muted/50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">
|
||||
{t('plugin.mcpEnv')}
|
||||
</label>
|
||||
<textarea
|
||||
value={envText}
|
||||
onChange={(e) => setEnvText(e.target.value)}
|
||||
placeholder={t('plugin.mcpEnvPlaceholder')}
|
||||
rows={3}
|
||||
className="w-full px-3.5 py-2.5 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent placeholder:text-text-muted/50 font-mono"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{message && <div className="text-xs text-success">{message}</div>}
|
||||
{error && <div className="text-xs text-danger">{error}</div>}
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={submitting}
|
||||
className="flex items-center gap-2 px-5 py-2.5 bg-accent text-white rounded-xl hover:bg-accent-hover transition-all shadow-lg shadow-accent/15 text-sm font-medium disabled:opacity-50"
|
||||
>
|
||||
<Plus size={14} />
|
||||
{t('plugin.mcpAddBtn')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-bg-card rounded-2xl border border-border-primary shadow-sm overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-border-primary">
|
||||
<h2 className="text-sm font-bold text-text-primary">
|
||||
{t('plugin.mcpRegistered', { count: servers.length })}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
{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>
|
||||
) : servers.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-text-muted">
|
||||
<Server size={32} className="mb-3 opacity-40" />
|
||||
<span className="text-sm">{t('plugin.mcpNoServers')}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{servers.map((srv) => {
|
||||
const isOpen = expanded === srv.server_id;
|
||||
const toolInfo = toolsByServer[srv.server_id];
|
||||
return (
|
||||
<div
|
||||
key={srv.server_id}
|
||||
className="bg-bg-secondary border border-border-secondary rounded-xl overflow-hidden hover:border-accent/30 transition-all"
|
||||
>
|
||||
<div className="flex items-center justify-between p-3.5">
|
||||
<button
|
||||
onClick={() => setExpanded(isOpen ? null : srv.server_id)}
|
||||
className="flex items-center gap-3 flex-1 text-left"
|
||||
>
|
||||
{isOpen ? (
|
||||
<ChevronDown size={14} className="text-text-muted" />
|
||||
) : (
|
||||
<ChevronRight size={14} className="text-text-muted" />
|
||||
)}
|
||||
<div className="w-8 h-8 rounded-lg bg-bg-card border border-border-primary flex items-center justify-center">
|
||||
<Server size={14} className="text-text-muted" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-text-primary truncate">
|
||||
{srv.name}
|
||||
</span>
|
||||
<span className="text-[10px] px-1.5 py-0.5 bg-accent-light text-accent rounded font-medium uppercase">
|
||||
{srv.transport}
|
||||
</span>
|
||||
{srv.tool_prefix && (
|
||||
<span className="text-[10px] px-1.5 py-0.5 bg-bg-card border border-border-primary text-text-muted rounded font-mono">
|
||||
{srv.tool_prefix}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-[11px] text-text-muted truncate font-mono mt-0.5">
|
||||
{srv.transport === 'stdio'
|
||||
? `${srv.command || ''} ${(srv.args || []).join(' ')}`
|
||||
: srv.url}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(srv)}
|
||||
className="p-1.5 text-text-muted hover:text-danger hover:bg-danger-bg rounded-lg transition-all"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
</div>
|
||||
{isOpen && (
|
||||
<div className="px-3.5 pb-3.5 pl-12 border-t border-border-primary/50 pt-3">
|
||||
<div className="text-[11px] font-semibold text-text-secondary uppercase tracking-wider mb-2">
|
||||
{t('plugin.mcpTools')}
|
||||
</div>
|
||||
{toolInfo?.error ? (
|
||||
<div className="text-xs text-danger">{toolInfo.error}</div>
|
||||
) : toolInfo?.tools && toolInfo.tools.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{toolInfo.tools.map((tool) => (
|
||||
<span
|
||||
key={tool}
|
||||
className="text-[11px] px-2 py-1 bg-bg-card border border-border-primary rounded font-mono text-text-secondary"
|
||||
>
|
||||
{tool}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-xs text-text-muted">—</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +1,44 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Sparkles, Plug } from 'lucide-react';
|
||||
import { SkillSettings } from './SkillSettings';
|
||||
import { MCPSettings } from './MCPSettings';
|
||||
|
||||
type PluginTab = 'skill' | 'mcp';
|
||||
|
||||
export function PluginLayout() {
|
||||
const { t } = useTranslation();
|
||||
const [tab, setTab] = useState<PluginTab>('skill');
|
||||
|
||||
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 },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col bg-bg-secondary overflow-hidden">
|
||||
<div className="h-12 border-b border-border-primary bg-bg-card/80 backdrop-blur flex items-center px-6 shrink-0">
|
||||
<span className="text-xs font-semibold text-text-primary">{t('plugin.skillManagement')}</span>
|
||||
<div className="h-12 border-b border-border-primary bg-bg-card/80 backdrop-blur flex items-center px-6 shrink-0 gap-1">
|
||||
{tabs.map(({ key, label, icon: Icon }) => {
|
||||
const active = tab === key;
|
||||
return (
|
||||
<button
|
||||
key={key}
|
||||
onClick={() => setTab(key)}
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-semibold transition-all ${
|
||||
active
|
||||
? 'bg-accent-light text-accent'
|
||||
: 'text-text-muted hover:text-text-primary hover:bg-bg-secondary'
|
||||
}`}
|
||||
>
|
||||
<Icon size={13} />
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto p-8">
|
||||
<SkillSettings />
|
||||
{tab === 'skill' && <SkillSettings />}
|
||||
{tab === 'mcp' && <MCPSettings />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -252,7 +252,34 @@
|
||||
"skillDeleteFailed": "Failed to delete skill",
|
||||
"installedSkills": "Installed Skills ({{count}})",
|
||||
"noSkills": "No skills installed yet.",
|
||||
"install": "Install"
|
||||
"install": "Install",
|
||||
"skillTab": "Skills",
|
||||
"mcpTab": "MCP Servers",
|
||||
"mcpManagement": "MCP Server Management",
|
||||
"mcpDesc": "Manage Model Context Protocol servers to extend agent tools",
|
||||
"mcpAdd": "Add MCP Server",
|
||||
"mcpName": "Name",
|
||||
"mcpNamePlaceholder": "e.g. filesystem",
|
||||
"mcpTransport": "Transport",
|
||||
"mcpCommand": "Command",
|
||||
"mcpCommandPlaceholder": "npx",
|
||||
"mcpArgs": "Arguments",
|
||||
"mcpArgsPlaceholder": "-y, @modelcontextprotocol/server-filesystem, /data",
|
||||
"mcpUrl": "URL",
|
||||
"mcpUrlPlaceholder": "https://example.com/mcp",
|
||||
"mcpToolPrefix": "Tool Prefix (optional)",
|
||||
"mcpToolPrefixPlaceholder": "fs",
|
||||
"mcpEnv": "Environment Variables (optional, KEY=VALUE per line)",
|
||||
"mcpEnvPlaceholder": "API_KEY=xxx\nDEBUG=true",
|
||||
"mcpDelete": "Delete",
|
||||
"mcpDeleteConfirm": "Delete MCP server {{name}}?",
|
||||
"mcpNoServers": "No MCP servers registered yet.",
|
||||
"mcpTools": "Exposed Tools",
|
||||
"mcpAddSuccess": "Added successfully",
|
||||
"mcpAddFailed": "Failed to add",
|
||||
"mcpDeleteFailed": "Failed to delete",
|
||||
"mcpRegistered": "Registered Servers ({{count}})",
|
||||
"mcpAddBtn": "Add"
|
||||
},
|
||||
"topbar": {
|
||||
"switchToEn": "Switch to English",
|
||||
|
||||
@@ -252,7 +252,34 @@
|
||||
"skillDeleteFailed": "删除技能失败",
|
||||
"installedSkills": "已安装技能 ({{count}})",
|
||||
"noSkills": "暂无已安装的技能",
|
||||
"install": "安装"
|
||||
"install": "安装",
|
||||
"skillTab": "技能",
|
||||
"mcpTab": "MCP 服务",
|
||||
"mcpManagement": "MCP 服务管理",
|
||||
"mcpDesc": "管理 Model Context Protocol 服务器,扩展 agent 工具能力",
|
||||
"mcpAdd": "添加 MCP 服务",
|
||||
"mcpName": "服务名称",
|
||||
"mcpNamePlaceholder": "例如:filesystem",
|
||||
"mcpTransport": "传输协议",
|
||||
"mcpCommand": "启动命令",
|
||||
"mcpCommandPlaceholder": "npx",
|
||||
"mcpArgs": "命令参数",
|
||||
"mcpArgsPlaceholder": "-y, @modelcontextprotocol/server-filesystem, /data",
|
||||
"mcpUrl": "服务地址",
|
||||
"mcpUrlPlaceholder": "https://example.com/mcp",
|
||||
"mcpToolPrefix": "工具前缀(可选)",
|
||||
"mcpToolPrefixPlaceholder": "fs",
|
||||
"mcpEnv": "环境变量(可选,每行 KEY=VALUE)",
|
||||
"mcpEnvPlaceholder": "API_KEY=xxx\nDEBUG=true",
|
||||
"mcpDelete": "删除",
|
||||
"mcpDeleteConfirm": "确定要删除 MCP 服务 {{name}} 吗?",
|
||||
"mcpNoServers": "暂无已注册的 MCP 服务",
|
||||
"mcpTools": "暴露的工具",
|
||||
"mcpAddSuccess": "添加成功",
|
||||
"mcpAddFailed": "添加失败",
|
||||
"mcpDeleteFailed": "删除失败",
|
||||
"mcpRegistered": "已注册服务 ({{count}})",
|
||||
"mcpAddBtn": "添加"
|
||||
},
|
||||
"topbar": {
|
||||
"switchToEn": "Switch to English",
|
||||
|
||||
@@ -97,8 +97,8 @@ class GlobalStateMachine:
|
||||
{
|
||||
"toolset_id": "system_workflow",
|
||||
"name": "系统工作流工具集",
|
||||
"description": "工作流场景专用工具(审批等)",
|
||||
"tools": ["approval"],
|
||||
"description": "工作流场景专用工具(审批、发送文件等)",
|
||||
"tools": ["approval", "send_file"],
|
||||
"is_system": True,
|
||||
"category": "system_workflow",
|
||||
},
|
||||
|
||||
@@ -35,20 +35,25 @@ class SendFileToolData(BaseToolData):
|
||||
category: str = "system"
|
||||
|
||||
|
||||
async def send_file(filename: str, content: str, trace_id: str) -> str:
|
||||
async def send_file(filename: str, content: str, trace_id: str = "") -> str:
|
||||
"""把 agent 生成的文件作为附件发送给当前对话窗口。
|
||||
|
||||
通过 global_workflow_manager 的 pending 队列推送一条带特殊前缀的 JSON 消息,
|
||||
前端识别后渲染为可下载的文件卡片。
|
||||
工作流场景下调用方会在 deps 中注入 trace_id,工具通过 global_workflow_manager
|
||||
的 pending 队列推送一条带特殊前缀的 JSON 消息,前端识别后渲染为可下载的卡片。
|
||||
|
||||
聊天场景下 trace_id 为空时退化为直接返回文件内容字符串,由模型把内容贴到回复里。
|
||||
|
||||
Args:
|
||||
filename: 文件名(含扩展名),如 "report.md" / "main.py"
|
||||
content: 文件内容(UTF-8 文本)
|
||||
trace_id: 当前会话/工作流的 trace_id
|
||||
trace_id: 当前会话/工作流的 trace_id;为空时退化为直接返回内容
|
||||
|
||||
Returns:
|
||||
发送结果说明
|
||||
发送结果说明或文件内容
|
||||
"""
|
||||
if not trace_id:
|
||||
return f"文件 {filename} 内容如下:\n\n```\n{content}\n```"
|
||||
|
||||
payload = json.dumps(
|
||||
{"type": "file", "filename": filename, "content": content},
|
||||
ensure_ascii=False,
|
||||
|
||||
Reference in New Issue
Block a user