feat(phase2): complete P1 tasks - Channels, Triggers, Skills CRUD and UI enhancements

Phase 2 P1 Tasks Completed:

API Layer (gateway-client.ts, gatewayStore.ts):
- Add Channels CRUD: getChannel, createChannel, updateChannel, deleteChannel
- Add Triggers CRUD: getTrigger, createTrigger, updateTrigger, deleteTrigger
- Add Skills CRUD: getSkill, createSkill, updateSkill, deleteSkill
- Add Scheduled Tasks API: createScheduledTask, deleteScheduledTask, toggleScheduledTask
- Add loadModels action for dynamic model list

UI Components:
- ModelsAPI.tsx: Dynamic model loading from API with loading/error states
- SchedulerPanel.tsx: Full CreateJobModal with cron/interval/once scheduling
- SecurityStatus.tsx: Loading states, error handling, retry functionality
- WorkflowEditor.tsx: New workflow creation/editing modal (new file)
- WorkflowHistory.tsx: Workflow execution history viewer (new file)
- WorkflowList.tsx: Integrated editor and history access

Configuration:
- Add 4 Hands TOML configs: clip, collector, predictor, twitter

Documentation (SYSTEM_ANALYSIS.md):
- Update API coverage: 65% → 89% (53/62 endpoints)
- Update UI completion: 85% → 92%
- Mark Phase 2 P1 tasks as completed
- Update technical debt cleanup status

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
iven
2026-03-15 01:38:34 +08:00
parent 1f9b6553fc
commit 5599c1a4db
15 changed files with 3216 additions and 296 deletions

View File

@@ -1,23 +1,22 @@
import { useEffect, useState } from 'react';
import { useEffect, useState } from 'react';
import { getStoredGatewayToken, getStoredGatewayUrl } from '../../lib/gateway-client';
import { useGatewayStore } from '../../store/gatewayStore';
import { useChatStore } from '../../store/chatStore';
interface ModelEntry {
id: string;
name: string;
provider: string;
// Helper function to format context window size
function formatContextWindow(tokens?: number): string {
if (!tokens) return '';
if (tokens >= 1000000) {
return `${(tokens / 1000000).toFixed(1)}M`;
}
if (tokens >= 1000) {
return `${(tokens / 1000).toFixed(0)}K`;
}
return `${tokens}`;
}
const AVAILABLE_MODELS: ModelEntry[] = [
{ id: 'glm-5', name: 'glm-5', provider: '智谱 AI' },
{ id: 'qwen3.5-plus', name: 'qwen3.5-plus', provider: '通义千问' },
{ id: 'kimi-k2.5', name: 'kimi-k2.5', provider: '月之暗面' },
{ id: 'minimax-m2.5', name: 'MiniMax-M2.5', provider: 'MiniMax' },
];
export function ModelsAPI() {
const { connectionState, connect, disconnect, quickConfig, saveQuickConfig } = useGatewayStore();
const { connectionState, connect, disconnect, quickConfig, saveQuickConfig, models, modelsLoading, modelsError, loadModels } = useGatewayStore();
const { currentModel, setCurrentModel } = useChatStore();
const [gatewayUrl, setGatewayUrl] = useState(getStoredGatewayUrl());
const [gatewayToken, setGatewayToken] = useState(quickConfig.gatewayToken || getStoredGatewayToken());
@@ -25,6 +24,13 @@ export function ModelsAPI() {
const connected = connectionState === 'connected';
const connecting = connectionState === 'connecting' || connectionState === 'reconnecting';
// Load models when connected
useEffect(() => {
if (connected && models.length === 0 && !modelsLoading) {
loadModels();
}
}, [connected, models.length, modelsLoading, loadModels]);
useEffect(() => {
setGatewayUrl(quickConfig.gatewayUrl || getStoredGatewayUrl());
setGatewayToken(quickConfig.gatewayToken || getStoredGatewayToken());
@@ -45,6 +51,10 @@ export function ModelsAPI() {
}).catch(() => {});
};
const handleRefreshModels = () => {
loadModels();
};
return (
<div className="max-w-3xl">
<div className="flex justify-between items-center mb-6">
@@ -77,29 +87,115 @@ export function ModelsAPI() {
<div className="mb-6">
<div className="flex justify-between items-center mb-3">
<h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wider"></h3>
<span className="text-xs text-gray-400"></span>
<div className="flex items-center gap-2">
<span className="text-xs text-gray-400"></span>
{connected && (
<button
onClick={handleRefreshModels}
disabled={modelsLoading}
className="text-xs text-orange-600 hover:text-orange-700 disabled:opacity-50"
>
{modelsLoading ? '加载中...' : '刷新'}
</button>
)}
</div>
</div>
<div className="bg-white rounded-xl border border-gray-200 divide-y divide-gray-100 shadow-sm">
{AVAILABLE_MODELS.map((model) => {
const isActive = model.id === currentModel;
return (
<div key={model.id} className={`flex justify-between items-center p-4 ${isActive ? 'bg-orange-50/50' : ''}`}>
<div>
<div className="text-sm text-gray-900">{model.name}</div>
<div className="text-xs text-gray-400 mt-1">{model.provider}</div>
</div>
<div className="flex gap-2 text-xs items-center">
{isActive ? (
<span className="px-2 py-1 bg-green-100 text-green-700 rounded text-xs"></span>
) : (
<button onClick={() => setCurrentModel(model.id)} className="text-orange-600 hover:underline"></button>
)}
</div>
{/* Loading state */}
{modelsLoading && (
<div className="bg-white rounded-xl border border-gray-200 p-8 shadow-sm">
<div className="flex items-center justify-center">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-orange-500"></div>
<span className="ml-3 text-sm text-gray-500">...</span>
</div>
</div>
)}
{/* Error state */}
{modelsError && !modelsLoading && (
<div className="bg-white rounded-xl border border-red-200 p-4 shadow-sm">
<div className="flex items-start gap-3">
<svg className="w-5 h-5 text-red-500 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div>
<p className="text-sm font-medium text-red-800"></p>
<p className="text-xs text-red-600 mt-1">{modelsError}</p>
<button
onClick={handleRefreshModels}
className="mt-2 text-xs text-red-600 hover:text-red-700 underline"
>
</button>
</div>
);
})}
</div>
</div>
</div>
)}
{/* Not connected state */}
{!connected && !modelsLoading && !modelsError && (
<div className="bg-white rounded-xl border border-gray-200 p-6 shadow-sm">
<div className="text-center">
<svg className="w-8 h-8 text-gray-300 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
<p className="text-sm text-gray-500"> Gateway </p>
</div>
</div>
)}
{/* Model list */}
{connected && !modelsLoading && !modelsError && models.length > 0 && (
<div className="bg-white rounded-xl border border-gray-200 divide-y divide-gray-100 shadow-sm">
{models.map((model) => {
const isActive = model.id === currentModel;
return (
<div key={model.id} className={`flex justify-between items-center p-4 ${isActive ? 'bg-orange-50/50' : ''}`}>
<div>
<div className="text-sm text-gray-900">{model.name}</div>
<div className="flex items-center gap-2 mt-1">
{model.provider && (
<span className="text-xs text-gray-400">{model.provider}</span>
)}
{model.contextWindow && (
<span className="text-xs text-gray-400">
{model.provider && '|'}
{formatContextWindow(model.contextWindow)}
</span>
)}
{model.maxOutput && (
<span className="text-xs text-gray-400">
{formatContextWindow(model.maxOutput)}
</span>
)}
</div>
</div>
<div className="flex gap-2 text-xs items-center">
{isActive ? (
<span className="px-2 py-1 bg-green-100 text-green-700 rounded text-xs"></span>
) : (
<button onClick={() => setCurrentModel(model.id)} className="text-orange-600 hover:underline"></button>
)}
</div>
</div>
);
})}
</div>
)}
{/* Empty state */}
{connected && !modelsLoading && !modelsError && models.length === 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-6 shadow-sm">
<div className="text-center">
<svg className="w-8 h-8 text-gray-300 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M12 20a8 8 0 100-16 8 8 0 000 16z" />
</svg>
<p className="text-sm text-gray-500"></p>
<p className="text-xs text-gray-400 mt-1"> Gateway Provider </p>
</div>
</div>
)}
<div className="mt-3 text-xs text-amber-700 bg-amber-50 rounded-lg p-3">
Gateway Provider Key
</div>
@@ -141,4 +237,3 @@ export function ModelsAPI() {
</div>
);
}