fix: enforce subagent prompt files and refine webui

This commit is contained in:
lpf
2026-03-09 11:24:31 +08:00
parent ba3be33c91
commit acf8a22c0a
25 changed files with 257 additions and 211 deletions

View File

@@ -147,15 +147,11 @@ const Dashboard: React.FC = () => {
</div>
<div className="brand-card rounded-[28px] border border-zinc-800 p-5 min-h-[148px]">
<div className="flex items-center gap-2 text-zinc-200 mb-2">
<Workflow className="w-4 h-4 text-sky-400" />
<div className="text-sm font-medium">{t('nodeP2P')}</div>
</div>
<div className="text-2xl font-semibold text-zinc-100 truncate">
{p2pEnabled ? `${p2pConfiguredIce} ICE · ${p2pConfiguredStun} STUN` : t('disabled')}
</div>
<div className="mt-2 text-xs text-zinc-500">
{t('dashboardNodeP2PDetail', { transport: p2pTransport, sessions: p2pSessions, retries: p2pRetryCount })}
<Sparkles className="w-4 h-4 text-sky-400" />
<div className="text-sm font-medium">{t('ekgTopProvidersWorkload')}</div>
</div>
<div className="text-2xl font-semibold text-zinc-100 truncate">{ekgTopProvider}</div>
<div className="mt-2 text-xs text-zinc-500">{t('dashboardWorkloadSnapshot')}</div>
</div>
<div className="brand-card rounded-[28px] border border-zinc-800 p-5 min-h-[148px]">
<div className="flex items-center gap-2 text-zinc-200 mb-2">

View File

@@ -1,6 +1,7 @@
import React, { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { Check } from 'lucide-react';
import { useAppContext } from '../context/AppContext';
import { formatLocalDateTime } from '../utils/time';
@@ -247,20 +248,29 @@ const Nodes: React.FC = () => {
<button
key={nodeID}
onClick={() => setSelectedNodeID(nodeID)}
className={`w-full text-left px-3 py-3 border-b border-zinc-800/60 hover:bg-zinc-800/20 ${active ? 'bg-indigo-500/15' : ''}`}
className={`w-full text-left px-3 py-3 border-b border-zinc-800/60 transition-colors ${active ? 'bg-indigo-500/15' : ''}`}
>
<div className="text-sm font-medium text-zinc-100 truncate">{String(node?.name || nodeID)}</div>
<div className="text-xs text-zinc-400 truncate">{nodeID} · {String(node?.os || '-')} / {String(node?.arch || '-')}</div>
<div className="text-[11px] text-zinc-500 truncate">{String(node?.online ? t('online') : t('offline'))} · {String(node?.version || '-')}</div>
{tags.length > 0 && (
<div className="flex flex-wrap gap-1 mt-2">
{tags.slice(0, 4).map((tag: string) => (
<span key={`${nodeID}-${tag}`} className="rounded-full border border-zinc-700 bg-zinc-900/60 px-2 py-0.5 text-[10px] text-zinc-300">
{tag}
</span>
))}
<div className="flex items-start gap-3">
<div className="min-w-0 flex-1">
<div className="text-sm font-medium text-zinc-100 truncate">{String(node?.name || nodeID)}</div>
<div className="text-xs text-zinc-400 truncate">{nodeID} · {String(node?.os || '-')} / {String(node?.arch || '-')}</div>
<div className="text-[11px] text-zinc-500 truncate">{String(node?.online ? t('online') : t('offline'))} · {String(node?.version || '-')}</div>
{tags.length > 0 && (
<div className="flex flex-wrap gap-1 mt-2">
{tags.slice(0, 4).map((tag: string) => (
<span key={`${nodeID}-${tag}`} className="rounded-full border border-zinc-700 bg-zinc-900/60 px-2 py-0.5 text-[10px] text-zinc-300">
{tag}
</span>
))}
</div>
)}
</div>
)}
{active && (
<span className="mt-0.5 inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-indigo-500/15 text-indigo-300">
<Check className="w-3.5 h-3.5" />
</span>
)}
</div>
</button>
);
})}
@@ -416,10 +426,19 @@ const Nodes: React.FC = () => {
<button
key={key || `dispatch-${index}`}
onClick={() => setSelectedDispatchKey(key)}
className={`w-full text-left px-3 py-2 border-b border-zinc-800/60 hover:bg-zinc-800/20 ${active ? 'bg-indigo-500/15' : ''}`}
className={`w-full text-left px-3 py-2 border-b border-zinc-800/60 transition-colors ${active ? 'bg-indigo-500/15' : ''}`}
>
<div className="text-sm font-medium text-zinc-100 truncate">{`${item?.action || '-'} · ${item?.used_transport || '-'}`}</div>
<div className="text-xs text-zinc-400 truncate">{formatLocalDateTime(item?.time)} · {Number(item?.duration_ms || 0)}ms · {Number(item?.artifact_count || 0)} {t('dashboardNodeDispatchArtifacts')}</div>
<div className="flex items-start gap-3">
<div className="min-w-0 flex-1">
<div className="text-sm font-medium text-zinc-100 truncate">{`${item?.action || '-'} · ${item?.used_transport || '-'}`}</div>
<div className="text-xs text-zinc-400 truncate">{formatLocalDateTime(item?.time)} · {Number(item?.duration_ms || 0)}ms · {Number(item?.artifact_count || 0)} {t('dashboardNodeDispatchArtifacts')}</div>
</div>
{active && (
<span className="mt-0.5 inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-indigo-500/15 text-indigo-300">
<Check className="w-3.5 h-3.5" />
</span>
)}
</div>
</button>
);
})}

View File

@@ -202,13 +202,13 @@ const Skills: React.FC = () => {
</label>
</div>
<div className="flex items-center gap-3 flex-wrap">
<div className={`text-xs px-2 py-1 rounded-md border ${clawhubInstalled ? 'text-emerald-300 border-emerald-700/50 bg-emerald-900/20' : 'text-amber-300 border-amber-700/50 bg-amber-900/20'}`} title={clawhubPath || t('skillsClawhubNotFound')}>
<div className={`text-xs px-2 py-1 rounded-md border font-medium ${clawhubInstalled ? 'text-emerald-200 border-emerald-500/35 bg-emerald-500/12' : 'text-amber-100 border-amber-500/45 bg-amber-500/14 shadow-[inset_0_1px_0_rgba(255,255,255,0.04)]'}`} title={clawhubPath || t('skillsClawhubNotFound')}>
{t('skillsClawhubStatus')}: {clawhubInstalled ? t('installed') : t('notInstalled')}
</div>
{!clawhubInstalled && (
<button
onClick={installClawHubIfNeeded}
className="flex items-center gap-2 px-4 py-2 bg-amber-600 hover:bg-amber-500 text-white rounded-xl text-sm font-medium transition-colors shadow-sm"
className="flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-medium transition-colors shadow-sm bg-cyan-500/15 text-cyan-200 border border-cyan-400/25 hover:bg-cyan-500/25 hover:border-cyan-300/35"
>
<Zap className="w-4 h-4" /> {t('skillsInstallNow')}
</button>
@@ -223,9 +223,16 @@ const Skills: React.FC = () => {
</div>
{!clawhubInstalled && (
<div className="rounded-2xl border border-amber-700/40 bg-amber-950/20 p-4 text-sm text-amber-100">
<div className="font-medium mb-1">{t('skillsClawhubMissingTitle')}</div>
<div className="text-amber-200/90">{t('skillsInstallPanelHint')}</div>
<div className="rounded-2xl border border-zinc-800/80 bg-zinc-950/45 p-4 text-sm shadow-sm">
<div className="flex items-start gap-3">
<div className="mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-xl border border-amber-400/20 bg-amber-500/10 text-amber-300">
<Zap className="w-4 h-4" />
</div>
<div className="min-w-0">
<div className="font-medium text-zinc-100 mb-1">{t('skillsClawhubMissingTitle')}</div>
<div className="text-zinc-400 leading-6">{t('skillsInstallPanelHint')}</div>
</div>
</div>
</div>
)}

View File

@@ -1,5 +1,6 @@
import React, { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Check } from 'lucide-react';
import { useAppContext } from '../context/AppContext';
import { useUI } from '../context/UIContext';
@@ -8,7 +9,6 @@ type SubagentProfile = {
name?: string;
notify_main_policy?: string;
role?: string;
system_prompt?: string;
system_prompt_file?: string;
tool_allowlist?: string[];
memory_namespace?: string;
@@ -33,7 +33,6 @@ const emptyDraft: SubagentProfile = {
name: '',
notify_main_policy: 'final_only',
role: '',
system_prompt: '',
system_prompt_file: '',
memory_namespace: '',
status: 'active',
@@ -81,7 +80,6 @@ const SubagentProfiles: React.FC = () => {
name: next.name || '',
notify_main_policy: next.notify_main_policy || 'final_only',
role: next.role || '',
system_prompt: next.system_prompt || '',
system_prompt_file: next.system_prompt_file || '',
memory_namespace: next.memory_namespace || '',
status: (next.status as string) || 'active',
@@ -142,7 +140,6 @@ const SubagentProfiles: React.FC = () => {
name: p.name || '',
notify_main_policy: p.notify_main_policy || 'final_only',
role: p.role || '',
system_prompt: p.system_prompt || '',
system_prompt_file: p.system_prompt_file || '',
memory_namespace: p.memory_namespace || '',
status: (p.status as string) || 'active',
@@ -195,7 +192,6 @@ const SubagentProfiles: React.FC = () => {
name: draft.name || '',
notify_main_policy: draft.notify_main_policy || 'final_only',
role: draft.role || '',
system_prompt: draft.system_prompt || '',
system_prompt_file: draft.system_prompt_file || '',
memory_namespace: draft.memory_namespace || '',
status: draft.status || 'active',
@@ -295,11 +291,20 @@ const SubagentProfiles: React.FC = () => {
<button
key={it.agent_id}
onClick={() => onSelect(it)}
className={`w-full text-left px-3 py-2 border-b border-zinc-800/50 hover:bg-zinc-800/20 ${selectedId === it.agent_id ? 'bg-indigo-500/15' : ''}`}
className={`w-full text-left px-3 py-2 border-b border-zinc-800/50 transition-colors ${selectedId === it.agent_id ? 'bg-indigo-500/15' : ''}`}
>
<div className="text-sm text-zinc-100 truncate">{it.agent_id || '-'}</div>
<div className="text-xs text-zinc-400 truncate">
{(it.status || 'active')} · {it.role || '-'} · {(it.memory_namespace || '-')}
<div className="flex items-start gap-3">
<div className="min-w-0 flex-1">
<div className="text-sm text-zinc-100 truncate">{it.agent_id || '-'}</div>
<div className="text-xs text-zinc-400 truncate">
{(it.status || 'active')} · {it.role || '-'} · {(it.memory_namespace || '-')}
</div>
</div>
{selectedId === it.agent_id && (
<span className="mt-0.5 inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-indigo-500/15 text-indigo-300">
<Check className="w-3.5 h-3.5" />
</span>
)}
</div>
</button>
))}
@@ -409,15 +414,6 @@ const SubagentProfiles: React.FC = () => {
</div>
)}
</div>
<div className="md:col-span-2">
<div className="text-xs text-zinc-400 mb-1">System Prompt</div>
<textarea
value={draft.system_prompt || ''}
onChange={(e) => setDraft({ ...draft, system_prompt: e.target.value })}
className="w-full px-2 py-1 text-xs bg-zinc-900 border border-zinc-700 rounded min-h-[140px]"
placeholder="You are a coding specialist..."
/>
</div>
<div className="md:col-span-2">
<div className="flex items-center justify-between mb-1 gap-3">
<div className="text-xs text-zinc-400">system_prompt_file content</div>

View File

@@ -91,7 +91,6 @@ type RegistrySubagent = {
display_name?: string;
role?: string;
description?: string;
system_prompt?: string;
system_prompt_file?: string;
prompt_file_found?: boolean;
memory_namespace?: string;
@@ -392,7 +391,6 @@ const Subagents: React.FC = () => {
const [configAgentID, setConfigAgentID] = useState('');
const [configRole, setConfigRole] = useState('');
const [configDisplayName, setConfigDisplayName] = useState('');
const [configSystemPrompt, setConfigSystemPrompt] = useState('');
const [configSystemPromptFile, setConfigSystemPromptFile] = useState('');
const [configToolAllowlist, setConfigToolAllowlist] = useState('');
const [configRoutingKeywords, setConfigRoutingKeywords] = useState('');

View File

@@ -1,5 +1,6 @@
import React, { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Check } from 'lucide-react';
import { useAppContext } from '../context/AppContext';
import { formatLocalDateTime } from '../utils/time';
@@ -143,11 +144,20 @@ const TaskAudit: React.FC = () => {
<button
key={`${it.task_id || idx}-${it.time || idx}`}
onClick={() => setSelected(it)}
className={`w-full text-left px-3 py-2 border-b border-zinc-800/60 hover:bg-zinc-800/20 ${active ? 'bg-indigo-500/15' : ''}`}
className={`w-full text-left px-3 py-2 border-b border-zinc-800/60 transition-colors ${active ? 'bg-indigo-500/15' : ''}`}
>
<div className="text-sm font-medium text-zinc-100 truncate">{it.task_id || `task-${idx + 1}`}</div>
<div className="text-xs text-zinc-400 truncate">{it.channel || '-'} · {it.status} · attempts:{it.attempts || 1} · {it.duration_ms || 0}ms · retry:{it.retry_count || 0} · {it.source || '-'} · {it.provider || '-'} / {it.model || '-'}</div>
<div className="text-[11px] text-zinc-500 truncate">{formatLocalDateTime(it.time)}</div>
<div className="flex items-start gap-3">
<div className="min-w-0 flex-1">
<div className="text-sm font-medium text-zinc-100 truncate">{it.task_id || `task-${idx + 1}`}</div>
<div className="text-xs text-zinc-400 truncate">{it.channel || '-'} · {it.status} · attempts:{it.attempts || 1} · {it.duration_ms || 0}ms · retry:{it.retry_count || 0} · {it.source || '-'} · {it.provider || '-'} / {it.model || '-'}</div>
<div className="text-[11px] text-zinc-500 truncate">{formatLocalDateTime(it.time)}</div>
</div>
{active && (
<span className="mt-0.5 inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-indigo-500/15 text-indigo-300">
<Check className="w-3.5 h-3.5" />
</span>
)}
</div>
</button>
);
})}
@@ -240,11 +250,20 @@ const TaskAudit: React.FC = () => {
<button
key={`${it.time || idx}-${it.node || idx}-${it.action || idx}`}
onClick={() => setSelectedNode(it)}
className={`w-full text-left px-3 py-2 border-b border-zinc-800/60 hover:bg-zinc-800/20 ${active ? 'bg-indigo-500/15' : ''}`}
className={`w-full text-left px-3 py-2 border-b border-zinc-800/60 transition-colors ${active ? 'bg-indigo-500/15' : ''}`}
>
<div className="text-sm font-medium text-zinc-100 truncate">{`${it.node || '-'} · ${it.action || '-'}`}</div>
<div className="text-xs text-zinc-400 truncate">{it.used_transport || '-'} · {(it.duration_ms || 0)}ms · {(it.artifact_count || 0)} {t('dashboardNodeDispatchArtifacts')}</div>
<div className="text-[11px] text-zinc-500 truncate">{formatLocalDateTime(it.time)}</div>
<div className="flex items-start gap-3">
<div className="min-w-0 flex-1">
<div className="text-sm font-medium text-zinc-100 truncate">{`${it.node || '-'} · ${it.action || '-'}`}</div>
<div className="text-xs text-zinc-400 truncate">{it.used_transport || '-'} · {(it.duration_ms || 0)}ms · {(it.artifact_count || 0)} {t('dashboardNodeDispatchArtifacts')}</div>
<div className="text-[11px] text-zinc-500 truncate">{formatLocalDateTime(it.time)}</div>
</div>
{active && (
<span className="mt-0.5 inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-indigo-500/15 text-indigo-300">
<Check className="w-3.5 h-3.5" />
</span>
)}
</div>
</button>
);
})}