mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-04-28 03:47:29 +08:00
feat: refine webui config workflows
This commit is contained in:
@@ -119,19 +119,47 @@ const ChannelSettings: React.FC = () => {
|
||||
(nextCfg as any).channels = {};
|
||||
}
|
||||
(nextCfg as any).channels[definition.id] = cloneJSON(draft);
|
||||
const res = await fetch(`/webui/api/config${q}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(nextCfg),
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(await res.text());
|
||||
const submit = async (confirmRisky: boolean) => {
|
||||
const body = confirmRisky ? { ...nextCfg, confirm_risky: true } : nextCfg;
|
||||
return ui.withLoading(async () => {
|
||||
const res = await fetch(`/webui/api/config${q}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const text = await res.text();
|
||||
let data: any = null;
|
||||
try {
|
||||
data = text ? JSON.parse(text) : null;
|
||||
} catch {
|
||||
data = null;
|
||||
}
|
||||
return { ok: res.ok, text, data };
|
||||
}, t('saving'));
|
||||
};
|
||||
|
||||
let result = await submit(false);
|
||||
if (!result.ok && result.data?.requires_confirm) {
|
||||
const changedFields = Array.isArray(result.data?.changed_fields) ? result.data.changed_fields.join(', ') : '';
|
||||
const ok = await ui.confirmDialog({
|
||||
title: t('configRiskyChangeConfirmTitle'),
|
||||
message: t('configRiskyChangeConfirmMessage', { fields: changedFields || '-' }),
|
||||
danger: true,
|
||||
confirmText: t('saveChanges'),
|
||||
});
|
||||
if (!ok) return;
|
||||
result = await submit(true);
|
||||
}
|
||||
|
||||
if (!result.ok) {
|
||||
throw new Error(result.data?.error || result.text || 'save failed');
|
||||
}
|
||||
|
||||
setCfg(nextCfg);
|
||||
await loadConfig(true);
|
||||
await ui.notify(t('configSaved'));
|
||||
await ui.notify({ title: t('saved'), message: t('configSaved') });
|
||||
} catch (err: any) {
|
||||
await ui.notify(String(err?.message || err || 'save failed'));
|
||||
await ui.notify({ title: t('requestFailed'), message: `${t('saveConfigFailed')}: ${String(err?.message || err || 'save failed')}` });
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
|
||||
@@ -226,6 +226,7 @@ const Providers: React.FC = () => {
|
||||
onStartOAuthLogin={() => startOAuthLogin(activeProviderEntry[0], activeProviderEntry[1])}
|
||||
onTriggerOAuthImport={() => triggerOAuthImport(activeProviderEntry[0], activeProviderEntry[1])}
|
||||
proxy={activeProviderEntry[1]}
|
||||
runtimeItem={providerRuntimeMap[activeProviderEntry[0]]}
|
||||
runtimeSummary={providerRuntimeMap[activeProviderEntry[0]] ? (
|
||||
<ProviderRuntimeSummary
|
||||
item={providerRuntimeMap[activeProviderEntry[0]]}
|
||||
|
||||
@@ -214,12 +214,24 @@ const Skills: React.FC = () => {
|
||||
}
|
||||
/>
|
||||
|
||||
<ToolbarRow className="w-full">
|
||||
<TextField disabled={installingSkill} value={installName} onChange={(e) => setInstallName(e.target.value)} placeholder={t('skillsNamePlaceholder')} className="w-full sm:w-72 disabled:opacity-60" />
|
||||
<FixedButton disabled={installingSkill} onClick={installSkill} variant="success" label={installingSkill ? t('loading') : t('install')}>
|
||||
<Zap className="w-4 h-4" />
|
||||
</FixedButton>
|
||||
<label className="flex items-center gap-2 text-xs text-zinc-400">
|
||||
<ToolbarRow className="w-full flex-nowrap items-center">
|
||||
<TextField
|
||||
disabled={installingSkill}
|
||||
value={installName}
|
||||
onChange={(e) => setInstallName(e.target.value)}
|
||||
placeholder={t('skillsNamePlaceholder')}
|
||||
className="min-w-0 flex-1 disabled:opacity-60"
|
||||
/>
|
||||
<Button
|
||||
disabled={installingSkill}
|
||||
onClick={installSkill}
|
||||
variant="success"
|
||||
size="md"
|
||||
noShrink
|
||||
>
|
||||
{installingSkill ? t('loading') : t('install')}
|
||||
</Button>
|
||||
<label className="flex shrink-0 items-center gap-2 whitespace-nowrap text-xs text-zinc-400">
|
||||
<CheckboxField
|
||||
checked={ignoreSuspicious}
|
||||
disabled={installingSkill}
|
||||
|
||||
@@ -9,6 +9,26 @@ import ProfileEditorPanel from '../components/subagentProfiles/ProfileEditorPane
|
||||
import ProfileListPanel from '../components/subagentProfiles/ProfileListPanel';
|
||||
import { emptyDraft, toProfileDraft, type SubagentProfile, type ToolAllowlistGroup } from '../components/subagentProfiles/profileDraft';
|
||||
|
||||
function validatePromptFilePath(pathValue: string): string | null {
|
||||
const path = String(pathValue || '').trim();
|
||||
if (!path) return null;
|
||||
const normalized = path.replace(/\\/g, '/');
|
||||
if (/^[a-zA-Z]:\//.test(normalized) || normalized.startsWith('/')) {
|
||||
return 'promptFileRelativePathOnly';
|
||||
}
|
||||
const segments = normalized.split('/').filter(Boolean);
|
||||
if (segments.some((segment) => segment === '..')) {
|
||||
return 'promptFileWorkspaceOnly';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const promptPathMessages: Record<string, string> = {
|
||||
promptFileRelativePathHint: 'Use a workspace-relative path such as agents/coder/AGENT.md.',
|
||||
promptFileRelativePathOnly: 'system_prompt_file must be a workspace-relative path, not an absolute path.',
|
||||
promptFileWorkspaceOnly: 'system_prompt_file must stay within the workspace.',
|
||||
};
|
||||
|
||||
const SubagentProfiles: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { q } = useAppContext();
|
||||
@@ -21,6 +41,11 @@ const SubagentProfiles: React.FC = () => {
|
||||
const [groups, setGroups] = useState<ToolAllowlistGroup[]>([]);
|
||||
const [promptFileContent, setPromptFileContent] = useState('');
|
||||
const [promptFileFound, setPromptFileFound] = useState(false);
|
||||
const promptPathErrorKey = validatePromptFilePath(String(draft.system_prompt_file || ''));
|
||||
const promptPathHint = t('promptFileRelativePathHint', { defaultValue: promptPathMessages.promptFileRelativePathHint });
|
||||
const promptPathError = promptPathErrorKey
|
||||
? t(promptPathErrorKey, { defaultValue: promptPathMessages[promptPathErrorKey] || promptPathMessages.promptFileWorkspaceOnly })
|
||||
: '';
|
||||
|
||||
const selected = useMemo(
|
||||
() => items.find((p) => p.agent_id === selectedId) || null,
|
||||
@@ -66,6 +91,11 @@ const SubagentProfiles: React.FC = () => {
|
||||
setPromptFileFound(false);
|
||||
return;
|
||||
}
|
||||
if (promptPathErrorKey) {
|
||||
setPromptFileFound(false);
|
||||
setPromptFileContent('');
|
||||
return;
|
||||
}
|
||||
fetch(`/webui/api/subagents_runtime${q}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -84,7 +114,7 @@ const SubagentProfiles: React.FC = () => {
|
||||
setPromptFileFound(false);
|
||||
setPromptFileContent('');
|
||||
});
|
||||
}, [draft.system_prompt_file, q]);
|
||||
}, [draft.system_prompt_file, promptPathErrorKey, q]);
|
||||
|
||||
const onSelect = (p: SubagentProfile) => {
|
||||
setSelectedId(p.agent_id || '');
|
||||
@@ -110,6 +140,10 @@ const SubagentProfiles: React.FC = () => {
|
||||
await ui.notify({ title: t('requestFailed'), message: 'agent_id is required' });
|
||||
return;
|
||||
}
|
||||
if (promptPathErrorKey) {
|
||||
await ui.notify({ title: t('requestFailed'), message: promptPathError });
|
||||
return;
|
||||
}
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
@@ -144,22 +178,6 @@ const SubagentProfiles: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const setStatus = async (status: 'active' | 'disabled') => {
|
||||
const agentId = String(draft.agent_id || '').trim();
|
||||
if (!agentId) return;
|
||||
const action = status === 'active' ? 'enable' : 'disable';
|
||||
const r = await fetch(`/webui/api/subagent_profiles${q}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action, agent_id: agentId }),
|
||||
});
|
||||
if (!r.ok) {
|
||||
await ui.notify({ title: t('requestFailed'), message: await r.text() });
|
||||
return;
|
||||
}
|
||||
await load();
|
||||
};
|
||||
|
||||
const remove = async () => {
|
||||
const agentId = String(draft.agent_id || '').trim();
|
||||
if (!agentId) return;
|
||||
@@ -185,6 +203,10 @@ const SubagentProfiles: React.FC = () => {
|
||||
await ui.notify({ title: t('requestFailed'), message: 'system_prompt_file is required' });
|
||||
return;
|
||||
}
|
||||
if (promptPathErrorKey) {
|
||||
await ui.notify({ title: t('requestFailed'), message: promptPathError });
|
||||
return;
|
||||
}
|
||||
const r = await fetch(`/webui/api/subagents_runtime${q}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -233,14 +255,14 @@ const SubagentProfiles: React.FC = () => {
|
||||
onAddAllowlistToken={addAllowlistToken}
|
||||
onChange={setDraft}
|
||||
onDelete={remove}
|
||||
onDisable={() => setStatus('disabled')}
|
||||
onEnable={() => setStatus('active')}
|
||||
onPromptContentChange={setPromptFileContent}
|
||||
onSave={save}
|
||||
onSavePromptFile={savePromptFile}
|
||||
promptContent={promptFileContent}
|
||||
promptMeta={promptFileFound ? t('promptFileReady') : t('promptFileMissing')}
|
||||
promptMeta={promptPathErrorKey ? promptPathError : (promptFileFound ? t('promptFileReady') : t('promptFileMissing'))}
|
||||
promptPlaceholder={t('agentPromptContentPlaceholder')}
|
||||
promptPathHint={promptPathHint}
|
||||
promptPathInvalid={!!promptPathErrorKey}
|
||||
roleLabel="Role"
|
||||
saving={saving}
|
||||
statusLabel={t('status')}
|
||||
|
||||
@@ -111,26 +111,26 @@ const TaskAudit: React.FC = () => {
|
||||
title={t('taskAudit')}
|
||||
titleClassName="text-xl md:text-2xl font-semibold"
|
||||
actions={(
|
||||
<ToolbarRow>
|
||||
<SelectField dense value={sourceFilter} onChange={(e)=>setSourceFilter(e.target.value)}>
|
||||
<option value="all">{t('allSources')}</option>
|
||||
<option value="direct">{t('sourceDirect')}</option>
|
||||
<option value="memory_todo">{t('sourceMemoryTodo')}</option>
|
||||
<option value="task_watchdog">task_watchdog</option>
|
||||
<option value="-">-</option>
|
||||
</SelectField>
|
||||
<SelectField dense value={statusFilter} onChange={(e)=>setStatusFilter(e.target.value)}>
|
||||
<option value="all">{t('allStatus')}</option>
|
||||
<option value="running">{t('statusRunning')}</option>
|
||||
<option value="waiting">{t('statusWaiting')}</option>
|
||||
<option value="blocked">{t('statusBlocked')}</option>
|
||||
<option value="success">{t('statusSuccess')}</option>
|
||||
<option value="error">{t('statusError')}</option>
|
||||
<option value="suppressed">{t('statusSuppressed')}</option>
|
||||
</SelectField>
|
||||
<FixedButton onClick={fetchData} variant="primary" label={loading ? t('loading') : t('refresh')}>
|
||||
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
|
||||
</FixedButton>
|
||||
<ToolbarRow className="flex-nowrap">
|
||||
<SelectField dense className="w-[152px] shrink-0" value={sourceFilter} onChange={(e) => setSourceFilter(e.target.value)}>
|
||||
<option value="all">{t('allSources')}</option>
|
||||
<option value="direct">{t('sourceDirect')}</option>
|
||||
<option value="memory_todo">{t('sourceMemoryTodo')}</option>
|
||||
<option value="task_watchdog">task_watchdog</option>
|
||||
<option value="-">-</option>
|
||||
</SelectField>
|
||||
<SelectField dense className="w-[152px] shrink-0" value={statusFilter} onChange={(e) => setStatusFilter(e.target.value)}>
|
||||
<option value="all">{t('allStatus')}</option>
|
||||
<option value="running">{t('statusRunning')}</option>
|
||||
<option value="waiting">{t('statusWaiting')}</option>
|
||||
<option value="blocked">{t('statusBlocked')}</option>
|
||||
<option value="success">{t('statusSuccess')}</option>
|
||||
<option value="error">{t('statusError')}</option>
|
||||
<option value="suppressed">{t('statusSuppressed')}</option>
|
||||
</SelectField>
|
||||
<FixedButton onClick={fetchData} variant="primary" label={loading ? t('loading') : t('refresh')}>
|
||||
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
|
||||
</FixedButton>
|
||||
</ToolbarRow>
|
||||
)}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user