Initial commit of integrated agent system

This commit is contained in:
cillin
2026-03-30 17:46:44 +08:00
commit 0fa413380c
337 changed files with 75268 additions and 0 deletions

View File

@@ -0,0 +1,932 @@
import React, { useEffect, useMemo, useState } from 'react';
import { createPortal } from 'react-dom';
import JSZip from 'jszip';
import { getModelIcon, getShortModelName } from '../utils/modelIcons';
import LobeModelLogo from './LobeModelLogo.jsx';
export default function TraderView({
agents,
agentProfilesByAgent,
agentSkillsByAgent,
workspaceFilesByAgent,
selectedAgentId,
selectedAgentProfile,
selectedAgentSkills,
skillDetailsByName,
localSkillDraftsByKey,
skillDetailLoadingKey,
editableFiles,
selectedWorkspaceFile,
workspaceFileContent,
workspaceDraftContent,
isConnected,
isAgentSkillsLoading,
agentSkillsSavingKey,
agentSkillsFeedback,
isWorkspaceFileLoading,
workspaceFileSavingKey,
workspaceFileFeedback,
onAgentChange,
onCreateLocalSkill,
onSkillDetailRequest,
onLocalSkillDraftChange,
onLocalSkillDelete,
onLocalSkillSave,
onRemoveSharedSkill,
onSkillToggle,
onWorkspaceFileChange,
onWorkspaceDraftChange,
onWorkspaceFileSave,
onUploadExternalSkill
}) {
const srOnlyStyle = {
position: 'absolute',
width: 1,
height: 1,
padding: 0,
margin: -1,
overflow: 'hidden',
clip: 'rect(0, 0, 0, 0)',
whiteSpace: 'nowrap',
border: 0
};
const [expandedSkillKey, setExpandedSkillKey] = useState(null);
const [newLocalSkillName, setNewLocalSkillName] = useState('');
const [externalSkillFile, setExternalSkillFile] = useState(null);
const [isExternalSkillChecking, setIsExternalSkillChecking] = useState(false);
const [externalSkillCheck, setExternalSkillCheck] = useState({ type: null, text: '' });
const [isSkillPickerOpen, setIsSkillPickerOpen] = useState(false);
const selectedAgent = useMemo(
() => agents.find((agent) => agent.id === selectedAgentId) || agents[0] || null,
[agents, selectedAgentId]
);
useEffect(() => {
setExpandedSkillKey(null);
}, [selectedAgentId]);
if (!selectedAgent) {
return null;
}
const profile = selectedAgentProfile || {};
const modelInfo = getModelIcon(profile.model_name, profile.model_provider);
const activeSkills = selectedAgentSkills.filter((item) => item.status === 'enabled' || item.status === 'active');
const installedSkills = selectedAgentSkills.filter((item) => item.status !== 'available');
const availableSkills = selectedAgentSkills.filter((item) => item.status === 'available');
const validateExternalSkillZip = async (file) => {
if (!(file instanceof File)) {
setExternalSkillCheck({ type: 'error', text: '请选择 zip 文件' });
return false;
}
if (!file.name.toLowerCase().endsWith('.zip')) {
setExternalSkillCheck({ type: 'error', text: '仅支持 .zip 文件' });
return false;
}
setIsExternalSkillChecking(true);
setExternalSkillCheck({ type: null, text: '' });
try {
const zip = await JSZip.loadAsync(file);
const entries = Object.keys(zip.files);
const skillFilePath = entries.find((entry) => {
const item = zip.files[entry];
return !item.dir && /(^|\/)SKILL\.md$/i.test(entry);
});
if (!skillFilePath) {
setExternalSkillCheck({
type: 'error',
text: '压缩包中未检测到 SKILL.md请检查目录结构'
});
return false;
}
setExternalSkillCheck({
type: 'success',
text: `预检通过,检测到: ${skillFilePath}`
});
return true;
} catch (error) {
setExternalSkillCheck({
type: 'error',
text: `无法解析 zip: ${error?.message || '未知错误'}`
});
return false;
} finally {
setIsExternalSkillChecking(false);
}
};
return (
<div style={{
height: '100%',
overflow: 'hidden',
padding: '18px',
background: 'linear-gradient(180deg, #ffffff 0%, #f4f7fb 100%)',
display: 'grid',
gridTemplateRows: 'auto auto 1fr',
gap: 18
}}>
<div style={{ display: 'grid', gap: 4 }}>
<div style={{ fontSize: 12, fontWeight: 800, letterSpacing: '0.5px', color: '#111111' }}>
交易员档案
</div>
<div style={{ fontSize: 11, color: '#6B7280' }}>
聚焦查看每个 Agent 的模型工具组技能编排和工作区记忆不展示交易表现数据
</div>
</div>
<div style={{
display: 'grid',
gridTemplateColumns: '120px minmax(0, 1fr)',
gap: 16,
alignItems: 'stretch',
minHeight: 0,
overflow: 'hidden'
}}>
{/* Left: agent avatar list */}
<div style={{
border: '1px solid #D9E0E7',
borderRadius: 14,
background: '#FFFFFF',
boxShadow: '0 10px 24px rgba(15, 23, 42, 0.06)',
padding: 12,
display: 'grid',
gap: 10,
minHeight: 0,
overflowY: 'auto',
alignContent: 'start'
}}>
{agents.map((agent) => {
const isSelected = agent.id === selectedAgentId;
return (
<button
key={agent.id}
type="button"
onClick={() => onAgentChange(agent.id)}
title={agent.name}
style={{
border: isSelected ? `2px solid ${agent.colors.accent}` : '1px solid #D9E0E7',
borderRadius: 16,
background: isSelected ? `${agent.colors.accent}10` : '#FFFFFF',
boxShadow: isSelected ? `0 10px 20px ${agent.colors.accent}18` : 'none',
padding: 8,
display: 'grid',
gap: 6,
justifyItems: 'center',
cursor: 'pointer'
}}
>
<img
src={agent.avatar}
alt={agent.name}
style={{
width: 56,
height: 56,
borderRadius: 14,
objectFit: 'cover',
border: `1px solid ${agent.colors.accent}33`
}}
/>
<div style={{
fontSize: 10,
fontWeight: 800,
color: isSelected ? agent.colors.accent : '#374151',
textAlign: 'center',
lineHeight: 1.4
}}>
{agent.name}
</div>
</button>
);
})}
</div>
{/* Right: agent detail content */}
<div style={{
border: '1px solid #D9E0E7',
borderRadius: 14,
background: '#FFFFFF',
boxShadow: '0 10px 24px rgba(15, 23, 42, 0.06)',
padding: 18,
display: 'grid',
gap: 16,
minHeight: 0,
overflowY: 'auto',
alignContent: 'start'
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 16, flexWrap: 'wrap' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<img
src={selectedAgent.avatar}
alt={selectedAgent.name}
style={{
width: 58,
height: 58,
borderRadius: 12,
objectFit: 'cover',
border: `1px solid ${selectedAgent.colors.accent}33`
}}
/>
<div style={{ display: 'grid', gap: 4 }}>
<div style={{ fontSize: 15, fontWeight: 800, color: '#111111' }}>{selectedAgent.name}</div>
<div style={{ fontSize: 12, color: '#6B7280' }}>{selectedAgent.role}</div>
<div style={{ fontSize: 11, color: selectedAgent.colors.accent, fontWeight: 700 }}>
当前档案已展开
</div>
</div>
</div>
<div style={{
border: `1px solid ${modelInfo.color}2e`,
background: modelInfo.bgColor,
borderRadius: 12,
padding: '10px 12px',
display: 'flex',
alignItems: 'center',
gap: 10
}}>
<LobeModelLogo
model={profile.model_name}
provider={profile.model_provider}
fallbackSrc={modelInfo.logoPath}
alt={modelInfo.provider}
size={26}
shape="circle"
type="color"
style={{ borderRadius: 999 }}
/>
<div style={{ display: 'grid', gap: 2 }}>
<div style={{ fontSize: 11, color: '#4B5563', fontWeight: 700 }}>模型</div>
<div style={{ fontSize: 12, color: '#111111', fontWeight: 800 }}>
{getShortModelName(profile.model_name)}
</div>
</div>
</div>
</div>
<div style={{
display: 'grid',
gridTemplateColumns: 'minmax(300px, 420px) minmax(0, 1fr)',
gap: 16,
alignItems: 'start',
minHeight: 0
}}>
<div style={{ display: 'grid', gap: 10 }}>
<div style={{
border: '1px solid #E5EAF1',
borderRadius: 12,
background: '#FCFDFE',
padding: 14,
display: 'grid',
gap: 10
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 10, alignItems: 'center' }}>
<div style={{ display: 'grid', gap: 2 }}>
<div style={{ fontSize: 12, fontWeight: 800, color: '#111111' }}>技能</div>
<div style={{ fontSize: 10, color: '#6B7280', fontFamily: '"Courier New", monospace' }}>
已启用: {activeSkills.length} / 已安装: {installedSkills.length}
</div>
</div>
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
<button
type="button"
onClick={() => setIsSkillPickerOpen(true)}
style={{
padding: '7px 10px',
borderRadius: 6,
border: '1px solid #1565C0',
background: '#EFF6FF',
color: '#1565C0',
fontSize: 10,
fontWeight: 700,
cursor: 'pointer',
whiteSpace: 'nowrap'
}}
aria-label="管理技能"
>
技能管理
</button>
</div>
</div>
<div style={{
border: '1px solid #E5E7EB',
background: '#F8FAFC',
borderRadius: 8,
padding: '10px 12px',
display: 'grid',
gap: 10,
maxHeight: 520,
overflowY: 'auto'
}}>
{isAgentSkillsLoading ? (
<div style={{ fontSize: 11, color: '#666666', fontFamily: '"Courier New", monospace' }}>加载技能中...</div>
) : installedSkills.length === 0 ? (
<div style={{ fontSize: 11, color: '#666666', fontFamily: '"Courier New", monospace' }}>暂无技能</div>
) : installedSkills.map((skill) => {
const isEnabled = skill.status === 'enabled' || skill.status === 'active';
const saving = agentSkillsSavingKey === `${selectedAgentId}:${skill.skill_name}` || agentSkillsSavingKey === `${selectedAgentId}:${skill.skill_name}:content` || agentSkillsSavingKey === `${selectedAgentId}:${skill.skill_name}:delete` || agentSkillsSavingKey === `${selectedAgentId}:${skill.skill_name}:remove`;
const isExpanded = expandedSkillKey === skill.skill_name;
const detailKey = `${selectedAgentId}:${skill.skill_name}`;
const skillDetail = skillDetailsByName?.[detailKey] || null;
const skillDraft = localSkillDraftsByKey?.[detailKey] ?? '';
const isDetailLoading = skillDetailLoadingKey === detailKey;
const isLocalSkill = skill.source === 'local';
return (
<div
key={skill.skill_name}
style={{
display: 'grid',
gap: 7,
paddingBottom: 10,
borderBottom: '1px dashed #D7DEE7'
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 10, alignItems: 'flex-start' }}>
<button
type="button"
onClick={() => {
if (!isExpanded && !skillDetail && onSkillDetailRequest) {
onSkillDetailRequest(skill.skill_name);
}
setExpandedSkillKey((prev) => (prev === skill.skill_name ? null : skill.skill_name));
}}
style={{
flex: 1,
minWidth: 0,
border: 'none',
background: 'transparent',
padding: 0,
textAlign: 'left',
cursor: 'pointer',
display: 'grid',
gap: 4
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ fontSize: 12, color: '#6B7280', fontWeight: 700 }}>
{isExpanded ? '▾' : '▸'}
</span>
<span style={{ fontSize: 12, fontWeight: 700, color: '#111111' }}>
{skill.name || '未命名技能'}
</span>
<span style={{
padding: '2px 6px',
borderRadius: 999,
border: `1px solid ${isLocalSkill ? selectedAgent.colors.accent : '#D0D7DE'}`,
color: isLocalSkill ? selectedAgent.colors.accent : '#6B7280',
fontSize: 9,
fontWeight: 700
}}>
{isLocalSkill ? '本地' : '共享'}
</span>
</div>
<div style={{ fontSize: 11, color: '#4B5563', marginLeft: 20 }}>
{skill.description || '-'}
</div>
<div style={{ fontSize: 10, color: '#6B7280', marginLeft: 20 }}>
{isExpanded ? '点击收起详情' : '点击展开详情'}
</div>
</button>
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
<button
type="button"
onClick={() => onSkillToggle(skill.skill_name, !isEnabled)}
disabled={!isConnected || saving}
style={{
padding: '7px 10px',
borderRadius: 6,
border: `1px solid ${isEnabled ? '#C62828' : '#1565C0'}`,
background: isConnected && !saving ? (isEnabled ? '#FFF5F5' : '#EFF6FF') : '#E5E7EB',
color: isEnabled ? '#C62828' : '#1565C0',
fontSize: 10,
fontWeight: 700,
cursor: isConnected && !saving ? 'pointer' : 'not-allowed',
whiteSpace: 'nowrap'
}}
>
{saving ? '处理中' : isEnabled ? '禁用' : '启用'}
</button>
{isLocalSkill ? (
<button
type="button"
onClick={() => onLocalSkillDelete(skill.skill_name)}
disabled={!isConnected || saving}
style={{
padding: '7px 10px',
borderRadius: 6,
border: '1px solid #C62828',
background: isConnected && !saving ? '#FFF5F5' : '#E5E7EB',
color: '#C62828',
fontSize: 10,
fontWeight: 700,
cursor: isConnected && !saving ? 'pointer' : 'not-allowed',
whiteSpace: 'nowrap'
}}
>
{saving ? '处理中' : '删除'}
</button>
) : (
<button
type="button"
onClick={() => onRemoveSharedSkill(skill.skill_name)}
disabled={!isConnected || saving}
style={{
padding: '7px 10px',
borderRadius: 6,
border: '1px solid #C62828',
background: isConnected && !saving ? '#FFF5F5' : '#E5E7EB',
color: '#C62828',
fontSize: 10,
fontWeight: 700,
cursor: isConnected && !saving ? 'pointer' : 'not-allowed',
whiteSpace: 'nowrap'
}}
>
{saving ? '处理中' : '移除'}
</button>
)}
</div>
</div>
{isExpanded && (
<div style={{
marginLeft: 20,
borderRadius: 8,
border: '1px solid #E5E7EB',
background: '#FFFFFF',
padding: '10px 12px',
display: 'grid',
gap: 8
}}>
<div style={{
fontSize: 11,
color: '#1F2937',
lineHeight: 1.6,
whiteSpace: 'pre-wrap',
fontFamily: '"Courier New", monospace'
}}>
{isDetailLoading
? '加载技能说明中...'
: (skillDetail?.content || '暂无更详细的技能说明')}
</div>
{isLocalSkill && !isDetailLoading && (
<div style={{ display: 'grid', gap: 8 }}>
<div style={{ fontSize: 10, color: '#6B7280', fontWeight: 700 }}>
本地技能 SKILL.md
</div>
<textarea
id={`local-skill-${selectedAgentId}-${skill.skill_name}`}
name={`local_skill_${selectedAgentId}_${skill.skill_name}`}
aria-label={`${skill.skill_name} 本地技能内容`}
value={skillDraft}
onChange={(e) => onLocalSkillDraftChange(skill.skill_name, e.target.value)}
style={{
minHeight: 220,
resize: 'vertical',
borderRadius: 8,
border: '1px solid #D0D7DE',
background: '#FFFFFF',
color: '#111111',
padding: '10px 12px',
fontSize: 11,
lineHeight: 1.6,
fontFamily: '"Courier New", monospace'
}}
/>
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<button
type="button"
onClick={() => onLocalSkillSave(skill.skill_name)}
disabled={!isConnected || saving || skillDraft === (skillDetail?.content || '')}
style={{
padding: '8px 12px',
borderRadius: 6,
border: '1px solid #1565C0',
background: isConnected && !saving && skillDraft !== (skillDetail?.content || '') ? '#0D47A1' : '#94A3B8',
color: '#FFFFFF',
fontSize: 10,
fontWeight: 700,
cursor: isConnected && !saving && skillDraft !== (skillDetail?.content || '') ? 'pointer' : 'not-allowed'
}}
>
{saving ? '保存中' : '保存本地技能'}
</button>
</div>
</div>
)}
</div>
)}
</div>
);
})}
</div>
{agentSkillsFeedback && (
<span style={{
color: agentSkillsFeedback.type === 'success' ? '#00C853' : '#FF5252',
fontSize: 11,
fontFamily: '"Courier New", monospace'
}}>
{agentSkillsFeedback.text}
</span>
)}
</div>
</div>
<div style={{ display: 'grid', gap: 10 }}>
<div style={{
border: '1px solid #E5EAF1',
borderRadius: 12,
background: '#FCFDFE',
padding: 14,
display: 'grid',
gap: 10
}}>
<div style={{ display: 'grid', gap: 4 }}>
<div style={{ fontSize: 12, fontWeight: 800, color: '#111111' }}>工作区文件编辑</div>
<div style={{ fontSize: 11, color: '#6B7280' }}>
直接调整该交易员的人设协作方式和长期记忆文件
</div>
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
{editableFiles.map((filename) => {
const isActive = filename === selectedWorkspaceFile;
return (
<button
key={filename}
onClick={() => onWorkspaceFileChange(filename)}
style={{
padding: '7px 10px',
borderRadius: 999,
border: `1px solid ${isActive ? selectedAgent.colors.accent : '#D0D7DE'}`,
background: isActive ? `${selectedAgent.colors.accent}12` : '#FFFFFF',
color: isActive ? selectedAgent.colors.accent : '#4B5563',
fontSize: 10,
fontWeight: 700,
cursor: 'pointer',
fontFamily: '"Courier New", monospace'
}}
>
{filename}
</button>
);
})}
</div>
<textarea
id={`workspace-editor-${selectedAgentId}-${selectedWorkspaceFile || 'file'}`}
name={`workspace_editor_${selectedAgentId}_${selectedWorkspaceFile || 'file'}`}
aria-label={`编辑 ${selectedWorkspaceFile || '工作区文件'} 内容`}
value={workspaceDraftContent}
onChange={(e) => onWorkspaceDraftChange(e.target.value)}
placeholder={isWorkspaceFileLoading ? '加载中...' : '输入 markdown 内容'}
style={{
minHeight: 280,
resize: 'vertical',
borderRadius: 8,
border: '1px solid #D0D7DE',
background: '#FFFFFF',
color: '#111111',
padding: '12px 14px',
fontSize: 12,
lineHeight: 1.6,
fontFamily: '"Courier New", monospace'
}}
/>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
<span style={{ fontSize: 10, color: '#6B7280', fontFamily: '"Courier New", monospace' }}>
当前文件: {selectedWorkspaceFile}
</span>
<button
onClick={onWorkspaceFileSave}
disabled={!isConnected || isWorkspaceFileLoading || workspaceFileSavingKey !== null || workspaceDraftContent === workspaceFileContent}
style={{
padding: '9px 14px',
borderRadius: 6,
border: '1px solid #1565C0',
background: isConnected && !isWorkspaceFileLoading && workspaceFileSavingKey === null && workspaceDraftContent !== workspaceFileContent ? '#0D47A1' : '#94A3B8',
color: '#FFFFFF',
fontSize: 11,
fontWeight: 700,
cursor: isConnected && !isWorkspaceFileLoading && workspaceFileSavingKey === null && workspaceDraftContent !== workspaceFileContent ? 'pointer' : 'not-allowed'
}}
>
{workspaceFileSavingKey ? '保存中' : '保存文件'}
</button>
</div>
{workspaceFileFeedback && (
<span style={{
color: workspaceFileFeedback.type === 'success' ? '#00C853' : '#FF5252',
fontSize: 11,
fontFamily: '"Courier New", monospace'
}}>
{workspaceFileFeedback.text}
</span>
)}
</div>
</div>
</div>
</div>
</div>
{isSkillPickerOpen && createPortal((
<div
onClick={() => setIsSkillPickerOpen(false)}
style={{
position: 'fixed',
inset: 0,
background: 'rgba(15, 23, 42, 0.28)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: 24,
zIndex: 9998
}}
>
<div
onClick={(e) => e.stopPropagation()}
style={{
width: 'min(760px, 92vw)',
maxHeight: '80vh',
overflowY: 'auto',
borderRadius: 16,
border: '1px solid #D9E0E7',
background: '#FFFFFF',
boxShadow: '0 24px 60px rgba(15, 23, 42, 0.18)',
padding: 18,
paddingTop: 22,
display: 'grid',
gap: 16,
position: 'relative',
zIndex: 9999
}}
>
<button
type="button"
onClick={() => setIsSkillPickerOpen(false)}
style={{
position: 'absolute',
top: 16,
right: 16,
border: '1px solid #D0D7DE',
background: '#FFFFFF',
borderRadius: 999,
width: 40,
height: 40,
fontSize: 16,
lineHeight: 1,
color: '#111111',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
boxShadow: '0 4px 12px rgba(15, 23, 42, 0.08)'
}}
aria-label="关闭技能管理"
>
×
</button>
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 12, alignItems: 'center', paddingRight: 56 }}>
<div style={{ display: 'grid', gap: 4 }}>
<div style={{ fontSize: 14, fontWeight: 800, color: '#111111' }}>技能管理</div>
<div style={{ fontSize: 11, color: '#6B7280' }}>
{selectedAgent.name} 添加共享技能或创建本地技能
</div>
</div>
</div>
<div style={{
border: '1px solid #E5EAF1',
borderRadius: 12,
background: '#FCFDFE',
padding: 14,
display: 'grid',
gap: 10
}}>
<div style={{ fontSize: 12, fontWeight: 800, color: '#111111' }}>创建本地技能</div>
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<label htmlFor="new-local-skill-name" style={srOnlyStyle}>
输入本地技能名称
</label>
<input
id="new-local-skill-name"
name="new_local_skill_name"
aria-label="输入本地技能名称"
value={newLocalSkillName}
onChange={(e) => setNewLocalSkillName(e.target.value)}
placeholder="输入技能名,例如 event_playbook"
style={{
flex: 1,
padding: '8px 10px',
borderRadius: 8,
border: '1px solid #D0D7DE',
background: '#FFFFFF',
color: '#111111',
fontSize: 11,
fontFamily: '"Courier New", monospace'
}}
/>
<button
type="button"
onClick={() => {
if (onCreateLocalSkill) {
onCreateLocalSkill(newLocalSkillName);
setNewLocalSkillName('');
}
}}
disabled={!isConnected || !newLocalSkillName.trim()}
style={{
padding: '8px 12px',
borderRadius: 8,
border: '1px solid #1565C0',
background: isConnected && newLocalSkillName.trim() ? '#EFF6FF' : '#E5E7EB',
color: '#1565C0',
fontSize: 11,
fontWeight: 700,
cursor: isConnected && newLocalSkillName.trim() ? 'pointer' : 'not-allowed',
whiteSpace: 'nowrap'
}}
>
创建
</button>
</div>
</div>
<div style={{
border: '1px solid #E5EAF1',
borderRadius: 12,
background: '#FCFDFE',
padding: 14,
display: 'grid',
gap: 10
}}>
<div style={{ fontSize: 12, fontWeight: 800, color: '#111111' }}>上传外部技能包</div>
<div style={{ fontSize: 11, color: '#6B7280' }}>
支持上传 .zip包内需包含一个技能目录及 SKILL.md
</div>
<div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
<label htmlFor="external-skill-zip" style={srOnlyStyle}>
上传外部技能 zip
</label>
<input
id="external-skill-zip"
name="external_skill_zip"
aria-label="上传外部技能 zip 包"
type="file"
accept=".zip,application/zip"
onChange={async (e) => {
const file = e.target.files?.[0] || null;
setExternalSkillFile(file);
if (!file) {
setExternalSkillCheck({ type: null, text: '' });
return;
}
await validateExternalSkillZip(file);
}}
style={{
flex: 1,
minWidth: 220,
padding: '6px 8px',
borderRadius: 8,
border: '1px solid #D0D7DE',
background: '#FFFFFF',
color: '#111111',
fontSize: 11
}}
/>
<button
type="button"
onClick={async () => {
if (!onUploadExternalSkill || !externalSkillFile) {
return;
}
const valid = await validateExternalSkillZip(externalSkillFile);
if (!valid) {
return;
}
await onUploadExternalSkill(externalSkillFile);
setExternalSkillFile(null);
setExternalSkillCheck({ type: null, text: '' });
}}
disabled={!isConnected || !externalSkillFile || isExternalSkillChecking || externalSkillCheck.type === 'error'}
style={{
padding: '8px 12px',
borderRadius: 8,
border: '1px solid #1565C0',
background: isConnected && externalSkillFile && !isExternalSkillChecking && externalSkillCheck.type !== 'error' ? '#EFF6FF' : '#E5E7EB',
color: '#1565C0',
fontSize: 11,
fontWeight: 700,
cursor: isConnected && externalSkillFile && !isExternalSkillChecking && externalSkillCheck.type !== 'error' ? 'pointer' : 'not-allowed',
whiteSpace: 'nowrap'
}}
>
{isExternalSkillChecking ? '预检中...' : '上传并安装'}
</button>
</div>
{externalSkillCheck.text ? (
<div
style={{
fontSize: 11,
color: externalSkillCheck.type === 'success' ? '#00C853' : '#FF5252',
fontFamily: '"Courier New", monospace'
}}
>
{externalSkillCheck.text}
</div>
) : null}
</div>
<div style={{
border: '1px solid #E5EAF1',
borderRadius: 12,
background: '#FCFDFE',
padding: 14,
display: 'grid',
gap: 10
}}>
<div style={{ fontSize: 12, fontWeight: 800, color: '#111111' }}>添加共享技能</div>
<div style={{
border: '1px solid #E5E7EB',
background: '#FFFFFF',
borderRadius: 8,
padding: '10px 12px',
display: 'grid',
gap: 10,
maxHeight: 360,
overflowY: 'auto'
}}>
{availableSkills.length === 0 ? (
<div style={{ fontSize: 11, color: '#6B7280' }}>没有可添加的共享技能</div>
) : availableSkills.map((skill) => {
const saving = agentSkillsSavingKey === `${selectedAgentId}:${skill.skill_name}`;
return (
<div
key={skill.skill_name}
style={{
display: 'flex',
justifyContent: 'space-between',
gap: 12,
alignItems: 'flex-start',
paddingBottom: 10,
borderBottom: '1px dashed #D7DEE7'
}}
>
<div style={{ display: 'grid', gap: 4 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ fontSize: 12, fontWeight: 700, color: '#111111' }}>
{skill.name || skill.skill_name}
</span>
<span style={{
padding: '2px 6px',
borderRadius: 999,
border: '1px solid #D0D7DE',
color: '#6B7280',
fontSize: 9,
fontWeight: 700
}}>
共享
</span>
</div>
<div style={{ fontSize: 11, color: '#4B5563' }}>
{skill.description || '-'}
</div>
</div>
<button
type="button"
onClick={() => onSkillToggle(skill.skill_name, true)}
disabled={!isConnected || saving}
style={{
padding: '7px 10px',
borderRadius: 6,
border: '1px solid #1565C0',
background: isConnected && !saving ? '#EFF6FF' : '#E5E7EB',
color: '#1565C0',
fontSize: 10,
fontWeight: 700,
cursor: isConnected && !saving ? 'pointer' : 'not-allowed',
whiteSpace: 'nowrap'
}}
>
{saving ? '处理中' : '添加'}
</button>
</div>
);
})}
</div>
</div>
</div>
</div>
), document.body)}
</div>
);
}