Compare commits

...

4 Commits

Author SHA1 Message Date
朝夕 e6fcb08ce3 wip: 同步 2026-04-25 00:01:59 +08:00
朝夕 1df416ac4d Merge branch 'dev' of https://github.com/zhaoxi826/Pretor into dev
# Conflicts:
#	frontend/src/components/Agent/WorkerIndividualSettings.tsx
#	frontend/src/components/Resource/ResourceLayout.tsx
#	frontend/src/components/Resource/SkillSettings.tsx
2026-04-24 23:50:27 +08:00
朝夕 9ace4d4996
fix: resolve provider owner type, resource UI, and db ready check (#22)
*  [Feature] Add frontend authentication page and 401 error interceptor (#21)

* feat: add frontend authentication page and 401 interceptor

Adds a new AuthPage component for user login and registration, integrates it into App.tsx to protect routes, and sets up an Axios interceptor to handle 401 Unauthorized responses by clearing local storage and reloading. Also fixes a missing logger attribute in WorkflowEngine for backend tests.

Co-authored-by: zhaoxi826 <198742034+zhaoxi826@users.noreply.github.com>

* fix: gracefully handle closed websockets

Updates the websocket endpoints in `pretor/api/cluster.py` and `pretor/api/workflow.py` to catch `RuntimeError` alongside `WebSocketDisconnect`. This prevents the application from crashing and spamming error logs when the frontend client unexpectedly closes the connection and the underlying TCP transport is closed.

Co-authored-by: zhaoxi826 <198742034+zhaoxi826@users.noreply.github.com>

* feat: add worker form and update global settings

Adds a new form in the Worker Individual Settings page to create custom worker individuals via the `/api/v1/agent/worker` endpoint. Also updates the System Settings page to remove the obsolete "Max Concurrent Workflows" setting and makes the system language and theme toggles functional by persisting to local storage and updating the document root class.

Co-authored-by: zhaoxi826 <198742034+zhaoxi826@users.noreply.github.com>

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Co-authored-by: zhaoxi826 <198742034+zhaoxi826@users.noreply.github.com>

* fix: resolve provider owner type, resource UI, and db ready check

This commit fixes the following issues:
1. `provider_owner` type bug: Changed type from `int` to `str` in DB models and Pydantic schemas.
2. Frontend Provider Dropdown: `WorkerIndividualSettings.tsx` now uses a dropdown to select a created provider instead of a free-form input field.
3. Database Initialization Sync: Added an `asyncio.Event()` to `postgres.py` to prevent any DB actions from executing before `init_db()` is complete.
4. Resource Management UI: Added new pages `SkillSettings.tsx` and `WorkflowTemplateSettings.tsx` to handle frontend requests to manage skills and workflow templates.

Co-authored-by: zhaoxi826 <198742034+zhaoxi826@users.noreply.github.com>

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Co-authored-by: zhaoxi826 <198742034+zhaoxi826@users.noreply.github.com>
2026-04-24 23:34:49 +08:00
朝夕 3a96f287c7
[Feature] Add frontend authentication page and 401 error interceptor (#21)
* feat: add frontend authentication page and 401 interceptor

Adds a new AuthPage component for user login and registration, integrates it into App.tsx to protect routes, and sets up an Axios interceptor to handle 401 Unauthorized responses by clearing local storage and reloading. Also fixes a missing logger attribute in WorkflowEngine for backend tests.

Co-authored-by: zhaoxi826 <198742034+zhaoxi826@users.noreply.github.com>

* fix: gracefully handle closed websockets

Updates the websocket endpoints in `pretor/api/cluster.py` and `pretor/api/workflow.py` to catch `RuntimeError` alongside `WebSocketDisconnect`. This prevents the application from crashing and spamming error logs when the frontend client unexpectedly closes the connection and the underlying TCP transport is closed.

Co-authored-by: zhaoxi826 <198742034+zhaoxi826@users.noreply.github.com>

* feat: add worker form and update global settings

Adds a new form in the Worker Individual Settings page to create custom worker individuals via the `/api/v1/agent/worker` endpoint. Also updates the System Settings page to remove the obsolete "Max Concurrent Workflows" setting and makes the system language and theme toggles functional by persisting to local storage and updating the document root class.

Co-authored-by: zhaoxi826 <198742034+zhaoxi826@users.noreply.github.com>

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Co-authored-by: zhaoxi826 <198742034+zhaoxi826@users.noreply.github.com>
2026-04-24 18:22:39 +08:00
25 changed files with 965 additions and 76 deletions

2
.env
View File

@ -1,5 +1,5 @@
POSTGRES_USER=postgres POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres POSTGRES_PASSWORD=postgrespassword
POSTGRES_HOST=127.0.0.1 POSTGRES_HOST=127.0.0.1
POSTGRES_PORT=5432 POSTGRES_PORT=5432
POSTGRES_DB=pretor POSTGRES_DB=pretor

View File

@ -33,5 +33,3 @@ services:
- POSTGRES_DB=pretor - POSTGRES_DB=pretor
- SECRET_KEY=changethiskey12345 - SECRET_KEY=changethiskey12345
volumes:
postgres_data:

View File

@ -2,23 +2,12 @@
--- ---
## 问题栏 ## 问题栏
#### 🔴 核心缺陷与修复 (Bug Fixes & Stability) #### 🔴 核心缺陷与修复 (Bug Fixes & Stability)
- [x] /pretor/core/individual每个template进行优化
- [x] /pretor/worker_individual待完善复合子个体和基础子个体
#### 🛡️ 安全与合规 (Security & Auth) #### 🛡️ 安全与合规 (Security & Auth)
- [ ] 优化安全架构防止模型注入
- [ ] 设计workflowEngine的自动扩缩容设计
- [ ] 完善错误捕获和日志系统
#### ⚡ 性能与资源优化 (Performance & Scalability) #### ⚡ 性能与资源优化 (Performance & Scalability)
- [ ] 增加对应全workflow的情况追踪使得在任务运行中人机交互更加自然方便
- [ ] 优化import
#### 🏗️ 架构演进 (Architecture & Refactoring) #### 🏗️ 架构演进 (Architecture & Refactoring)
- [x] ~~使用fastapi-users完善用户系统~~(2026/4/19 fastapi-users会严重摧毁代码的优雅性)
- [x] 升级auth功能
- [x] /pretor/api的接口函数进行重构
- [x] /dockerfile待完善
- [ ] 完善沙箱功能
- [ ] 完善爬虫功能
- [ ] 对接更多的provider

View File

@ -1,13 +1,5 @@
## Pretor项目开发 ## Pretor项目开发
#项目规划
---
#### 全局规划:
- [ ] 实现监管模型的资源调度
- [ ] 实现子个体的工作传递
- [ ] 实现用户交互接口与ray集群的交互
- [ ] 实现监管模型调度ray资源的接口
- [ ] 实现由监管模型理解并发布,子个体向下布置任务,完成任务向上传递,监管模型检查的全工作流
---
#### 简介 #### 简介
**Pretor**是一款python开发实现将小模型进行微调后整理为一个大型集群从而实现低算力情况下高复杂度任务的实现。 **Pretor**是一款python开发实现将小模型进行微调后整理为一个大型集群从而实现低算力情况下高复杂度任务的实现。
系统模型分为以下部分: 系统模型分为以下部分:
@ -15,13 +7,6 @@
- **管控节点**:负责调度系统资源; - **管控节点**:负责调度系统资源;
- **意识节点**:负责复杂任务的处理; - **意识节点**:负责复杂任务的处理;
- **生长节点**:负责获取资源并且将基础模型训练为特化模型; - **生长节点**:负责获取资源并且将基础模型训练为特化模型;
- **感知模块**与外界交互的模型如embedding模型tts模型等 - **特殊子个体**与外界交互的模型如embedding模型tts模型等
- **复合子个体**:将监管节点的任务领取并进行专业的拆解任务并进行分配; - **专家子个体**
- **生产子个体**:领取任务最小单位并执行; - **基础子个体**普通的agent对象
---
#### 短期规划
v0.1版本
- [ ] **workflow构建**:构建任务的工作流
- [ ] **接口构建**:对接vllmopenai接口和gemini接口
- [ ] **工具构建**:配置供模型调用的爬虫工具箱docker接口
- [ ] **平台对接构建**:对接telegram等消息平台

View File

@ -1,18 +1,36 @@
import { useState } from 'react'; import { useState, useEffect } from 'react';
import { Sidebar } from './components/Layout/Sidebar'; import { Sidebar } from './components/Layout/Sidebar';
import { MonitoringLayout } from './components/Monitoring/MonitoringLayout'; import { MonitoringLayout } from './components/Monitoring/MonitoringLayout';
import { SettingsLayout } from './components/Settings/SettingsLayout'; import { SettingsLayout } from './components/Settings/SettingsLayout';
import { AgentLayout } from './components/Agent/AgentLayout';
import { ResourceLayout } from './components/Resource/ResourceLayout';
import { LeftPanel } from './components/Chat/LeftPanel'; import { LeftPanel } from './components/Chat/LeftPanel';
import { ChatPanel } from './components/Chat/ChatPanel'; import { ChatPanel } from './components/Chat/ChatPanel';
import { RightPanel } from './components/Chat/RightPanel'; import { RightPanel } from './components/Chat/RightPanel';
import { AuthPage } from './components/Auth/AuthPage';
function App() { function App() {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [activeTab, setActiveTab] = useState('chats'); // For LeftPanel const [activeTab, setActiveTab] = useState('chats'); // For LeftPanel
const [currentView, setCurrentView] = useState('dashboard'); // 'dashboard', 'settings', or 'monitoring' const [currentView, setCurrentView] = useState('dashboard'); // 'dashboard', 'settings', 'monitoring', 'agent', 'resource'
const [settingsTab, setSettingsTab] = useState('users'); // For SettingsLayout const [settingsTab, setSettingsTab] = useState('users'); // For SettingsLayout
const [agentTab, setAgentTab] = useState('worker'); // For AgentLayout
const [resourceTab, setResourceTab] = useState('skill'); // For ResourceLayout
const [selectedWorkflow, setSelectedWorkflow] = useState<string | null>(null); const [selectedWorkflow, setSelectedWorkflow] = useState<string | null>(null);
useEffect(() => {
// Check if token exists in localStorage on mount
const token = localStorage.getItem('token');
if (token) {
setIsAuthenticated(true);
}
}, []);
if (!isAuthenticated) {
return <AuthPage onLoginSuccess={() => setIsAuthenticated(true)} />;
}
return ( return (
<div className="flex h-screen w-screen bg-slate-50 text-slate-800 font-sans overflow-hidden"> <div className="flex h-screen w-screen bg-slate-50 text-slate-800 font-sans overflow-hidden">
@ -22,6 +40,10 @@ function App() {
{/* Main Content Area depending on view */} {/* Main Content Area depending on view */}
{currentView === 'monitoring' ? ( {currentView === 'monitoring' ? (
<MonitoringLayout /> <MonitoringLayout />
) : currentView === 'agent' ? (
<AgentLayout agentTab={agentTab} setAgentTab={setAgentTab} />
) : currentView === 'resource' ? (
<ResourceLayout resourceTab={resourceTab} setResourceTab={setResourceTab} />
) : currentView === 'dashboard' ? ( ) : currentView === 'dashboard' ? (
<> <>
{/* 2. Left Panel - Cluster Status & Workflows/Chats */} {/* 2. Left Panel - Cluster Status & Workflows/Chats */}

View File

@ -19,4 +19,18 @@ apiClient.interceptors.request.use((config) => {
return config; return config;
}); });
// Interceptor to catch 401 Unauthorized errors and force login
apiClient.interceptors.response.use(
(response) => response,
(error) => {
if (error.response && error.response.status === 401) {
// Clear token
localStorage.removeItem('token');
// Reload the page to force the user back to the Auth view
window.location.reload();
}
return Promise.reject(error);
}
);
export default apiClient; export default apiClient;

View File

@ -0,0 +1,43 @@
import { Bot, Key } from 'lucide-react';
import { ProvidersSettings } from './ProvidersSettings';
import { WorkerIndividualSettings } from './WorkerIndividualSettings';
interface AgentLayoutProps {
agentTab: string;
setAgentTab: (tab: string) => void;
}
export function AgentLayout({ agentTab, setAgentTab }: AgentLayoutProps) {
return (
<div className="flex-1 flex bg-slate-50 overflow-hidden">
{/* Agent Inner Sidebar */}
<div className="w-64 bg-white border-r border-slate-200 flex flex-col z-0">
<div className="p-6 border-b border-slate-100">
<h2 className="text-lg font-semibold text-slate-800">Agents</h2>
</div>
<div className="flex-1 p-4 space-y-2 overflow-y-auto">
<button
onClick={() => setAgentTab('worker')}
className={`w-full flex items-center px-4 py-3 text-sm font-medium rounded-xl transition-all ${agentTab === 'worker' ? 'bg-blue-50 text-blue-600' : 'text-slate-600 hover:bg-slate-50 hover:text-slate-900'}`}
>
<Bot size={18} className="mr-3" />
Worker Individual
</button>
<button
onClick={() => setAgentTab('providers')}
className={`w-full flex items-center px-4 py-3 text-sm font-medium rounded-xl transition-all ${agentTab === 'providers' ? 'bg-blue-50 text-blue-600' : 'text-slate-600 hover:bg-slate-50 hover:text-slate-900'}`}
>
<Key size={18} className="mr-3" />
Provider Management
</button>
</div>
</div>
{/* Agent Main Content */}
<div className="flex-1 overflow-y-auto p-8">
{agentTab === 'worker' && <WorkerIndividualSettings />}
{agentTab === 'providers' && <ProvidersSettings />}
</div>
</div>
);
}

View File

@ -0,0 +1,285 @@
import { useState, useEffect } from 'react';
import apiClient from '../../api/client';
import { Bot, Save } from 'lucide-react';
import type { Provider } from '../../types';
function WorkerIndividualForm({ providers }: { providers: Provider[] }) {
const [formData, setFormData] = useState({
agent_name: '',
agent_type: 'OrdinaryIndividual',
description: '',
provider_title: providers.length > 0 ? providers[0].provider_title : '',
model_id: '',
system_prompt: '',
output_template: '{}',
bound_skill: '{}',
workspace: '[]'
});
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState('');
// Update initial provider_title when providers load
useEffect(() => {
if (providers.length > 0 && !formData.provider_title) {
setFormData(prev => ({ ...prev, provider_title: providers[0].provider_title }));
}
}, [providers, formData.provider_title]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setMessage('');
try {
const payload = {
...formData,
output_template: JSON.parse(formData.output_template),
bound_skill: JSON.parse(formData.bound_skill),
workspace: JSON.parse(formData.workspace)
};
await apiClient.post('/api/v1/agent/worker', payload);
setMessage('Successfully created worker individual');
setFormData({
agent_name: '',
agent_type: 'OrdinaryIndividual',
description: '',
provider_title: '',
model_id: '',
system_prompt: '',
output_template: '{}',
bound_skill: '{}',
workspace: '[]'
});
} catch (err: any) {
console.error(err);
setMessage(err.response?.data?.detail || 'Failed to create worker individual. Ensure JSON fields are valid.');
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Agent Name</label>
<input required type="text" name="agent_name" value={formData.agent_name} onChange={handleChange} className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500" />
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Agent Type</label>
<select name="agent_type" value={formData.agent_type} onChange={handleChange} className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500">
<option value="OrdinaryIndividual">OrdinaryIndividual</option>
<option value="SkillIndividual">SkillIndividual</option>
<option value="SpecialIndividual">SpecialIndividual</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Description</label>
<input required type="text" name="description" value={formData.description} onChange={handleChange} className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500" />
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Provider Title</label>
<select
required
name="provider_title"
value={formData.provider_title}
onChange={handleChange}
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500"
>
{providers.length === 0 ? (
<option value="" disabled>No providers available. Create one first.</option>
) : (
providers.map((p) => (
<option key={p.provider_title} value={p.provider_title}>
{p.provider_title}
</option>
))
)}
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Model ID</label>
<input required type="text" name="model_id" value={formData.model_id} onChange={handleChange} className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500" />
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium text-slate-700 mb-1">System Prompt</label>
<textarea required name="system_prompt" value={formData.system_prompt} onChange={handleChange} rows={3} className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500" />
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Output Template (JSON dict)</label>
<input required type="text" name="output_template" value={formData.output_template} onChange={handleChange} className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 font-mono text-sm" />
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Bound Skill (JSON dict)</label>
<input required type="text" name="bound_skill" value={formData.bound_skill} onChange={handleChange} className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 font-mono text-sm" />
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Workspace (JSON list)</label>
<input required type="text" name="workspace" value={formData.workspace} onChange={handleChange} className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 font-mono text-sm" />
</div>
</div>
{message && (
<div className={`p-3 rounded-lg text-sm ${message.includes('Success') ? 'bg-green-50 text-green-700' : 'bg-red-50 text-red-700'}`}>
{message}
</div>
)}
<div className="flex justify-end pt-2">
<button type="submit" disabled={loading} className="flex items-center space-x-2 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors disabled:opacity-50">
<Save size={16} />
<span>{loading ? 'Creating...' : 'Create Worker'}</span>
</button>
</div>
</form>
);
}
export function WorkerIndividualSettings() {
const [nodeType, setNodeType] = useState('supervisory_node');
const [providerTitle, setProviderTitle] = useState('');
const [modelId, setModelId] = useState('');
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState('');
const [providers, setProviders] = useState<Provider[]>([]);
useEffect(() => {
const fetchProviders = async () => {
try {
const response = await apiClient.get('/api/v1/provider/list');
const data = response.data.provider_list || {};
const providerArray: Provider[] = Object.values(data);
setProviders(providerArray);
if (providerArray.length > 0) {
setProviderTitle(providerArray[0].provider_title);
}
} catch (error) {
console.error("Failed to fetch providers", error);
setProviders([]);
}
};
fetchProviders();
}, []);
const handleCreateNode = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setMessage('');
try {
await apiClient.post('/api/v1/agent', {
provider_title: providerTitle,
model_id: modelId,
individual_name: nodeType
});
setMessage(`Successfully loaded ${nodeType}`);
setProviderTitle('');
setModelId('');
} catch (err: any) {
console.error(err);
setMessage(err.response?.data?.detail || 'Failed to load agent node');
} finally {
setLoading(false);
}
};
return (
<div className="max-w-4xl space-y-6">
<div className="mb-8">
<h1 className="text-2xl font-bold text-slate-800">Worker Individual Settings</h1>
<p className="text-slate-500 mt-1">Configure your system agents and custom workers.</p>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
<div className="p-6 border-b border-slate-100 flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className="w-10 h-10 bg-indigo-50 text-indigo-600 rounded-lg flex items-center justify-center">
<Bot size={20} />
</div>
<div>
<h2 className="text-lg font-semibold text-slate-800">System Nodes</h2>
<p className="text-sm text-slate-500">Initialize core system agents</p>
</div>
</div>
</div>
<div className="p-6">
<form onSubmit={handleCreateNode} className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Node Type</label>
<select
value={nodeType}
onChange={(e) => setNodeType(e.target.value)}
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500"
>
<option value="supervisory_node">Supervisory Node</option>
<option value="consciousness_node">Consciousness Node</option>
<option value="control_node">Control Node</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Provider Title</label>
<select
value={providerTitle}
onChange={(e) => setProviderTitle(e.target.value)}
required
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500"
>
{providers.length === 0 ? (
<option value="" disabled>No providers available. Create one first.</option>
) : (
providers.map((p) => (
<option key={p.provider_title} value={p.provider_title}>
{p.provider_title}
</option>
))
)}
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Model ID</label>
<input
type="text"
value={modelId}
onChange={(e) => setModelId(e.target.value)}
placeholder="e.g. gpt-4"
required
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500"
/>
</div>
</div>
{message && (
<div className={`p-3 rounded-lg text-sm ${message.includes('Success') ? 'bg-green-50 text-green-700' : 'bg-red-50 text-red-700'}`}>
{message}
</div>
)}
<div className="flex justify-end">
<button
type="submit"
disabled={loading}
className="flex items-center space-x-2 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors disabled:opacity-50"
>
<Save size={16} />
<span>{loading ? 'Saving...' : 'Load Node'}</span>
</button>
</div>
</form>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
<div className="p-6 border-b border-slate-100">
<h2 className="text-lg font-semibold text-slate-800">Create Worker Individual</h2>
<p className="text-sm text-slate-500">Add a new custom worker to the system.</p>
</div>
<div className="p-6">
<WorkerIndividualForm providers={providers} />
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,123 @@
import React, { useState } from 'react';
import apiClient from '../../api/client';
import { Activity } from 'lucide-react';
interface AuthPageProps {
onLoginSuccess: () => void;
}
export function AuthPage({ onLoginSuccess }: AuthPageProps) {
const [isLogin, setIsLogin] = useState(true);
const [userName, setUserName] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setLoading(true);
try {
if (isLogin) {
// Login
const response = await apiClient.post('/api/v1/auth/login', {
user_name: userName,
password: password,
});
if (response.data.token) {
localStorage.setItem('token', response.data.token);
onLoginSuccess();
}
} else {
// Register
const response = await apiClient.post('/api/v1/auth/register', {
user_name: userName,
password: password,
});
// After successful register, we can automatically log them in
// or just show a message and switch to login. Let's switch to login.
if (response.data.message === 'success') {
setIsLogin(true);
setError('Registration successful. Please log in.');
}
}
} catch (err: any) {
console.error(err);
setError(err.response?.data?.detail || err.response?.data?.message || 'Authentication failed');
} finally {
setLoading(false);
}
};
return (
<div className="flex min-h-screen w-full items-center justify-center bg-slate-50">
<div className="w-full max-w-md bg-white rounded-xl shadow-lg p-8 border border-slate-100">
<div className="flex flex-col items-center mb-8">
<div className="w-12 h-12 bg-blue-600 rounded-xl flex items-center justify-center text-white mb-4 shadow-md shadow-blue-200">
<Activity size={24} />
</div>
<h2 className="text-2xl font-bold text-slate-800">
{isLogin ? 'Welcome Back' : 'Create Account'}
</h2>
<p className="text-slate-500 mt-2 text-sm">
{isLogin ? 'Enter your credentials to access your account' : 'Sign up to start using the platform'}
</p>
</div>
{error && (
<div className={`mb-4 p-3 rounded-lg text-sm ${error.includes('successful') ? 'bg-green-50 text-green-700 border border-green-200' : 'bg-red-50 text-red-700 border border-red-200'}`}>
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Username</label>
<input
type="text"
value={userName}
onChange={(e) => setUserName(e.target.value)}
className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-shadow"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Password</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-shadow"
required
/>
</div>
<button
type="submit"
disabled={loading}
className="w-full py-2.5 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors disabled:opacity-50"
>
{loading ? 'Processing...' : (isLogin ? 'Sign In' : 'Sign Up')}
</button>
</form>
<div className="mt-6 text-center text-sm text-slate-500">
{isLogin ? "Don't have an account? " : "Already have an account? "}
<button
onClick={() => {
setIsLogin(!isLogin);
setError('');
}}
className="text-blue-600 font-medium hover:text-blue-700 focus:outline-none"
>
{isLogin ? 'Sign up' : 'Sign in'}
</button>
</div>
</div>
</div>
);
}

View File

@ -1,5 +1,5 @@
import { Activity, MessageSquare, MonitorPlay, Settings } from 'lucide-react'; import { Activity, MessageSquare, MonitorPlay, Settings, Bot, Box } from 'lucide-react';
interface SidebarProps { interface SidebarProps {
currentView: string; currentView: string;
@ -31,6 +31,20 @@ export function Sidebar({ currentView, setCurrentView }: SidebarProps) {
> >
<MonitorPlay size={18} /> <MonitorPlay size={18} />
</button> </button>
<button
onClick={() => setCurrentView('agent')}
className={`p-1.5 rounded-lg transition-colors ${currentView === 'agent' ? 'text-blue-600 bg-blue-50' : 'text-slate-400 hover:text-blue-500 hover:bg-blue-50'}`}
title="Agents"
>
<Bot size={18} />
</button>
<button
onClick={() => setCurrentView('resource')}
className={`p-1.5 rounded-lg transition-colors ${currentView === 'resource' ? 'text-blue-600 bg-blue-50' : 'text-slate-400 hover:text-blue-500 hover:bg-blue-50'}`}
title="Resources"
>
<Box size={18} />
</button>
<button <button
onClick={() => setCurrentView('settings')} onClick={() => setCurrentView('settings')}
className={`p-1.5 rounded-lg transition-colors ${currentView === 'settings' ? 'text-blue-600 bg-blue-50' : 'text-slate-400 hover:text-blue-500 hover:bg-blue-50'}`} className={`p-1.5 rounded-lg transition-colors ${currentView === 'settings' ? 'text-blue-600 bg-blue-50' : 'text-slate-400 hover:text-blue-500 hover:bg-blue-50'}`}

View File

@ -0,0 +1,52 @@
import { Wrench, Database, FileCode } from 'lucide-react';
import { SkillSettings } from './SkillSettings';
import { ResourceSettings } from './ResourceSettings';
import { WorkflowTemplateSettings } from './WorkflowTemplateSettings';
interface ResourceLayoutProps {
resourceTab: string;
setResourceTab: (tab: string) => void;
}
export function ResourceLayout({ resourceTab, setResourceTab }: ResourceLayoutProps) {
return (
<div className="flex-1 flex bg-slate-50 overflow-hidden">
{/* Resource Inner Sidebar */}
<div className="w-64 bg-white border-r border-slate-200 flex flex-col z-0">
<div className="p-6 border-b border-slate-100">
<h2 className="text-lg font-semibold text-slate-800">Resources</h2>
</div>
<div className="flex-1 p-4 space-y-2 overflow-y-auto">
<button
onClick={() => setResourceTab('skill')}
className={`w-full flex items-center px-4 py-3 text-sm font-medium rounded-xl transition-all ${resourceTab === 'skill' ? 'bg-blue-50 text-blue-600' : 'text-slate-600 hover:bg-slate-50 hover:text-slate-900'}`}
>
<Wrench size={18} className="mr-3" />
Skills
</button>
<button
onClick={() => setResourceTab('workflow_template')}
className={`w-full flex items-center px-4 py-3 text-sm font-medium rounded-xl transition-all ${resourceTab === 'workflow_template' ? 'bg-blue-50 text-blue-600' : 'text-slate-600 hover:bg-slate-50 hover:text-slate-900'}`}
>
<FileCode size={18} className="mr-3" />
Workflow Templates
</button>
<button
onClick={() => setResourceTab('resource')}
className={`w-full flex items-center px-4 py-3 text-sm font-medium rounded-xl transition-all ${resourceTab === 'resource' ? 'bg-blue-50 text-blue-600' : 'text-slate-600 hover:bg-slate-50 hover:text-slate-900'}`}
>
<Database size={18} className="mr-3" />
Resources
</button>
</div>
</div>
{/* Resource Main Content */}
<div className="flex-1 overflow-y-auto p-8">
{resourceTab === 'skill' && <SkillSettings />}
{resourceTab === 'workflow_template' && <WorkflowTemplateSettings />}
{resourceTab === 'resource' && <ResourceSettings />}
</div>
</div>
);
}

View File

@ -0,0 +1,13 @@
export function ResourceSettings() {
return (
<div className="max-w-4xl space-y-6">
<div className="mb-8">
<h1 className="text-2xl font-bold text-slate-800">Resource Management</h1>
<p className="text-slate-500 mt-1">Manage external and internal resources.</p>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden p-6 text-slate-500 text-sm">
Resource management configuration coming soon...
</div>
</div>
);
}

View File

@ -0,0 +1,160 @@
import { useState, useEffect } from 'react';
import apiClient from '../../api/client';
import { Download, Trash2, Plus, Box } from 'lucide-react';
export function SkillSettings() {
const [skills, setSkills] = useState<string[]>([]);
const [loading, setLoading] = useState(true);
const [repoUrl, setRepoUrl] = useState('');
const [path, setPath] = useState('');
const [installing, setInstalling] = useState(false);
const [message, setMessage] = useState('');
const [error, setError] = useState('');
const fetchSkills = async () => {
setLoading(true);
try {
const response = await apiClient.get('/api/v1/resource/skill');
setSkills(response.data.skills || []);
} catch (err) {
console.error('Failed to fetch skills:', err);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchSkills();
}, []);
const handleInstall = async (e: React.FormEvent) => {
e.preventDefault();
if (!repoUrl) return;
setInstalling(true);
setMessage('');
setError('');
try {
await apiClient.post('/api/v1/resource/skill', {
repo_url: repoUrl,
path: path || null
});
setMessage('Skill installed successfully');
setRepoUrl('');
setPath('');
fetchSkills();
} catch (err: any) {
console.error(err);
setError(err.response?.data?.message || 'Failed to install skill');
} finally {
setInstalling(false);
}
};
const handleDelete = async (skillName: string) => {
if (!confirm(`Are you sure you want to delete ${skillName}?`)) return;
try {
await apiClient.delete(`/api/v1/resource/skill/${skillName}`);
fetchSkills();
} catch (err: any) {
console.error('Failed to delete skill:', err);
alert('Failed to delete skill');
}
};
return (
<div className="max-w-4xl space-y-6">
<div className="mb-8">
<h1 className="text-2xl font-bold text-slate-800">Skill Management</h1>
<p className="text-slate-500 mt-1">Manage agent skills and functions.</p>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
<div className="p-6 border-b border-slate-100 flex items-center space-x-3">
<div className="w-10 h-10 bg-indigo-50 text-indigo-600 rounded-lg flex items-center justify-center">
<Download size={20} />
</div>
<div>
<h2 className="text-lg font-semibold text-slate-800">Install Skill</h2>
<p className="text-sm text-slate-500">Install a new skill from a repository.</p>
</div>
</div>
<div className="p-6">
<form onSubmit={handleInstall} className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Repository URL</label>
<input
type="text"
required
value={repoUrl}
onChange={(e) => setRepoUrl(e.target.value)}
placeholder="https://github.com/user/repo"
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Path (Optional)</label>
<input
type="text"
value={path}
onChange={(e) => setPath(e.target.value)}
placeholder="e.g. subfolder/path"
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500"
/>
</div>
</div>
{message && <div className="text-green-600 text-sm">{message}</div>}
{error && <div className="text-red-600 text-sm">{error}</div>}
<div className="flex justify-end">
<button
type="submit"
disabled={installing}
className="flex items-center px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors disabled:opacity-50"
>
<Plus size={16} className="mr-2" />
{installing ? 'Installing...' : 'Install'}
</button>
</div>
</form>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
<div className="p-6 border-b border-slate-100">
<h2 className="text-lg font-semibold text-slate-800">Installed Skills</h2>
</div>
<div className="p-6">
{loading ? (
<div className="text-slate-500 text-sm">Loading skills...</div>
) : skills.length === 0 ? (
<div className="text-slate-500 text-sm">No skills installed yet.</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{skills.map((skill) => (
<div key={skill} className="p-4 border border-slate-200 rounded-xl flex items-center justify-between hover:shadow-sm transition-shadow">
<div className="flex items-center space-x-3">
<div className="w-8 h-8 rounded-lg bg-slate-100 flex items-center justify-center text-slate-500">
<Box size={16} />
</div>
<span className="font-medium text-slate-800">{skill}</span>
</div>
<button
onClick={() => handleDelete(skill)}
className="p-2 text-slate-400 hover:text-red-500 hover:bg-red-50 rounded-lg transition-colors"
title="Delete Skill"
>
<Trash2 size={16} />
</button>
</div>
))}
</div>
)}
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,150 @@
import { useState, useEffect } from 'react';
import apiClient from '../../api/client';
import { FileCode, Trash2, Plus, LayoutTemplate } from 'lucide-react';
interface WorkflowTemplate {
name: string;
[key: string]: any;
}
export function WorkflowTemplateSettings() {
const [templates, setTemplates] = useState<Record<string, WorkflowTemplate>>({});
const [loading, setLoading] = useState(true);
const [templateJson, setTemplateJson] = useState('{\n "name": "my_template"\n}');
const [creating, setCreating] = useState(false);
const [message, setMessage] = useState('');
const [error, setError] = useState('');
const fetchTemplates = async () => {
setLoading(true);
try {
const response = await apiClient.get('/api/v1/resource/workflow_template');
setTemplates(response.data.templates || {});
} catch (err) {
console.error('Failed to fetch templates:', err);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchTemplates();
}, []);
const handleCreate = async (e: React.FormEvent) => {
e.preventDefault();
setCreating(true);
setMessage('');
setError('');
try {
const parsedJson = JSON.parse(templateJson);
await apiClient.post('/api/v1/resource/workflow_template', parsedJson);
setMessage('Workflow template created successfully');
setTemplateJson('{\n "name": "my_template"\n}');
fetchTemplates();
} catch (err: any) {
console.error(err);
if (err instanceof SyntaxError) {
setError('Invalid JSON format');
} else {
setError(err.response?.data?.message || 'Failed to create workflow template');
}
} finally {
setCreating(false);
}
};
const handleDelete = async (templateName: string) => {
if (!confirm(`Are you sure you want to delete ${templateName}?`)) return;
try {
await apiClient.delete(`/api/v1/resource/workflow_template/${templateName}`);
fetchTemplates();
} catch (err: any) {
console.error('Failed to delete template:', err);
alert('Failed to delete template');
}
};
return (
<div className="max-w-4xl space-y-6">
<div className="mb-8">
<h1 className="text-2xl font-bold text-slate-800">Workflow Templates</h1>
<p className="text-slate-500 mt-1">Manage and create reusable workflow templates.</p>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
<div className="p-6 border-b border-slate-100 flex items-center space-x-3">
<div className="w-10 h-10 bg-indigo-50 text-indigo-600 rounded-lg flex items-center justify-center">
<FileCode size={20} />
</div>
<div>
<h2 className="text-lg font-semibold text-slate-800">Create Template</h2>
<p className="text-sm text-slate-500">Provide the JSON definition for a new workflow template.</p>
</div>
</div>
<div className="p-6">
<form onSubmit={handleCreate} className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Template JSON Definition</label>
<textarea
required
rows={8}
value={templateJson}
onChange={(e) => setTemplateJson(e.target.value)}
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 font-mono text-sm"
/>
</div>
{message && <div className="text-green-600 text-sm">{message}</div>}
{error && <div className="text-red-600 text-sm">{error}</div>}
<div className="flex justify-end">
<button
type="submit"
disabled={creating}
className="flex items-center px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors disabled:opacity-50"
>
<Plus size={16} className="mr-2" />
{creating ? 'Creating...' : 'Create Template'}
</button>
</div>
</form>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
<div className="p-6 border-b border-slate-100">
<h2 className="text-lg font-semibold text-slate-800">Available Templates</h2>
</div>
<div className="p-6">
{loading ? (
<div className="text-slate-500 text-sm">Loading templates...</div>
) : Object.keys(templates).length === 0 ? (
<div className="text-slate-500 text-sm">No workflow templates created yet.</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{Object.keys(templates).map((name) => (
<div key={name} className="p-4 border border-slate-200 rounded-xl flex items-center justify-between hover:shadow-sm transition-shadow">
<div className="flex items-center space-x-3">
<div className="w-8 h-8 rounded-lg bg-slate-100 flex items-center justify-center text-slate-500">
<LayoutTemplate size={16} />
</div>
<span className="font-medium text-slate-800">{name}</span>
</div>
<button
onClick={() => handleDelete(name)}
className="p-2 text-slate-400 hover:text-red-500 hover:bg-red-50 rounded-lg transition-colors"
title="Delete Template"
>
<Trash2 size={16} />
</button>
</div>
))}
</div>
)}
</div>
</div>
</div>
);
}

View File

@ -1,7 +1,6 @@
import { Users, Key, Sliders } from 'lucide-react'; import { Users, Sliders } from 'lucide-react';
import { UsersSettings } from './UsersSettings'; import { UsersSettings } from './UsersSettings';
import { ProvidersSettings } from './ProvidersSettings';
import { SystemSettings } from './SystemSettings'; import { SystemSettings } from './SystemSettings';
interface SettingsLayoutProps { interface SettingsLayoutProps {
@ -25,13 +24,6 @@ export function SettingsLayout({ settingsTab, setSettingsTab }: SettingsLayoutPr
<Users size={18} className="mr-3" /> <Users size={18} className="mr-3" />
User Management User Management
</button> </button>
<button
onClick={() => setSettingsTab('providers')}
className={`w-full flex items-center px-4 py-3 text-sm font-medium rounded-xl transition-all ${settingsTab === 'providers' ? 'bg-blue-50 text-blue-600' : 'text-slate-600 hover:bg-slate-50 hover:text-slate-900'}`}
>
<Key size={18} className="mr-3" />
Provider Management
</button>
<button <button
onClick={() => setSettingsTab('system')} onClick={() => setSettingsTab('system')}
className={`w-full flex items-center px-4 py-3 text-sm font-medium rounded-xl transition-all ${settingsTab === 'system' ? 'bg-blue-50 text-blue-600' : 'text-slate-600 hover:bg-slate-50 hover:text-slate-900'}`} className={`w-full flex items-center px-4 py-3 text-sm font-medium rounded-xl transition-all ${settingsTab === 'system' ? 'bg-blue-50 text-blue-600' : 'text-slate-600 hover:bg-slate-50 hover:text-slate-900'}`}
@ -45,7 +37,6 @@ export function SettingsLayout({ settingsTab, setSettingsTab }: SettingsLayoutPr
{/* Settings Main Content */} {/* Settings Main Content */}
<div className="flex-1 overflow-y-auto p-8"> <div className="flex-1 overflow-y-auto p-8">
{settingsTab === 'users' && <UsersSettings />} {settingsTab === 'users' && <UsersSettings />}
{settingsTab === 'providers' && <ProvidersSettings />}
{settingsTab === 'system' && <SystemSettings />} {settingsTab === 'system' && <SystemSettings />}
</div> </div>
</div> </div>

View File

@ -1,7 +1,36 @@
import { useState, useEffect } from 'react';
import { Globe, Server, Save } from 'lucide-react'; import { Globe, Server, Save } from 'lucide-react';
export function SystemSettings() { export function SystemSettings() {
const [language, setLanguage] = useState(localStorage.getItem('language') || 'English');
const [theme, setTheme] = useState(localStorage.getItem('theme') || 'Light');
const [debugMode, setDebugMode] = useState(true);
const handleSave = () => {
localStorage.setItem('language', language);
localStorage.setItem('theme', theme);
// Apply theme
if (theme === 'Dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
// In a real app, you would dispatch a language change event or context update here
alert(`Settings saved!\nLanguage: ${language}\nTheme: ${theme}`);
};
// Initialize theme on mount if needed
useEffect(() => {
if (theme === 'Dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}, [theme]);
return ( return (
<div className="max-w-4xl mx-auto"> <div className="max-w-4xl mx-auto">
<div className="mb-6"> <div className="mb-6">
@ -18,17 +47,25 @@ export function SystemSettings() {
<div className="space-y-4 max-w-md"> <div className="space-y-4 max-w-md">
<div> <div>
<label className="block text-sm font-medium text-slate-700 mb-1">System Language</label> <label className="block text-sm font-medium text-slate-700 mb-1">System Language</label>
<select className="w-full bg-slate-50 border border-slate-200 text-sm rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all"> <select
<option>English</option> value={language}
<option></option> onChange={(e) => setLanguage(e.target.value)}
className="w-full bg-slate-50 border border-slate-200 text-sm rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all"
>
<option value="English">English</option>
<option value="简体中文"></option>
</select> </select>
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-slate-700 mb-1">Theme</label> <label className="block text-sm font-medium text-slate-700 mb-1">Theme</label>
<select className="w-full bg-slate-50 border border-slate-200 text-sm rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all"> <select
<option>Light</option> value={theme}
<option>Dark</option> onChange={(e) => setTheme(e.target.value)}
<option>System Default</option> className="w-full bg-slate-50 border border-slate-200 text-sm rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all"
>
<option value="Light">Light</option>
<option value="Dark">Dark</option>
<option value="System Default">System Default</option>
</select> </select>
</div> </div>
</div> </div>
@ -40,12 +77,14 @@ export function SystemSettings() {
Cluster & Runtime Cluster & Runtime
</h4> </h4>
<div className="space-y-4 max-w-md"> <div className="space-y-4 max-w-md">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Max Concurrent Workflows</label>
<input type="number" defaultValue={10} className="w-full bg-slate-50 border border-slate-200 text-sm rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all" />
</div>
<div className="flex items-center mt-4"> <div className="flex items-center mt-4">
<input type="checkbox" id="debug_mode" defaultChecked className="w-4 h-4 text-blue-600 border-slate-300 rounded focus:ring-blue-500" /> <input
type="checkbox"
id="debug_mode"
checked={debugMode}
onChange={(e) => setDebugMode(e.target.checked)}
className="w-4 h-4 text-blue-600 border-slate-300 rounded focus:ring-blue-500"
/>
<label htmlFor="debug_mode" className="ml-2 block text-sm text-slate-700"> <label htmlFor="debug_mode" className="ml-2 block text-sm text-slate-700">
Enable debug logging Enable debug logging
</label> </label>
@ -54,7 +93,10 @@ export function SystemSettings() {
</div> </div>
<div className="flex justify-end"> <div className="flex justify-end">
<button className="flex items-center px-6 py-2.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-sm font-medium transition-colors shadow-sm"> <button
onClick={handleSave}
className="flex items-center px-6 py-2.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-sm font-medium transition-colors shadow-sm cursor-pointer"
>
<Save size={16} className="mr-2" /> <Save size={16} className="mr-2" />
Save Changes Save Changes
</button> </button>

14
main.py
View File

@ -15,15 +15,13 @@ import os
async def start_system(): async def start_system():
# 1. 初始化 Ray
db_host = os.getenv("POSTGRES_HOST", "db")
env_vars = { env_vars = {
"POSTGRES_USER": "postgres", "POSTGRES_USER": os.getenv("POSTGRES_USER", "postgres"),
"POSTGRES_PASSWORD": "postgres", "POSTGRES_PASSWORD": os.getenv("POSTGRES_PASSWORD", ""),
"POSTGRES_HOST": db_host, "POSTGRES_HOST": os.getenv("POSTGRES_HOST", "db"),
"POSTGRES_PORT": "5432", "POSTGRES_PORT": os.getenv("POSTGRES_PORT", "5432"),
"POSTGRES_DB": "postgres", "POSTGRES_DB": os.getenv("POSTGRES_DB", "postgres"),
"SECRET_KEY": "yoursecretkey" "SECRET_KEY": os.getenv("SECRET_KEY", "secret"),
} }
ray.init(ignore_reinit_error=True, ray.init(ignore_reinit_error=True,

View File

@ -36,5 +36,5 @@ async def update_cluster_state(websocket: WebSocket):
] ]
await websocket.send_json(payload) await websocket.send_json(payload)
await asyncio.sleep(10) await asyncio.sleep(10)
except WebSocketDisconnect: except (WebSocketDisconnect, RuntimeError):
pass pass

View File

@ -29,6 +29,6 @@ async def get_workflow(websocket: WebSocket, event_id: str):
await websocket.send_text(await global_state_machine.get_pending.remote(event_id)) await websocket.send_text(await global_state_machine.get_pending.remote(event_id))
response = await websocket.receive_text() response = await websocket.receive_text()
await global_state_machine.put_received(event_id, response) await global_state_machine.put_received(event_id, response)
except WebSocketDisconnect: except (WebSocketDisconnect, RuntimeError):
pass pass

View File

@ -13,6 +13,7 @@
# limitations under the License. # limitations under the License.
import os import os
import asyncio
import ray import ray
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
@ -39,6 +40,8 @@ class PostgresDatabase:
self._provider_database = ProviderDatabase(self.async_session_maker) self._provider_database = ProviderDatabase(self.async_session_maker)
self._individual_database = IndividualDatabase(self.async_session_maker) self._individual_database = IndividualDatabase(self.async_session_maker)
self.ready_event = asyncio.Event()
async def init_db(self) -> None: async def init_db(self) -> None:
try: try:
async with self.async_engine.begin() as conn: async with self.async_engine.begin() as conn:
@ -47,15 +50,20 @@ class PostgresDatabase:
# Provide a warning if the database is not accessible, allowing # Provide a warning if the database is not accessible, allowing
# the app to start up for development/UI tests without crashing immediately. # the app to start up for development/UI tests without crashing immediately.
print(f"Warning: Failed to initialize PostgreSQL database: {e}") print(f"Warning: Failed to initialize PostgreSQL database: {e}")
finally:
self.ready_event.set()
async def auth_database(self, method_name: str, *args, **kwargs): async def auth_database(self, method_name: str, *args, **kwargs):
await self.ready_event.wait()
method = getattr(self._auth_database, method_name) method = getattr(self._auth_database, method_name)
return await method(*args, **kwargs) return await method(*args, **kwargs)
async def provider_database(self, method_name: str, *args, **kwargs): async def provider_database(self, method_name: str, *args, **kwargs):
await self.ready_event.wait()
method = getattr(self._provider_database, method_name) method = getattr(self._provider_database, method_name)
return await method(*args, **kwargs) return await method(*args, **kwargs)
async def individual_database(self, method_name: str, *args, **kwargs): async def individual_database(self, method_name: str, *args, **kwargs):
await self.ready_event.wait()
method = getattr(self._individual_database, method_name) method = getattr(self._individual_database, method_name)
return await method(*args, **kwargs) return await method(*args, **kwargs)

View File

@ -28,5 +28,5 @@ class Provider(SQLModel, table=True):
provider_models: List[str] = Field(sa_column=Column(JSON)) provider_models: List[str] = Field(sa_column=Column(JSON))
provider_owner: int provider_owner: str
is_active: bool = Field(default=True, description="该服务商节点是否在线/启用") is_active: bool = Field(default=True, description="该服务商节点是否在线/启用")

View File

@ -27,14 +27,14 @@ class Provider(BaseModel):
provider_apikey: str provider_apikey: str
provider_models: List[str] provider_models: List[str]
provider_type: str provider_type: str
provider_owner: int | None = None provider_owner: str | None = None
provider_status: ProviderStatus = ProviderStatus.UP provider_status: ProviderStatus = ProviderStatus.UP
class ProviderArgs(BaseModel): class ProviderArgs(BaseModel):
provider_title: str provider_title: str
provider_url: str provider_url: str
provider_apikey: str provider_apikey: str
provider_owner: int provider_owner: str
class BaseProvider(ABC): class BaseProvider(ABC):
@staticmethod @staticmethod

View File

@ -47,6 +47,8 @@ class WorkflowEngine:
consciousness_node=None, consciousness_node=None,
control_node=None, control_node=None,
supervisory_node=None): supervisory_node=None):
from pretor.utils.logger import get_logger
self.logger = get_logger('workflow_runner')
self.workflow: PretorWorkflow = workflow self.workflow: PretorWorkflow = workflow
"""工作流当前WorkflowEngine待执行的workflow""" """工作流当前WorkflowEngine待执行的workflow"""
self._steps_by_id: Dict[int, WorkStep] = {step.step: step for step in self.workflow.work_link} self._steps_by_id: Dict[int, WorkStep] = {step.step: step for step in self.workflow.work_link}

View File

@ -10,7 +10,7 @@ def test_provider_args():
provider_title="title", provider_title="title",
provider_url="url", provider_url="url",
provider_apikey="key", provider_apikey="key",
provider_owner=1 provider_owner="1"
) )
assert args.provider_title == "title" assert args.provider_title == "title"