feat(frontend):优化前端页面设计
This commit is contained in:
+4
-1
@@ -3,4 +3,7 @@ POSTGRES_PASSWORD=postgrespassword
|
||||
POSTGRES_HOST=127.0.0.1
|
||||
POSTGRES_PORT=5432
|
||||
POSTGRES_DB=kilostar
|
||||
SECRET_KEY=114514
|
||||
# 必须填写一个高熵随机字符串,建议生成命令:
|
||||
# python -c "import secrets; print(secrets.token_urlsafe(32))"
|
||||
# 留空或填 "secret" / "114514" / "changethiskey12345" 等弱值会被拒绝。
|
||||
SECRET_KEY=
|
||||
|
||||
@@ -37,10 +37,7 @@
|
||||
| 项目名称 | 代号 | 功能定位 | 当前状态 |
|
||||
|:-----------------------------------------------------------|:--------| :--- | :--- |
|
||||
| **[kilostar-viceroy](https://github.com/zhaoxi826/viceroy)** | **总督** | **资源管理**:负责系统 Skill 的动态安装、元数据解析与全集群分发。 | ✅ 已发布 |
|
||||
| **kilostar-stardomain** | **星域** | **安全沙箱**:为 Agent 自动生成的代码提供轻量化的隔离运行环境,防止逃逸。 | 📅 规划中 |
|
||||
| **kilostar-explorer** | **探索者** | **网页感知**:自动化爬虫引擎,赋予智能体实时互联网信息搜索与内容抓取能力。 | 📅 规划中 |
|
||||
| **kilostar-pioneer** | **先驱者** | **知识增强**:RAG 检索增强引擎,管理私有知识库的向量化、索引与精准检索。 | 📅 规划中 |
|
||||
|
||||
| **[kilostar-thought](https://github.com/zhaoxi826/thought)** | **思绪** | **记忆系统**:增强agent的记忆系统。 | 开发中 |
|
||||
---
|
||||
## 🚀 快速开始 (Quick Start)
|
||||
|
||||
|
||||
+3
-6
@@ -1,12 +1,10 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
container_name: kilostar_db
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgrespassword
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgrespassword}
|
||||
POSTGRES_DB: kilostar
|
||||
ports:
|
||||
- "5432:5432"
|
||||
@@ -27,9 +25,8 @@ services:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=postgrespassword
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-postgrespassword}
|
||||
- POSTGRES_HOST=db
|
||||
- POSTGRES_PORT=5432
|
||||
- POSTGRES_DB=kilostar
|
||||
- SECRET_KEY=changethiskey12345
|
||||
|
||||
- SECRET_KEY=${SECRET_KEY:?SECRET_KEY must be set; generate one via: python -c \"import secrets;print(secrets.token_urlsafe(32))\"}
|
||||
|
||||
Generated
+155
-11
@@ -8,11 +8,17 @@
|
||||
"name": "kilostar-dashboard",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@fontsource/inter": "^5.2.8",
|
||||
"@fontsource/jetbrains-mono": "^5.2.8",
|
||||
"@xyflow/react": "^12.10.2",
|
||||
"axios": "^1.15.1",
|
||||
"i18next": "^26.3.0",
|
||||
"i18next-browser-languagedetector": "^8.2.1",
|
||||
"lucide-react": "^1.8.0",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4"
|
||||
"react-dom": "^19.2.4",
|
||||
"react-i18next": "^17.0.8",
|
||||
"zustand": "^5.0.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.4",
|
||||
@@ -223,6 +229,15 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
|
||||
"integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
|
||||
@@ -462,6 +477,24 @@
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@fontsource/inter": {
|
||||
"version": "5.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-5.2.8.tgz",
|
||||
"integrity": "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg==",
|
||||
"license": "OFL-1.1",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ayuhito"
|
||||
}
|
||||
},
|
||||
"node_modules/@fontsource/jetbrains-mono": {
|
||||
"version": "5.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/jetbrains-mono/-/jetbrains-mono-5.2.8.tgz",
|
||||
"integrity": "sha512-6w8/SG4kqvIMu7xd7wt6x3idn1Qux3p9N62s6G3rfldOUYHpWcc2FKrqf+Vo44jRvqWj2oAtTHrZXEP23oSKwQ==",
|
||||
"license": "OFL-1.1",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ayuhito"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanfs/core": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
|
||||
@@ -1583,6 +1616,34 @@
|
||||
"react-dom": ">=17"
|
||||
}
|
||||
},
|
||||
"node_modules/@xyflow/react/node_modules/zustand": {
|
||||
"version": "4.5.7",
|
||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
|
||||
"integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"use-sync-external-store": "^1.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.7.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=16.8",
|
||||
"immer": ">=9.0.6",
|
||||
"react": ">=16.8"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"immer": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@xyflow/system": {
|
||||
"version": "0.0.76",
|
||||
"resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.76.tgz",
|
||||
@@ -2624,6 +2685,52 @@
|
||||
"hermes-estree": "0.25.1"
|
||||
}
|
||||
},
|
||||
"node_modules/html-parse-stringify": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
|
||||
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"void-elements": "3.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/i18next": {
|
||||
"version": "26.3.0",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.0.tgz",
|
||||
"integrity": "sha512-gHSgGpUXVmuqE2El1W61DmxeyeTlFfZgdJRWMo9jScAn5pu7TuTuiccb1zh3E2J9hEBVGJ23+96x0ieBhfuIHA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://www.locize.com/i18next"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://www.locize.com"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"typescript": "^5 || ^6"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/i18next-browser-languagedetector": {
|
||||
"version": "8.2.1",
|
||||
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.1.tgz",
|
||||
"integrity": "sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.23.2"
|
||||
}
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
@@ -3370,6 +3477,33 @@
|
||||
"react": "^19.2.5"
|
||||
}
|
||||
},
|
||||
"node_modules/react-i18next": {
|
||||
"version": "17.0.8",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.8.tgz",
|
||||
"integrity": "sha512-0ooKbGLU8JXhe1zwpQUWIeXSgLPOfwJmgheWRIUpcoA0CpyabpGhayjdG+/eA5esC1AQ8h2jWpXjJfzQzeDOCw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.29.2",
|
||||
"html-parse-stringify": "^3.0.1",
|
||||
"use-sync-external-store": "^1.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"i18next": ">= 26.2.0",
|
||||
"react": ">= 16.8.0",
|
||||
"typescript": "^5 || ^6"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native": {
|
||||
"optional": true
|
||||
},
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-from": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||
@@ -3572,7 +3706,7 @@
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
|
||||
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
@@ -3741,6 +3875,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/void-elements": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
|
||||
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
@@ -3811,20 +3954,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/zustand": {
|
||||
"version": "4.5.7",
|
||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
|
||||
"integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
|
||||
"version": "5.0.14",
|
||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.14.tgz",
|
||||
"integrity": "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"use-sync-external-store": "^1.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.7.0"
|
||||
"node": ">=12.20.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=16.8",
|
||||
"@types/react": ">=18.0.0",
|
||||
"immer": ">=9.0.6",
|
||||
"react": ">=16.8"
|
||||
"react": ">=18.0.0",
|
||||
"use-sync-external-store": ">=1.2.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
@@ -3835,6 +3976,9 @@
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"use-sync-external-store": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,11 +10,17 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/inter": "^5.2.8",
|
||||
"@fontsource/jetbrains-mono": "^5.2.8",
|
||||
"@xyflow/react": "^12.10.2",
|
||||
"axios": "^1.15.1",
|
||||
"i18next": "^26.3.0",
|
||||
"i18next-browser-languagedetector": "^8.2.1",
|
||||
"lucide-react": "^1.8.0",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4"
|
||||
"react-dom": "^19.2.4",
|
||||
"react-i18next": "^17.0.8",
|
||||
"zustand": "^5.0.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.4",
|
||||
|
||||
+63
-123
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { TopBar } from './components/Layout/TopBar';
|
||||
import { CollapsibleSidebar } from './components/Layout/CollapsibleSidebar';
|
||||
import { SettingsLayout } from './components/Settings/SettingsLayout';
|
||||
@@ -10,162 +10,75 @@ import { RightPanel } from './components/Chat/RightPanel';
|
||||
import { WorkflowListView } from './components/Chat/WorkflowListView';
|
||||
import { NewWorkflowDialog } from './components/Chat/NewWorkflowDialog';
|
||||
import { AuthPage } from './components/Auth/AuthPage';
|
||||
import apiClient from './api/client';
|
||||
import type { ChatSessionDB } from './types';
|
||||
|
||||
export interface Message {
|
||||
id: string;
|
||||
role: 'user' | 'assistant' | 'system';
|
||||
content: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export interface ChatSession {
|
||||
id: string;
|
||||
title: string;
|
||||
messages: Message[];
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
function mapSessionFromDB(s: ChatSessionDB): ChatSession {
|
||||
return {
|
||||
id: s.chat_id,
|
||||
title: s.title,
|
||||
messages: [],
|
||||
updatedAt: new Date(s.updated_at).getTime(),
|
||||
};
|
||||
}
|
||||
import { useAppStore } from './store/useAppStore';
|
||||
import { useChatStore } from './store/useChatStore';
|
||||
|
||||
function App() {
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
const {
|
||||
isAuthenticated,
|
||||
setIsAuthenticated,
|
||||
mode,
|
||||
showSettings,
|
||||
workTab,
|
||||
agentTab,
|
||||
applyTheme,
|
||||
} = useAppStore();
|
||||
|
||||
const [mode, setMode] = useState<'work' | 'agent'>('work');
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
|
||||
const { loadSessions } = useChatStore();
|
||||
|
||||
const [workTab, setWorkTab] = useState<'chat' | 'workflow'>('chat');
|
||||
const [selectedWorkflow, setSelectedWorkflow] = useState<string | null>(null);
|
||||
|
||||
const [agentTab, setAgentTab] = useState<'plugin' | 'agents'>('plugin');
|
||||
const [settingsTab, setSettingsTab] = useState('users');
|
||||
const [innerAgentTab, setInnerAgentTab] = useState('worker');
|
||||
const [resourceTab, setResourceTab] = useState('skill');
|
||||
|
||||
const [chatSessions, setChatSessions] = useState<ChatSession[]>([]);
|
||||
const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
|
||||
|
||||
const loadChatSessions = useCallback(async () => {
|
||||
try {
|
||||
const response = await apiClient.get('/api/v1/chat');
|
||||
const sessions: ChatSessionDB[] = response.data?.sessions || [];
|
||||
setChatSessions(sessions.map(mapSessionFromDB));
|
||||
} catch (error) {
|
||||
console.error('Failed to load chat sessions', error);
|
||||
}
|
||||
}, []);
|
||||
// Initialize theme on mount
|
||||
useEffect(() => {
|
||||
applyTheme();
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const handler = () => applyTheme();
|
||||
mediaQuery.addEventListener('change', handler);
|
||||
return () => mediaQuery.removeEventListener('change', handler);
|
||||
}, [applyTheme]);
|
||||
|
||||
// Check auth and load sessions
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
setIsAuthenticated(true);
|
||||
}
|
||||
}, []);
|
||||
}, [setIsAuthenticated]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthenticated) {
|
||||
loadChatSessions();
|
||||
loadSessions();
|
||||
}
|
||||
}, [isAuthenticated, loadChatSessions]);
|
||||
}, [isAuthenticated, loadSessions]);
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <AuthPage onLoginSuccess={() => setIsAuthenticated(true)} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-screen w-screen bg-slate-50 text-slate-800 font-sans overflow-hidden">
|
||||
<TopBar
|
||||
mode={mode}
|
||||
setMode={setMode}
|
||||
showSettings={showSettings}
|
||||
setShowSettings={setShowSettings}
|
||||
/>
|
||||
<div className="flex flex-col h-screen w-screen bg-bg-primary text-text-primary font-sans overflow-hidden">
|
||||
<TopBar />
|
||||
|
||||
<div className="flex flex-1 overflow-hidden relative">
|
||||
{showSettings ? (
|
||||
<SettingsLayout settingsTab={settingsTab} setSettingsTab={setSettingsTab} />
|
||||
<SettingsLayout />
|
||||
) : (
|
||||
<>
|
||||
<CollapsibleSidebar
|
||||
mode={mode}
|
||||
isOpen={isSidebarOpen}
|
||||
setIsOpen={setIsSidebarOpen}
|
||||
workTab={workTab}
|
||||
setWorkTab={setWorkTab}
|
||||
agentTab={agentTab}
|
||||
setAgentTab={setAgentTab}
|
||||
/>
|
||||
<CollapsibleSidebar />
|
||||
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
{mode === 'work' && workTab === 'chat' && (
|
||||
<div className="flex-1 p-6 flex overflow-hidden">
|
||||
<div className="flex-1 flex bg-white rounded-3xl shadow-md border border-slate-200 overflow-hidden relative">
|
||||
<LeftPanel
|
||||
activeTab="chats"
|
||||
selectedWorkflow={null}
|
||||
setSelectedWorkflow={() => {}}
|
||||
chatSessions={chatSessions}
|
||||
setChatSessions={setChatSessions}
|
||||
activeSessionId={activeSessionId}
|
||||
setActiveSessionId={setActiveSessionId}
|
||||
onSessionsChanged={loadChatSessions}
|
||||
/>
|
||||
<ChatPanel
|
||||
chatSessions={chatSessions}
|
||||
setChatSessions={setChatSessions}
|
||||
activeSessionId={activeSessionId}
|
||||
setActiveSessionId={setActiveSessionId}
|
||||
onSessionsChanged={loadChatSessions}
|
||||
/>
|
||||
<div className="flex-1 p-4 flex overflow-hidden gap-4">
|
||||
<div className="flex-1 flex bg-bg-card rounded-2xl shadow-sm border border-border-primary overflow-hidden relative">
|
||||
<LeftPanel activeTab="chats" />
|
||||
<ChatPanel />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mode === 'work' && workTab === 'workflow' && (
|
||||
<>
|
||||
{selectedWorkflow === 'new' ? (
|
||||
<>
|
||||
<LeftPanel
|
||||
activeTab="workflows"
|
||||
selectedWorkflow={selectedWorkflow}
|
||||
setSelectedWorkflow={setSelectedWorkflow}
|
||||
/>
|
||||
<NewWorkflowDialog
|
||||
onClose={() => setSelectedWorkflow(null)}
|
||||
onSuccess={(traceId: string) => setSelectedWorkflow(traceId)}
|
||||
/>
|
||||
</>
|
||||
) : selectedWorkflow ? (
|
||||
<>
|
||||
<LeftPanel
|
||||
activeTab="workflows"
|
||||
selectedWorkflow={selectedWorkflow}
|
||||
setSelectedWorkflow={setSelectedWorkflow}
|
||||
/>
|
||||
<RightPanel selectedWorkflow={selectedWorkflow} />
|
||||
</>
|
||||
) : (
|
||||
<WorkflowListView onSelectWorkflow={setSelectedWorkflow} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{mode === 'work' && workTab === 'workflow' && <WorkflowShell />}
|
||||
|
||||
{mode === 'agent' && agentTab === 'agents' && (
|
||||
<AgentLayout agentTab={innerAgentTab} setAgentTab={setInnerAgentTab} />
|
||||
)}
|
||||
{mode === 'agent' && agentTab === 'agents' && <AgentLayout />}
|
||||
|
||||
{mode === 'agent' && agentTab === 'plugin' && (
|
||||
<PluginLayout resourceTab={resourceTab} setResourceTab={setResourceTab} />
|
||||
)}
|
||||
{mode === 'agent' && agentTab === 'plugin' && <PluginLayout />}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@@ -174,4 +87,31 @@ function App() {
|
||||
);
|
||||
}
|
||||
|
||||
function WorkflowShell() {
|
||||
const { selectedWorkflow, setSelectedWorkflow } = useChatStore();
|
||||
|
||||
if (selectedWorkflow === 'new') {
|
||||
return (
|
||||
<>
|
||||
<LeftPanel activeTab="workflows" />
|
||||
<NewWorkflowDialog
|
||||
onClose={() => setSelectedWorkflow(null)}
|
||||
onSuccess={(traceId: string) => setSelectedWorkflow(traceId)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (selectedWorkflow) {
|
||||
return (
|
||||
<>
|
||||
<LeftPanel activeTab="workflows" />
|
||||
<RightPanel selectedWorkflow={selectedWorkflow} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return <WorkflowListView onSelectWorkflow={setSelectedWorkflow} />;
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -1,38 +1,37 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAppStore } from '../../store/useAppStore';
|
||||
import { ProvidersSettings } from './ProvidersSettings';
|
||||
import { WorkerIndividualSettings } from './WorkerIndividualSettings';
|
||||
|
||||
interface AgentLayoutProps {
|
||||
agentTab: string;
|
||||
setAgentTab: (tab: string) => void;
|
||||
}
|
||||
export function AgentLayout() {
|
||||
const { t } = useTranslation();
|
||||
const { innerAgentTab, setInnerAgentTab } = useAppStore();
|
||||
|
||||
const tabs = [
|
||||
{ key: 'worker', label: t('agent.individual') },
|
||||
{ key: 'providers', label: t('agent.providerManagement') },
|
||||
];
|
||||
|
||||
export function AgentLayout({ agentTab, setAgentTab }: AgentLayoutProps) {
|
||||
return (
|
||||
<div className="flex-1 flex flex-col bg-slate-50 overflow-hidden">
|
||||
{/* Top Tabs for Agent Module */}
|
||||
<div className="h-14 border-b border-slate-200 bg-white flex items-center px-6 shadow-sm z-10 shrink-0 space-x-6">
|
||||
<button
|
||||
onClick={() => setAgentTab('worker')}
|
||||
className={`py-4 text-sm font-medium border-b-2 transition-colors ${
|
||||
agentTab === 'worker' ? 'border-blue-600 text-blue-600' : 'border-transparent text-slate-500 hover:text-slate-800'
|
||||
}`}
|
||||
>
|
||||
Individual
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setAgentTab('providers')}
|
||||
className={`py-4 text-sm font-medium border-b-2 transition-colors ${
|
||||
agentTab === 'providers' ? 'border-blue-600 text-blue-600' : 'border-transparent text-slate-500 hover:text-slate-800'
|
||||
}`}
|
||||
>
|
||||
Provider Management
|
||||
</button>
|
||||
<div className="flex-1 flex flex-col bg-bg-secondary overflow-hidden">
|
||||
<div className="h-12 border-b border-border-primary bg-bg-card/80 backdrop-blur flex items-center px-6 shrink-0 gap-1">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.key}
|
||||
onClick={() => setInnerAgentTab(tab.key)}
|
||||
className={`px-4 py-2 text-xs font-semibold rounded-lg transition-all ${
|
||||
innerAgentTab === tab.key
|
||||
? 'bg-accent-light text-accent'
|
||||
: 'text-text-muted hover:text-text-secondary hover:bg-bg-hover'
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex-1 overflow-y-auto p-8">
|
||||
{agentTab === 'worker' && <WorkerIndividualSettings />}
|
||||
{agentTab === 'providers' && <ProvidersSettings />}
|
||||
{innerAgentTab === 'worker' && <WorkerIndividualSettings />}
|
||||
{innerAgentTab === 'providers' && <ProvidersSettings />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Box, Plus, X } from 'lucide-react';
|
||||
import { Box, Plus, X, Server } from 'lucide-react';
|
||||
import type { Provider } from '../../types';
|
||||
import apiClient from '../../api/client';
|
||||
|
||||
@@ -7,12 +7,7 @@ export function ProvidersSettings() {
|
||||
const [providers, setProviders] = useState<Provider[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [formData, setFormData] = useState({
|
||||
provider_type: 'openai',
|
||||
provider_title: '',
|
||||
provider_url: '',
|
||||
provider_apikey: ''
|
||||
});
|
||||
const [formData, setFormData] = useState({ provider_type: 'openai', provider_title: '', provider_url: '', provider_apikey: '' });
|
||||
const [submitLoading, setSubmitLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
@@ -20,9 +15,7 @@ export function ProvidersSettings() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await apiClient.get('/api/v1/provider/list');
|
||||
const data = response.data.provider_list || {};
|
||||
const providerArray: Provider[] = Object.values(data);
|
||||
setProviders(providerArray);
|
||||
setProviders(Object.values(response.data.provider_list || {}));
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch providers", error);
|
||||
setProviders([]);
|
||||
@@ -31,32 +24,7 @@ export function ProvidersSettings() {
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
fetchProviders();
|
||||
}, []);
|
||||
|
||||
const handleOpenModal = () => {
|
||||
setFormData({
|
||||
provider_type: 'openai',
|
||||
provider_title: '',
|
||||
provider_url: '',
|
||||
provider_apikey: ''
|
||||
});
|
||||
setError('');
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setIsModalOpen(false);
|
||||
};
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
[e.target.name]: e.target.value
|
||||
});
|
||||
};
|
||||
useEffect(() => { fetchProviders(); }, []);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -64,15 +32,13 @@ export function ProvidersSettings() {
|
||||
setError('Please fill in all fields.');
|
||||
return;
|
||||
}
|
||||
|
||||
setSubmitLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
await apiClient.post('/api/v1/provider', formData);
|
||||
await fetchProviders();
|
||||
handleCloseModal();
|
||||
setIsModalOpen(false);
|
||||
} catch (err) {
|
||||
console.error("Error adding provider", err);
|
||||
setError('Failed to add provider. Please check your inputs and try again.');
|
||||
} finally {
|
||||
setSubmitLoading(false);
|
||||
@@ -80,174 +46,97 @@ export function ProvidersSettings() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<div className="max-w-4xl mx-auto space-y-6">
|
||||
<div className="flex justify-between items-end">
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-slate-800">Provider Management</h3>
|
||||
<p className="text-sm text-slate-500 mt-1">Configure external AI model providers and API keys.</p>
|
||||
<h3 className="text-lg font-bold text-text-primary">Provider Management</h3>
|
||||
<p className="text-sm text-text-muted mt-0.5">Configure external AI model providers</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleOpenModal}
|
||||
className="flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-sm font-medium transition-colors shadow-sm cursor-pointer"
|
||||
>
|
||||
<Plus size={16} className="mr-2" />
|
||||
Add Provider
|
||||
<button onClick={() => { setFormData({ provider_type: 'openai', provider_title: '', provider_url: '', provider_apikey: '' }); setError(''); setIsModalOpen(true); }}
|
||||
className="flex items-center gap-2 px-4 py-2.5 bg-accent text-white rounded-xl hover:bg-accent-hover transition-all shadow-lg shadow-accent/15 text-sm font-medium">
|
||||
<Plus size={14} /> Add Provider
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="text-center text-slate-500 py-8">Loading providers...</div>
|
||||
<div className="text-center text-text-muted py-12 text-sm">Loading providers...</div>
|
||||
) : providers.length === 0 ? (
|
||||
<div className="text-center text-slate-500 py-8 bg-white rounded-xl border border-slate-200">
|
||||
No providers configured yet. Click "Add Provider" to get started.
|
||||
<div className="text-center text-text-muted py-12 bg-bg-card rounded-2xl border border-border-primary border-dashed text-sm">
|
||||
No providers configured yet
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{providers.map((provider, i) => (
|
||||
<div key={i} className="bg-white border border-slate-200 p-5 rounded-xl shadow-sm hover:border-blue-200 transition-colors flex flex-col justify-between">
|
||||
<div>
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div className="flex items-center">
|
||||
<div className="w-10 h-10 rounded-lg bg-slate-50 border border-slate-100 flex items-center justify-center mr-3">
|
||||
<Box size={20} className="text-slate-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-slate-800">{provider.provider_title}</h4>
|
||||
<span className="text-xs text-slate-500 font-mono uppercase">{provider.provider_type}</span>
|
||||
</div>
|
||||
<div key={i} className="bg-bg-card border border-border-primary rounded-2xl p-5 card-hover">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-9 h-9 rounded-xl bg-bg-secondary border border-border-secondary flex items-center justify-center">
|
||||
<Box size={18} className="text-text-secondary" />
|
||||
</div>
|
||||
<span className={`flex items-center text-xs font-medium px-2 py-1 rounded-md border ${provider.status === 'Connected' ? 'bg-green-50 text-green-700 border-green-200' : 'bg-slate-50 text-slate-500 border-slate-200'}`}>
|
||||
{provider.status === 'Connected' && <span className="w-1.5 h-1.5 rounded-full bg-green-500 mr-1.5"></span>}
|
||||
{provider.status || 'Unknown'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<p className="text-sm text-slate-600 mb-1">URL / Endpoint:</p>
|
||||
<div className="bg-slate-50 border border-slate-100 rounded text-sm px-3 py-1.5 font-mono text-slate-700 truncate" title={provider.provider_url}>
|
||||
{provider.provider_url || 'Default'}
|
||||
<div>
|
||||
<h4 className="font-semibold text-sm text-text-primary">{provider.provider_title}</h4>
|
||||
<span className="text-[10px] text-text-muted font-mono uppercase">{provider.provider_type}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className={`flex items-center gap-1 text-[10px] font-medium px-2 py-1 rounded-lg border ${provider.status === 'Connected' ? 'bg-success-bg text-success border-success/20' : 'bg-bg-secondary text-text-muted border-border-primary'}`}>
|
||||
{provider.status === 'Connected' && <span className="w-1 h-1 rounded-full bg-success" />}
|
||||
{provider.status || 'Unknown'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-end space-x-2 mt-2">
|
||||
<button className="px-3 py-1.5 text-sm font-medium text-slate-600 bg-white border border-slate-200 rounded hover:bg-slate-50 transition-colors cursor-pointer">Edit</button>
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (!confirm('Are you sure you want to delete this provider?')) return;
|
||||
try {
|
||||
await apiClient.delete(`/api/v1/provider/${provider.provider_title}`);
|
||||
fetchProviders();
|
||||
} catch (err) {
|
||||
console.error('Failed to delete provider', err);
|
||||
alert('Failed to delete provider');
|
||||
}
|
||||
}}
|
||||
className="px-3 py-1.5 text-sm font-medium text-red-600 bg-white border border-slate-200 rounded hover:bg-red-50 transition-colors cursor-pointer"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
<div className="bg-bg-secondary rounded-lg px-3 py-2 mb-4">
|
||||
<p className="text-[10px] text-text-muted mb-0.5">Endpoint</p>
|
||||
<p className="text-xs font-mono text-text-secondary truncate">{provider.provider_url || 'Default'}</p>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<button className="px-3 py-1.5 text-xs font-medium text-text-secondary bg-bg-secondary hover:bg-bg-hover rounded-lg transition-colors border border-border-primary">Edit</button>
|
||||
<button onClick={async () => {
|
||||
if (!confirm('Delete this provider?')) return;
|
||||
try { await apiClient.delete(`/api/v1/provider/${provider.provider_title}`); fetchProviders(); } catch { alert('Failed'); }
|
||||
}} className="px-3 py-1.5 text-xs font-medium text-danger bg-danger-bg hover:bg-danger-bg/80 rounded-lg transition-colors border border-danger/20">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Add Provider Modal */}
|
||||
{/* Modal */}
|
||||
{isModalOpen && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm transition-opacity">
|
||||
<div className="bg-white rounded-2xl shadow-xl w-full max-w-md overflow-hidden animate-in fade-in zoom-in-95 duration-200">
|
||||
<div className="flex justify-between items-center p-5 border-b border-slate-100">
|
||||
<h3 className="text-lg font-semibold text-slate-800">Add New Provider</h3>
|
||||
<button
|
||||
onClick={handleCloseModal}
|
||||
className="text-slate-400 hover:text-slate-600 p-1 rounded-md transition-colors cursor-pointer"
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm">
|
||||
<div className="bg-bg-card rounded-2xl shadow-2xl w-full max-w-md overflow-hidden border border-border-primary animate-fade-in-scale">
|
||||
<div className="flex justify-between items-center p-5 border-b border-border-primary">
|
||||
<div className="flex items-center gap-2">
|
||||
<Server size={16} className="text-accent" />
|
||||
<h3 className="text-base font-bold text-text-primary">Add New Provider</h3>
|
||||
</div>
|
||||
<button onClick={() => setIsModalOpen(false)} className="p-1 text-text-muted hover:text-text-primary rounded-lg transition-colors"><X size={18} /></button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="p-6 space-y-4">
|
||||
{error && (
|
||||
<div className="p-3 bg-red-50 text-red-600 text-sm rounded-lg border border-red-100">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="p-5 space-y-4">
|
||||
{error && <div className="p-3 bg-danger-bg text-danger text-sm rounded-xl border border-danger/20">{error}</div>}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1.5">Provider Type</label>
|
||||
<select
|
||||
name="provider_type"
|
||||
value={formData.provider_type}
|
||||
onChange={handleChange}
|
||||
className="w-full bg-slate-50 border border-slate-200 text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all cursor-pointer"
|
||||
>
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">Type</label>
|
||||
<select name="provider_type" value={formData.provider_type} onChange={(e) => setFormData({...formData, provider_type: e.target.value})}
|
||||
className="w-full bg-bg-input border border-border-primary text-sm rounded-xl px-3.5 py-2.5 focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent text-text-primary">
|
||||
<option value="openai">OpenAI</option>
|
||||
<option value="deepseek">DeepSeek</option>
|
||||
<option value="claude">Claude</option>
|
||||
<option value="local">Local</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1.5">Provider Title</label>
|
||||
<input
|
||||
type="text"
|
||||
name="provider_title"
|
||||
placeholder="e.g. My OpenAI Instance"
|
||||
value={formData.provider_title}
|
||||
onChange={handleChange}
|
||||
className="w-full bg-white border border-slate-200 text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all placeholder:text-slate-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1.5">Base URL</label>
|
||||
<input
|
||||
type="url"
|
||||
name="provider_url"
|
||||
placeholder="e.g. https://api.openai.com/v1"
|
||||
value={formData.provider_url}
|
||||
onChange={handleChange}
|
||||
className="w-full bg-white border border-slate-200 text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all placeholder:text-slate-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1.5">API Key</label>
|
||||
<input
|
||||
type="password"
|
||||
name="provider_apikey"
|
||||
placeholder="sk-..."
|
||||
value={formData.provider_apikey}
|
||||
onChange={handleChange}
|
||||
className="w-full bg-white border border-slate-200 text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all placeholder:text-slate-400 font-mono"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 flex justify-end space-x-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCloseModal}
|
||||
className="px-4 py-2 text-sm font-medium text-slate-600 bg-white border border-slate-200 rounded-lg hover:bg-slate-50 transition-colors cursor-pointer"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={submitLoading}
|
||||
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors cursor-pointer disabled:opacity-70 flex items-center"
|
||||
>
|
||||
{submitLoading ? (
|
||||
<span className="flex items-center">
|
||||
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Saving...
|
||||
</span>
|
||||
) : (
|
||||
'Add Provider'
|
||||
)}
|
||||
</button>
|
||||
{['provider_title', 'provider_url', 'provider_apikey'].map((field) => (
|
||||
<div key={field}>
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">
|
||||
{field === 'provider_title' ? 'Title' : field === 'provider_url' ? 'Base URL' : 'API Key'}
|
||||
</label>
|
||||
<input type={field === 'provider_apikey' ? 'password' : field === 'provider_url' ? 'url' : 'text'}
|
||||
name={field} value={(formData as any)[field]}
|
||||
onChange={(e) => setFormData({...formData, [field]: e.target.value})}
|
||||
placeholder={field === 'provider_title' ? 'My OpenAI' : field === 'provider_url' ? 'https://api.openai.com/v1' : 'sk-...'}
|
||||
className="w-full bg-bg-input border border-border-primary text-sm rounded-xl px-3.5 py-2.5 focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent text-text-primary placeholder:text-text-muted/50 font-mono" />
|
||||
</div>
|
||||
))}
|
||||
<div className="pt-2 flex justify-end gap-2">
|
||||
<button type="button" onClick={() => setIsModalOpen(false)} className="px-4 py-2 text-sm font-medium text-text-secondary hover:bg-bg-hover rounded-xl transition-colors">Cancel</button>
|
||||
<button type="submit" disabled={submitLoading} className="px-4 py-2 text-sm font-medium text-white bg-accent rounded-xl hover:bg-accent-hover transition-colors disabled:opacity-50">{submitLoading ? 'Saving...' : 'Add Provider'}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import apiClient from '../../api/client';
|
||||
import { Save, Plus, Edit2, Trash2, X } from 'lucide-react';
|
||||
import { Save, Plus, Edit2, Trash2, X, Bot } from 'lucide-react';
|
||||
import type { Provider } from '../../types';
|
||||
|
||||
interface WorkerIndividual {
|
||||
@@ -11,10 +11,10 @@ interface WorkerIndividual {
|
||||
provider_title: string;
|
||||
model_id: string;
|
||||
system_prompt?: string;
|
||||
output_template?: string; // Change to string for the form state
|
||||
bound_skill?: string; // Change to string for the form state
|
||||
workspace?: string; // Change to string for the form state
|
||||
tools?: string; // Form state for tools JSON array
|
||||
output_template?: string;
|
||||
bound_skill?: string;
|
||||
workspace?: string;
|
||||
tools?: string;
|
||||
}
|
||||
|
||||
export function WorkerIndividualSettings() {
|
||||
@@ -25,12 +25,11 @@ export function WorkerIndividualSettings() {
|
||||
const [availableTools, setAvailableTools] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [editData, setEditData] = useState<Partial<WorkerIndividual>>({});
|
||||
const [isNew, setIsNew] = useState(false);
|
||||
|
||||
const [modalMessage, setModalMessage] = useState('');
|
||||
const [submitLoading, setSubmitLoading] = useState(false);
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
@@ -44,43 +43,34 @@ export function WorkerIndividualSettings() {
|
||||
]);
|
||||
setProviders(Object.values(provRes.data.provider_list || {}));
|
||||
setWorkers(workRes.data.workers || []);
|
||||
|
||||
const allTools = toolsRes.data.tools || [];
|
||||
setAvailableTools(allTools);
|
||||
setAvailableTools(toolsRes.data.tools || []);
|
||||
setAvailableSkills(Object.keys(skillsRes.data.skills || {}));
|
||||
|
||||
const sysNodesData = sysRes.data.system_nodes || [];
|
||||
const defaultSysNodes = ['regulatory_node', 'consciousness_node', 'control_node'];
|
||||
|
||||
const providersList = Object.values(provRes.data.provider_list || {}) as Provider[];
|
||||
const defaultProvider = providersList.length > 0 ? providersList[0].provider_title : '';
|
||||
const sysNodesData = sysRes.data.system_nodes || [];
|
||||
const defaultSysNodes = ['regulatory_node', 'consciousness_node', 'control_node'];
|
||||
|
||||
const formattedSysNodes = defaultSysNodes.map(nodeName => {
|
||||
setSystemNodes(defaultSysNodes.map(nodeName => {
|
||||
const found = sysNodesData.find((n: any) => n.node_name === nodeName);
|
||||
return {
|
||||
agent_id: nodeName,
|
||||
agent_name: nodeName,
|
||||
agent_type: 'System Node',
|
||||
provider_title: found && found.provider_title ? found.provider_title : defaultProvider,
|
||||
model_id: found && found.model_id ? found.model_id : '',
|
||||
tools: found && found.tools ? JSON.stringify(found.tools) : '[]',
|
||||
agent_id: nodeName, agent_name: nodeName, agent_type: 'System Node',
|
||||
provider_title: found?.provider_title || defaultProvider,
|
||||
model_id: found?.model_id || '',
|
||||
tools: found?.tools ? JSON.stringify(found.tools) : '[]',
|
||||
is_system: true
|
||||
};
|
||||
});
|
||||
setSystemNodes(formattedSysNodes);
|
||||
}));
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
setError('Failed to load data');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
useEffect(() => { fetchData(); }, []);
|
||||
|
||||
const handleEdit = (worker: any) => { // Accept the backend object which might have objects instead of strings
|
||||
const handleEdit = (worker: any) => {
|
||||
setEditData({
|
||||
...worker,
|
||||
output_template: typeof worker.output_template === 'string' ? worker.output_template : JSON.stringify(worker.output_template || {}),
|
||||
@@ -94,46 +84,31 @@ export function WorkerIndividualSettings() {
|
||||
};
|
||||
|
||||
const handleAddNew = () => {
|
||||
setEditData({
|
||||
agent_name: '',
|
||||
agent_type: 'ordinary_individual',
|
||||
description: '',
|
||||
provider_title: providers.length > 0 ? providers[0].provider_title : '',
|
||||
model_id: '',
|
||||
system_prompt: '',
|
||||
output_template: '{}',
|
||||
bound_skill: '{}',
|
||||
workspace: '[]',
|
||||
tools: '[]'
|
||||
});
|
||||
setEditData({ agent_name: '', agent_type: 'ordinary_individual', description: '',
|
||||
provider_title: providers.length > 0 ? providers[0].provider_title : '', model_id: '',
|
||||
system_prompt: '', output_template: '{}', bound_skill: '{}', workspace: '[]', tools: '[]' });
|
||||
setIsNew(true);
|
||||
setIsEditing(true);
|
||||
setModalMessage('');
|
||||
};
|
||||
|
||||
const handleDelete = async (agent_id: string) => {
|
||||
if (!confirm('Are you sure you want to delete this agent?')) return;
|
||||
try {
|
||||
await apiClient.delete(`/api/v1/agent/worker/${agent_id}`);
|
||||
fetchData();
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
alert('Failed to delete agent');
|
||||
}
|
||||
if (!confirm('Delete this agent?')) return;
|
||||
try { await apiClient.delete(`/api/v1/agent/worker/${agent_id}`); fetchData(); } catch { alert('Failed'); }
|
||||
};
|
||||
|
||||
const handleModalSave = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setModalMessage('');
|
||||
setSubmitLoading(true);
|
||||
try {
|
||||
if ((editData as any).is_system) {
|
||||
const payload = {
|
||||
await apiClient.post('/api/v1/agent', {
|
||||
individual_name: editData.agent_name,
|
||||
provider_title: editData.provider_title,
|
||||
model_id: editData.model_id,
|
||||
tools: JSON.parse(editData.tools || '[]')
|
||||
};
|
||||
await apiClient.post('/api/v1/agent', payload);
|
||||
});
|
||||
} else {
|
||||
const payload = {
|
||||
...editData,
|
||||
@@ -142,305 +117,208 @@ export function WorkerIndividualSettings() {
|
||||
workspace: JSON.parse(editData.workspace || '[]'),
|
||||
tools: JSON.parse(editData.tools || '[]')
|
||||
};
|
||||
|
||||
if (isNew) {
|
||||
await apiClient.post('/api/v1/agent/worker', payload);
|
||||
} else {
|
||||
await apiClient.put(`/api/v1/agent/worker/${editData.agent_id}`, payload);
|
||||
}
|
||||
if (isNew) await apiClient.post('/api/v1/agent/worker', payload);
|
||||
else await apiClient.put(`/api/v1/agent/worker/${editData.agent_id}`, payload);
|
||||
}
|
||||
|
||||
setIsEditing(false);
|
||||
fetchData();
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
setModalMessage(err.response?.data?.detail || err.message || 'Failed to save');
|
||||
} finally {
|
||||
setSubmitLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getTypeBadge = (type: string, isSystem?: boolean) => {
|
||||
if (isSystem) return <span className="px-2 py-0.5 rounded-md text-[10px] font-bold bg-accent-light text-accent uppercase tracking-wider">System</span>;
|
||||
const colors: Record<string, string> = {
|
||||
ordinary_individual: 'bg-bg-secondary text-text-muted',
|
||||
skill_individual: 'bg-success-bg text-success',
|
||||
special_individual: 'bg-warning-bg text-warning',
|
||||
};
|
||||
return <span className={`px-2 py-0.5 rounded-md text-[10px] font-medium ${colors[type] || colors.ordinary_individual}`}>{type.replace('_', ' ')}</span>;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-5xl space-y-6 relative">
|
||||
<div className="mb-8 flex justify-between items-end">
|
||||
<div className="max-w-5xl space-y-6">
|
||||
<div className="flex justify-between items-end">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-800">Individual</h1>
|
||||
<p className="text-slate-500 mt-1">Manage all system nodes and custom workers.</p>
|
||||
<h1 className="text-lg font-bold text-text-primary">Individual</h1>
|
||||
<p className="text-sm text-text-muted mt-0.5">Manage system nodes and custom workers</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleAddNew}
|
||||
className="flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
<Plus size={16} className="mr-2" />
|
||||
Add Worker
|
||||
<button onClick={handleAddNew} className="flex items-center gap-2 px-4 py-2.5 bg-accent text-white rounded-xl hover:bg-accent-hover transition-all shadow-lg shadow-accent/15 text-sm font-medium">
|
||||
<Plus size={14} /> Add Worker
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && <div className="text-red-600">{error}</div>}
|
||||
{error && <div className="text-sm text-danger bg-danger-bg border border-danger/20 rounded-xl p-3">{error}</div>}
|
||||
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
|
||||
<div className="p-0">
|
||||
{loading ? (
|
||||
<div className="p-6 text-slate-500">Loading...</div>
|
||||
) : (workers.length === 0 && systemNodes.length === 0) ? (
|
||||
<div className="p-6 text-slate-500">No individuals found.</div>
|
||||
) : (
|
||||
<table className="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-slate-50 border-b border-slate-200 text-slate-600 text-sm">
|
||||
<th className="p-4 font-semibold">Name</th>
|
||||
<th className="p-4 font-semibold">Type</th>
|
||||
<th className="p-4 font-semibold">Provider / Model ID</th>
|
||||
<th className="p-4 font-semibold text-right">Actions</th>
|
||||
<div className="bg-bg-card rounded-2xl border border-border-primary shadow-sm overflow-hidden">
|
||||
{loading ? (
|
||||
<div className="p-6 text-text-muted text-sm">Loading...</div>
|
||||
) : (workers.length === 0 && systemNodes.length === 0) ? (
|
||||
<div className="p-6 text-text-muted text-sm">No individuals found.</div>
|
||||
) : (
|
||||
<table className="w-full text-left text-sm">
|
||||
<thead>
|
||||
<tr className="bg-bg-secondary border-b border-border-primary text-text-muted text-xs uppercase tracking-wider">
|
||||
<th className="px-5 py-3 font-semibold">Name</th>
|
||||
<th className="px-5 py-3 font-semibold">Type</th>
|
||||
<th className="px-5 py-3 font-semibold">Provider / Model</th>
|
||||
<th className="px-5 py-3 font-semibold text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border-secondary">
|
||||
{systemNodes.map((w) => (
|
||||
<tr key={w.agent_id} className="bg-bg-secondary/50 hover:bg-bg-hover transition-colors">
|
||||
<td className="px-5 py-3">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="w-7 h-7 rounded-lg bg-accent-light flex items-center justify-center">
|
||||
<Bot size={14} className="text-accent" />
|
||||
</div>
|
||||
<span className="font-medium text-text-primary text-xs">{w.agent_name}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-5 py-3">{getTypeBadge(w.agent_type, true)}</td>
|
||||
<td className="px-5 py-3 text-xs text-text-muted">{w.provider_title} / {w.model_id}</td>
|
||||
<td className="px-5 py-3 text-right">
|
||||
<button onClick={() => handleEdit(w)} className="p-1.5 text-text-muted hover:text-accent hover:bg-accent-light rounded-lg transition-all"><Edit2 size={14} /></button>
|
||||
</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{systemNodes.map((w) => (
|
||||
<tr key={w.agent_id} className="border-b border-slate-100 bg-slate-50 hover:bg-slate-100 transition-colors">
|
||||
<td className="p-4 font-medium text-slate-800">{w.agent_name}</td>
|
||||
<td className="p-4 text-slate-600">
|
||||
<span className="px-2 py-1 bg-blue-100 text-blue-800 rounded text-xs">{w.agent_type}</span>
|
||||
</td>
|
||||
<td className="p-4 text-slate-600 text-sm">
|
||||
{w.provider_title} <span className="text-slate-400">/</span> {w.model_id}
|
||||
</td>
|
||||
<td className="p-4 text-right space-x-2">
|
||||
<button onClick={() => handleEdit(w)} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors" title="Edit">
|
||||
<Edit2 size={16} />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{workers.map((w) => (
|
||||
<tr key={w.agent_id} className="border-b border-slate-100 hover:bg-slate-50 transition-colors">
|
||||
<td className="p-4 font-medium text-slate-800">{w.agent_name}</td>
|
||||
<td className="p-4 text-slate-600">
|
||||
<span className="px-2 py-1 bg-slate-100 rounded text-xs">{w.agent_type}</span>
|
||||
</td>
|
||||
<td className="p-4 text-slate-600 text-sm">
|
||||
{w.provider_title} <span className="text-slate-400">/</span> {w.model_id}
|
||||
</td>
|
||||
<td className="p-4 text-right space-x-2">
|
||||
<button onClick={() => handleEdit(w)} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors" title="Edit">
|
||||
<Edit2 size={16} />
|
||||
</button>
|
||||
<button onClick={() => handleDelete(w.agent_id)} className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors" title="Delete">
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{workers.map((w) => (
|
||||
<tr key={w.agent_id} className="hover:bg-bg-hover transition-colors">
|
||||
<td className="px-5 py-3">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="w-7 h-7 rounded-lg bg-bg-secondary border border-border-primary flex items-center justify-center">
|
||||
<Bot size={14} className="text-text-muted" />
|
||||
</div>
|
||||
<span className="font-medium text-text-primary text-xs">{w.agent_name}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-5 py-3">{getTypeBadge(w.agent_type)}</td>
|
||||
<td className="px-5 py-3 text-xs text-text-muted">{w.provider_title} / {w.model_id}</td>
|
||||
<td className="px-5 py-3 text-right">
|
||||
<button onClick={() => handleEdit(w)} className="p-1.5 text-text-muted hover:text-accent hover:bg-accent-light rounded-lg transition-all mr-0.5"><Edit2 size={14} /></button>
|
||||
<button onClick={() => handleDelete(w.agent_id)} className="p-1.5 text-text-muted hover:text-danger hover:bg-danger-bg rounded-lg transition-all"><Trash2 size={14} /></button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Edit/Create Modal */}
|
||||
{/* Modal */}
|
||||
{isEditing && (
|
||||
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
|
||||
<div className="bg-white rounded-xl shadow-xl w-full max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
<div className="flex justify-between items-center p-6 border-b border-slate-100 sticky top-0 bg-white z-10">
|
||||
<h2 className="text-xl font-bold text-slate-800">
|
||||
{(editData as any).is_system ? 'Edit System Node' : (isNew ? 'Create Worker' : 'Edit Worker')}
|
||||
</h2>
|
||||
<button onClick={() => setIsEditing(false)} className="text-slate-400 hover:text-slate-600">
|
||||
<X size={24} />
|
||||
</button>
|
||||
<div className="bg-bg-card rounded-2xl shadow-2xl w-full max-w-2xl max-h-[90vh] overflow-y-auto border border-border-primary">
|
||||
<div className="flex justify-between items-center p-5 border-b border-border-primary sticky top-0 bg-bg-card z-10">
|
||||
<h2 className="text-base font-bold text-text-primary">{(editData as any).is_system ? 'Edit System Node' : (isNew ? 'Create Worker' : 'Edit Worker')}</h2>
|
||||
<button onClick={() => setIsEditing(false)} className="p-1 text-text-muted hover:text-text-primary rounded-lg transition-colors"><X size={20} /></button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleModalSave} className="p-6 space-y-4">
|
||||
<form onSubmit={handleModalSave} className="p-5 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
|
||||
type="text"
|
||||
required
|
||||
value={editData.agent_name || ''}
|
||||
onChange={(e) => setEditData({...editData, agent_name: e.target.value})}
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
disabled={(editData as any).is_system}
|
||||
/>
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">Name</label>
|
||||
<input type="text" required value={editData.agent_name || ''} onChange={(e) => setEditData({...editData, agent_name: e.target.value})}
|
||||
className="w-full px-3 py-2 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent" disabled={(editData as any).is_system} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Agent Type</label>
|
||||
<select
|
||||
value={editData.agent_type || 'ordinary_individual'}
|
||||
onChange={(e) => setEditData({...editData, agent_type: e.target.value})}
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
disabled={(editData as any).is_system}
|
||||
>
|
||||
<option value="ordinary_individual">Ordinary Individual</option>
|
||||
<option value="skill_individual">Skill Individual</option>
|
||||
<option value="special_individual">Special Individual</option>
|
||||
{(editData as any).is_system && (
|
||||
<option value="System Node">System Node</option>
|
||||
)}
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">Type</label>
|
||||
<select value={editData.agent_type || 'ordinary_individual'} onChange={(e) => setEditData({...editData, agent_type: e.target.value})}
|
||||
className="w-full px-3 py-2 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent" disabled={(editData as any).is_system}>
|
||||
<option value="ordinary_individual">Ordinary</option>
|
||||
<option value="skill_individual">Skill</option>
|
||||
<option value="special_individual">Special</option>
|
||||
{(editData as any).is_system && <option value="System Node">System Node</option>}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">Provider Title</label>
|
||||
<select
|
||||
value={editData.provider_title || ''}
|
||||
onChange={(e) => setEditData({...editData, provider_title: e.target.value, model_id: ''})}
|
||||
required
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="" disabled>Select Provider</option>
|
||||
{providers.map((p) => (
|
||||
<option key={p.provider_title} value={p.provider_title}>{p.provider_title}</option>
|
||||
))}
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">Provider</label>
|
||||
<select value={editData.provider_title || ''} onChange={(e) => setEditData({...editData, provider_title: e.target.value, model_id: ''})} required
|
||||
className="w-full px-3 py-2 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent">
|
||||
<option value="" disabled>Select</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>
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">Model</label>
|
||||
{(() => {
|
||||
const selectedProvider = providers.find(p => p.provider_title === editData.provider_title);
|
||||
const models = selectedProvider?.provider_models || [];
|
||||
const sp = providers.find(p => p.provider_title === editData.provider_title);
|
||||
const models = sp?.provider_models || [];
|
||||
return (
|
||||
<select
|
||||
value={editData.model_id || ''}
|
||||
onChange={(e) => setEditData({...editData, model_id: e.target.value})}
|
||||
required
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="" disabled>Select a model</option>
|
||||
<select value={editData.model_id || ''} onChange={(e) => setEditData({...editData, model_id: e.target.value})} required
|
||||
className="w-full px-3 py-2 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent">
|
||||
<option value="" disabled>Select</option>
|
||||
{models.map(m => <option key={m} value={m}>{m}</option>)}
|
||||
</select>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!(editData as any).is_system && (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Description</label>
|
||||
<textarea
|
||||
value={editData.description || ''}
|
||||
onChange={(e) => setEditData({...editData, description: e.target.value})}
|
||||
rows={2}
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">Description</label>
|
||||
<textarea value={editData.description || ''} onChange={(e) => setEditData({...editData, description: e.target.value})} rows={2}
|
||||
className="w-full px-3 py-2 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">System Prompt</label>
|
||||
<textarea
|
||||
value={editData.system_prompt || ''}
|
||||
onChange={(e) => setEditData({...editData, system_prompt: e.target.value})}
|
||||
rows={3}
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500 font-mono text-sm"
|
||||
/>
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">System Prompt</label>
|
||||
<textarea value={editData.system_prompt || ''} onChange={(e) => setEditData({...editData, system_prompt: e.target.value})} rows={3}
|
||||
className="w-full px-3 py-2 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary font-mono focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent" />
|
||||
</div>
|
||||
|
||||
<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">Output Template (JSON)</label>
|
||||
<textarea
|
||||
value={editData.output_template || '{}'}
|
||||
onChange={(e) => setEditData({...editData, output_template: e.target.value})}
|
||||
rows={3}
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500 font-mono text-sm"
|
||||
/>
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">Output Template (JSON)</label>
|
||||
<textarea value={editData.output_template || '{}'} onChange={(e) => setEditData({...editData, output_template: e.target.value})} rows={3}
|
||||
className="w-full px-3 py-2 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary font-mono focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Bound Skill (Select)</label>
|
||||
<select
|
||||
value={(() => {
|
||||
try {
|
||||
const parsed = JSON.parse(editData.bound_skill || '{}');
|
||||
return Object.keys(parsed)[0] || '';
|
||||
} catch { return ''; }
|
||||
})()}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
const newSkill = val ? { [val]: [] } : {};
|
||||
setEditData({...editData, bound_skill: JSON.stringify(newSkill)});
|
||||
}}
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
disabled={editData.agent_type !== 'skill_individual'}
|
||||
>
|
||||
<option value="">No Skill Bound</option>
|
||||
{availableSkills.map(skill => (
|
||||
<option key={skill} value={skill}>{skill}</option>
|
||||
))}
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">Bound Skill</label>
|
||||
<select value={(() => { try { return Object.keys(JSON.parse(editData.bound_skill || '{}'))[0] || ''; } catch { return ''; } })()}
|
||||
onChange={(e) => setEditData({...editData, bound_skill: JSON.stringify(e.target.value ? { [e.target.value]: [] } : {})})}
|
||||
className="w-full px-3 py-2 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent" disabled={editData.agent_type !== 'skill_individual'}>
|
||||
<option value="">None</option>
|
||||
{availableSkills.map(s => <option key={s} value={s}>{s}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Workspace (JSON Array)</label>
|
||||
<textarea
|
||||
value={editData.workspace || '[]'}
|
||||
onChange={(e) => setEditData({...editData, workspace: e.target.value})}
|
||||
rows={2}
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500 font-mono text-sm"
|
||||
/>
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">Workspace (JSON)</label>
|
||||
<textarea value={editData.workspace || '[]'} onChange={(e) => setEditData({...editData, workspace: e.target.value})} rows={2}
|
||||
className="w-full px-3 py-2 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary font-mono focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent" />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Tools (Select Multiple)</label>
|
||||
<div className="flex flex-wrap gap-2 p-4 border border-slate-200 rounded-lg max-h-48 overflow-y-auto">
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">Tools</label>
|
||||
<div className="flex flex-wrap gap-1.5 p-3 bg-bg-input border border-border-primary rounded-xl max-h-40 overflow-y-auto">
|
||||
{availableTools.map(tool => {
|
||||
let currentTools: string[] = [];
|
||||
try {
|
||||
currentTools = JSON.parse(editData.tools || '[]');
|
||||
} catch { currentTools = []; }
|
||||
|
||||
try { currentTools = JSON.parse(editData.tools || '[]'); } catch { }
|
||||
const isSelected = currentTools.includes(tool);
|
||||
return (
|
||||
<button
|
||||
key={tool}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
let updatedTools = [...currentTools];
|
||||
if (isSelected) {
|
||||
updatedTools = updatedTools.filter(t => t !== tool);
|
||||
} else {
|
||||
updatedTools.push(tool);
|
||||
}
|
||||
setEditData({...editData, tools: JSON.stringify(updatedTools)});
|
||||
}}
|
||||
className={`px-3 py-1.5 text-sm rounded-full transition-colors ${
|
||||
isSelected
|
||||
? 'bg-blue-100 text-blue-700 border border-blue-200'
|
||||
: 'bg-slate-50 text-slate-600 border border-slate-200 hover:bg-slate-100'
|
||||
}`}
|
||||
>
|
||||
<button key={tool} type="button" onClick={() => {
|
||||
const updated = isSelected ? currentTools.filter(t => t !== tool) : [...currentTools, tool];
|
||||
setEditData({...editData, tools: JSON.stringify(updated)});
|
||||
}}
|
||||
className={`px-2.5 py-1 rounded-lg text-xs font-medium transition-all ${isSelected ? 'bg-accent-light text-accent border border-accent/20' : 'bg-bg-secondary text-text-muted border border-border-primary hover:border-text-muted'}`}>
|
||||
{tool}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{availableTools.length === 0 && (
|
||||
<span className="text-sm text-slate-500">No tools available</span>
|
||||
)}
|
||||
{availableTools.length === 0 && <span className="text-xs text-text-muted">No tools</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{modalMessage && (
|
||||
<div className="p-3 bg-red-50 text-red-700 text-sm rounded-lg">
|
||||
{modalMessage}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="pt-4 flex justify-end space-x-3 border-t border-slate-100">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsEditing(false)}
|
||||
className="px-4 py-2 text-slate-600 hover:bg-slate-100 rounded-lg transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
<Save size={16} className="mr-2" />
|
||||
Save Worker
|
||||
{modalMessage && <div className="p-3 bg-danger-bg text-danger text-sm rounded-xl border border-danger/20">{modalMessage}</div>}
|
||||
<div className="pt-3 flex justify-end gap-2 border-t border-border-primary">
|
||||
<button type="button" onClick={() => setIsEditing(false)} className="px-4 py-2 text-sm font-medium text-text-secondary hover:bg-bg-hover rounded-xl transition-colors">Cancel</button>
|
||||
<button type="submit" disabled={submitLoading} className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-accent rounded-xl hover:bg-accent-hover transition-colors disabled:opacity-50">
|
||||
<Save size={14} /> {submitLoading ? 'Saving...' : 'Save'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import apiClient from '../../api/client';
|
||||
import { Activity } from 'lucide-react';
|
||||
import { Zap, ArrowRight, ShieldCheck } from 'lucide-react';
|
||||
|
||||
interface AuthPageProps {
|
||||
onLoginSuccess: () => void;
|
||||
}
|
||||
|
||||
export function AuthPage({ onLoginSuccess }: AuthPageProps) {
|
||||
const { t } = useTranslation();
|
||||
const [isLogin, setIsLogin] = useState(true);
|
||||
const [userName, setUserName] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
@@ -20,28 +22,22 @@ export function AuthPage({ onLoginSuccess }: AuthPageProps) {
|
||||
|
||||
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.');
|
||||
setError(t('auth.registerSuccess'));
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
@@ -53,70 +49,97 @@ export function AuthPage({ onLoginSuccess }: AuthPageProps) {
|
||||
};
|
||||
|
||||
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 className="flex min-h-screen w-full relative overflow-hidden">
|
||||
{/* Background decoration */}
|
||||
<div className="absolute inset-0 bg-bg-primary">
|
||||
<div className="absolute top-0 left-1/4 w-96 h-96 bg-accent/5 rounded-full blur-3xl" />
|
||||
<div className="absolute bottom-0 right-1/4 w-96 h-96 bg-glow-purple/5 rounded-full blur-3xl" />
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 flex w-full items-center justify-center p-6">
|
||||
<div className="w-full max-w-md animate-fade-in-scale">
|
||||
{/* Logo */}
|
||||
<div className="flex flex-col items-center mb-8">
|
||||
<div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-accent to-glow-purple flex items-center justify-center text-white shadow-xl shadow-accent/20 mb-5">
|
||||
<Zap size={28} fill="currentColor" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-text-primary tracking-tight">{t('app.name')}</h1>
|
||||
<p className="text-sm text-text-muted mt-1.5">{t('app.tagline')}</p>
|
||||
</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'}
|
||||
|
||||
<div className="bg-bg-card/80 backdrop-blur-xl rounded-2xl border border-border-primary shadow-xl shadow-black/5 p-8">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<ShieldCheck size={18} className="text-accent" />
|
||||
<h2 className="text-lg font-semibold text-text-primary">
|
||||
{isLogin ? t('auth.welcomeBack') : t('auth.createAccount')}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className={`mb-5 p-3 rounded-xl text-sm border ${error.includes('success') || error.includes('成功') ? 'bg-success-bg/50 text-success border-success/20' : 'bg-danger-bg/50 text-danger border-danger/20'}`}>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('auth.username')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={userName}
|
||||
onChange={(e) => setUserName(e.target.value)}
|
||||
className="w-full px-4 py-2.5 bg-bg-input border border-border-primary rounded-xl focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent transition-all text-text-primary placeholder:text-text-muted/60 text-sm"
|
||||
placeholder="Enter your username"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('auth.password')}</label>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full px-4 py-2.5 bg-bg-input border border-border-primary rounded-xl focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent transition-all text-text-primary placeholder:text-text-muted/60 text-sm"
|
||||
placeholder="Enter your password"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full py-2.5 bg-accent text-white rounded-xl font-semibold hover:bg-accent-hover focus:outline-none focus:ring-2 focus:ring-accent/30 transition-all disabled:opacity-50 cursor-pointer text-sm flex items-center justify-center gap-2 group"
|
||||
>
|
||||
{loading ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
||||
{t('auth.processing')}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
{isLogin ? t('auth.signIn') : t('auth.signUp')}
|
||||
<ArrowRight size={16} className="transition-transform group-hover:translate-x-0.5" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="mt-6 text-center text-sm text-text-muted">
|
||||
{isLogin ? t('auth.noAccount') : t('auth.hasAccount')}{' '}
|
||||
<button
|
||||
onClick={() => { setIsLogin(!isLogin); setError(''); }}
|
||||
className="text-accent font-semibold hover:text-accent-hover transition-colors"
|
||||
>
|
||||
{isLogin ? t('auth.signUp') : t('auth.signIn')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-center text-xs text-text-muted/60 mt-6">
|
||||
KiloStar Distributed Multi-Agent System
|
||||
</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>
|
||||
);
|
||||
|
||||
@@ -1,275 +1,213 @@
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { MessageSquare, Activity, ChevronRight, Plus } from 'lucide-react';
|
||||
import apiClient from '../../api/client';
|
||||
import type { ChatSession, Message } from '../../App';
|
||||
import type { ChatMessageDB } from '../../types';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { MessageSquare, Activity, ArrowUp, Plus, Sparkles, Code, FileText, Search } from 'lucide-react';
|
||||
import { useChatStore } from '../../store/useChatStore';
|
||||
|
||||
interface ChatPanelProps {
|
||||
chatSessions: ChatSession[];
|
||||
setChatSessions: React.Dispatch<React.SetStateAction<ChatSession[]>>;
|
||||
activeSessionId: string | null;
|
||||
setActiveSessionId: React.Dispatch<React.SetStateAction<string | null>>;
|
||||
onSessionsChanged?: () => void;
|
||||
}
|
||||
const QUICK_ACTIONS = [
|
||||
{ icon: Sparkles, label: ' brainstorm', prompt: '帮我头脑风暴一些创意' },
|
||||
{ icon: Code, label: '写代码', prompt: '帮我写一段 Python 代码' },
|
||||
{ icon: FileText, label: '总结文档', prompt: '帮我总结这篇文档' },
|
||||
{ icon: Search, label: '查找资料', prompt: '帮我搜索相关资料' },
|
||||
];
|
||||
|
||||
export function ChatPanel({ chatSessions, setChatSessions, activeSessionId, setActiveSessionId, onSessionsChanged }: ChatPanelProps) {
|
||||
export function ChatPanel() {
|
||||
const { t } = useTranslation();
|
||||
const [input, setInput] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [mode, setMode] = useState<'chat' | 'deploy'>('chat');
|
||||
const [loadingMessages, setLoadingMessages] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const activeSession = chatSessions.find((s) => s.id === activeSessionId) || null;
|
||||
const {
|
||||
sessions,
|
||||
activeSessionId,
|
||||
loadingMessages,
|
||||
loadMessages,
|
||||
createChat,
|
||||
sendMessage,
|
||||
} = useChatStore();
|
||||
|
||||
const activeSession = sessions.find((s) => s.id === activeSessionId) || null;
|
||||
const messages = activeSession ? activeSession.messages : [];
|
||||
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
}, [messages]);
|
||||
|
||||
const loadMessages = useCallback(async (chatId: string) => {
|
||||
setLoadingMessages(true);
|
||||
try {
|
||||
const response = await apiClient.get(`/api/v1/chat/${chatId}`);
|
||||
const dbMessages: ChatMessageDB[] = response.data?.messages || [];
|
||||
const mapped: Message[] = dbMessages.map((m) => ({
|
||||
id: m.message_id,
|
||||
role: m.message_owner === 'user' ? 'user' : 'assistant',
|
||||
content: m.message,
|
||||
timestamp: new Date(m.created_at).getTime(),
|
||||
}));
|
||||
setChatSessions((prev) =>
|
||||
prev.map((s) => (s.id === chatId ? { ...s, messages: mapped } : s))
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Failed to load messages', error);
|
||||
} finally {
|
||||
setLoadingMessages(false);
|
||||
}
|
||||
}, [setChatSessions]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeSessionId) {
|
||||
const session = chatSessions.find((s) => s.id === activeSessionId);
|
||||
const session = sessions.find((s) => s.id === activeSessionId);
|
||||
if (session && session.messages.length === 0) {
|
||||
loadMessages(activeSessionId);
|
||||
}
|
||||
}
|
||||
}, [activeSessionId]);
|
||||
|
||||
const updateSessionMessages = (newMessages: Message[]) => {
|
||||
if (!activeSessionId) return;
|
||||
setChatSessions((prev) =>
|
||||
prev.map((s) =>
|
||||
s.id === activeSessionId
|
||||
? { ...s, messages: newMessages, updatedAt: Date.now() }
|
||||
: s
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const handleNewChat = async () => {
|
||||
if (!onSessionsChanged) return;
|
||||
try {
|
||||
const response = await apiClient.post('/api/v1/chat', {
|
||||
title: '新对话',
|
||||
initial_message: '你好',
|
||||
});
|
||||
const chatId: string = response.data.chat_id;
|
||||
const reply: string = response.data.reply || '你好!我是 kilostar 助手,有什么可以帮你的吗?';
|
||||
|
||||
const newSession: ChatSession = {
|
||||
id: chatId,
|
||||
title: '新对话',
|
||||
messages: [
|
||||
{ id: chatId + '_user', role: 'user', content: '你好', timestamp: Date.now() },
|
||||
{ id: chatId + '_ai', role: 'assistant', content: reply, timestamp: Date.now() },
|
||||
],
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
setChatSessions((prev) => [newSession, ...prev]);
|
||||
setActiveSessionId(chatId);
|
||||
onSessionsChanged();
|
||||
} catch (error) {
|
||||
console.error('Failed to create chat session', error);
|
||||
}
|
||||
await createChat(t('chat.newChat'), '你好');
|
||||
};
|
||||
|
||||
const handleSendMessage = async () => {
|
||||
if (!input.trim() || !activeSessionId) return;
|
||||
|
||||
const userText = input;
|
||||
const userMessage: Message = {
|
||||
id: Date.now().toString(),
|
||||
role: 'user',
|
||||
content: userText,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
const currentMessages = activeSession?.messages || [];
|
||||
updateSessionMessages([...currentMessages, userMessage]);
|
||||
const text = input.trim();
|
||||
setInput('');
|
||||
setLoading(true);
|
||||
await sendMessage(activeSessionId, text);
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await apiClient.post(`/api/v1/chat/${activeSessionId}/reply`, {
|
||||
message: userText,
|
||||
const handleQuickAction = (prompt: string) => {
|
||||
if (!activeSessionId) {
|
||||
createChat(prompt.slice(0, 20), prompt).then((id) => {
|
||||
if (id) sendMessage(id, prompt);
|
||||
});
|
||||
|
||||
const replyContent: string = response.data?.reply || '收到你的消息。';
|
||||
|
||||
if (currentMessages.length <= 1 && userText.length > 0) {
|
||||
setChatSessions((prev) =>
|
||||
prev.map((s) =>
|
||||
s.id === activeSessionId
|
||||
? { ...s, title: userText.slice(0, 20) + (userText.length > 20 ? '...' : '') }
|
||||
: s
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const aiMessage: Message = {
|
||||
id: (Date.now() + 1).toString(),
|
||||
role: 'assistant',
|
||||
content: replyContent,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
const updatedMessages = [...(currentMessages.length > 0 ? currentMessages : []), userMessage, aiMessage];
|
||||
updateSessionMessages(updatedMessages);
|
||||
} catch (error) {
|
||||
console.error('Error sending message', error);
|
||||
const errorMessage: Message = {
|
||||
id: (Date.now() + 1).toString(),
|
||||
role: 'assistant',
|
||||
content: '抱歉,与服务器通信时出错。',
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
updateSessionMessages([...currentMessages, userMessage, errorMessage]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
} else {
|
||||
sendMessage(activeSessionId, prompt);
|
||||
}
|
||||
};
|
||||
|
||||
if (!activeSessionId) {
|
||||
return (
|
||||
<div className="flex-1 flex flex-col bg-white overflow-hidden items-center justify-center">
|
||||
<Activity size={48} className="text-slate-300 mb-4" />
|
||||
<h2 className="text-xl font-semibold text-slate-600">kilostar Assistant</h2>
|
||||
<p className="text-slate-400 mt-2">Select a chat history or create a new one to start.</p>
|
||||
<button
|
||||
onClick={handleNewChat}
|
||||
className="mt-6 px-6 py-2 bg-blue-200 text-slate-800 rounded-xl shadow-sm hover:bg-blue-300 transition-colors"
|
||||
>
|
||||
Start New Chat
|
||||
</button>
|
||||
<div className="flex-1 flex flex-col items-center justify-center p-8 relative overflow-hidden">
|
||||
{/* Decorative background */}
|
||||
<div className="absolute top-1/3 left-1/2 -translate-x-1/2 -translate-y-1/2 w-64 h-64 bg-accent/5 rounded-full blur-3xl" />
|
||||
|
||||
<div className="relative z-10 flex flex-col items-center animate-fade-in-scale">
|
||||
<div className="w-16 h-16 rounded-2xl bg-gradient-to-br from-accent to-glow-purple flex items-center justify-center text-white shadow-xl shadow-accent/20 mb-6">
|
||||
<Activity size={32} />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-text-primary mb-2">{t('chat.assistantName')}</h2>
|
||||
<p className="text-text-muted text-sm mb-8 text-center max-w-sm">
|
||||
{t('chat.selectChat')}
|
||||
</p>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div className="grid grid-cols-2 gap-3 mb-8 w-full max-w-md">
|
||||
{QUICK_ACTIONS.map((action) => (
|
||||
<button
|
||||
key={action.label}
|
||||
onClick={() => handleQuickAction(action.prompt)}
|
||||
className="flex items-center gap-2.5 px-4 py-3 bg-bg-card border border-border-primary rounded-xl text-left hover:border-accent hover:bg-bg-hover transition-all group"
|
||||
>
|
||||
<action.icon size={16} className="text-text-muted group-hover:text-accent transition-colors" />
|
||||
<span className="text-sm text-text-secondary group-hover:text-text-primary transition-colors">{action.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleNewChat}
|
||||
className="px-6 py-2.5 bg-accent text-white rounded-xl font-medium hover:bg-accent-hover transition-all shadow-lg shadow-accent/20 text-sm flex items-center gap-2"
|
||||
>
|
||||
<Plus size={16} />
|
||||
{t('chat.newChat')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col bg-white overflow-hidden relative">
|
||||
<div className="h-14 border-b border-slate-100 bg-white flex items-center justify-between px-6 z-10 shrink-0">
|
||||
<div className="flex items-center">
|
||||
<MessageSquare size={18} className="text-blue-600 mr-3" />
|
||||
<h1 className="font-semibold text-slate-800">{activeSession?.title || 'Chat'}</h1>
|
||||
</div>
|
||||
<div className="flex space-x-2 bg-slate-50 p-1 rounded-lg">
|
||||
<button
|
||||
onClick={() => setMode('chat')}
|
||||
className={`px-3 py-1 text-sm font-medium rounded-md transition-colors ${
|
||||
mode === 'chat' ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'
|
||||
}`}
|
||||
>
|
||||
Chat
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setMode('deploy')}
|
||||
className={`px-3 py-1 text-sm font-medium rounded-md transition-colors ${
|
||||
mode === 'deploy' ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'
|
||||
}`}
|
||||
>
|
||||
Deploy Task
|
||||
</button>
|
||||
<div className="flex-1 flex flex-col bg-bg-card overflow-hidden relative">
|
||||
{/* Header */}
|
||||
<div className="h-12 border-b border-border-primary/60 bg-bg-card/80 backdrop-blur flex items-center justify-between px-5 z-10 shrink-0">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="w-7 h-7 rounded-lg bg-accent-light flex items-center justify-center">
|
||||
<MessageSquare size={14} className="text-accent" />
|
||||
</div>
|
||||
<h1 className="font-semibold text-sm text-text-primary">{activeSession?.title || 'Chat'}</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 p-6 overflow-y-auto space-y-6 bg-white">
|
||||
{/* Messages */}
|
||||
<div className="flex-1 overflow-y-auto px-4 py-6 space-y-5">
|
||||
{loadingMessages ? (
|
||||
<div className="flex justify-center items-center h-full">
|
||||
<div className="flex space-x-2">
|
||||
<span className="h-2 w-2 bg-slate-400 rounded-full animate-bounce"></span>
|
||||
<span className="h-2 w-2 bg-slate-400 rounded-full animate-bounce delay-75"></span>
|
||||
<span className="h-2 w-2 bg-slate-400 rounded-full animate-bounce delay-150"></span>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-accent animate-typing-dot" />
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-accent animate-typing-dot" />
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-accent animate-typing-dot" />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
messages.map((msg) => (
|
||||
<div key={msg.id} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}>
|
||||
messages.map((msg, idx) => (
|
||||
<div
|
||||
key={msg.id}
|
||||
className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'} animate-fade-in`}
|
||||
style={{ animationDelay: `${idx * 30}ms` }}
|
||||
>
|
||||
{msg.role === 'assistant' && (
|
||||
<div className="w-8 h-8 rounded-full bg-white border border-blue-100 flex items-center justify-center mr-3 mt-1 shadow-sm flex-shrink-0">
|
||||
<Activity size={16} className="text-blue-600" />
|
||||
<div className="w-7 h-7 rounded-full bg-gradient-to-br from-accent to-glow-purple flex items-center justify-center mr-2.5 mt-0.5 shadow-sm flex-shrink-0">
|
||||
<Activity size={13} className="text-white" />
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`${
|
||||
msg.role === 'user'
|
||||
? 'bg-blue-100 text-slate-800 rounded-2xl rounded-tr-sm'
|
||||
: 'bg-slate-50 border border-slate-100 text-slate-700 rounded-2xl rounded-tl-sm'
|
||||
} p-4 max-w-[80%] shadow-sm`}
|
||||
>
|
||||
<p className="text-sm leading-relaxed mb-1 whitespace-pre-wrap">{msg.content}</p>
|
||||
<div className={`max-w-[85%] ${msg.role === 'user' ? 'mr-1' : ''}`}>
|
||||
<div
|
||||
className={`px-4 py-2.5 text-sm leading-relaxed whitespace-pre-wrap ${
|
||||
msg.role === 'user'
|
||||
? 'bg-text-primary text-bg-primary rounded-2xl rounded-tr-sm'
|
||||
: 'bg-bg-secondary border border-border-primary rounded-2xl rounded-tl-sm'
|
||||
}`}
|
||||
>
|
||||
{msg.content}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
{loading && (
|
||||
<div className="flex justify-start">
|
||||
<div className="w-8 h-8 rounded-full bg-white border border-blue-100 flex items-center justify-center mr-3 mt-1 shadow-sm flex-shrink-0">
|
||||
<Activity size={16} className="text-blue-600 animate-spin" />
|
||||
|
||||
{/* Typing indicator */}
|
||||
{activeSession && activeSession.messages.length > 0 && activeSession.messages[activeSession.messages.length - 1].role === 'user' && (
|
||||
<div className="flex justify-start animate-fade-in">
|
||||
<div className="w-7 h-7 rounded-full bg-gradient-to-br from-accent to-glow-purple flex items-center justify-center mr-2.5 mt-0.5 shadow-sm flex-shrink-0">
|
||||
<Activity size={13} className="text-white animate-pulse" />
|
||||
</div>
|
||||
<div className="bg-slate-50 border border-slate-100 text-slate-700 p-4 rounded-2xl rounded-tl-sm max-w-[80%] shadow-sm">
|
||||
<span className="flex space-x-1">
|
||||
<span className="h-2 w-2 bg-slate-400 rounded-full animate-bounce"></span>
|
||||
<span className="h-2 w-2 bg-slate-400 rounded-full animate-bounce delay-75"></span>
|
||||
<span className="h-2 w-2 bg-slate-400 rounded-full animate-bounce delay-150"></span>
|
||||
</span>
|
||||
<div className="bg-bg-secondary border border-border-primary rounded-2xl rounded-tl-sm px-4 py-3">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-accent animate-typing-dot" />
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-accent animate-typing-dot" />
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-accent animate-typing-dot" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-white border-t border-slate-100 shrink-0">
|
||||
<div className="relative flex items-center">
|
||||
{/* Input */}
|
||||
<div className="p-4 bg-bg-card border-t border-border-primary/60 shrink-0">
|
||||
<div className="relative flex items-end gap-2 max-w-3xl mx-auto">
|
||||
<input type="file" ref={fileInputRef} className="hidden" />
|
||||
<button
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className="absolute left-2 p-1.5 text-slate-400 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-colors z-10 cursor-pointer"
|
||||
className="p-2.5 text-text-muted hover:text-accent hover:bg-accent-light rounded-xl transition-all flex-shrink-0 mb-0.5"
|
||||
title="Add attachment"
|
||||
>
|
||||
<Plus size={20} />
|
||||
<Plus size={18} />
|
||||
</button>
|
||||
<input
|
||||
type="text"
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSendMessage()}
|
||||
placeholder="Ask kilostar to do something..."
|
||||
className="w-full bg-slate-50 border border-slate-200 text-sm rounded-2xl pl-12 pr-12 py-3.5 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-400 transition-all"
|
||||
/>
|
||||
<div className="flex-1 relative">
|
||||
<textarea
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSendMessage();
|
||||
}
|
||||
}}
|
||||
placeholder={t('chat.placeholder')}
|
||||
rows={1}
|
||||
className="w-full bg-bg-input border border-border-primary rounded-xl pl-4 pr-12 py-3 focus:outline-none focus:ring-2 focus:ring-accent/15 focus:border-accent/40 transition-all text-text-primary placeholder:text-text-muted/50 text-sm resize-none min-h-[44px] max-h-[120px]"
|
||||
style={{ height: 'auto' }}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSendMessage}
|
||||
disabled={loading || !input.trim()}
|
||||
className="absolute right-2 p-1.5 bg-blue-200 text-slate-800 rounded-xl hover:bg-blue-300 transition-colors shadow-sm disabled:opacity-50 cursor-pointer"
|
||||
disabled={!input.trim()}
|
||||
className="p-2.5 bg-accent text-white rounded-xl hover:bg-accent-hover transition-all shadow-lg shadow-accent/15 disabled:opacity-30 disabled:shadow-none disabled:hover:bg-accent flex-shrink-0 mb-0.5"
|
||||
>
|
||||
<ChevronRight size={18} />
|
||||
<ArrowUp size={18} />
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-center text-[10px] text-text-muted/50 mt-2">
|
||||
KiloStar can make mistakes. Please verify important information.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,33 +1,29 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Plus, Trash2 } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Plus, Trash2, MessageSquare, Workflow as WorkflowIcon } from 'lucide-react';
|
||||
import apiClient from '../../api/client';
|
||||
import type { Workflow } from '../../types';
|
||||
import type { ChatSession } from '../../App';
|
||||
import { useChatStore } from '../../store/useChatStore';
|
||||
|
||||
interface LeftPanelProps {
|
||||
activeTab: string;
|
||||
selectedWorkflow: string | null;
|
||||
setSelectedWorkflow: (id: string | null) => void;
|
||||
chatSessions?: ChatSession[];
|
||||
setChatSessions?: React.Dispatch<React.SetStateAction<ChatSession[]>>;
|
||||
activeSessionId?: string | null;
|
||||
setActiveSessionId?: React.Dispatch<React.SetStateAction<string | null>>;
|
||||
onSessionsChanged?: () => void;
|
||||
}
|
||||
|
||||
export function LeftPanel({
|
||||
activeTab,
|
||||
selectedWorkflow,
|
||||
setSelectedWorkflow,
|
||||
chatSessions,
|
||||
setChatSessions,
|
||||
activeSessionId,
|
||||
setActiveSessionId,
|
||||
onSessionsChanged,
|
||||
}: LeftPanelProps) {
|
||||
export function LeftPanel({ activeTab }: LeftPanelProps) {
|
||||
const { t } = useTranslation();
|
||||
const [workflows, setWorkflows] = useState<Workflow[]>([]);
|
||||
const [loadingWorkflows, setLoadingWorkflows] = useState(false);
|
||||
|
||||
const {
|
||||
sessions,
|
||||
activeSessionId,
|
||||
setActiveSessionId,
|
||||
removeSession,
|
||||
createChat,
|
||||
selectedWorkflow,
|
||||
setSelectedWorkflow,
|
||||
} = useChatStore();
|
||||
|
||||
useEffect(() => {
|
||||
let intervalId: ReturnType<typeof setInterval>;
|
||||
|
||||
@@ -62,148 +58,105 @@ export function LeftPanel({
|
||||
}, [activeTab]);
|
||||
|
||||
const handleNewChat = async () => {
|
||||
if (!setChatSessions || !setActiveSessionId || !onSessionsChanged) return;
|
||||
try {
|
||||
const response = await apiClient.post('/api/v1/chat', {
|
||||
title: '新对话',
|
||||
initial_message: '你好',
|
||||
});
|
||||
const chatId: string = response.data.chat_id;
|
||||
const reply: string = response.data.reply || '你好!我是 kilostar 助手,有什么可以帮你的吗?';
|
||||
|
||||
const newSession: ChatSession = {
|
||||
id: chatId,
|
||||
title: '新对话',
|
||||
messages: [
|
||||
{ id: chatId + '_user', role: 'user', content: '你好', timestamp: Date.now() },
|
||||
{ id: chatId + '_ai', role: 'assistant', content: reply, timestamp: Date.now() },
|
||||
],
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
setChatSessions((prev) => [newSession, ...prev]);
|
||||
setActiveSessionId(chatId);
|
||||
onSessionsChanged();
|
||||
} catch (error) {
|
||||
console.error('Failed to create chat session', error);
|
||||
}
|
||||
await createChat(t('chat.newChat'), '你好');
|
||||
};
|
||||
|
||||
const handleDeleteChat = (e: React.MouseEvent, id: string) => {
|
||||
e.stopPropagation();
|
||||
if (!setChatSessions || !setActiveSessionId || !chatSessions) return;
|
||||
const updated = chatSessions.filter((s) => s.id !== id);
|
||||
setChatSessions(updated);
|
||||
if (activeSessionId === id) {
|
||||
setActiveSessionId(updated.length > 0 ? updated[0].id : null);
|
||||
}
|
||||
removeSession(id);
|
||||
};
|
||||
|
||||
const isChats = activeTab === 'chats';
|
||||
|
||||
return (
|
||||
<div className="w-72 bg-white border-r border-slate-100 flex flex-col z-0 shrink-0">
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
<div className="w-64 bg-bg-sidebar border-r border-border-primary flex flex-col shrink-0">
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-border-primary">
|
||||
<span className="text-[11px] font-bold text-text-muted uppercase tracking-widest">
|
||||
{isChats ? t('chat.chatHistory') : t('nav.workflow')}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (isChats) handleNewChat();
|
||||
else setSelectedWorkflow('new');
|
||||
}}
|
||||
className="p-1.5 rounded-lg bg-bg-hover text-text-muted hover:text-accent hover:bg-accent-light transition-all"
|
||||
title={isChats ? t('chat.newChat') : t('workflow.createWorkflow')}
|
||||
>
|
||||
<Plus size={14} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-3 border-b border-slate-100 bg-slate-50">
|
||||
<span className="text-sm font-semibold text-slate-600 uppercase tracking-wider">
|
||||
{activeTab === 'chats' ? 'Chat History' : 'Workflows'}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (activeTab === 'chats') {
|
||||
handleNewChat();
|
||||
} else {
|
||||
setSelectedWorkflow('new');
|
||||
}
|
||||
}}
|
||||
className="p-1.5 bg-blue-100 text-blue-600 rounded hover:bg-blue-200 transition-colors"
|
||||
title={activeTab === 'chats' ? 'New Chat' : 'New Workflow'}
|
||||
>
|
||||
<Plus size={16} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 p-3 overflow-y-auto">
|
||||
{activeTab === 'workflows' && (
|
||||
<div className="space-y-2">
|
||||
{loadingWorkflows ? (
|
||||
<div className="text-center text-slate-400 text-sm py-4">Loading workflows...</div>
|
||||
) : workflows.length === 0 ? (
|
||||
<div className="text-center text-slate-400 text-sm py-4">暂无工作流<br/>点击右上角 + 创建</div>
|
||||
) : (
|
||||
workflows.map((wf) => (
|
||||
<div
|
||||
key={wf.trace_id}
|
||||
onClick={() => setSelectedWorkflow(wf.trace_id)}
|
||||
className={`p-3 rounded-lg border cursor-pointer transition-all ${
|
||||
selectedWorkflow === wf.trace_id
|
||||
? 'border-blue-300 bg-blue-50 shadow-sm'
|
||||
: 'border-slate-100 hover:border-blue-200 hover:bg-slate-50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<span
|
||||
className={`font-medium text-sm ${
|
||||
selectedWorkflow === wf.trace_id ? 'text-blue-700' : 'text-slate-700'
|
||||
}`}
|
||||
>
|
||||
{wf.title || 'Unnamed Workflow'}
|
||||
</span>
|
||||
<span
|
||||
className={`flex h-2 w-2 rounded-full ${
|
||||
wf.status && (wf.status.includes('working'))
|
||||
? 'bg-green-400 animate-pulse'
|
||||
: wf.status === 'failed'
|
||||
? 'bg-red-400'
|
||||
: wf.status === 'completed'
|
||||
? 'bg-green-500'
|
||||
: 'bg-slate-300'
|
||||
}`}
|
||||
></span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 font-mono line-clamp-1">ID: {wf.trace_id}</p>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
<div className="flex-1 overflow-y-auto py-2 px-2 space-y-0.5">
|
||||
{isChats ? (
|
||||
sessions.length === 0 ? (
|
||||
<div className="px-3 py-8 text-center text-text-muted text-xs">
|
||||
{t('chat.noHistory')}
|
||||
</div>
|
||||
)}
|
||||
{activeTab === 'chats' && chatSessions && (
|
||||
<div className="space-y-2">
|
||||
{chatSessions.length === 0 ? (
|
||||
<div className="text-center text-slate-400 text-sm py-8">
|
||||
No chat history.<br/>Click + to start a new chat.
|
||||
) : (
|
||||
sessions.map((session) => (
|
||||
<div
|
||||
key={session.id}
|
||||
onClick={() => setActiveSessionId(session.id)}
|
||||
className={`group flex items-center gap-2.5 px-3 py-2.5 rounded-lg cursor-pointer transition-all ${
|
||||
activeSessionId === session.id
|
||||
? 'bg-accent-light text-accent'
|
||||
: 'hover:bg-bg-hover text-text-secondary'
|
||||
}`}
|
||||
>
|
||||
<MessageSquare size={14} className={`flex-shrink-0 ${activeSessionId === session.id ? 'text-accent' : 'text-text-muted'}`} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className={`text-xs font-medium truncate ${activeSessionId === session.id ? 'text-accent' : 'text-text-secondary'}`}>
|
||||
{session.title}
|
||||
</h3>
|
||||
<p className="text-[10px] text-text-muted mt-0.5">
|
||||
{new Date(session.updatedAt).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
chatSessions.map((session) => (
|
||||
<div
|
||||
key={session.id}
|
||||
onClick={() => setActiveSessionId?.(session.id)}
|
||||
className={`group flex items-center justify-between p-3 rounded-lg border cursor-pointer transition-all ${
|
||||
activeSessionId === session.id
|
||||
? 'border-blue-300 bg-blue-50 shadow-sm'
|
||||
: 'border-slate-100 hover:border-blue-200 hover:bg-slate-50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex-1 min-w-0 mr-2">
|
||||
<h3 className={`font-medium text-sm truncate ${
|
||||
activeSessionId === session.id ? 'text-blue-700' : 'text-slate-700'
|
||||
}`}>
|
||||
{session.title}
|
||||
</h3>
|
||||
<p className="text-xs text-slate-400 mt-1">
|
||||
{new Date(session.updatedAt).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => handleDeleteChat(e, session.id)}
|
||||
className="text-slate-400 opacity-0 group-hover:opacity-100 hover:text-red-500 transition-all"
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
<button
|
||||
onClick={(e) => handleDeleteChat(e, session.id)}
|
||||
className="opacity-0 group-hover:opacity-100 p-1 rounded text-text-muted hover:text-danger hover:bg-danger-bg transition-all"
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
)
|
||||
) : (
|
||||
loadingWorkflows ? (
|
||||
<div className="px-3 py-8 text-center text-text-muted text-xs">{t('workflow.loading')}</div>
|
||||
) : workflows.length === 0 ? (
|
||||
<div className="px-3 py-8 text-center text-text-muted text-xs">
|
||||
{t('workflow.noWorkflows')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
workflows.map((wf) => (
|
||||
<div
|
||||
key={wf.trace_id}
|
||||
onClick={() => setSelectedWorkflow(wf.trace_id)}
|
||||
className={`group flex items-center gap-2.5 px-3 py-2.5 rounded-lg cursor-pointer transition-all ${
|
||||
selectedWorkflow === wf.trace_id
|
||||
? 'bg-accent-light'
|
||||
: 'hover:bg-bg-hover'
|
||||
}`}
|
||||
>
|
||||
<WorkflowIcon size={14} className={`flex-shrink-0 ${selectedWorkflow === wf.trace_id ? 'text-accent' : 'text-text-muted'}`} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className={`text-xs font-medium truncate ${selectedWorkflow === wf.trace_id ? 'text-accent' : 'text-text-secondary'}`}>
|
||||
{wf.title || 'Unnamed'}
|
||||
</h3>
|
||||
<div className="flex items-center gap-1.5 mt-0.5">
|
||||
<span className={`w-1.5 h-1.5 rounded-full ${
|
||||
wf.status?.includes('working') ? 'bg-accent animate-pulse' :
|
||||
wf.status === 'failed' ? 'bg-danger' :
|
||||
wf.status === 'completed' ? 'bg-success' : 'bg-text-muted'
|
||||
}`} />
|
||||
<span className="text-[10px] text-text-muted font-mono truncate">{wf.trace_id.slice(-8)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Terminal, Sparkles, ArrowLeft } from 'lucide-react';
|
||||
import apiClient from '../../api/client';
|
||||
|
||||
@@ -8,6 +9,7 @@ interface NewWorkflowDialogProps {
|
||||
}
|
||||
|
||||
export function NewWorkflowDialog({ onClose, onSuccess }: NewWorkflowDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const [command, setCommand] = useState('');
|
||||
const [title, setTitle] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -15,26 +17,14 @@ export function NewWorkflowDialog({ onClose, onSuccess }: NewWorkflowDialogProps
|
||||
|
||||
const handleSubmit = async (e?: React.FormEvent) => {
|
||||
if (e) e.preventDefault();
|
||||
if (!title.trim()) {
|
||||
setError('请输入工作流标题');
|
||||
return;
|
||||
}
|
||||
if (!command.trim()) {
|
||||
setError('请输入具体需求描述');
|
||||
return;
|
||||
}
|
||||
if (!title.trim()) { setError('请输入工作流标题'); return; }
|
||||
if (!command.trim()) { setError('请输入具体需求描述'); return; }
|
||||
|
||||
setLoading(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
const response = await apiClient.post('/api/v1/workflow', {
|
||||
title: title.trim(),
|
||||
command: command.trim()
|
||||
});
|
||||
if (response.data && response.data.trace_id) {
|
||||
onSuccess(response.data.trace_id);
|
||||
}
|
||||
const response = await apiClient.post('/api/v1/workflow', { title: title.trim(), command: command.trim() });
|
||||
if (response.data?.trace_id) onSuccess(response.data.trace_id);
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.message || '创建工作流失败,请重试');
|
||||
} finally {
|
||||
@@ -42,103 +32,62 @@ export function NewWorkflowDialog({ onClose, onSuccess }: NewWorkflowDialogProps
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault();
|
||||
handleSubmit();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col bg-slate-50 overflow-hidden">
|
||||
<div className="h-14 border-b border-slate-200 bg-white flex items-center px-6 shrink-0">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="flex items-center gap-1.5 text-sm text-slate-500 hover:text-blue-600 transition-colors"
|
||||
>
|
||||
<ArrowLeft size={16} />
|
||||
返回工作流列表
|
||||
<div className="flex-1 flex flex-col bg-bg-secondary overflow-hidden">
|
||||
<div className="h-12 border-b border-border-primary/60 bg-bg-card/80 backdrop-blur flex items-center px-5 shrink-0">
|
||||
<button onClick={onClose} className="flex items-center gap-1.5 text-xs text-text-muted hover:text-accent transition-colors">
|
||||
<ArrowLeft size={14} />
|
||||
{t('workflow.backToList')}
|
||||
</button>
|
||||
<div className="flex-1 flex items-center justify-center gap-2">
|
||||
<Terminal size={18} className="text-blue-600" />
|
||||
<h2 className="font-semibold text-slate-800">新建工作流</h2>
|
||||
<Terminal size={15} className="text-accent" />
|
||||
<h2 className="font-semibold text-sm text-text-primary">{t('workflow.newWorkflow')}</h2>
|
||||
</div>
|
||||
<div className="w-28" />
|
||||
<div className="w-20" />
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="flex-1 p-6 overflow-y-auto">
|
||||
<div className="max-w-3xl mx-auto h-full flex flex-col gap-6">
|
||||
<form onSubmit={handleSubmit} className="flex-1 overflow-y-auto p-6">
|
||||
<div className="max-w-2xl mx-auto space-y-5">
|
||||
{error && (
|
||||
<div className="flex items-center gap-2 p-4 text-sm text-red-700 bg-red-50 border border-red-200 rounded-xl shrink-0">
|
||||
<span className="text-base">⚠️</span>
|
||||
{error}
|
||||
<div className="flex items-center gap-2 p-3 text-xs text-danger bg-danger-bg border border-danger/20 rounded-xl">
|
||||
<span>⚠️</span> {error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm p-6 shrink-0">
|
||||
<label className="flex items-center gap-2 text-sm font-semibold text-slate-700 mb-3">
|
||||
<span className="flex items-center justify-center w-6 h-6 rounded-full bg-blue-100 text-blue-600 text-xs font-bold">1</span>
|
||||
工作流标题
|
||||
<div className="bg-bg-card rounded-2xl border border-border-primary shadow-sm p-5">
|
||||
<label className="flex items-center gap-2 text-xs font-bold text-text-secondary mb-2 uppercase tracking-wider">
|
||||
<span className="flex items-center justify-center w-5 h-5 rounded-lg bg-accent-light text-accent text-[10px] font-bold">1</span>
|
||||
{t('workflow.workflowTitle')}
|
||||
</label>
|
||||
<p className="text-xs text-slate-400 mb-3">为你的工作流起一个简洁、描述性的名称</p>
|
||||
<input
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="例如:爬取最新技术新闻并生成摘要"
|
||||
className="w-full px-4 py-3 text-lg font-medium text-slate-800 bg-slate-50 border border-slate-200 rounded-xl placeholder:text-slate-300 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-400 focus:bg-white transition-all"
|
||||
autoFocus
|
||||
/>
|
||||
<p className="text-[11px] text-text-muted mb-3">{t('workflow.titleHint')}</p>
|
||||
<input type="text" value={title} onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder={t('workflow.titlePlaceholder')}
|
||||
className="w-full px-4 py-2.5 text-sm font-medium text-text-primary bg-bg-input border border-border-primary rounded-xl placeholder:text-text-muted/50 focus:outline-none focus:ring-2 focus:ring-accent/15 focus:border-accent/40 transition-all"
|
||||
autoFocus />
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm p-6 flex flex-col flex-1 min-h-0">
|
||||
<label className="flex items-center gap-2 text-sm font-semibold text-slate-700 mb-3 shrink-0">
|
||||
<span className="flex items-center justify-center w-6 h-6 rounded-full bg-blue-100 text-blue-600 text-xs font-bold">2</span>
|
||||
需求描述
|
||||
<div className="bg-bg-card rounded-2xl border border-border-primary shadow-sm p-5">
|
||||
<label className="flex items-center gap-2 text-xs font-bold text-text-secondary mb-2 uppercase tracking-wider">
|
||||
<span className="flex items-center justify-center w-5 h-5 rounded-lg bg-accent-light text-accent text-[10px] font-bold">2</span>
|
||||
{t('workflow.command')}
|
||||
</label>
|
||||
<p className="text-xs text-slate-400 mb-3 shrink-0">详细描述你希望 AI 完成的任务,越具体效果越好</p>
|
||||
<textarea
|
||||
value={command}
|
||||
onChange={(e) => setCommand(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="例如:请帮我自动执行以下任务:
|
||||
|
||||
1. 访问指定的技术新闻网站
|
||||
2. 抓取过去24小时内发布的所有文章
|
||||
3. 对每篇文章进行智能摘要
|
||||
4. 将结果整理为结构化表格
|
||||
5. 通过邮件发送给我
|
||||
|
||||
提示:按 Ctrl+Enter 快速提交"
|
||||
className="flex-1 w-full px-4 py-3 text-sm text-slate-700 bg-slate-50 border border-slate-200 rounded-xl resize-none placeholder:text-slate-300 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-400 focus:bg-white transition-all min-h-[200px]"
|
||||
/>
|
||||
<div className="flex items-center justify-between mt-3 shrink-0">
|
||||
<span className="text-xs text-slate-400">
|
||||
{command.length > 0 ? `已输入 ${command.length} 个字符` : 'Ctrl + Enter 快速提交'}
|
||||
</span>
|
||||
<p className="text-[11px] text-text-muted mb-3">{t('workflow.commandHint')}</p>
|
||||
<textarea value={command} onChange={(e) => setCommand(e.target.value)}
|
||||
placeholder={t('workflow.commandPlaceholder')}
|
||||
rows={8}
|
||||
className="w-full px-4 py-3 text-sm text-text-primary bg-bg-input border border-border-primary rounded-xl resize-none placeholder:text-text-muted/50 focus:outline-none focus:ring-2 focus:ring-accent/15 focus:border-accent/40 transition-all" />
|
||||
<div className="flex items-center justify-between mt-3">
|
||||
<span className="text-[10px] text-text-muted">{command.length > 0 ? `${command.length} chars` : 'Ctrl + Enter to submit'}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-5 py-2.5 text-sm font-medium text-slate-600 bg-slate-100 hover:bg-slate-200 rounded-xl transition-colors"
|
||||
>
|
||||
取消
|
||||
<button type="button" onClick={onClose} className="px-4 py-2 text-xs font-medium text-text-secondary hover:bg-bg-hover rounded-xl transition-colors">
|
||||
{t('workflow.cancel')}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="flex items-center gap-2 px-6 py-2.5 text-sm font-semibold text-white bg-blue-600 hover:bg-blue-700 rounded-xl transition-all shadow-sm hover:shadow-md disabled:opacity-50 disabled:hover:bg-blue-600"
|
||||
>
|
||||
<button type="submit" disabled={loading}
|
||||
className="flex items-center gap-2 px-5 py-2 text-xs font-semibold text-white bg-accent hover:bg-accent-hover rounded-xl transition-all shadow-lg shadow-accent/15 disabled:opacity-50">
|
||||
{loading ? (
|
||||
<>
|
||||
<Sparkles size={16} className="animate-spin" />
|
||||
正在创建...
|
||||
</>
|
||||
<><span className="w-3 h-3 border-2 border-white/30 border-t-white rounded-full animate-spin" /> {t('workflow.creating')}</>
|
||||
) : (
|
||||
<>
|
||||
<Sparkles size={16} />
|
||||
创建并启动工作流
|
||||
</>
|
||||
<><Sparkles size={13} /> {t('workflow.createAndStart')}</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { Terminal, RefreshCw, SendHorizontal, LayoutList, GitFork } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Terminal, RefreshCw, SendHorizontal, LayoutList, GitFork, Radio } from 'lucide-react';
|
||||
import apiClient from '../../api/client';
|
||||
import type { WorkflowDetail } from '../../types';
|
||||
import { WorkflowDiagram } from './WorkflowDiagram';
|
||||
@@ -9,6 +10,7 @@ interface RightPanelProps {
|
||||
}
|
||||
|
||||
export function RightPanel({ selectedWorkflow }: RightPanelProps) {
|
||||
const { t } = useTranslation();
|
||||
const [detail, setDetail] = useState<WorkflowDetail | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [logs, setLogs] = useState<string[]>([]);
|
||||
@@ -37,33 +39,18 @@ export function RightPanel({ selectedWorkflow }: RightPanelProps) {
|
||||
setLogs([]);
|
||||
return;
|
||||
}
|
||||
|
||||
fetchDetail(selectedWorkflow);
|
||||
setLogs([]);
|
||||
|
||||
const protocol = window.location.protocol;
|
||||
const host = window.location.host;
|
||||
const apiBase = import.meta.env.VITE_API_BASE_URL || `${protocol}//${host}`;
|
||||
const apiBase = import.meta.env.VITE_API_BASE_URL || `${window.location.protocol}//${window.location.host}`;
|
||||
const es = new EventSource(`${apiBase}/api/v1/workflow/sse/${selectedWorkflow}`);
|
||||
eventSourceRef.current = es;
|
||||
|
||||
es.onopen = () => setSseConnected(true);
|
||||
|
||||
es.onmessage = (event) => {
|
||||
setLogs(prev => [...prev, event.data]);
|
||||
};
|
||||
|
||||
es.onmessage = (event) => setLogs((prev) => [...prev, event.data]);
|
||||
es.onerror = () => setSseConnected(false);
|
||||
|
||||
const interval = setInterval(() => {
|
||||
fetchDetail(selectedWorkflow);
|
||||
}, 3000);
|
||||
|
||||
return () => {
|
||||
es.close();
|
||||
eventSourceRef.current = null;
|
||||
clearInterval(interval);
|
||||
};
|
||||
const interval = setInterval(() => fetchDetail(selectedWorkflow), 3000);
|
||||
return () => { es.close(); eventSourceRef.current = null; clearInterval(interval); };
|
||||
}, [selectedWorkflow]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -75,113 +62,87 @@ export function RightPanel({ selectedWorkflow }: RightPanelProps) {
|
||||
const handleReplySubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!replyText.trim() || !selectedWorkflow) return;
|
||||
|
||||
const message = replyText.trim();
|
||||
setReplyText('');
|
||||
setLogs(prev => [...prev, `[You]: ${message}`]);
|
||||
|
||||
setLogs((prev) => [...prev, `[You]: ${message}`]);
|
||||
try {
|
||||
await apiClient.post(`/api/v1/workflow/reply/${selectedWorkflow}`, { message });
|
||||
} catch (err) {
|
||||
console.error("Failed to send reply", err);
|
||||
setLogs(prev => [...prev, `[System Error]: Failed to send reply.`]);
|
||||
} catch {
|
||||
setLogs((prev) => [...prev, `[System Error]: Failed to send reply.`]);
|
||||
}
|
||||
};
|
||||
|
||||
if (!selectedWorkflow) return null;
|
||||
|
||||
return (
|
||||
<div className="flex-1 bg-white border-l border-slate-200 flex flex-col z-0 relative">
|
||||
<div className="h-14 border-b border-slate-100 flex items-center px-6 justify-between bg-white z-10 shrink-0">
|
||||
<div className="flex items-center gap-4">
|
||||
<h2 className="font-semibold text-slate-800 flex items-center gap-2">
|
||||
<Terminal size={18} className="text-blue-500" />
|
||||
<span className="truncate max-w-[200px]" title={detail?.title || 'Loading...'}>
|
||||
{detail?.title || 'Workflow Details'}
|
||||
</span>
|
||||
</h2>
|
||||
<span className={`px-2 py-0.5 text-xs rounded-full font-medium ${sseConnected ? 'bg-green-100 text-green-700' : 'bg-slate-100 text-slate-500'}`}>
|
||||
{sseConnected ? 'Live' : 'Disconnected'}
|
||||
<div className="flex-1 bg-bg-card border-l border-border-primary flex flex-col relative">
|
||||
<div className="h-12 border-b border-border-primary/60 bg-bg-card/80 backdrop-blur flex items-center px-5 justify-between shrink-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-7 h-7 rounded-lg bg-accent-light flex items-center justify-center">
|
||||
<Terminal size={14} className="text-accent" />
|
||||
</div>
|
||||
<h2 className="font-semibold text-sm text-text-primary truncate max-w-[180px]">{detail?.title || t('workflow.workflowDetails')}</h2>
|
||||
<span className={`flex items-center gap-1.5 px-2 py-0.5 rounded-full text-[10px] font-medium ${sseConnected ? 'bg-success-bg text-success' : 'bg-bg-secondary text-text-muted'}`}>
|
||||
<Radio size={10} className={sseConnected ? 'animate-pulse' : ''} />
|
||||
{sseConnected ? t('workflow.live') : t('workflow.disconnected')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center bg-slate-100 rounded-lg p-1">
|
||||
<button
|
||||
onClick={() => setActiveTab('chat')}
|
||||
className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${activeTab === 'chat' ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'}`}
|
||||
>
|
||||
<LayoutList size={16} />
|
||||
交流日志
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('diagram')}
|
||||
className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${activeTab === 'diagram' ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'}`}
|
||||
>
|
||||
<GitFork size={16} />
|
||||
流程图
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center bg-bg-secondary rounded-lg p-0.5">
|
||||
<button onClick={() => setActiveTab('chat')} className={`flex items-center gap-1.5 px-2.5 py-1.5 rounded-md text-xs font-medium transition-all ${activeTab === 'chat' ? 'bg-bg-card text-accent shadow-sm' : 'text-text-muted hover:text-text-secondary'}`}>
|
||||
<LayoutList size={12} /> {t('workflow.chatLog')}
|
||||
</button>
|
||||
<button onClick={() => setActiveTab('diagram')} className={`flex items-center gap-1.5 px-2.5 py-1.5 rounded-md text-xs font-medium transition-all ${activeTab === 'diagram' ? 'bg-bg-card text-accent shadow-sm' : 'text-text-muted hover:text-text-secondary'}`}>
|
||||
<GitFork size={12} /> {t('workflow.diagram')}
|
||||
</button>
|
||||
</div>
|
||||
<button onClick={() => fetchDetail(selectedWorkflow)} className="p-1.5 text-text-muted hover:text-accent hover:bg-accent-light rounded-lg transition-all">
|
||||
<RefreshCw size={14} className={loading ? 'animate-spin' : ''} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => fetchDetail(selectedWorkflow)}
|
||||
className="p-1.5 text-slate-400 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
|
||||
title="Refresh Data"
|
||||
>
|
||||
<RefreshCw size={16} className={loading ? "animate-spin" : ""} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 flex overflow-hidden bg-slate-50 relative">
|
||||
<div className="flex-1 flex overflow-hidden bg-bg-secondary relative">
|
||||
{activeTab === 'diagram' ? (
|
||||
<div className="absolute inset-0">
|
||||
{detail?.steps && detail.steps.length > 0 ? (
|
||||
<WorkflowDiagram steps={detail.steps} currentStep={0} status={detail.status} />
|
||||
) : (
|
||||
<div className="h-full flex items-center justify-center text-slate-400">
|
||||
Workflow steps are not yet generated.
|
||||
</div>
|
||||
<div className="h-full flex items-center justify-center text-text-muted text-sm">Workflow steps are not yet generated.</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 flex flex-col p-6 overflow-hidden">
|
||||
{detail?.command && (
|
||||
<div className="bg-white border border-slate-200 rounded-xl p-4 mb-4 shadow-sm shrink-0">
|
||||
<h3 className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-2">Original Command</h3>
|
||||
<p className="text-slate-700 text-sm">{detail.command}</p>
|
||||
<div className="flex-1 flex flex-col p-5 overflow-hidden">
|
||||
{detail?.command && (
|
||||
<div className="bg-bg-card border border-border-primary rounded-xl p-4 mb-4 shadow-sm shrink-0">
|
||||
<h3 className="text-[10px] font-bold text-text-muted uppercase tracking-widest mb-1.5">{t('workflow.originalCommand')}</h3>
|
||||
<p className="text-text-secondary text-sm">{detail.command}</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1 bg-bg-card border border-border-primary rounded-xl shadow-sm overflow-y-auto p-4 mb-4 space-y-2 font-mono text-xs">
|
||||
{logs.length === 0 ? (
|
||||
<div className="h-full flex items-center justify-center text-text-muted">
|
||||
{t('workflow.waitingEvents')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex-1 bg-white border border-slate-200 rounded-xl shadow-sm overflow-y-auto p-4 mb-4 space-y-3 font-mono text-sm">
|
||||
{logs.length === 0 ? (
|
||||
<div className="h-full flex items-center justify-center text-slate-400">
|
||||
Waiting for events...
|
||||
) : (
|
||||
logs.map((log, index) => (
|
||||
<div key={index} className={`p-2.5 rounded-lg text-xs ${log.startsWith('[You]') ? 'bg-accent-light/50 text-accent-text ml-8' : 'bg-bg-secondary text-text-secondary mr-8'}`}>
|
||||
{log}
|
||||
</div>
|
||||
) : (
|
||||
logs.map((log, index) => (
|
||||
<div key={index} className={`p-3 rounded-lg ${log.startsWith('[You]') ? 'bg-blue-50 border border-blue-100 text-blue-800 self-end ml-12' : 'bg-slate-50 border border-slate-100 text-slate-700 mr-12'}`}>
|
||||
{log}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
<div ref={logsEndRef} />
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleReplySubmit} className="relative shrink-0">
|
||||
<input
|
||||
type="text"
|
||||
value={replyText}
|
||||
onChange={(e) => setReplyText(e.target.value)}
|
||||
placeholder="Reply to the workflow..."
|
||||
className="w-full bg-white border border-slate-200 rounded-xl pl-4 pr-12 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent shadow-sm"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!replyText.trim()}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 p-1.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:hover:bg-blue-600 transition-colors"
|
||||
>
|
||||
<SendHorizontal size={16} />
|
||||
</button>
|
||||
</form>
|
||||
))
|
||||
)}
|
||||
<div ref={logsEndRef} />
|
||||
</div>
|
||||
<form onSubmit={handleReplySubmit} className="relative shrink-0">
|
||||
<input type="text" value={replyText} onChange={(e) => setReplyText(e.target.value)}
|
||||
placeholder={t('workflow.replyPlaceholder')}
|
||||
className="w-full bg-bg-card border border-border-primary rounded-xl pl-4 pr-11 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-accent/15 focus:border-accent/40 text-text-primary placeholder:text-text-muted/50" />
|
||||
<button type="submit" disabled={!replyText.trim()}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 p-1.5 bg-accent text-white rounded-lg hover:bg-accent-hover disabled:opacity-30 transition-colors">
|
||||
<SendHorizontal size={14} />
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
MarkerType,
|
||||
BackgroundVariant
|
||||
BackgroundVariant,
|
||||
} from '@xyflow/react';
|
||||
import '@xyflow/react/dist/style.css';
|
||||
import type { WorkflowStep } from '../../types';
|
||||
@@ -27,43 +27,43 @@ export function WorkflowDiagram({ steps, currentStep, status }: WorkflowDiagramP
|
||||
const isCompleted = step.status === 'completed';
|
||||
const isFailed = step.status === 'failed';
|
||||
|
||||
let bgColor = '#ffffff';
|
||||
let borderColor = '#e2e8f0'; // slate-200
|
||||
let textColor = '#334155'; // slate-700
|
||||
let bgColor = 'var(--bg-card)';
|
||||
let borderColor = 'var(--border-primary)';
|
||||
let textColor = 'var(--text-secondary)';
|
||||
|
||||
if (isCurrent) {
|
||||
bgColor = '#eff6ff'; // blue-50
|
||||
borderColor = '#3b82f6'; // blue-500
|
||||
textColor = '#1e40af'; // blue-800
|
||||
bgColor = 'var(--bg-active)';
|
||||
borderColor = 'var(--accent)';
|
||||
textColor = 'var(--accent)';
|
||||
} else if (isFailed) {
|
||||
bgColor = '#fef2f2'; // red-50
|
||||
borderColor = '#ef4444'; // red-500
|
||||
textColor = '#991b1b'; // red-800
|
||||
bgColor = 'var(--danger-bg)';
|
||||
borderColor = 'var(--danger)';
|
||||
textColor = 'var(--danger)';
|
||||
} else if (isCompleted) {
|
||||
bgColor = '#f0fdf4'; // green-50
|
||||
borderColor = '#22c55e'; // green-500
|
||||
textColor = '#166534'; // green-800
|
||||
bgColor = 'var(--success-bg)';
|
||||
borderColor = 'var(--success)';
|
||||
textColor = 'var(--success)';
|
||||
}
|
||||
|
||||
return {
|
||||
id: step.step.toString(),
|
||||
position: { x: 250, y: index * 120 + 50 },
|
||||
position: { x: 250, y: index * 100 + 40 },
|
||||
data: {
|
||||
label: (
|
||||
<div className="flex flex-col items-center p-2 min-w-[150px]">
|
||||
<div className="text-xs font-semibold mb-1 opacity-70 uppercase tracking-wider">{step.node}</div>
|
||||
<div className="text-sm font-medium">{step.name}</div>
|
||||
<div className="flex flex-col items-center p-1.5 min-w-[140px]">
|
||||
<div className="text-[10px] font-bold mb-0.5 opacity-60 uppercase tracking-wider">{step.node}</div>
|
||||
<div className="text-xs font-semibold">{step.name}</div>
|
||||
</div>
|
||||
)
|
||||
),
|
||||
},
|
||||
style: {
|
||||
background: bgColor,
|
||||
border: `2px solid ${borderColor}`,
|
||||
borderRadius: '8px',
|
||||
borderRadius: '10px',
|
||||
color: textColor,
|
||||
boxShadow: isCurrent ? '0 4px 6px -1px rgba(59, 130, 246, 0.2)' : '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
|
||||
padding: '4px',
|
||||
}
|
||||
boxShadow: isCurrent ? '0 0 20px -4px var(--accent-glow)' : 'none',
|
||||
fontSize: '12px',
|
||||
},
|
||||
};
|
||||
});
|
||||
}, [steps, currentStep, isWorkflowActive]);
|
||||
@@ -76,11 +76,8 @@ export function WorkflowDiagram({ steps, currentStep, status }: WorkflowDiagramP
|
||||
source: steps[i].step.toString(),
|
||||
target: steps[i + 1].step.toString(),
|
||||
animated: steps[i].step === currentStep && isWorkflowActive,
|
||||
style: { stroke: '#cbd5e1', strokeWidth: 2 },
|
||||
markerEnd: {
|
||||
type: MarkerType.ArrowClosed,
|
||||
color: '#cbd5e1',
|
||||
},
|
||||
style: { stroke: 'var(--border-primary)', strokeWidth: 2 },
|
||||
markerEnd: { type: MarkerType.ArrowClosed, color: 'var(--border-primary)' },
|
||||
});
|
||||
}
|
||||
return edges;
|
||||
@@ -89,29 +86,19 @@ export function WorkflowDiagram({ steps, currentStep, status }: WorkflowDiagramP
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
|
||||
// Update nodes and edges if props change
|
||||
useEffect(() => {
|
||||
setNodes(initialNodes);
|
||||
setEdges(initialEdges);
|
||||
}, [initialNodes, initialEdges, setNodes, setEdges]);
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
fitView
|
||||
attributionPosition="bottom-right"
|
||||
>
|
||||
<Background variant={BackgroundVariant.Dots} gap={16} size={1} color="#cbd5e1" />
|
||||
<Controls className="bg-white border-slate-200 fill-slate-500 shadow-sm rounded-md" />
|
||||
<ReactFlow nodes={nodes} edges={edges} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} fitView attributionPosition="bottom-right">
|
||||
<Background variant={BackgroundVariant.Dots} gap={20} size={1} color="var(--border-primary)" />
|
||||
<Controls className="bg-bg-card border-border-primary fill-text-muted shadow-sm rounded-lg" />
|
||||
<MiniMap
|
||||
nodeColor={(n) => {
|
||||
if (n.style?.background) return n.style.background as string;
|
||||
return '#e2e8f0';
|
||||
}}
|
||||
maskColor="rgba(248, 250, 252, 0.7)"
|
||||
nodeColor={(n) => (n.style?.background as string) || 'var(--border-primary)'}
|
||||
maskColor="rgba(0, 0, 0, 0.6)"
|
||||
className="bg-bg-card border border-border-primary rounded-lg"
|
||||
/>
|
||||
</ReactFlow>
|
||||
);
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import apiClient from '../../api/client';
|
||||
import type { Workflow } from '../../types';
|
||||
import { PlayCircle, CheckCircle, XCircle, Clock } from 'lucide-react';
|
||||
import { PlayCircle, CheckCircle, XCircle, Clock, ArrowRight, Zap } from 'lucide-react';
|
||||
|
||||
interface WorkflowListViewProps {
|
||||
onSelectWorkflow: (id: string) => void;
|
||||
}
|
||||
|
||||
export function WorkflowListView({ onSelectWorkflow }: WorkflowListViewProps) {
|
||||
const { t } = useTranslation();
|
||||
const [workflows, setWorkflows] = useState<Workflow[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
@@ -35,102 +37,99 @@ export function WorkflowListView({ onSelectWorkflow }: WorkflowListViewProps) {
|
||||
return () => clearInterval(intervalId);
|
||||
}, []);
|
||||
|
||||
const getStatusIcon = (status?: string) => {
|
||||
if (status === 'completed') return <CheckCircle size={20} className="text-green-500" />;
|
||||
if (status === 'failed') return <XCircle size={20} className="text-red-500" />;
|
||||
if (status && (status.includes('working')))
|
||||
return <PlayCircle size={20} className="text-blue-500 animate-pulse" />;
|
||||
return <Clock size={20} className="text-slate-400" />;
|
||||
};
|
||||
|
||||
const getStatusBadge = (status?: string) => {
|
||||
let colorClass = "bg-slate-100 text-slate-600";
|
||||
let label = "Waiting";
|
||||
|
||||
if (status === 'completed') { colorClass = "bg-green-100 text-green-700"; label = "Completed"; }
|
||||
else if (status === 'failed') { colorClass = "bg-red-100 text-red-700"; label = "Failed"; }
|
||||
else if (status && status.includes('working')) { colorClass = "bg-blue-100 text-blue-700 animate-pulse"; label = "Running"; }
|
||||
|
||||
return (
|
||||
<span className={`px-2.5 py-1 text-xs font-medium rounded-full ${colorClass}`}>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
const getStatusMeta = (status?: string) => {
|
||||
if (status === 'completed') return { icon: CheckCircle, color: 'text-success', bg: 'bg-success-bg', border: 'border-success/20', glow: 'group-hover:shadow-success/20' };
|
||||
if (status === 'failed') return { icon: XCircle, color: 'text-danger', bg: 'bg-danger-bg', border: 'border-danger/20', glow: 'group-hover:shadow-danger/20' };
|
||||
if (status && status.includes('working')) return { icon: PlayCircle, color: 'text-accent', bg: 'bg-accent-light', border: 'border-accent/20', glow: 'group-hover:shadow-accent/20' };
|
||||
return { icon: Clock, color: 'text-text-muted', bg: 'bg-bg-secondary', border: 'border-border-primary', glow: 'group-hover:shadow-border-primary' };
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex-1 flex items-center justify-center p-6 bg-slate-50">
|
||||
<div className="text-slate-400">Loading Workflows...</div>
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<div className="flex items-center gap-2 text-text-muted">
|
||||
<span className="w-4 h-4 border-2 border-border-primary border-t-accent rounded-full animate-spin" />
|
||||
<span className="text-sm">{t('workflow.loading')}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col p-8 bg-slate-50 overflow-auto">
|
||||
<div className="mb-8 flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-800">Workflows</h1>
|
||||
<p className="text-slate-500 mt-1">Manage and monitor your automated processes.</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => onSelectWorkflow('new')}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 transition-colors shadow-sm"
|
||||
>
|
||||
<span className="text-xl leading-none">+</span> 创建工作流
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{workflows.length === 0 ? (
|
||||
<div className="flex-1 flex flex-col items-center justify-center border-2 border-dashed border-slate-200 rounded-2xl bg-white p-12 text-center">
|
||||
<div className="w-16 h-16 bg-slate-50 rounded-full flex items-center justify-center mb-4">
|
||||
<PlayCircle size={32} className="text-slate-400" />
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-slate-800 mb-2">No Workflows Found</h3>
|
||||
<p className="text-slate-500 max-w-sm">
|
||||
Workflows created from your chats will appear here automatically.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||
{workflows.map((wf) => (
|
||||
<div
|
||||
key={wf.trace_id}
|
||||
onClick={() => onSelectWorkflow(wf.trace_id)}
|
||||
className="bg-white rounded-2xl p-6 border border-slate-200 shadow-sm hover:shadow-md hover:border-blue-300 transition-all cursor-pointer group flex flex-col h-full"
|
||||
>
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div className="p-2.5 bg-blue-50 text-blue-600 rounded-xl group-hover:bg-blue-600 group-hover:text-white transition-colors">
|
||||
{getStatusIcon(wf.status)}
|
||||
</div>
|
||||
{getStatusBadge(wf.status)}
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg font-semibold text-slate-800 mb-2 line-clamp-1" title={wf.title || 'Unnamed Workflow'}>
|
||||
{wf.title || 'Unnamed Workflow'}
|
||||
</h3>
|
||||
|
||||
<div className="mt-auto">
|
||||
{wf.command && (
|
||||
<div className="text-sm text-slate-500 line-clamp-2 mt-4 bg-slate-50 p-3 rounded-lg border border-slate-100">
|
||||
<span className="font-medium text-slate-600 block mb-1">Command:</span>
|
||||
"{wf.command}"
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-between items-center mt-5 text-xs text-slate-400">
|
||||
<span className="font-mono bg-slate-100 px-2 py-1 rounded truncate max-w-[140px]" title={wf.trace_id}>
|
||||
{wf.trace_id}
|
||||
</span>
|
||||
{wf.created_at && (
|
||||
<span>{new Date(wf.created_at).toLocaleDateString()}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col p-8 overflow-auto">
|
||||
<div className="max-w-6xl mx-auto w-full">
|
||||
<div className="flex justify-between items-end mb-8">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Zap size={18} className="text-accent" />
|
||||
<h1 className="text-2xl font-bold text-text-primary tracking-tight">{t('workflow.workflows')}</h1>
|
||||
</div>
|
||||
))}
|
||||
<p className="text-sm text-text-muted">{t('workflow.manageWorkflows')}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => onSelectWorkflow('new')}
|
||||
className="flex items-center gap-2 px-5 py-2.5 bg-accent text-white font-medium rounded-xl hover:bg-accent-hover transition-all shadow-lg shadow-accent/15 text-sm"
|
||||
>
|
||||
<span className="text-base leading-none">+</span>
|
||||
{t('workflow.createWorkflow')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{workflows.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center border border-dashed border-border-primary rounded-2xl bg-bg-card/50 p-16 text-center">
|
||||
<div className="w-14 h-14 bg-bg-secondary rounded-2xl flex items-center justify-center mb-4">
|
||||
<PlayCircle size={28} className="text-text-muted" />
|
||||
</div>
|
||||
<h3 className="text-base font-semibold text-text-primary mb-1">{t('workflow.noWorkflows')}</h3>
|
||||
<p className="text-sm text-text-muted max-w-xs">{t('workflow.workflowsAppearHere')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||
{workflows.map((wf) => {
|
||||
const meta = getStatusMeta(wf.status);
|
||||
const Icon = meta.icon;
|
||||
return (
|
||||
<div
|
||||
key={wf.trace_id}
|
||||
onClick={() => onSelectWorkflow(wf.trace_id)}
|
||||
className={`group relative bg-bg-card rounded-2xl p-5 border border-border-primary card-hover cursor-pointer overflow-hidden ${meta.glow}`}
|
||||
>
|
||||
{/* Status glow on hover */}
|
||||
<div className={`absolute top-0 right-0 w-24 h-24 ${meta.bg} rounded-full blur-2xl opacity-0 group-hover:opacity-40 transition-opacity -translate-y-1/2 translate-x-1/2`} />
|
||||
|
||||
<div className="relative">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div className={`p-2 rounded-xl ${meta.bg} ${meta.border} border`}>
|
||||
<Icon size={18} className={meta.color} />
|
||||
</div>
|
||||
<ArrowRight size={16} className="text-text-muted opacity-0 group-hover:opacity-100 transition-all group-hover:translate-x-0.5" />
|
||||
</div>
|
||||
|
||||
<h3 className="text-base font-semibold text-text-primary mb-1 line-clamp-1" title={wf.title || 'Unnamed'}>
|
||||
{wf.title || 'Unnamed Workflow'}
|
||||
</h3>
|
||||
|
||||
{wf.command && (
|
||||
<p className="text-xs text-text-muted line-clamp-2 mb-4">
|
||||
{wf.command}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between pt-3 border-t border-border-secondary">
|
||||
<span className="text-[10px] font-mono text-text-muted bg-bg-secondary px-2 py-0.5 rounded">
|
||||
{wf.trace_id.slice(0, 8)}...
|
||||
</span>
|
||||
{wf.created_at && (
|
||||
<span className="text-[10px] text-text-muted">{new Date(wf.created_at).toLocaleDateString()}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,99 +1,69 @@
|
||||
import { ChevronLeft, ChevronRight, MessageSquare, Workflow, Box, Bot } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { MessageSquare, Workflow, Box, Bot, ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import { useAppStore } from '../../store/useAppStore';
|
||||
|
||||
interface CollapsibleSidebarProps {
|
||||
mode: 'work' | 'agent';
|
||||
isOpen: boolean;
|
||||
setIsOpen: (isOpen: boolean) => void;
|
||||
workTab: 'chat' | 'workflow';
|
||||
setWorkTab: (tab: 'chat' | 'workflow') => void;
|
||||
agentTab: 'plugin' | 'agents';
|
||||
setAgentTab: (tab: 'plugin' | 'agents') => void;
|
||||
}
|
||||
export function CollapsibleSidebar() {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
mode,
|
||||
isSidebarOpen,
|
||||
setIsSidebarOpen,
|
||||
workTab,
|
||||
setWorkTab,
|
||||
agentTab,
|
||||
setAgentTab,
|
||||
} = useAppStore();
|
||||
|
||||
export function CollapsibleSidebar({
|
||||
mode,
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
workTab,
|
||||
setWorkTab,
|
||||
agentTab,
|
||||
setAgentTab,
|
||||
}: CollapsibleSidebarProps) {
|
||||
const navItems = mode === 'work'
|
||||
? [
|
||||
{ key: 'chat', label: t('nav.chat'), icon: MessageSquare },
|
||||
{ key: 'workflow', label: t('nav.workflow'), icon: Workflow },
|
||||
]
|
||||
: [
|
||||
{ key: 'plugin', label: t('nav.plugin'), icon: Box },
|
||||
{ key: 'agents', label: t('nav.agents'), icon: Bot },
|
||||
];
|
||||
|
||||
const getWorkNav = () => (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setWorkTab('chat')}
|
||||
className={`w-full flex items-center px-4 py-3 text-sm font-medium transition-all ${
|
||||
workTab === 'chat'
|
||||
? 'bg-blue-100 text-blue-800 border-r-4 border-blue-600'
|
||||
: 'text-slate-600 hover:bg-blue-50 hover:text-blue-800 border-r-4 border-transparent'
|
||||
}`}
|
||||
>
|
||||
<MessageSquare size={18} className="mr-3" />
|
||||
{isOpen && <span>Chat</span>}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setWorkTab('workflow')}
|
||||
className={`w-full flex items-center px-4 py-3 text-sm font-medium transition-all ${
|
||||
workTab === 'workflow'
|
||||
? 'bg-blue-100 text-blue-800 border-r-4 border-blue-600'
|
||||
: 'text-slate-600 hover:bg-blue-50 hover:text-blue-800 border-r-4 border-transparent'
|
||||
}`}
|
||||
>
|
||||
<Workflow size={18} className="mr-3" />
|
||||
{isOpen && <span>Workflow</span>}
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
const activeTab = mode === 'work' ? workTab : agentTab;
|
||||
const setTab = mode === 'work' ? setWorkTab as (v: string) => void : setAgentTab as (v: string) => void;
|
||||
|
||||
const getAgentNav = () => (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setAgentTab('plugin')}
|
||||
className={`w-full flex items-center px-4 py-3 text-sm font-medium transition-all ${
|
||||
agentTab === 'plugin'
|
||||
? 'bg-blue-100 text-blue-800 border-r-4 border-blue-600'
|
||||
: 'text-slate-600 hover:bg-blue-50 hover:text-blue-800 border-r-4 border-transparent'
|
||||
}`}
|
||||
>
|
||||
<Box size={18} className="mr-3" />
|
||||
{isOpen && <span>Plugin</span>}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setAgentTab('agents')}
|
||||
className={`w-full flex items-center px-4 py-3 text-sm font-medium transition-all ${
|
||||
agentTab === 'agents'
|
||||
? 'bg-blue-100 text-blue-800 border-r-4 border-blue-600'
|
||||
: 'text-slate-600 hover:bg-blue-50 hover:text-blue-800 border-r-4 border-transparent'
|
||||
}`}
|
||||
>
|
||||
<Bot size={18} className="mr-3" />
|
||||
{isOpen && <span>Agents</span>}
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
|
||||
// Background is slightly darker than the page background but lighter than the topbar
|
||||
// Let's make it the "main nav light blue" level. We will use bg-blue-50 for it,
|
||||
// and make the page background essentially white or extremely light slate,
|
||||
// but since the instruction says "page background is the next lightest",
|
||||
// let's use bg-blue-100 for Sidebar and bg-blue-50 for page background.
|
||||
return (
|
||||
<div
|
||||
className={`bg-blue-50 border-r border-blue-100 flex flex-col transition-all duration-300 relative z-10 ${
|
||||
isOpen ? 'w-64' : 'w-16'
|
||||
className={`bg-bg-sidebar border-r border-border-primary flex flex-col transition-all duration-300 relative z-10 ${
|
||||
isSidebarOpen ? 'w-56' : 'w-14'
|
||||
}`}
|
||||
>
|
||||
<div className="flex-1 py-4 space-y-2 overflow-y-auto">
|
||||
{mode === 'work' ? getWorkNav() : getAgentNav()}
|
||||
<div className="flex-1 py-3 space-y-1">
|
||||
{navItems.map((item) => {
|
||||
const isActive = activeTab === item.key;
|
||||
return (
|
||||
<button
|
||||
key={item.key}
|
||||
onClick={() => setTab(item.key as any)}
|
||||
className={`w-full flex items-center mx-1.5 rounded-lg transition-all duration-200 group ${
|
||||
isActive
|
||||
? 'bg-accent-light text-accent'
|
||||
: 'text-text-muted hover:text-text-secondary hover:bg-bg-hover'
|
||||
} ${isSidebarOpen ? 'px-3 py-2.5 gap-3' : 'px-0 py-2.5 justify-center'}`}
|
||||
style={{ width: isSidebarOpen ? 'calc(100% - 12px)' : 'calc(100% - 12px)' }}
|
||||
>
|
||||
<item.icon size={18} className={`flex-shrink-0 transition-transform group-hover:scale-110 ${isActive ? 'text-accent' : ''}`} />
|
||||
{isSidebarOpen && (
|
||||
<span className="text-xs font-medium">{item.label}</span>
|
||||
)}
|
||||
{isActive && isSidebarOpen && (
|
||||
<div className="ml-auto w-1 h-1 rounded-full bg-accent" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="h-10 w-10 absolute -right-5 top-1/2 -translate-y-1/2 bg-white border border-blue-200 rounded-full flex items-center justify-center text-slate-400 hover:text-blue-600 hover:border-blue-400 shadow-sm transition-all z-20"
|
||||
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
|
||||
className="absolute -right-3 top-6 w-6 h-6 bg-bg-card border border-border-primary rounded-full flex items-center justify-center text-text-muted hover:text-accent hover:border-accent shadow-sm transition-all z-20"
|
||||
>
|
||||
{isOpen ? <ChevronLeft size={16} /> : <ChevronRight size={16} />}
|
||||
{isSidebarOpen ? <ChevronLeft size={12} /> : <ChevronRight size={12} />}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,57 +1,118 @@
|
||||
import { Settings, BrainCircuit } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Settings,
|
||||
Sun,
|
||||
Moon,
|
||||
Globe,
|
||||
LogOut,
|
||||
Zap,
|
||||
} from 'lucide-react';
|
||||
import { useAppStore } from '../../store/useAppStore';
|
||||
|
||||
interface TopBarProps {
|
||||
mode: 'work' | 'agent';
|
||||
setMode: (mode: 'work' | 'agent') => void;
|
||||
showSettings: boolean;
|
||||
setShowSettings: (show: boolean) => void;
|
||||
}
|
||||
export function TopBar() {
|
||||
const { t, i18n } = useTranslation();
|
||||
const {
|
||||
mode,
|
||||
setMode,
|
||||
showSettings,
|
||||
setShowSettings,
|
||||
setTheme,
|
||||
resolvedTheme,
|
||||
setIsAuthenticated,
|
||||
} = useAppStore();
|
||||
|
||||
const toggleTheme = () => {
|
||||
setTheme(resolvedTheme === 'dark' ? 'light' : 'dark');
|
||||
};
|
||||
|
||||
const toggleLanguage = () => {
|
||||
i18n.changeLanguage(i18n.language.startsWith('zh') ? 'en' : 'zh');
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('token');
|
||||
setIsAuthenticated(false);
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
export function TopBar({ mode, setMode, showSettings, setShowSettings }: TopBarProps) {
|
||||
return (
|
||||
<div className="h-14 bg-blue-100 text-blue-900 flex items-center justify-between px-4 shrink-0 shadow-sm z-20 relative">
|
||||
{/* Left: Logo */}
|
||||
<div className="flex items-center space-x-2 font-bold text-xl tracking-tight text-blue-900">
|
||||
<BrainCircuit className="text-blue-600" size={24} />
|
||||
<span>kilostar</span>
|
||||
<div className="h-14 glass text-text-primary flex items-center justify-between px-5 shrink-0 z-50 relative">
|
||||
{/* Left: Logo with status */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative">
|
||||
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-accent to-glow-purple flex items-center justify-center text-white shadow-lg">
|
||||
<Zap size={18} fill="currentColor" />
|
||||
</div>
|
||||
<span className="status-dot absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 rounded-full bg-success border-2 border-bg-card" />
|
||||
</div>
|
||||
<div className="flex flex-col leading-none">
|
||||
<span className="font-bold text-sm tracking-tight text-text-primary">{t('app.name')}</span>
|
||||
<span className="text-[10px] text-text-muted font-medium tracking-wide">{t('app.tagline')}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Container: Mode Toggle Switch + Settings */}
|
||||
<div className="flex items-center space-x-4">
|
||||
{/* Center: Mode Switch */}
|
||||
<div className="hidden md:flex items-center bg-bg-secondary/80 rounded-full p-0.5 border border-border-primary">
|
||||
<button
|
||||
onClick={() => { setMode('work'); setShowSettings(false); }}
|
||||
className={`px-4 py-1.5 rounded-full text-xs font-semibold transition-all duration-200 ${
|
||||
mode === 'work' && !showSettings
|
||||
? 'bg-bg-card text-accent shadow-sm'
|
||||
: 'text-text-muted hover:text-text-secondary'
|
||||
}`}
|
||||
>
|
||||
{t('nav.work')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setMode('agent'); setShowSettings(false); }}
|
||||
className={`px-4 py-1.5 rounded-full text-xs font-semibold transition-all duration-200 ${
|
||||
mode === 'agent' && !showSettings
|
||||
? 'bg-bg-card text-accent shadow-sm'
|
||||
: 'text-text-muted hover:text-text-secondary'
|
||||
}`}
|
||||
>
|
||||
{t('nav.agent')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Mode Toggle Switch */}
|
||||
<div className="flex items-center space-x-1 bg-blue-200/50 p-1 rounded-full border border-blue-200">
|
||||
<button
|
||||
onClick={() => { setMode('work'); setShowSettings(false); }}
|
||||
className={`px-4 py-1.5 rounded-full text-sm font-medium transition-all ${
|
||||
mode === 'work' && !showSettings
|
||||
? 'bg-white text-blue-700 shadow-sm'
|
||||
: 'text-blue-700 hover:text-blue-900 hover:bg-white/50'
|
||||
}`}
|
||||
>
|
||||
Work
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setMode('agent'); setShowSettings(false); }}
|
||||
className={`px-4 py-1.5 rounded-full text-sm font-medium transition-all ${
|
||||
mode === 'agent' && !showSettings
|
||||
? 'bg-white text-blue-700 shadow-sm'
|
||||
: 'text-blue-700 hover:text-blue-900 hover:bg-white/50'
|
||||
}`}
|
||||
>
|
||||
Agent
|
||||
</button>
|
||||
</div>
|
||||
{/* Right: Actions */}
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={toggleLanguage}
|
||||
className="p-2 rounded-lg text-text-muted hover:text-text-primary hover:bg-bg-hover transition-all"
|
||||
title={i18n.language.startsWith('zh') ? 'Switch to English' : '切换到中文'}
|
||||
>
|
||||
<Globe size={16} />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className="p-2 rounded-lg text-text-muted hover:text-text-primary hover:bg-bg-hover transition-all"
|
||||
title={resolvedTheme === 'dark' ? 'Light mode' : 'Dark mode'}
|
||||
>
|
||||
{resolvedTheme === 'dark' ? <Sun size={16} /> : <Moon size={16} />}
|
||||
</button>
|
||||
|
||||
<div className="w-px h-4 bg-border-primary mx-1" />
|
||||
|
||||
{/* Settings */}
|
||||
<button
|
||||
onClick={() => setShowSettings(!showSettings)}
|
||||
className={`p-2 rounded-full transition-colors ${
|
||||
showSettings ? 'bg-white text-blue-700 shadow-sm' : 'text-blue-700 hover:bg-white/50 hover:text-blue-900'
|
||||
className={`p-2 rounded-lg transition-all ${
|
||||
showSettings
|
||||
? 'bg-accent-light text-accent'
|
||||
: 'text-text-muted hover:text-text-primary hover:bg-bg-hover'
|
||||
}`}
|
||||
title="Settings"
|
||||
title={t('nav.settings')}
|
||||
>
|
||||
<Settings size={20} />
|
||||
<Settings size={16} />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="p-2 rounded-lg text-text-muted hover:text-danger hover:bg-danger-bg transition-all ml-0.5"
|
||||
title="Logout"
|
||||
>
|
||||
<LogOut size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,35 +1,34 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAppStore } from '../../store/useAppStore';
|
||||
import { SkillSettings } from './SkillSettings';
|
||||
import { ToolSettings } from './ToolSettings';
|
||||
|
||||
interface PluginLayoutProps {
|
||||
resourceTab: string;
|
||||
setResourceTab: (tab: string) => void;
|
||||
}
|
||||
export function PluginLayout() {
|
||||
const { t } = useTranslation();
|
||||
const { resourceTab, setResourceTab } = useAppStore();
|
||||
|
||||
const tabs = [
|
||||
{ key: 'skill', label: t('agent.skills') },
|
||||
{ key: 'tool', label: t('agent.tools') },
|
||||
];
|
||||
|
||||
export function PluginLayout({ resourceTab, setResourceTab }: PluginLayoutProps) {
|
||||
return (
|
||||
<div className="flex-1 flex flex-col bg-slate-50 overflow-hidden">
|
||||
{/* Top Tabs for Plugin Module */}
|
||||
<div className="h-14 border-b border-slate-200 bg-white flex items-center px-6 shadow-sm z-10 shrink-0 space-x-6">
|
||||
<button
|
||||
onClick={() => setResourceTab('skill')}
|
||||
className={`py-4 text-sm font-medium border-b-2 transition-colors ${
|
||||
resourceTab === 'skill' ? 'border-blue-600 text-blue-600' : 'border-transparent text-slate-500 hover:text-slate-800'
|
||||
}`}
|
||||
>
|
||||
Skills
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setResourceTab('tool')}
|
||||
className={`py-4 text-sm font-medium border-b-2 transition-colors ${
|
||||
resourceTab === 'tool' ? 'border-blue-600 text-blue-600' : 'border-transparent text-slate-500 hover:text-slate-800'
|
||||
}`}
|
||||
>
|
||||
Tools
|
||||
</button>
|
||||
<div className="flex-1 flex flex-col bg-bg-secondary overflow-hidden">
|
||||
<div className="h-12 border-b border-border-primary bg-bg-card/80 backdrop-blur flex items-center px-6 shrink-0 gap-1">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.key}
|
||||
onClick={() => setResourceTab(tab.key)}
|
||||
className={`px-4 py-2 text-xs font-semibold rounded-lg transition-all ${
|
||||
resourceTab === tab.key
|
||||
? 'bg-accent-light text-accent'
|
||||
: 'text-text-muted hover:text-text-secondary hover:bg-bg-hover'
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex-1 overflow-y-auto p-8">
|
||||
{resourceTab === 'skill' && <SkillSettings />}
|
||||
{resourceTab === 'tool' && <ToolSettings />}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Download, Trash2, Plus, Box, Sparkles } from 'lucide-react';
|
||||
import apiClient from '../../api/client';
|
||||
import { Download, Trash2, Plus, Box } from 'lucide-react';
|
||||
|
||||
export function SkillSettings() {
|
||||
const [skills, setSkills] = useState<string[]>([]);
|
||||
@@ -15,14 +15,8 @@ export function SkillSettings() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await apiClient.get('/api/v1/resource/skill');
|
||||
const skillsData = response.data.skills || {};
|
||||
// skillsData might be an object mapping skill names to their details, or it might be an array in some versions.
|
||||
// We ensure it is an array of strings (skill names)
|
||||
if (Array.isArray(skillsData)) {
|
||||
setSkills(skillsData);
|
||||
} else {
|
||||
setSkills(Object.keys(skillsData));
|
||||
}
|
||||
const data = response.data.skills || {};
|
||||
setSkills(Array.isArray(data) ? data : Object.keys(data));
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch skills:', err);
|
||||
} finally {
|
||||
@@ -30,29 +24,21 @@ export function SkillSettings() {
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchSkills();
|
||||
}, []);
|
||||
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
|
||||
});
|
||||
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);
|
||||
@@ -60,69 +46,55 @@ export function SkillSettings() {
|
||||
};
|
||||
|
||||
const handleDelete = async (skillName: string) => {
|
||||
if (!confirm(`Are you sure you want to delete ${skillName}?`)) return;
|
||||
if (!confirm(`Delete ${skillName}?`)) return;
|
||||
try {
|
||||
await apiClient.delete(`/api/v1/resource/skill/${skillName}`);
|
||||
fetchSkills();
|
||||
} catch (err: any) {
|
||||
console.error('Failed to delete skill:', err);
|
||||
} catch {
|
||||
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="flex items-center gap-2 mb-1">
|
||||
<Sparkles size={16} className="text-accent" />
|
||||
<h1 className="text-lg font-bold text-text-primary">Skill Management</h1>
|
||||
</div>
|
||||
<p className="text-sm text-text-muted">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-blue-50 text-blue-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 className="bg-bg-card rounded-2xl border border-border-primary shadow-sm overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-border-primary flex items-center gap-3">
|
||||
<div className="w-9 h-9 rounded-xl bg-accent-light flex items-center justify-center">
|
||||
<Download size={16} className="text-accent" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-sm font-bold text-text-primary">Install Skill</h2>
|
||||
<p className="text-[11px] text-text-muted">Install 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-blue-500"
|
||||
/>
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">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-3.5 py-2.5 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent placeholder:text-text-muted/50" />
|
||||
</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-blue-500"
|
||||
/>
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">Path (Optional)</label>
|
||||
<input type="text" value={path} onChange={(e) => setPath(e.target.value)} placeholder="subfolder/path"
|
||||
className="w-full px-3.5 py-2.5 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent placeholder:text-text-muted/50" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{message && <div className="text-green-600 text-sm">{message}</div>}
|
||||
{error && <div className="text-red-600 text-sm">{error}</div>}
|
||||
|
||||
{message && <div className="text-xs text-success">{message}</div>}
|
||||
{error && <div className="text-xs text-danger">{error}</div>}
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={installing}
|
||||
className="flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50"
|
||||
>
|
||||
<Plus size={16} className="mr-2" />
|
||||
<button type="submit" disabled={installing}
|
||||
className="flex items-center gap-2 px-5 py-2.5 bg-accent text-white rounded-xl hover:bg-accent-hover transition-all shadow-lg shadow-accent/15 text-sm font-medium disabled:opacity-50">
|
||||
<Plus size={14} />
|
||||
{installing ? 'Installing...' : 'Install'}
|
||||
</button>
|
||||
</div>
|
||||
@@ -130,31 +102,27 @@ export function SkillSettings() {
|
||||
</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 className="bg-bg-card rounded-2xl border border-border-primary shadow-sm overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-border-primary">
|
||||
<h2 className="text-sm font-bold text-text-primary">Installed Skills ({skills.length})</h2>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
{loading ? (
|
||||
<div className="text-slate-500 text-sm">Loading skills...</div>
|
||||
<div className="text-text-muted text-sm">Loading skills...</div>
|
||||
) : skills.length === 0 ? (
|
||||
<div className="text-slate-500 text-sm">No skills installed yet.</div>
|
||||
<div className="text-text-muted text-sm">No skills installed yet.</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{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 key={skill} className="flex items-center justify-between p-3.5 bg-bg-secondary border border-border-secondary rounded-xl hover:border-accent/30 transition-all group">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-lg bg-bg-card border border-border-primary flex items-center justify-center">
|
||||
<Box size={14} className="text-text-muted" />
|
||||
</div>
|
||||
<span className="font-medium text-slate-800">{skill}</span>
|
||||
<span className="text-sm font-medium text-text-primary">{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 onClick={() => handleDelete(skill)} className="p-1.5 text-text-muted hover:text-danger hover:bg-danger-bg rounded-lg transition-all opacity-0 group-hover:opacity-100">
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Package } from 'lucide-react';
|
||||
import { Package, Wrench } from 'lucide-react';
|
||||
import apiClient from '../../api/client';
|
||||
|
||||
export function ToolSettings() {
|
||||
const [tools, setTools] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetchTools();
|
||||
}, []);
|
||||
useEffect(() => { fetchTools(); }, []);
|
||||
|
||||
const fetchTools = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await apiClient.get('/api/v1/resource/tool');
|
||||
const toolsData = response.data.tools || [];
|
||||
setTools(toolsData);
|
||||
setTools(response.data.tools || []);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch tools:', err);
|
||||
} finally {
|
||||
@@ -26,33 +23,33 @@ export function ToolSettings() {
|
||||
return (
|
||||
<div className="max-w-4xl space-y-6">
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-slate-800">Installed Tools</h3>
|
||||
<p className="text-slate-500 mt-1">Manage agent tools and functions.</p>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Wrench size={16} className="text-accent" />
|
||||
<h3 className="text-lg font-bold text-text-primary">Installed Tools</h3>
|
||||
</div>
|
||||
<p className="text-sm text-text-muted">Manage agent tools and functions</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white border border-slate-200 rounded-2xl shadow-sm overflow-hidden">
|
||||
<div className="p-6 border-b border-slate-100 flex justify-between items-center bg-slate-50/50">
|
||||
<div className="bg-bg-card border border-border-primary rounded-2xl shadow-sm overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-border-primary flex justify-between items-center bg-bg-secondary">
|
||||
<div>
|
||||
<h4 className="font-medium text-slate-800">Available Tools</h4>
|
||||
<p className="text-sm text-slate-500">List of installed tools available for agents.</p>
|
||||
<h4 className="text-sm font-bold text-text-primary">Available Tools</h4>
|
||||
<p className="text-[11px] text-text-muted">Installed tools for agents</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
{loading ? (
|
||||
<div className="text-slate-500 text-sm">Loading tools...</div>
|
||||
<div className="text-text-muted text-sm">Loading tools...</div>
|
||||
) : tools.length === 0 ? (
|
||||
<div className="text-slate-500 text-sm">No tools installed yet.</div>
|
||||
<div className="text-text-muted text-sm">No tools installed yet.</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{tools.map((tool) => (
|
||||
<div key={tool} className="p-4 border border-slate-200 rounded-xl flex items-center justify-between hover:shadow-sm transition-shadow">
|
||||
<div className="flex items-center">
|
||||
<div className="w-10 h-10 bg-purple-50 rounded-lg flex items-center justify-center mr-3">
|
||||
<Package size={20} className="text-purple-600" />
|
||||
</div>
|
||||
<span className="font-medium text-slate-800">{tool}</span>
|
||||
<div key={tool} className="flex items-center gap-3 p-3.5 bg-bg-secondary border border-border-secondary rounded-xl hover:border-accent/30 transition-all">
|
||||
<div className="w-9 h-9 rounded-lg bg-accent-light flex items-center justify-center">
|
||||
<Package size={16} className="text-accent" />
|
||||
</div>
|
||||
<span className="text-sm font-medium text-text-primary">{tool}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,44 +1,48 @@
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Users, Sliders } from 'lucide-react';
|
||||
import { useAppStore } from '../../store/useAppStore';
|
||||
import { UsersSettings } from './UsersSettings';
|
||||
import { SystemSettings } from './SystemSettings';
|
||||
|
||||
interface SettingsLayoutProps {
|
||||
settingsTab: string;
|
||||
setSettingsTab: (tab: string) => void;
|
||||
}
|
||||
export function SettingsLayout() {
|
||||
const { t } = useTranslation();
|
||||
const { settingsTab, setSettingsTab } = useAppStore();
|
||||
|
||||
const tabs = [
|
||||
{ key: 'users', label: t('settings.userManagement'), icon: Users },
|
||||
{ key: 'system', label: t('settings.systemSettings'), icon: Sliders },
|
||||
];
|
||||
|
||||
export function SettingsLayout({ settingsTab, setSettingsTab }: SettingsLayoutProps) {
|
||||
return (
|
||||
<div className="flex-1 flex bg-slate-50 overflow-hidden">
|
||||
{/* Settings 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">Settings</h2>
|
||||
</div>
|
||||
<div className="flex-1 p-4 space-y-2 overflow-y-auto">
|
||||
<button
|
||||
onClick={() => setSettingsTab('users')}
|
||||
className={`w-full flex items-center px-4 py-3 text-sm font-medium rounded-xl transition-all ${settingsTab === 'users' ? 'bg-blue-50 text-blue-600' : 'text-slate-600 hover:bg-slate-50 hover:text-slate-900'}`}
|
||||
>
|
||||
<Users size={18} className="mr-3" />
|
||||
User Management
|
||||
</button>
|
||||
<button
|
||||
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'}`}
|
||||
>
|
||||
<Sliders size={18} className="mr-3" />
|
||||
System Settings
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 flex bg-bg-secondary overflow-hidden">
|
||||
<div className="w-56 bg-bg-sidebar border-r border-border-primary flex flex-col">
|
||||
<div className="px-5 py-4 border-b border-border-primary">
|
||||
<h2 className="text-sm font-bold text-text-primary">{t('settings.settings')}</h2>
|
||||
</div>
|
||||
<div className="p-2 space-y-0.5">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.key}
|
||||
onClick={() => setSettingsTab(tab.key)}
|
||||
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-xs font-medium transition-all ${
|
||||
settingsTab === tab.key
|
||||
? 'bg-accent-light text-accent'
|
||||
: 'text-text-muted hover:text-text-secondary hover:bg-bg-hover'
|
||||
}`}
|
||||
>
|
||||
<tab.icon size={15} />
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Settings Main Content */}
|
||||
<div className="flex-1 overflow-y-auto p-8">
|
||||
<div className="flex-1 overflow-y-auto p-8">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
{settingsTab === 'users' && <UsersSettings />}
|
||||
{settingsTab === 'system' && <SystemSettings />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,106 +1,114 @@
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Globe, Server, Save } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Globe, Server, Save, Check } from 'lucide-react';
|
||||
import { useAppStore } from '../../store/useAppStore';
|
||||
|
||||
export function SystemSettings() {
|
||||
const [language, setLanguage] = useState(localStorage.getItem('language') || 'English');
|
||||
const [theme, setTheme] = useState(localStorage.getItem('theme') || 'Light');
|
||||
const { t, i18n } = useTranslation();
|
||||
const { theme, setTheme } = useAppStore();
|
||||
const [localLang, setLocalLang] = useState(i18n.language.startsWith('zh') ? 'zh' : 'en');
|
||||
const [localTheme, setLocalTheme] = useState(theme);
|
||||
const [debugMode, setDebugMode] = useState(true);
|
||||
const [saved, setSaved] = useState(false);
|
||||
|
||||
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}`);
|
||||
i18n.changeLanguage(localLang);
|
||||
setTheme(localTheme);
|
||||
setSaved(true);
|
||||
setTimeout(() => setSaved(false), 2000);
|
||||
};
|
||||
|
||||
// Initialize theme on mount if needed
|
||||
useEffect(() => {
|
||||
if (theme === 'Dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
}, [theme]);
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="mb-6">
|
||||
<h3 className="text-xl font-semibold text-slate-800">System Settings</h3>
|
||||
<p className="text-sm text-slate-500 mt-1">Global platform configurations.</p>
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-text-primary">{t('settings.systemSettings')}</h3>
|
||||
<p className="text-sm text-text-muted mt-0.5">Global platform configurations</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="bg-white border border-slate-200 rounded-xl shadow-sm p-6">
|
||||
<h4 className="text-sm font-semibold text-slate-800 mb-4 flex items-center">
|
||||
<Globe size={16} className="mr-2 text-slate-500" />
|
||||
General
|
||||
</h4>
|
||||
<div className="space-y-4 max-w-md">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">System Language</label>
|
||||
<select
|
||||
value={language}
|
||||
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>
|
||||
<div className="bg-bg-card rounded-2xl border border-border-primary shadow-sm overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-border-primary flex items-center gap-2">
|
||||
<Globe size={15} className="text-text-muted" />
|
||||
<h4 className="text-sm font-semibold text-text-primary">{t('settings.general')}</h4>
|
||||
</div>
|
||||
<div className="p-6 space-y-5 max-w-md">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-2 uppercase tracking-wider">{t('settings.language')}</label>
|
||||
<div className="flex gap-2">
|
||||
{[
|
||||
{ value: 'en', label: 'English' },
|
||||
{ value: 'zh', label: '简体中文' },
|
||||
].map((opt) => (
|
||||
<button
|
||||
key={opt.value}
|
||||
onClick={() => setLocalLang(opt.value)}
|
||||
className={`flex-1 px-4 py-2.5 rounded-xl text-sm font-medium border transition-all ${
|
||||
localLang === opt.value
|
||||
? 'bg-accent-light border-accent/30 text-accent'
|
||||
: 'bg-bg-input border-border-primary text-text-secondary hover:border-text-muted'
|
||||
}`}
|
||||
>
|
||||
{opt.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Theme</label>
|
||||
<select
|
||||
value={theme}
|
||||
onChange={(e) => setTheme(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="Light">Light</option>
|
||||
<option value="Dark">Dark</option>
|
||||
<option value="System Default">System Default</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-2 uppercase tracking-wider">{t('settings.theme')}</label>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{[
|
||||
{ value: 'light', label: t('settings.light') },
|
||||
{ value: 'dark', label: t('settings.dark') },
|
||||
{ value: 'system', label: t('settings.systemDefault') },
|
||||
].map((opt) => (
|
||||
<button
|
||||
key={opt.value}
|
||||
onClick={() => setLocalTheme(opt.value as any)}
|
||||
className={`px-3 py-2.5 rounded-xl text-xs font-medium border transition-all ${
|
||||
localTheme === opt.value
|
||||
? 'bg-accent-light border-accent/30 text-accent'
|
||||
: 'bg-bg-input border-border-primary text-text-secondary hover:border-text-muted'
|
||||
}`}
|
||||
>
|
||||
{opt.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white border border-slate-200 rounded-xl shadow-sm p-6">
|
||||
<h4 className="text-sm font-semibold text-slate-800 mb-4 flex items-center">
|
||||
<Server size={16} className="mr-2 text-slate-500" />
|
||||
Cluster & Runtime
|
||||
</h4>
|
||||
<div className="space-y-4 max-w-md">
|
||||
<div className="flex items-center mt-4">
|
||||
<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">
|
||||
Enable debug logging
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-bg-card rounded-2xl border border-border-primary shadow-sm overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-border-primary flex items-center gap-2">
|
||||
<Server size={15} className="text-text-muted" />
|
||||
<h4 className="text-sm font-semibold text-text-primary">{t('settings.clusterRuntime')}</h4>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<label className="flex items-center gap-3 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={debugMode}
|
||||
onChange={(e) => setDebugMode(e.target.checked)}
|
||||
className="w-4 h-4 rounded border-border-primary text-accent focus:ring-accent/20"
|
||||
/>
|
||||
<span className="text-sm text-text-secondary">{t('settings.debugLogging')}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<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 Changes
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex justify-end items-center gap-3">
|
||||
{saved && (
|
||||
<span className="flex items-center gap-1.5 text-xs text-success animate-fade-in">
|
||||
<Check size={14} />
|
||||
{t('settings.saved')}
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className="flex items-center gap-2 px-5 py-2.5 bg-accent text-white rounded-xl font-medium hover:bg-accent-hover transition-all shadow-lg shadow-accent/15 text-sm"
|
||||
>
|
||||
<Save size={14} />
|
||||
{t('settings.saveChanges')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { Plus, Edit2, Trash2, X } from 'lucide-react';
|
||||
import type { User } from '../../types';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Plus, Edit2, Trash2, X, User, Shield } from 'lucide-react';
|
||||
import type { User as UserType } from '../../types';
|
||||
import apiClient from '../../api/client';
|
||||
|
||||
export function UsersSettings() {
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
const { t } = useTranslation();
|
||||
const [users, setUsers] = useState<UserType[]>([]);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||
const [editingUser, setEditingUser] = useState<User | null>(null);
|
||||
const [editingUser, setEditingUser] = useState<UserType | null>(null);
|
||||
const [editRole, setEditRole] = useState('User');
|
||||
const [formData, setFormData] = useState({ username: '', password: '' });
|
||||
const [submitLoading, setSubmitLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const fetchUsers = async () => {
|
||||
try {
|
||||
@@ -20,54 +24,25 @@ export function UsersSettings() {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
fetchUsers();
|
||||
}, []);
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
username: '',
|
||||
password: ''
|
||||
});
|
||||
const [submitLoading, setSubmitLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const handleOpenModal = () => {
|
||||
setFormData({ username: '', password: '' });
|
||||
setError('');
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setIsModalOpen(false);
|
||||
};
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
[e.target.name]: e.target.value
|
||||
});
|
||||
};
|
||||
useEffect(() => { fetchUsers(); }, []);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!formData.username || !formData.password) {
|
||||
setError('Please fill in both username and password.');
|
||||
setError(t('settings.fillBoth'));
|
||||
return;
|
||||
}
|
||||
|
||||
setSubmitLoading(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
await apiClient.post('/api/v1/auth/register', {
|
||||
user_name: formData.username,
|
||||
password: formData.password
|
||||
password: formData.password,
|
||||
});
|
||||
await fetchUsers();
|
||||
handleCloseModal();
|
||||
setIsModalOpen(false);
|
||||
setFormData({ username: '', password: '' });
|
||||
} catch (err) {
|
||||
console.error("Failed to register user", err);
|
||||
setError('Registration failed. Please try again.');
|
||||
} finally {
|
||||
setSubmitLoading(false);
|
||||
@@ -75,18 +50,16 @@ export function UsersSettings() {
|
||||
};
|
||||
|
||||
const handleDeleteUser = async (userId: string | undefined) => {
|
||||
if (!userId) return;
|
||||
if (!confirm('Are you sure you want to delete this user?')) return;
|
||||
if (!userId || !confirm(t('settings.deleteConfirm'))) return;
|
||||
try {
|
||||
await apiClient.delete(`/api/v1/auth/${userId}`);
|
||||
await fetchUsers();
|
||||
} catch (err) {
|
||||
console.error("Failed to delete user", err);
|
||||
alert("Failed to delete user");
|
||||
} catch {
|
||||
alert(t('settings.deleteFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditClick = (user: User) => {
|
||||
const handleEditClick = (user: UserType) => {
|
||||
setEditingUser(user);
|
||||
setEditRole(user.role || 'User');
|
||||
setIsEditModalOpen(true);
|
||||
@@ -99,132 +72,113 @@ export function UsersSettings() {
|
||||
try {
|
||||
await apiClient.put('/api/v1/auth/authority', {
|
||||
user_id: editingUser.user_id,
|
||||
new_authority: editRole
|
||||
new_authority: editRole,
|
||||
});
|
||||
await fetchUsers();
|
||||
setIsEditModalOpen(false);
|
||||
} catch (err) {
|
||||
console.error("Failed to update user role", err);
|
||||
alert("Failed to update user role");
|
||||
} catch {
|
||||
alert(t('settings.updateFailed'));
|
||||
} finally {
|
||||
setSubmitLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-end">
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-slate-800">User Management</h3>
|
||||
<p className="text-sm text-slate-500 mt-1">Manage system users and their roles.</p>
|
||||
<h3 className="text-lg font-bold text-text-primary">{t('settings.userManagement')}</h3>
|
||||
<p className="text-sm text-text-muted mt-0.5">Manage system users and their roles</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleOpenModal}
|
||||
className="flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-sm font-medium transition-colors shadow-sm cursor-pointer"
|
||||
onClick={() => { setFormData({ username: '', password: '' }); setError(''); setIsModalOpen(true); }}
|
||||
className="flex items-center gap-2 px-4 py-2.5 bg-accent text-white rounded-xl hover:bg-accent-hover transition-all shadow-lg shadow-accent/15 text-sm font-medium"
|
||||
>
|
||||
<Plus size={16} className="mr-2" />
|
||||
Add User
|
||||
<Plus size={14} />
|
||||
{t('settings.addUser')}
|
||||
</button>
|
||||
</div>
|
||||
<div className="bg-white border border-slate-200 rounded-xl shadow-sm overflow-hidden">
|
||||
|
||||
<div className="bg-bg-card rounded-2xl border border-border-primary shadow-sm overflow-hidden">
|
||||
<table className="w-full text-left text-sm">
|
||||
<thead className="bg-slate-50 border-b border-slate-200 text-slate-500">
|
||||
<tr>
|
||||
<th className="px-6 py-4 font-medium">Username</th>
|
||||
<th className="px-6 py-4 font-medium">Role</th>
|
||||
<th className="px-6 py-4 font-medium">Status</th>
|
||||
<th className="px-6 py-4 font-medium text-right">Actions</th>
|
||||
<thead>
|
||||
<tr className="bg-bg-secondary border-b border-border-primary text-text-muted text-xs uppercase tracking-wider">
|
||||
<th className="px-5 py-3 font-semibold">{t('settings.username')}</th>
|
||||
<th className="px-5 py-3 font-semibold">{t('settings.role')}</th>
|
||||
<th className="px-5 py-3 font-semibold">{t('settings.status')}</th>
|
||||
<th className="px-5 py-3 font-semibold text-right">{t('settings.actions')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-100">
|
||||
{users.map((user, i) => (
|
||||
<tr key={i} className="hover:bg-slate-50 transition-colors">
|
||||
<td className="px-6 py-4 font-medium text-slate-800">{user.user_name}</td>
|
||||
<td className="px-6 py-4 text-slate-600">{user.role || 'User'}</td>
|
||||
<td className="px-6 py-4">
|
||||
<span className={`px-2.5 py-1 rounded-full text-xs font-medium ${user.status === 'Active' ? 'bg-green-100 text-green-700 border border-green-200' : 'bg-slate-100 text-slate-600 border border-slate-200'}`}>
|
||||
{user.status || 'Active'}
|
||||
<tbody className="divide-y divide-border-secondary">
|
||||
{users.map((user) => (
|
||||
<tr key={user.user_id || user.user_name} className="hover:bg-bg-hover transition-colors">
|
||||
<td className="px-5 py-3.5">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="w-7 h-7 rounded-full bg-accent-light flex items-center justify-center">
|
||||
<User size={13} className="text-accent" />
|
||||
</div>
|
||||
<span className="font-medium text-text-primary">{user.user_name}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-5 py-3.5">
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-xs font-medium bg-bg-secondary text-text-secondary border border-border-primary">
|
||||
<Shield size={10} />
|
||||
{user.role || 'User'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-right">
|
||||
<button onClick={() => handleEditClick(user)} className="text-slate-400 hover:text-blue-600 mr-3 transition-colors cursor-pointer" title="Edit"><Edit2 size={16} /></button>
|
||||
<button onClick={() => handleDeleteUser(user.user_id)} className="text-slate-400 hover:text-red-600 transition-colors cursor-pointer" title="Delete"><Trash2 size={16} /></button>
|
||||
<td className="px-5 py-3.5">
|
||||
<span className={`inline-block w-1.5 h-1.5 rounded-full mr-1.5 ${user.status === 'Active' ? 'bg-success' : 'bg-text-muted'}`} />
|
||||
<span className="text-xs text-text-secondary">{user.status || t('settings.active')}</span>
|
||||
</td>
|
||||
<td className="px-5 py-3.5 text-right">
|
||||
<button onClick={() => handleEditClick(user)} className="p-1.5 text-text-muted hover:text-accent hover:bg-accent-light rounded-lg transition-all mr-1">
|
||||
<Edit2 size={14} />
|
||||
</button>
|
||||
<button onClick={() => handleDeleteUser(user.user_id)} className="p-1.5 text-text-muted hover:text-danger hover:bg-danger-bg rounded-lg transition-all">
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{users.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={4} className="px-5 py-8 text-center text-text-muted text-sm">
|
||||
No users found
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Add User Modal */}
|
||||
{isModalOpen && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm transition-opacity">
|
||||
<div className="bg-white rounded-2xl shadow-xl w-full max-w-md overflow-hidden animate-in fade-in zoom-in-95 duration-200">
|
||||
<div className="flex justify-between items-center p-5 border-b border-slate-100">
|
||||
<h3 className="text-lg font-semibold text-slate-800">Add New User</h3>
|
||||
<button
|
||||
onClick={handleCloseModal}
|
||||
className="text-slate-400 hover:text-slate-600 p-1 rounded-md transition-colors cursor-pointer"
|
||||
>
|
||||
<X size={20} />
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm">
|
||||
<div className="bg-bg-card rounded-2xl shadow-2xl w-full max-w-md overflow-hidden border border-border-primary animate-fade-in-scale">
|
||||
<div className="flex justify-between items-center p-5 border-b border-border-primary">
|
||||
<h3 className="text-base font-bold text-text-primary">{t('settings.addNewUser')}</h3>
|
||||
<button onClick={() => setIsModalOpen(false)} className="p-1 text-text-muted hover:text-text-primary rounded-lg transition-colors">
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="p-6 space-y-4">
|
||||
{error && (
|
||||
<div className="p-3 bg-red-50 text-red-600 text-sm rounded-lg border border-red-100">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="p-5 space-y-4">
|
||||
{error && <div className="p-3 bg-danger-bg text-danger text-sm rounded-xl border border-danger/20">{error}</div>}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1.5">Username</label>
|
||||
<input
|
||||
type="text"
|
||||
name="username"
|
||||
placeholder="e.g. jsmith"
|
||||
value={formData.username}
|
||||
onChange={handleChange}
|
||||
className="w-full bg-white border border-slate-200 text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all placeholder:text-slate-400"
|
||||
/>
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('settings.username')}</label>
|
||||
<input type="text" value={formData.username} onChange={(e) => setFormData({...formData, username: e.target.value})}
|
||||
className="w-full bg-bg-input border border-border-primary text-sm rounded-xl px-3.5 py-2.5 focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent text-text-primary placeholder:text-text-muted/50" placeholder={t('settings.enterUsername')} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1.5">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
placeholder="Enter secure password"
|
||||
value={formData.password}
|
||||
onChange={handleChange}
|
||||
className="w-full bg-white border border-slate-200 text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all placeholder:text-slate-400"
|
||||
/>
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('settings.password')}</label>
|
||||
<input type="password" value={formData.password} onChange={(e) => setFormData({...formData, password: e.target.value})}
|
||||
className="w-full bg-bg-input border border-border-primary text-sm rounded-xl px-3.5 py-2.5 focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent text-text-primary placeholder:text-text-muted/50" placeholder={t('settings.enterPassword')} />
|
||||
</div>
|
||||
|
||||
<div className="pt-4 flex justify-end space-x-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCloseModal}
|
||||
className="px-4 py-2 text-sm font-medium text-slate-600 bg-white border border-slate-200 rounded-lg hover:bg-slate-50 transition-colors cursor-pointer"
|
||||
>
|
||||
Cancel
|
||||
<div className="pt-2 flex justify-end gap-2">
|
||||
<button type="button" onClick={() => setIsModalOpen(false)} className="px-4 py-2 text-sm font-medium text-text-secondary hover:bg-bg-hover rounded-xl transition-colors">
|
||||
{t('settings.cancel')}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={submitLoading}
|
||||
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors cursor-pointer disabled:opacity-70 flex items-center"
|
||||
>
|
||||
{submitLoading ? (
|
||||
<span className="flex items-center">
|
||||
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Creating...
|
||||
</span>
|
||||
) : (
|
||||
'Add User'
|
||||
)}
|
||||
<button type="submit" disabled={submitLoading} className="px-4 py-2 text-sm font-medium text-white bg-accent rounded-xl hover:bg-accent-hover transition-colors disabled:opacity-50">
|
||||
{submitLoading ? 'Creating...' : t('settings.create')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -232,58 +186,36 @@ export function UsersSettings() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Edit User Role Modal */}
|
||||
{/* Edit Modal */}
|
||||
{isEditModalOpen && editingUser && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm transition-opacity">
|
||||
<div className="bg-white rounded-2xl shadow-xl w-full max-w-md overflow-hidden animate-in fade-in zoom-in-95 duration-200">
|
||||
<div className="flex justify-between items-center p-5 border-b border-slate-100">
|
||||
<h3 className="text-lg font-semibold text-slate-800">Edit User Role</h3>
|
||||
<button
|
||||
onClick={() => setIsEditModalOpen(false)}
|
||||
className="text-slate-400 hover:text-slate-600 p-1 rounded-md transition-colors cursor-pointer"
|
||||
>
|
||||
<X size={20} />
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm">
|
||||
<div className="bg-bg-card rounded-2xl shadow-2xl w-full max-w-md overflow-hidden border border-border-primary animate-fade-in-scale">
|
||||
<div className="flex justify-between items-center p-5 border-b border-border-primary">
|
||||
<h3 className="text-base font-bold text-text-primary">{t('settings.editUserRole')}</h3>
|
||||
<button onClick={() => setIsEditModalOpen(false)} className="p-1 text-text-muted hover:text-text-primary rounded-lg transition-colors">
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleEditSubmit} className="p-6 space-y-4">
|
||||
<form onSubmit={handleEditSubmit} className="p-5 space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1.5">Username</label>
|
||||
<input
|
||||
type="text"
|
||||
disabled
|
||||
value={editingUser.user_name}
|
||||
className="w-full bg-slate-50 border border-slate-200 text-sm rounded-lg px-3 py-2.5 text-slate-500"
|
||||
/>
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('settings.username')}</label>
|
||||
<input type="text" disabled value={editingUser.user_name} className="w-full bg-bg-secondary border border-border-primary text-sm rounded-xl px-3.5 py-2.5 text-text-muted" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1.5">Role</label>
|
||||
<select
|
||||
value={editRole}
|
||||
onChange={(e) => setEditRole(e.target.value)}
|
||||
className="w-full bg-white border border-slate-200 text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all cursor-pointer"
|
||||
>
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('settings.role')}</label>
|
||||
<select value={editRole} onChange={(e) => setEditRole(e.target.value)}
|
||||
className="w-full bg-bg-input border border-border-primary text-sm rounded-xl px-3.5 py-2.5 focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent text-text-primary">
|
||||
<option value="User">User</option>
|
||||
<option value="Administrator">Administrator</option>
|
||||
<option value="SuperAdministrator">SuperAdministrator</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 flex justify-end space-x-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsEditModalOpen(false)}
|
||||
className="px-4 py-2 text-sm font-medium text-slate-600 bg-white border border-slate-200 rounded-lg hover:bg-slate-50 transition-colors cursor-pointer"
|
||||
>
|
||||
Cancel
|
||||
<div className="pt-2 flex justify-end gap-2">
|
||||
<button type="button" onClick={() => setIsEditModalOpen(false)} className="px-4 py-2 text-sm font-medium text-text-secondary hover:bg-bg-hover rounded-xl transition-colors">
|
||||
{t('settings.cancel')}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={submitLoading}
|
||||
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors cursor-pointer disabled:opacity-70 flex items-center"
|
||||
>
|
||||
{submitLoading ? 'Saving...' : 'Save Changes'}
|
||||
<button type="submit" disabled={submitLoading} className="px-4 py-2 text-sm font-medium text-white bg-accent rounded-xl hover:bg-accent-hover transition-colors disabled:opacity-50">
|
||||
{submitLoading ? 'Saving...' : t('settings.save')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
import en from './locales/en.json';
|
||||
import zh from './locales/zh.json';
|
||||
|
||||
const resources = {
|
||||
en: { translation: en },
|
||||
zh: { translation: zh },
|
||||
};
|
||||
|
||||
i18n
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources,
|
||||
fallbackLng: 'en',
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
detection: {
|
||||
order: ['localStorage', 'navigator'],
|
||||
caches: ['localStorage'],
|
||||
},
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
@@ -0,0 +1,123 @@
|
||||
{
|
||||
"app": {
|
||||
"name": "KiloStar",
|
||||
"tagline": "Distributed Multi-Agent System"
|
||||
},
|
||||
"nav": {
|
||||
"work": "Work",
|
||||
"agent": "Agent",
|
||||
"chat": "Chat",
|
||||
"workflow": "Workflow",
|
||||
"plugin": "Plugin",
|
||||
"agents": "Agents",
|
||||
"settings": "Settings"
|
||||
},
|
||||
"auth": {
|
||||
"welcomeBack": "Welcome Back",
|
||||
"createAccount": "Create Account",
|
||||
"enterCredentials": "Enter your credentials to access your account",
|
||||
"signUpToStart": "Sign up to start using the platform",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"signIn": "Sign In",
|
||||
"signUp": "Sign Up",
|
||||
"noAccount": "Don't have an account?",
|
||||
"hasAccount": "Already have an account?",
|
||||
"processing": "Processing...",
|
||||
"registerSuccess": "Registration successful. Please log in."
|
||||
},
|
||||
"chat": {
|
||||
"newChat": "New Chat",
|
||||
"chatHistory": "Chat History",
|
||||
"noHistory": "No chat history.",
|
||||
"startChat": "Click + to start a new chat.",
|
||||
"placeholder": "Ask kilostar to do something...",
|
||||
"send": "Send",
|
||||
"selectChat": "Select a chat history or create a new one to start.",
|
||||
"assistantName": "kilostar Assistant",
|
||||
"errorCommunication": "Sorry, an error occurred while communicating with the server."
|
||||
},
|
||||
"workflow": {
|
||||
"workflows": "Workflows",
|
||||
"manageWorkflows": "Manage and monitor your automated processes.",
|
||||
"createWorkflow": "Create Workflow",
|
||||
"noWorkflows": "No Workflows Found",
|
||||
"workflowsAppearHere": "Workflows created from your chats will appear here automatically.",
|
||||
"newWorkflow": "New Workflow",
|
||||
"backToList": "Back to workflow list",
|
||||
"workflowTitle": "Workflow Title",
|
||||
"titlePlaceholder": "e.g. Scrape tech news and generate summaries",
|
||||
"titleHint": "Give your workflow a concise, descriptive name",
|
||||
"command": "Command",
|
||||
"commandHint": "Describe the task you want the AI to complete in detail",
|
||||
"commandPlaceholder": "Describe the task you want the AI to complete...",
|
||||
"step1": "Step 1",
|
||||
"step2": "Step 2",
|
||||
"cancel": "Cancel",
|
||||
"createAndStart": "Create & Start Workflow",
|
||||
"creating": "Creating...",
|
||||
"live": "Live",
|
||||
"disconnected": "Disconnected",
|
||||
"chatLog": "Chat Log",
|
||||
"diagram": "Diagram",
|
||||
"originalCommand": "Original Command",
|
||||
"waitingEvents": "Waiting for events...",
|
||||
"replyPlaceholder": "Reply to the workflow...",
|
||||
"refresh": "Refresh Data",
|
||||
"workflowDetails": "Workflow Details",
|
||||
"loading": "Loading Workflows...",
|
||||
"status": {
|
||||
"waiting": "Waiting",
|
||||
"running": "Running",
|
||||
"completed": "Completed",
|
||||
"failed": "Failed"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"settings": "Settings",
|
||||
"userManagement": "User Management",
|
||||
"systemSettings": "System Settings",
|
||||
"general": "General",
|
||||
"language": "Language",
|
||||
"theme": "Theme",
|
||||
"light": "Light",
|
||||
"dark": "Dark",
|
||||
"systemDefault": "System Default",
|
||||
"clusterRuntime": "Cluster & Runtime",
|
||||
"debugLogging": "Enable debug logging",
|
||||
"saveChanges": "Save Changes",
|
||||
"saved": "Settings saved!",
|
||||
"addUser": "Add User",
|
||||
"username": "Username",
|
||||
"role": "Role",
|
||||
"status": "Status",
|
||||
"actions": "Actions",
|
||||
"active": "Active",
|
||||
"inactive": "Inactive",
|
||||
"cancel": "Cancel",
|
||||
"create": "Create",
|
||||
"save": "Save",
|
||||
"deleteConfirm": "Are you sure you want to delete this user?",
|
||||
"deleteFailed": "Failed to delete user",
|
||||
"updateFailed": "Failed to update user role",
|
||||
"addNewUser": "Add New User",
|
||||
"editUserRole": "Edit User Role",
|
||||
"enterUsername": "e.g. jsmith",
|
||||
"enterPassword": "Enter secure password",
|
||||
"fillBoth": "Please fill in both username and password."
|
||||
},
|
||||
"agent": {
|
||||
"individual": "Individual",
|
||||
"providerManagement": "Provider Management",
|
||||
"skills": "Skills",
|
||||
"tools": "Tools"
|
||||
},
|
||||
"common": {
|
||||
"close": "Close",
|
||||
"submit": "Submit",
|
||||
"loading": "Loading...",
|
||||
"error": "Error",
|
||||
"success": "Success",
|
||||
"back": "Back"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
{
|
||||
"app": {
|
||||
"name": "千星",
|
||||
"tagline": "分布式多智能体系统"
|
||||
},
|
||||
"nav": {
|
||||
"work": "工作",
|
||||
"agent": "智能体",
|
||||
"chat": "对话",
|
||||
"workflow": "工作流",
|
||||
"plugin": "插件",
|
||||
"agents": "智能体",
|
||||
"settings": "设置"
|
||||
},
|
||||
"auth": {
|
||||
"welcomeBack": "欢迎回来",
|
||||
"createAccount": "创建账号",
|
||||
"enterCredentials": "输入您的凭据以访问账户",
|
||||
"signUpToStart": "注册以开始使用平台",
|
||||
"username": "用户名",
|
||||
"password": "密码",
|
||||
"signIn": "登录",
|
||||
"signUp": "注册",
|
||||
"noAccount": "还没有账号?",
|
||||
"hasAccount": "已有账号?",
|
||||
"processing": "处理中...",
|
||||
"registerSuccess": "注册成功,请登录。"
|
||||
},
|
||||
"chat": {
|
||||
"newChat": "新对话",
|
||||
"chatHistory": "对话历史",
|
||||
"noHistory": "暂无对话记录",
|
||||
"startChat": "点击 + 开始新对话",
|
||||
"placeholder": "让 kilostar 做点什么...",
|
||||
"send": "发送",
|
||||
"selectChat": "选择对话记录或创建新对话以开始",
|
||||
"assistantName": "kilostar 助手",
|
||||
"errorCommunication": "抱歉,与服务器通信时出错。"
|
||||
},
|
||||
"workflow": {
|
||||
"workflows": "工作流",
|
||||
"manageWorkflows": "管理和监控您的自动化流程",
|
||||
"createWorkflow": "创建工作流",
|
||||
"noWorkflows": "暂无工作流",
|
||||
"workflowsAppearHere": "从对话创建的工作流将自动显示在这里",
|
||||
"newWorkflow": "新建工作流",
|
||||
"backToList": "返回工作流列表",
|
||||
"workflowTitle": "工作流标题",
|
||||
"titlePlaceholder": "例如:爬取最新技术新闻并生成摘要",
|
||||
"titleHint": "为你的工作流起一个简洁、描述性的名称",
|
||||
"command": "需求描述",
|
||||
"commandHint": "详细描述你希望 AI 完成的任务,越具体效果越好",
|
||||
"commandPlaceholder": "详细描述你希望 AI 完成的任务...",
|
||||
"step1": "第一步",
|
||||
"step2": "第二步",
|
||||
"cancel": "取消",
|
||||
"createAndStart": "创建并启动工作流",
|
||||
"creating": "正在创建...",
|
||||
"live": "实时",
|
||||
"disconnected": "已断开",
|
||||
"chatLog": "交流日志",
|
||||
"diagram": "流程图",
|
||||
"originalCommand": "原始指令",
|
||||
"waitingEvents": "等待事件...",
|
||||
"replyPlaceholder": "回复工作流...",
|
||||
"refresh": "刷新数据",
|
||||
"workflowDetails": "工作流详情",
|
||||
"loading": "正在加载工作流...",
|
||||
"status": {
|
||||
"waiting": "等待中",
|
||||
"running": "运行中",
|
||||
"completed": "已完成",
|
||||
"failed": "失败"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"settings": "设置",
|
||||
"userManagement": "用户管理",
|
||||
"systemSettings": "系统设置",
|
||||
"general": "通用",
|
||||
"language": "语言",
|
||||
"theme": "主题",
|
||||
"light": "浅色",
|
||||
"dark": "深色",
|
||||
"systemDefault": "跟随系统",
|
||||
"clusterRuntime": "集群与运行时",
|
||||
"debugLogging": "启用调试日志",
|
||||
"saveChanges": "保存更改",
|
||||
"saved": "设置已保存!",
|
||||
"addUser": "添加用户",
|
||||
"username": "用户名",
|
||||
"role": "角色",
|
||||
"status": "状态",
|
||||
"actions": "操作",
|
||||
"active": "活跃",
|
||||
"inactive": "未激活",
|
||||
"cancel": "取消",
|
||||
"create": "创建",
|
||||
"save": "保存",
|
||||
"deleteConfirm": "确定要删除此用户吗?",
|
||||
"deleteFailed": "删除用户失败",
|
||||
"updateFailed": "更新用户角色失败",
|
||||
"addNewUser": "添加新用户",
|
||||
"editUserRole": "编辑用户角色",
|
||||
"enterUsername": "例如:zhangsan",
|
||||
"enterPassword": "输入安全密码",
|
||||
"fillBoth": "请填写用户名和密码。"
|
||||
},
|
||||
"agent": {
|
||||
"individual": "个体",
|
||||
"providerManagement": "供应商管理",
|
||||
"skills": "技能",
|
||||
"tools": "工具"
|
||||
},
|
||||
"common": {
|
||||
"close": "关闭",
|
||||
"submit": "提交",
|
||||
"loading": "加载中...",
|
||||
"error": "错误",
|
||||
"success": "成功",
|
||||
"back": "返回"
|
||||
}
|
||||
}
|
||||
@@ -1 +1,249 @@
|
||||
@import "tailwindcss";
|
||||
@import "@fontsource/inter/400.css";
|
||||
@import "@fontsource/inter/500.css";
|
||||
@import "@fontsource/inter/600.css";
|
||||
@import "@fontsource/inter/700.css";
|
||||
@import "@fontsource/jetbrains-mono/400.css";
|
||||
@import "@fontsource/jetbrains-mono/500.css";
|
||||
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
@theme {
|
||||
--color-bg-primary: var(--bg-primary);
|
||||
--color-bg-secondary: var(--bg-secondary);
|
||||
--color-bg-tertiary: var(--bg-tertiary);
|
||||
--color-bg-card: var(--bg-card);
|
||||
--color-bg-sidebar: var(--bg-sidebar);
|
||||
--color-bg-topbar: var(--bg-topbar);
|
||||
--color-bg-input: var(--bg-input);
|
||||
--color-bg-hover: var(--bg-hover);
|
||||
--color-bg-active: var(--bg-active);
|
||||
--color-bg-glass: var(--bg-glass);
|
||||
--color-border-primary: var(--border-primary);
|
||||
--color-border-secondary: var(--border-secondary);
|
||||
--color-text-primary: var(--text-primary);
|
||||
--color-text-secondary: var(--text-secondary);
|
||||
--color-text-tertiary: var(--text-tertiary);
|
||||
--color-text-muted: var(--text-muted);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-hover: var(--accent-hover);
|
||||
--color-accent-light: var(--accent-light);
|
||||
--color-accent-text: var(--accent-text);
|
||||
--color-accent-glow: var(--accent-glow);
|
||||
--color-danger: var(--danger);
|
||||
--color-danger-bg: var(--danger-bg);
|
||||
--color-success: var(--success);
|
||||
--color-success-bg: var(--success-bg);
|
||||
--color-warning: var(--warning);
|
||||
--color-warning-bg: var(--warning-bg);
|
||||
--color-glow-purple: var(--glow-purple);
|
||||
--color-glow-cyan: var(--glow-cyan);
|
||||
--font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
}
|
||||
|
||||
:root {
|
||||
--bg-primary: #fafafa;
|
||||
--bg-secondary: #f4f4f5;
|
||||
--bg-tertiary: #e4e4e7;
|
||||
--bg-card: #ffffff;
|
||||
--bg-sidebar: #f4f4f5;
|
||||
--bg-topbar: rgba(255, 255, 255, 0.72);
|
||||
--bg-input: #f4f4f5;
|
||||
--bg-hover: #f4f4f5;
|
||||
--bg-active: #eff6ff;
|
||||
--bg-glass: rgba(255, 255, 255, 0.6);
|
||||
--border-primary: #e4e4e7;
|
||||
--border-secondary: #f4f4f5;
|
||||
--text-primary: #18181b;
|
||||
--text-secondary: #3f3f46;
|
||||
--text-tertiary: #52525b;
|
||||
--text-muted: #a1a1aa;
|
||||
--accent: #4f46e5;
|
||||
--accent-hover: #4338ca;
|
||||
--accent-light: #e0e7ff;
|
||||
--accent-text: #3730a3;
|
||||
--accent-glow: rgba(79, 70, 229, 0.15);
|
||||
--danger: #dc2626;
|
||||
--danger-bg: #fef2f2;
|
||||
--success: #16a34a;
|
||||
--success-bg: #f0fdf4;
|
||||
--warning: #d97706;
|
||||
--warning-bg: #fffbeb;
|
||||
--glow-purple: #a855f7;
|
||||
--glow-cyan: #06b6d4;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--bg-primary: #09090b;
|
||||
--bg-secondary: #0f0f11;
|
||||
--bg-tertiary: #18181b;
|
||||
--bg-card: #131316;
|
||||
--bg-sidebar: #0c0c0e;
|
||||
--bg-topbar: rgba(9, 9, 11, 0.72);
|
||||
--bg-input: #18181b;
|
||||
--bg-hover: #1c1c1f;
|
||||
--bg-active: rgba(79, 70, 229, 0.12);
|
||||
--bg-glass: rgba(19, 19, 22, 0.6);
|
||||
--border-primary: rgba(255, 255, 255, 0.08);
|
||||
--border-secondary: rgba(255, 255, 255, 0.04);
|
||||
--text-primary: #fafafa;
|
||||
--text-secondary: #e4e4e7;
|
||||
--text-tertiary: #a1a1aa;
|
||||
--text-muted: #71717a;
|
||||
--accent: #6366f1;
|
||||
--accent-hover: #818cf8;
|
||||
--accent-light: rgba(99, 102, 241, 0.15);
|
||||
--accent-text: #a5b4fc;
|
||||
--accent-glow: rgba(99, 102, 241, 0.25);
|
||||
--danger: #f87171;
|
||||
--danger-bg: rgba(248, 113, 113, 0.1);
|
||||
--success: #4ade80;
|
||||
--success-bg: rgba(74, 222, 128, 0.1);
|
||||
--warning: #fbbf24;
|
||||
--warning-bg: rgba(251, 191, 36, 0.1);
|
||||
--glow-purple: #c084fc;
|
||||
--glow-cyan: #67e8f9;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* Dot pattern background for dark mode */
|
||||
.dark body::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background-image: radial-gradient(circle at 1px 1px, rgba(255,255,255,0.04) 1px, transparent 0);
|
||||
background-size: 24px 24px;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--border-primary);
|
||||
border-radius: 10px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Selection */
|
||||
::selection {
|
||||
background-color: var(--accent-light);
|
||||
color: var(--accent-text);
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(6px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
@keyframes fadeInScale {
|
||||
from { opacity: 0; transform: scale(0.96); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
@keyframes slideUp {
|
||||
from { opacity: 0; transform: translateY(16px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
@keyframes shimmer {
|
||||
0% { background-position: -200% 0; }
|
||||
100% { background-position: 200% 0; }
|
||||
}
|
||||
@keyframes pulse-glow {
|
||||
0%, 100% { opacity: 0.5; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
@keyframes typing {
|
||||
0%, 60%, 100% { transform: translateY(0); }
|
||||
30% { transform: translateY(-4px); }
|
||||
}
|
||||
@keyframes gradient-shift {
|
||||
0% { background-position: 0% 50%; }
|
||||
50% { background-position: 100% 50%; }
|
||||
100% { background-position: 0% 50%; }
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.35s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||
}
|
||||
.animate-fade-in-scale {
|
||||
animation: fadeInScale 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||
}
|
||||
.animate-slide-up {
|
||||
animation: slideUp 0.5s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||
}
|
||||
.animate-shimmer {
|
||||
background: linear-gradient(90deg, transparent 0%, var(--accent-glow) 50%, transparent 100%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 2s infinite;
|
||||
}
|
||||
.animate-typing-dot {
|
||||
animation: typing 1.4s ease-in-out infinite;
|
||||
}
|
||||
.animate-typing-dot:nth-child(2) { animation-delay: 0.2s; }
|
||||
.animate-typing-dot:nth-child(3) { animation-delay: 0.4s; }
|
||||
.animate-gradient {
|
||||
background-size: 200% 200%;
|
||||
animation: gradient-shift 6s ease infinite;
|
||||
}
|
||||
|
||||
/* Glass effect */
|
||||
.glass {
|
||||
background: var(--bg-glass);
|
||||
backdrop-filter: blur(16px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(16px) saturate(180%);
|
||||
border-bottom: 1px solid var(--border-primary);
|
||||
}
|
||||
|
||||
/* Modern card hover */
|
||||
.card-hover {
|
||||
transition: transform 0.2s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
.card-hover:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 30px -8px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
.dark .card-hover:hover {
|
||||
box-shadow: 0 8px 30px -8px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
/* Glow effects */
|
||||
.glow-accent {
|
||||
box-shadow: 0 0 20px -4px var(--accent-glow);
|
||||
}
|
||||
.glow-purple {
|
||||
box-shadow: 0 0 30px -6px rgba(168, 85, 247, 0.3);
|
||||
}
|
||||
|
||||
/* Status indicator pulse */
|
||||
.status-dot {
|
||||
position: relative;
|
||||
}
|
||||
.status-dot::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: -2px;
|
||||
border-radius: 50%;
|
||||
background: inherit;
|
||||
opacity: 0.4;
|
||||
animation: pulse-glow 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './i18n'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
type AppMode = 'work' | 'agent';
|
||||
type WorkTab = 'chat' | 'workflow';
|
||||
type AgentTab = 'plugin' | 'agents';
|
||||
type ThemeMode = 'light' | 'dark' | 'system';
|
||||
|
||||
interface AppState {
|
||||
// Auth
|
||||
isAuthenticated: boolean;
|
||||
setIsAuthenticated: (v: boolean) => void;
|
||||
|
||||
// Layout
|
||||
mode: AppMode;
|
||||
setMode: (mode: AppMode) => void;
|
||||
showSettings: boolean;
|
||||
setShowSettings: (v: boolean) => void;
|
||||
isSidebarOpen: boolean;
|
||||
setIsSidebarOpen: (v: boolean) => void;
|
||||
|
||||
// Work tabs
|
||||
workTab: WorkTab;
|
||||
setWorkTab: (tab: WorkTab) => void;
|
||||
|
||||
// Agent tabs
|
||||
agentTab: AgentTab;
|
||||
setAgentTab: (tab: AgentTab) => void;
|
||||
|
||||
// Settings tabs
|
||||
settingsTab: string;
|
||||
setSettingsTab: (tab: string) => void;
|
||||
innerAgentTab: string;
|
||||
setInnerAgentTab: (tab: string) => void;
|
||||
resourceTab: string;
|
||||
setResourceTab: (tab: string) => void;
|
||||
|
||||
// Theme
|
||||
theme: ThemeMode;
|
||||
setTheme: (theme: ThemeMode) => void;
|
||||
resolvedTheme: 'light' | 'dark';
|
||||
applyTheme: () => void;
|
||||
}
|
||||
|
||||
function resolveTheme(theme: ThemeMode): 'light' | 'dark' {
|
||||
if (theme === 'system') {
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
}
|
||||
return theme;
|
||||
}
|
||||
|
||||
export const useAppStore = create<AppState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
isAuthenticated: false,
|
||||
setIsAuthenticated: (v) => set({ isAuthenticated: v }),
|
||||
|
||||
mode: 'work',
|
||||
setMode: (mode) => set({ mode }),
|
||||
showSettings: false,
|
||||
setShowSettings: (v) => set({ showSettings: v }),
|
||||
isSidebarOpen: true,
|
||||
setIsSidebarOpen: (v) => set({ isSidebarOpen: v }),
|
||||
|
||||
workTab: 'chat',
|
||||
setWorkTab: (workTab) => set({ workTab }),
|
||||
|
||||
agentTab: 'plugin',
|
||||
setAgentTab: (agentTab) => set({ agentTab }),
|
||||
|
||||
settingsTab: 'users',
|
||||
setSettingsTab: (settingsTab) => set({ settingsTab }),
|
||||
innerAgentTab: 'worker',
|
||||
setInnerAgentTab: (innerAgentTab) => set({ innerAgentTab }),
|
||||
resourceTab: 'skill',
|
||||
setResourceTab: (resourceTab) => set({ resourceTab }),
|
||||
|
||||
theme: 'system',
|
||||
setTheme: (theme) => {
|
||||
set({ theme });
|
||||
const resolved = resolveTheme(theme);
|
||||
set({ resolvedTheme: resolved });
|
||||
if (resolved === 'dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
},
|
||||
resolvedTheme: 'light',
|
||||
applyTheme: () => {
|
||||
const resolved = resolveTheme(get().theme);
|
||||
set({ resolvedTheme: resolved });
|
||||
if (resolved === 'dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'kilostar-app-storage',
|
||||
partialize: (state) => ({
|
||||
theme: state.theme,
|
||||
isSidebarOpen: state.isSidebarOpen,
|
||||
}),
|
||||
}
|
||||
)
|
||||
);
|
||||
@@ -0,0 +1,198 @@
|
||||
import { create } from 'zustand';
|
||||
import apiClient from '../api/client';
|
||||
import type { ChatSessionDB } from '../types';
|
||||
|
||||
export interface Message {
|
||||
id: string;
|
||||
role: 'user' | 'assistant' | 'system';
|
||||
content: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export interface ChatSession {
|
||||
id: string;
|
||||
title: string;
|
||||
messages: Message[];
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
interface ChatState {
|
||||
sessions: ChatSession[];
|
||||
activeSessionId: string | null;
|
||||
selectedWorkflow: string | null;
|
||||
loadingSessions: boolean;
|
||||
loadingMessages: boolean;
|
||||
|
||||
setSessions: (sessions: ChatSession[]) => void;
|
||||
setActiveSessionId: (id: string | null) => void;
|
||||
setSelectedWorkflow: (id: string | null) => void;
|
||||
addSession: (session: ChatSession) => void;
|
||||
updateSessionMessages: (sessionId: string, messages: Message[]) => void;
|
||||
updateSessionTitle: (sessionId: string, title: string) => void;
|
||||
removeSession: (sessionId: string) => void;
|
||||
|
||||
loadSessions: () => Promise<void>;
|
||||
loadMessages: (chatId: string) => Promise<void>;
|
||||
createChat: (title?: string, initialMessage?: string) => Promise<string | null>;
|
||||
sendMessage: (chatId: string, text: string) => Promise<void>;
|
||||
}
|
||||
|
||||
function mapSessionFromDB(s: ChatSessionDB): ChatSession {
|
||||
return {
|
||||
id: s.chat_id,
|
||||
title: s.title,
|
||||
messages: [],
|
||||
updatedAt: new Date(s.updated_at).getTime(),
|
||||
};
|
||||
}
|
||||
|
||||
export const useChatStore = create<ChatState>((set, get) => ({
|
||||
sessions: [],
|
||||
activeSessionId: null,
|
||||
selectedWorkflow: null,
|
||||
loadingSessions: false,
|
||||
loadingMessages: false,
|
||||
|
||||
setSessions: (sessions) => set({ sessions }),
|
||||
setActiveSessionId: (id) => set({ activeSessionId: id }),
|
||||
setSelectedWorkflow: (id) => set({ selectedWorkflow: id }),
|
||||
|
||||
addSession: (session) =>
|
||||
set((state) => ({ sessions: [session, ...state.sessions], activeSessionId: session.id })),
|
||||
|
||||
updateSessionMessages: (sessionId, messages) =>
|
||||
set((state) => ({
|
||||
sessions: state.sessions.map((s) =>
|
||||
s.id === sessionId ? { ...s, messages, updatedAt: Date.now() } : s
|
||||
),
|
||||
})),
|
||||
|
||||
updateSessionTitle: (sessionId, title) =>
|
||||
set((state) => ({
|
||||
sessions: state.sessions.map((s) => (s.id === sessionId ? { ...s, title } : s)),
|
||||
})),
|
||||
|
||||
removeSession: (sessionId) =>
|
||||
set((state) => {
|
||||
const filtered = state.sessions.filter((s) => s.id !== sessionId);
|
||||
return {
|
||||
sessions: filtered,
|
||||
activeSessionId:
|
||||
state.activeSessionId === sessionId
|
||||
? filtered.length > 0
|
||||
? filtered[0].id
|
||||
: null
|
||||
: state.activeSessionId,
|
||||
};
|
||||
}),
|
||||
|
||||
loadSessions: async () => {
|
||||
set({ loadingSessions: true });
|
||||
try {
|
||||
const response = await apiClient.get('/api/v1/chat');
|
||||
const data: ChatSessionDB[] = response.data?.sessions || [];
|
||||
set({ sessions: data.map(mapSessionFromDB) });
|
||||
} catch (error) {
|
||||
console.error('Failed to load chat sessions', error);
|
||||
} finally {
|
||||
set({ loadingSessions: false });
|
||||
}
|
||||
},
|
||||
|
||||
loadMessages: async (chatId) => {
|
||||
set({ loadingMessages: true });
|
||||
try {
|
||||
const response = await apiClient.get(`/api/v1/chat/${chatId}`);
|
||||
const dbMessages = response.data?.messages || [];
|
||||
const mapped: Message[] = dbMessages.map((m: any) => ({
|
||||
id: m.message_id,
|
||||
role: m.message_owner === 'user' ? 'user' : 'assistant',
|
||||
content: m.message,
|
||||
timestamp: new Date(m.created_at).getTime(),
|
||||
}));
|
||||
set((state) => ({
|
||||
sessions: state.sessions.map((s) =>
|
||||
s.id === chatId ? { ...s, messages: mapped } : s
|
||||
),
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Failed to load messages', error);
|
||||
} finally {
|
||||
set({ loadingMessages: false });
|
||||
}
|
||||
},
|
||||
|
||||
createChat: async (title = '新对话', initialMessage = '你好') => {
|
||||
try {
|
||||
const response = await apiClient.post('/api/v1/chat', {
|
||||
title,
|
||||
initial_message: initialMessage,
|
||||
});
|
||||
const chatId: string = response.data.chat_id;
|
||||
const reply: string =
|
||||
response.data.reply || '你好!我是 kilostar 助手,有什么可以帮你的吗?';
|
||||
|
||||
const newSession: ChatSession = {
|
||||
id: chatId,
|
||||
title,
|
||||
messages: [
|
||||
{ id: chatId + '_user', role: 'user', content: initialMessage, timestamp: Date.now() },
|
||||
{ id: chatId + '_ai', role: 'assistant', content: reply, timestamp: Date.now() },
|
||||
],
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
get().addSession(newSession);
|
||||
return chatId;
|
||||
} catch (error) {
|
||||
console.error('Failed to create chat session', error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
sendMessage: async (chatId, text) => {
|
||||
const state = get();
|
||||
const session = state.sessions.find((s) => s.id === chatId);
|
||||
if (!session) return;
|
||||
|
||||
const userMessage: Message = {
|
||||
id: Date.now().toString(),
|
||||
role: 'user',
|
||||
content: text,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
const currentMessages = [...session.messages, userMessage];
|
||||
get().updateSessionMessages(chatId, currentMessages);
|
||||
|
||||
try {
|
||||
const response = await apiClient.post(`/api/v1/chat/${chatId}/reply`, {
|
||||
message: text,
|
||||
});
|
||||
const replyContent: string = response.data?.reply || '收到你的消息。';
|
||||
|
||||
// Auto-update title on first user message
|
||||
if (session.messages.length <= 1 && text.length > 0) {
|
||||
const newTitle = text.slice(0, 20) + (text.length > 20 ? '...' : '');
|
||||
get().updateSessionTitle(chatId, newTitle);
|
||||
}
|
||||
|
||||
const aiMessage: Message = {
|
||||
id: (Date.now() + 1).toString(),
|
||||
role: 'assistant',
|
||||
content: replyContent,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
get().updateSessionMessages(chatId, [...currentMessages, aiMessage]);
|
||||
} catch (error) {
|
||||
console.error('Error sending message', error);
|
||||
const errorMessage: Message = {
|
||||
id: (Date.now() + 1).toString(),
|
||||
role: 'assistant',
|
||||
content: '抱歉,与服务器通信时出错。',
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
get().updateSessionMessages(chatId, [...currentMessages, errorMessage]);
|
||||
}
|
||||
},
|
||||
}));
|
||||
@@ -26,8 +26,11 @@ from kilostar.utils.error import ModelNotExistError
|
||||
|
||||
|
||||
class AgentFactory:
|
||||
"""AgentFactory 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 AgentFactory 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。"""
|
||||
"""模型工厂:把内部的 ``Provider`` 元数据翻译成 pydantic-ai 的 ``Agent``。
|
||||
|
||||
支持 openai / claude / deepseek / gemini 四类后端,差异通过
|
||||
``_models_mapping`` 中的 ``model_class`` + ``provider_class`` 键值对屏蔽。
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._models_mapping = {
|
||||
|
||||
@@ -22,18 +22,13 @@ T = TypeVar("T", bound=BaseModel)
|
||||
|
||||
|
||||
class AgentRunResultProxy:
|
||||
"""AgentRunResultProxy 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 AgentRunResultProxy 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。"""
|
||||
"""``Agent.run`` 结果的轻量代理:把已解析的结构化对象暴露为 ``.data`` / ``.output``。"""
|
||||
|
||||
def __init__(self, original, parsed):
|
||||
self._original = original
|
||||
self._parsed = parsed
|
||||
|
||||
def __getattr__(self, name):
|
||||
"""检索并获取特定的 getattr 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Args: name: 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
if name == "data":
|
||||
return self._parsed
|
||||
if name == "output":
|
||||
@@ -102,10 +97,7 @@ class DeepSeekReasonerAgent(Generic[T]):
|
||||
)
|
||||
|
||||
def _parse_output(self, text: str) -> Any:
|
||||
"""执行与 parse output 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: text (str): 控制逻辑流向的具体字符串参数,指定了期望的 text 内容。
|
||||
Returns: (Any): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""从模型自由文本中抽取 ```json 块并按 ``output_schema`` 校验为对象。"""
|
||||
if not self.has_custom_output:
|
||||
return text
|
||||
|
||||
@@ -142,20 +134,13 @@ class DeepSeekReasonerAgent(Generic[T]):
|
||||
|
||||
def __getattr__(self, item):
|
||||
# Delegate any unknown attributes (like .system_prompt, .tool) to the underlying pydantic_ai Agent
|
||||
"""检索并获取特定的 getattr 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Args: item: 参与 getattr 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
return getattr(self.agent, item)
|
||||
|
||||
async def run(
|
||||
self, user_prompt: str, deps: Any = None, message_history: list = None, **kwargs
|
||||
) -> Any:
|
||||
# Custom retry loop
|
||||
"""执行与 run 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: user_prompt (str): 控制逻辑流向的具体字符串参数,指定了期望的 user prompt 内容。 deps (Any): 参与 run 逻辑运算或数据构建的上下文依赖对象。 message_history (list): 批量操作所需的列表集合,囊括了需要统一处理的多个 message history 元素。
|
||||
Returns: (Any): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""运行一次 deepseek-reasoner 推理:失败时根据错误反馈让模型重试,最多 ``self.retries`` 轮。"""
|
||||
current_history = message_history or []
|
||||
last_exception = None
|
||||
|
||||
|
||||
@@ -141,7 +141,7 @@ else:
|
||||
|
||||
@serve.deployment
|
||||
@serve.ingress(app)
|
||||
class kilostarGateway:
|
||||
class KiloStarGateway:
|
||||
gateway: Dict[str, WebSocket]
|
||||
|
||||
def __init__(self):
|
||||
|
||||
+12
-42
@@ -28,8 +28,7 @@ agent_router = APIRouter(prefix="/api/v1/agent", tags=["agent"])
|
||||
|
||||
|
||||
class AgentRegister(BaseModel):
|
||||
"""AgentRegister 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 AgentRegister 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。"""
|
||||
"""``POST /agent`` 入参(远程模型):通过 provider + model_id 加载系统节点。"""
|
||||
|
||||
provider_title: str
|
||||
model_id: str
|
||||
@@ -38,8 +37,7 @@ class AgentRegister(BaseModel):
|
||||
|
||||
|
||||
class AgentLocalRegister(BaseModel):
|
||||
"""AgentLocalRegister 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 AgentLocalRegister 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。"""
|
||||
"""``POST /agent`` 入参(本地模型):通过本地路径加载系统节点。"""
|
||||
|
||||
path: str
|
||||
individual_name: str
|
||||
@@ -50,10 +48,7 @@ class AgentLocalRegister(BaseModel):
|
||||
async def get_system_nodes(
|
||||
_: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.USER)),
|
||||
):
|
||||
"""处理针对 get system nodes 相关的 HTTP API 请求。
|
||||
该接口负责解析前端传入的载荷数据,调用底层核心业务逻辑进行处理,并组装标准化的 JSON 响应。
|
||||
Args: _ (TokenData): 参与 get system nodes 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: : 序列化后的标准网络响应模型(如包含业务状态码、成功标志及对应的数据载荷 Data)。"""
|
||||
"""返回三大系统节点(regulatory/consciousness/control)当前的持久化配置。"""
|
||||
postgres_database = ray_actor_hook("postgres_database").postgres_database
|
||||
configs = await postgres_database.get_all_system_node_configs.remote()
|
||||
return {"system_nodes": configs}
|
||||
@@ -64,10 +59,7 @@ async def load_agent(
|
||||
agent_register: Union[AgentRegister, AgentLocalRegister],
|
||||
_: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.USER)),
|
||||
):
|
||||
"""处理针对 load agent 相关的 HTTP API 请求。
|
||||
该接口负责解析前端传入的载荷数据,调用底层核心业务逻辑进行处理,并组装标准化的 JSON 响应。
|
||||
Args: agent_register (Union[AgentRegister, AgentLocalRegister]): 参与 load agent 逻辑运算或数据构建的上下文依赖对象。 _ (TokenData): 参与 load agent 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: : 序列化后的标准网络响应模型(如包含业务状态码、成功标志及对应的数据载荷 Data)。"""
|
||||
"""加载/重载某个系统节点的 Agent:先持久化配置,再调用对应节点 Actor 的 ``create_agent``。"""
|
||||
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
|
||||
postgres_database = ray_actor_hook("postgres_database").postgres_database
|
||||
|
||||
@@ -76,7 +68,6 @@ async def load_agent(
|
||||
|
||||
elif isinstance(agent_register, AgentRegister):
|
||||
try:
|
||||
# Persist configuration
|
||||
await postgres_database.upsert_system_node_config.remote(
|
||||
agent_register.individual_name,
|
||||
agent_register.provider_title,
|
||||
@@ -84,7 +75,6 @@ async def load_agent(
|
||||
agent_register.tools,
|
||||
)
|
||||
|
||||
# Load agent into state machine
|
||||
match agent_register.individual_name:
|
||||
case "regulatory_node":
|
||||
node = ray_actor_hook("regulatory_node").regulatory_node
|
||||
@@ -118,8 +108,7 @@ async def load_agent(
|
||||
|
||||
|
||||
class WorkerIndividualCreate(BaseModel):
|
||||
"""WorkerIndividualCreate 核心组件类。
|
||||
这是一个具体的 Worker 智能体实体类,代表着具备特定人设、领域技能或长文本处理能力的数字员工。它可以被控制器动态拉起,并在安全沙箱内执行复杂的工作流指令与多步骤推理任务。"""
|
||||
"""``POST /worker`` 入参:创建一个 Worker Agent 所需的完整配置。"""
|
||||
|
||||
agent_name: str
|
||||
agent_type: AgentType
|
||||
@@ -134,8 +123,7 @@ class WorkerIndividualCreate(BaseModel):
|
||||
|
||||
|
||||
class WorkerIndividualUpdate(BaseModel):
|
||||
"""WorkerIndividualUpdate 核心组件类。
|
||||
这是一个具体的 Worker 智能体实体类,代表着具备特定人设、领域技能或长文本处理能力的数字员工。它可以被控制器动态拉起,并在安全沙箱内执行复杂的工作流指令与多步骤推理任务。"""
|
||||
"""``PUT /worker/{agent_id}`` 入参:可选字段构成的局部更新载荷。"""
|
||||
|
||||
agent_name: Optional[str] = None
|
||||
agent_type: Optional[AgentType] = None
|
||||
@@ -154,10 +142,7 @@ async def create_worker_individual(
|
||||
worker_data: WorkerIndividualCreate,
|
||||
token_data: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.USER)),
|
||||
):
|
||||
"""处理针对 create worker individual 相关的 HTTP API 请求。
|
||||
该接口负责解析前端传入的载荷数据,调用底层核心业务逻辑进行处理,并组装标准化的 JSON 响应。
|
||||
Args: worker_data (WorkerIndividualCreate): 从客户端传递过来或由上游组件生成的核心业务数据体,通常需要进一步的清洗和结构化解析。 token_data (TokenData): 从客户端传递过来或由上游组件生成的核心业务数据体,通常需要进一步的清洗和结构化解析。
|
||||
Returns: : 序列化后的标准网络响应模型(如包含业务状态码、成功标志及对应的数据载荷 Data)。"""
|
||||
"""创建一个 Worker Agent,``owner_id`` 自动绑定为当前登录用户。"""
|
||||
postgres_database = ray_actor_hook("postgres_database").postgres_database
|
||||
data_dict = worker_data.model_dump()
|
||||
data_dict["owner_id"] = token_data.user_id
|
||||
@@ -169,10 +154,7 @@ async def create_worker_individual(
|
||||
async def get_worker_individual_list(
|
||||
token_data: TokenData = Depends(Accessor.get_current_user),
|
||||
):
|
||||
"""处理针对 get worker individual list 相关的 HTTP API 请求。
|
||||
该接口负责解析前端传入的载荷数据,调用底层核心业务逻辑进行处理,并组装标准化的 JSON 响应。
|
||||
Args: token_data (TokenData): 从客户端传递过来或由上游组件生成的核心业务数据体,通常需要进一步的清洗和结构化解析。
|
||||
Returns: : 序列化后的标准网络响应模型(如包含业务状态码、成功标志及对应的数据载荷 Data)。"""
|
||||
"""列出当前登录用户名下的全部 Worker Agent。"""
|
||||
postgres_database = ray_actor_hook("postgres_database").postgres_database
|
||||
workers = await postgres_database.get_worker_individual_list.remote(
|
||||
owner_id=token_data.user_id
|
||||
@@ -184,10 +166,7 @@ async def get_worker_individual_list(
|
||||
async def get_worker_individual(
|
||||
agent_id: str, token_data: TokenData = Depends(Accessor.get_current_user)
|
||||
):
|
||||
"""处理针对 get worker individual 相关的 HTTP API 请求。
|
||||
该接口负责解析前端传入的载荷数据,调用底层核心业务逻辑进行处理,并组装标准化的 JSON 响应。
|
||||
Args: agent_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 agent 实例。 token_data (TokenData): 从客户端传递过来或由上游组件生成的核心业务数据体,通常需要进一步的清洗和结构化解析。
|
||||
Returns: : 序列化后的标准网络响应模型(如包含业务状态码、成功标志及对应的数据载荷 Data)。"""
|
||||
"""按 ``agent_id`` 查询 Worker Agent;非本人的 Agent 返回 403。"""
|
||||
postgres_database = ray_actor_hook("postgres_database").postgres_database
|
||||
worker = await postgres_database.get_worker_individual.remote(agent_id=agent_id)
|
||||
if not worker:
|
||||
@@ -205,10 +184,7 @@ async def update_worker_individual(
|
||||
worker_data: WorkerIndividualUpdate,
|
||||
token_data: TokenData = Depends(Accessor.get_current_user),
|
||||
):
|
||||
"""处理针对 update worker individual 相关的 HTTP API 请求。
|
||||
该接口负责解析前端传入的载荷数据,调用底层核心业务逻辑进行处理,并组装标准化的 JSON 响应。
|
||||
Args: agent_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 agent 实例。 worker_data (WorkerIndividualUpdate): 从客户端传递过来或由上游组件生成的核心业务数据体,通常需要进一步的清洗和结构化解析。 token_data (TokenData): 从客户端传递过来或由上游组件生成的核心业务数据体,通常需要进一步的清洗和结构化解析。
|
||||
Returns: : 序列化后的标准网络响应模型(如包含业务状态码、成功标志及对应的数据载荷 Data)。"""
|
||||
"""局部更新 Worker Agent 配置;同时把状态机里的旧实例移除等待懒加载。"""
|
||||
postgres_database = ray_actor_hook("postgres_database").postgres_database
|
||||
worker = await postgres_database.get_worker_individual.remote(agent_id=agent_id)
|
||||
if not worker:
|
||||
@@ -236,10 +212,7 @@ async def update_worker_individual(
|
||||
async def reload_worker_individual(
|
||||
agent_id: str, token_data: TokenData = Depends(Accessor.get_current_user)
|
||||
):
|
||||
"""处理针对 reload worker individual 相关的 HTTP API 请求。
|
||||
该接口负责解析前端传入的载荷数据,调用底层核心业务逻辑进行处理,并组装标准化的 JSON 响应。
|
||||
Args: agent_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 agent 实例。 token_data (TokenData): 从客户端传递过来或由上游组件生成的核心业务数据体,通常需要进一步的清洗和结构化解析。
|
||||
Returns: : 序列化后的标准网络响应模型(如包含业务状态码、成功标志及对应的数据载荷 Data)。"""
|
||||
"""强制把 Worker 从内存池中卸载,下次调用时按最新配置重新加载。"""
|
||||
postgres_database = ray_actor_hook("postgres_database").postgres_database
|
||||
worker = await postgres_database.get_worker_individual.remote(agent_id=agent_id)
|
||||
if not worker:
|
||||
@@ -259,10 +232,7 @@ async def reload_worker_individual(
|
||||
async def delete_worker_individual(
|
||||
agent_id: str, token_data: TokenData = Depends(Accessor.get_current_user)
|
||||
):
|
||||
"""处理针对 delete worker individual 相关的 HTTP API 请求。
|
||||
该接口负责解析前端传入的载荷数据,调用底层核心业务逻辑进行处理,并组装标准化的 JSON 响应。
|
||||
Args: agent_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 agent 实例。 token_data (TokenData): 从客户端传递过来或由上游组件生成的核心业务数据体,通常需要进一步的清洗和结构化解析。
|
||||
Returns: : 序列化后的标准网络响应模型(如包含业务状态码、成功标志及对应的数据载荷 Data)。"""
|
||||
"""删除 Worker Agent;非本人 Agent 返回 403。"""
|
||||
postgres_database = ray_actor_hook("postgres_database").postgres_database
|
||||
worker = await postgres_database.get_worker_individual.remote(agent_id=agent_id)
|
||||
if not worker:
|
||||
|
||||
+5
-14
@@ -26,8 +26,7 @@ auth_router = APIRouter(prefix="/api/v1/auth", tags=["auth"])
|
||||
|
||||
|
||||
class UserRegister(BaseModel):
|
||||
"""UserRegister 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 UserRegister 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。"""
|
||||
"""``POST /register`` 入参:用户名 + 明文密码。"""
|
||||
|
||||
user_name: str
|
||||
password: str
|
||||
@@ -35,10 +34,7 @@ class UserRegister(BaseModel):
|
||||
|
||||
@auth_router.post("/register")
|
||||
async def create_user(user_register: UserRegister):
|
||||
"""处理针对 create user 相关的 HTTP API 请求。
|
||||
该接口负责解析前端传入的载荷数据,调用底层核心业务逻辑进行处理,并组装标准化的 JSON 响应。
|
||||
Args: user_register (UserRegister): 参与 create user 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: : 序列化后的标准网络响应模型(如包含业务状态码、成功标志及对应的数据载荷 Data)。"""
|
||||
"""注册新用户:异步线程池里做 argon2 哈希,再交由 PostgresDatabase Actor 落库。"""
|
||||
postgres_database = ray_actor_hook("postgres_database").postgres_database
|
||||
hashed_password = await run_in_threadpool(
|
||||
Accessor.hash_password, user_register.password
|
||||
@@ -50,8 +46,7 @@ async def create_user(user_register: UserRegister):
|
||||
|
||||
|
||||
class UserLogin(BaseModel):
|
||||
"""UserLogin 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 UserLogin 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。"""
|
||||
"""``POST /login`` 入参:用户名 + 明文密码。"""
|
||||
|
||||
user_name: str
|
||||
password: str
|
||||
@@ -59,10 +54,7 @@ class UserLogin(BaseModel):
|
||||
|
||||
@auth_router.post("/login")
|
||||
async def login_user(user_login: UserLogin):
|
||||
"""处理针对 login user 相关的 HTTP API 请求。
|
||||
该接口负责解析前端传入的载荷数据,调用底层核心业务逻辑进行处理,并组装标准化的 JSON 响应。
|
||||
Args: user_login (UserLogin): 参与 login user 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: : 序列化后的标准网络响应模型(如包含业务状态码、成功标志及对应的数据载荷 Data)。"""
|
||||
"""用户登录:查询用户后在线程池中校验口令,校验成功则签发 JWT。"""
|
||||
postgres_database = ray_actor_hook("postgres_database").postgres_database
|
||||
user = await postgres_database.login_user.remote(user_login.user_name)
|
||||
if not user:
|
||||
@@ -74,8 +66,7 @@ async def login_user(user_login: UserLogin):
|
||||
|
||||
|
||||
class ChangeAuthorityRequest(BaseModel):
|
||||
"""ChangeAuthorityRequest 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 ChangeAuthorityRequest 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。"""
|
||||
"""``PUT /authority`` 入参:目标用户 ID 及新的权限枚举。"""
|
||||
|
||||
user_id: str
|
||||
new_authority: UserAuthority
|
||||
|
||||
@@ -26,8 +26,7 @@ client_router = APIRouter(prefix="/api/v1/adapter/client", tags=["client"])
|
||||
|
||||
|
||||
class Message(BaseModel):
|
||||
"""Message 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 Message 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。"""
|
||||
"""``POST /client`` 入参:来自前端的一段聊天文本。"""
|
||||
|
||||
message: str
|
||||
|
||||
@@ -36,10 +35,7 @@ class Message(BaseModel):
|
||||
async def create_message(
|
||||
message: Message, token_data: TokenData = Depends(Accessor.get_current_user)
|
||||
):
|
||||
"""处理针对 create message 相关的 HTTP API 请求。
|
||||
该接口负责解析前端传入的载荷数据,调用底层核心业务逻辑进行处理,并组装标准化的 JSON 响应。
|
||||
Args: message (Message): 参与 create message 逻辑运算或数据构建的上下文依赖对象。 token_data (TokenData): 从客户端传递过来或由上游组件生成的核心业务数据体,通常需要进一步的清洗和结构化解析。
|
||||
Returns: : 序列化后的标准网络响应模型(如包含业务状态码、成功标志及对应的数据载荷 Data)。"""
|
||||
"""把前端消息转交给 RegulatoryNode 处理,并把回复透传给前端。"""
|
||||
logger.info("收到消息,来源:客户端")
|
||||
logger.debug(f"消息内容:{message.message}")
|
||||
regulatory_node = ray_actor_hook("regulatory_node").regulatory_node
|
||||
@@ -56,10 +52,7 @@ async def upload_file(
|
||||
file: UploadFile = File(...),
|
||||
token_data: TokenData = Depends(Accessor.get_current_user),
|
||||
):
|
||||
"""处理针对 upload file 相关的 HTTP API 请求。
|
||||
该接口负责解析前端传入的载荷数据,调用底层核心业务逻辑进行处理,并组装标准化的 JSON 响应。
|
||||
Args: file (UploadFile): 参与 upload file 逻辑运算或数据构建的上下文依赖对象。 token_data (TokenData): 从客户端传递过来或由上游组件生成的核心业务数据体,通常需要进一步的清洗和结构化解析。
|
||||
Returns: : 序列化后的标准网络响应模型(如包含业务状态码、成功标志及对应的数据载荷 Data)。"""
|
||||
"""以流式方式把上传文件落到 ``uploads/`` 目录;失败抛 500。"""
|
||||
try:
|
||||
upload_dir = "uploads"
|
||||
os.makedirs(upload_dir, exist_ok=True)
|
||||
|
||||
@@ -26,8 +26,7 @@ provider_router = APIRouter(prefix="/api/v1/provider", tags=["provider"])
|
||||
|
||||
|
||||
class ProviderRegister(BaseModel):
|
||||
"""ProviderRegister 核心组件类。
|
||||
这是一个模型/服务提供商适配器类,屏蔽了外部不同供应商(如 OpenAI、Anthropic 等)的底层 API 差异。它负责标准化参数组装、网络请求发送、鉴权处理以及响应结构的反序列化。"""
|
||||
"""``POST /provider`` 入参:注册一个模型 Provider 的最小字段集。"""
|
||||
|
||||
provider_type: Literal["openai", "claude", "deepseek"]
|
||||
provider_title: str
|
||||
@@ -40,10 +39,7 @@ async def create_provider(
|
||||
provider_register: ProviderRegister,
|
||||
token_data: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.USER)),
|
||||
) -> None:
|
||||
"""处理针对 create provider 相关的 HTTP API 请求。
|
||||
该接口负责解析前端传入的载荷数据,调用底层核心业务逻辑进行处理,并组装标准化的 JSON 响应。
|
||||
Args: provider_register (ProviderRegister): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_register 实例。 token_data (TokenData): 从客户端传递过来或由上游组件生成的核心业务数据体,通常需要进一步的清洗和结构化解析。
|
||||
Returns: (None): 序列化后的标准网络响应模型(如包含业务状态码、成功标志及对应的数据载荷 Data)。"""
|
||||
"""注册一个 Provider;owner 为当前登录用户的 ``user_id``。"""
|
||||
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
|
||||
await global_state_machine.add_provider_wrap.remote(
|
||||
provider_type=provider_register.provider_type,
|
||||
@@ -58,10 +54,7 @@ async def create_provider(
|
||||
async def get_provider_list(
|
||||
_: TokenData = Depends(Accessor.get_current_user),
|
||||
) -> Dict[str, Dict[str, Provider]]:
|
||||
"""处理针对 get provider list 相关的 HTTP API 请求。
|
||||
该接口负责解析前端传入的载荷数据,调用底层核心业务逻辑进行处理,并组装标准化的 JSON 响应。
|
||||
Args: _ (TokenData): 参与 get provider list 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: (Dict[str, Dict[str, Provider]]): 序列化后的标准网络响应模型(如包含业务状态码、成功标志及对应的数据载荷 Data)。"""
|
||||
"""返回当前所有已注册的 Provider,前端用以展示模型清单。"""
|
||||
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
|
||||
provider_list: Dict[
|
||||
str, Provider
|
||||
@@ -76,10 +69,7 @@ async def delete_provider(
|
||||
RoleChecker(allowed_roles=UserAuthority.SUPER_ADMINISTRATOR)
|
||||
),
|
||||
) -> dict:
|
||||
"""处理针对 delete provider 相关的 HTTP API 请求。
|
||||
该接口负责解析前端传入的载荷数据,调用底层核心业务逻辑进行处理,并组装标准化的 JSON 响应。
|
||||
Args: provider_title (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_title 实例。 _ (TokenData): 参与 delete provider 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: (dict): 序列化后的标准网络响应模型(如包含业务状态码、成功标志及对应的数据载荷 Data)。"""
|
||||
"""删除指定 ``provider_title`` 的 Provider;仅超管可调用。"""
|
||||
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
|
||||
await global_state_machine.delete_provider.remote(provider_title=provider_title)
|
||||
return {"message": "success"}
|
||||
|
||||
@@ -24,8 +24,7 @@ resource_router = APIRouter(prefix="/api/v1/resource")
|
||||
|
||||
|
||||
class Skill(BaseModel):
|
||||
"""Skill 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 Skill 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。"""
|
||||
"""``POST /skill`` 入参:技能仓库地址及可选子目录路径。"""
|
||||
|
||||
repo_url: str
|
||||
path: str | None
|
||||
@@ -35,10 +34,7 @@ class Skill(BaseModel):
|
||||
async def install_skill(
|
||||
skill: Skill, _: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.USER))
|
||||
):
|
||||
"""处理针对 install skill 相关的 HTTP API 请求。
|
||||
该接口负责解析前端传入的载荷数据,调用底层核心业务逻辑进行处理,并组装标准化的 JSON 响应。
|
||||
Args: skill (Skill): 参与 install skill 逻辑运算或数据构建的上下文依赖对象。 _ (TokenData): 参与 install skill 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: : 序列化后的标准网络响应模型(如包含业务状态码、成功标志及对应的数据载荷 Data)。"""
|
||||
"""通过 viceroy 把 skill 仓库克隆到 ``plugin/skill``,并在状态机中登记。"""
|
||||
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
|
||||
# noinspection PyUnresolvedReferences
|
||||
import os
|
||||
@@ -62,10 +58,7 @@ async def install_skill(
|
||||
async def get_skills(
|
||||
_: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.USER)),
|
||||
):
|
||||
"""处理针对 get skills 相关的 HTTP API 请求。
|
||||
该接口负责解析前端传入的载荷数据,调用底层核心业务逻辑进行处理,并组装标准化的 JSON 响应。
|
||||
Args: _ (TokenData): 参与 get skills 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: : 序列化后的标准网络响应模型(如包含业务状态码、成功标志及对应的数据载荷 Data)。"""
|
||||
"""返回当前状态机中已登记的所有 skill 名称列表。"""
|
||||
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
|
||||
skills = await global_state_machine.get_skill_list.remote()
|
||||
return {"skills": skills}
|
||||
@@ -78,10 +71,7 @@ async def delete_skill(
|
||||
RoleChecker(allowed_roles=UserAuthority.SUPER_ADMINISTRATOR)
|
||||
),
|
||||
):
|
||||
"""处理针对 delete skill 相关的 HTTP API 请求。
|
||||
该接口负责解析前端传入的载荷数据,调用底层核心业务逻辑进行处理,并组装标准化的 JSON 响应。
|
||||
Args: skill_name (str): 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。 _ (TokenData): 参与 delete skill 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: : 序列化后的标准网络响应模型(如包含业务状态码、成功标志及对应的数据载荷 Data)。"""
|
||||
"""从状态机中移除 skill 注册项;不会删除磁盘上的代码文件。"""
|
||||
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
|
||||
# Note: this only removes it from the state machine manager.
|
||||
await global_state_machine.remove_skill.remote(skill_name)
|
||||
@@ -92,10 +82,7 @@ async def delete_skill(
|
||||
async def get_tools(
|
||||
_: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.USER)),
|
||||
):
|
||||
"""处理针对 get tools 相关的 HTTP API 请求。
|
||||
该接口负责解析前端传入的载荷数据,调用底层核心业务逻辑进行处理,并组装标准化的 JSON 响应。
|
||||
Args: _ (TokenData): 参与 get tools 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: : 序列化后的标准网络响应模型(如包含业务状态码、成功标志及对应的数据载荷 Data)。"""
|
||||
"""汇总各作用域 tool_mapper,返回去重后的工具名称列表。"""
|
||||
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
|
||||
tool_mapper = await global_state_machine.get_tool_mapper.remote()
|
||||
all_tool_names = set()
|
||||
|
||||
@@ -24,8 +24,11 @@ from kilostar.core.global_state_machine.individual_manager import (
|
||||
|
||||
@ray.remote
|
||||
class GlobalStateMachine:
|
||||
"""GlobalStateMachine 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 GlobalStateMachine 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。"""
|
||||
"""全局状态机 Actor,统一持有 Provider/Tool/Skill/Individual 四个注册表。
|
||||
|
||||
其它 Actor 通过 ``ray.get_actor("global_state_machine")`` 拿到本实例,
|
||||
再调用本类暴露的方法来读写各注册表,避免每个 Actor 各自维护一份状态。
|
||||
"""
|
||||
|
||||
def __init__(self, postgres_database: PostgresDatabase):
|
||||
import sys
|
||||
@@ -44,8 +47,7 @@ class GlobalStateMachine:
|
||||
print("GSM __init__ DONE", file=sys.stderr, flush=True)
|
||||
|
||||
async def init_state_machine(self):
|
||||
"""完成 state machine 模块的启动与依赖初始化。
|
||||
在系统引导或服务拉起阶段被调用,负责建立网络连接、分配基础内存资源及注册核心服务组件。"""
|
||||
"""从数据库加载 Provider/Individual 注册表到内存。"""
|
||||
await self._global_provider_manager.init_provider_register(
|
||||
self.postgres_database
|
||||
)
|
||||
@@ -61,10 +63,7 @@ class GlobalStateMachine:
|
||||
provider_apikey,
|
||||
provider_owner,
|
||||
):
|
||||
"""创建并持久化新的 provider wrap 实体。
|
||||
接收构建参数,执行必要的数据校验与默认值填充后,将新记录安全地写入底层存储或系统注册表中。
|
||||
Args: provider_type: 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_type 实例。 provider_title: 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_title 实例。 provider_url: 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_url 实例。 provider_apikey: 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_apikey 实例。 provider_owner: 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_owner 实例。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""新增一个模型 Provider:内存注册 + 数据库持久化一并完成。"""
|
||||
return await self._global_provider_manager.add_provider(
|
||||
provider_type=provider_type,
|
||||
provider_title=provider_title,
|
||||
@@ -76,41 +75,26 @@ class GlobalStateMachine:
|
||||
|
||||
# Provider Manager Methods
|
||||
def get_provider_list(self):
|
||||
"""检索并获取特定的 provider list 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""返回内存中已登记的全部 Provider。"""
|
||||
return self._global_provider_manager.get_provider_list()
|
||||
|
||||
def get_provider(self, provider_title):
|
||||
"""检索并获取特定的 provider 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Args: provider_title: 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_title 实例。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""按 provider_title 取出单个 Provider 实例。"""
|
||||
return self._global_provider_manager.get_provider(provider_title)
|
||||
|
||||
async def delete_provider(self, provider_title: str):
|
||||
"""安全地移除或注销 provider。
|
||||
执行物理删除或逻辑删除操作,并妥善清理相关的关联数据及占用资源。
|
||||
Args: provider_title (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_title 实例。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""删除一个 Provider:内存注册 + 数据库持久化一并完成。"""
|
||||
return await self._global_provider_manager.delete_provider(
|
||||
provider_title, self.postgres_database
|
||||
)
|
||||
|
||||
# Tool Manager Methods
|
||||
def get_tool_mapper(self):
|
||||
"""检索并获取特定的 tool mapper 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""返回 agent_name -> {tool_name: callable} 的全量映射。"""
|
||||
return self._global_tool_manager.tool_mapper
|
||||
|
||||
def get_tool_list(self, agent_name: str):
|
||||
# get_tool_list didn't actually exist on tool_manager, let's implement it to return the tools
|
||||
# for a specific agent name (or scope)
|
||||
"""检索并获取特定的 tool list 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Args: agent_name (str): 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""返回某个 agent 可用的工具集(其专属工具与 default 工具的并集)。"""
|
||||
tools = self._global_tool_manager.tool_mapper.get(agent_name, {})
|
||||
# also include default tools
|
||||
default_tools = self._global_tool_manager.tool_mapper.get("default", {})
|
||||
@@ -119,49 +103,30 @@ class GlobalStateMachine:
|
||||
|
||||
# Skill Manager Methods
|
||||
def add_skill(self, skill_name: str):
|
||||
"""创建并持久化新的 skill 实体。
|
||||
接收构建参数,执行必要的数据校验与默认值填充后,将新记录安全地写入底层存储或系统注册表中。
|
||||
Args: skill_name (str): 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""注册一个新的 Skill 名称到 Skill 注册表。"""
|
||||
return self._global_skill_manager.add_skill(skill_name)
|
||||
|
||||
def get_skill_list(self):
|
||||
"""检索并获取特定的 skill list 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""返回全部已注册的 Skill 名称。"""
|
||||
return self._global_skill_manager.get_skill_list()
|
||||
|
||||
def remove_skill(self, skill_name: str):
|
||||
"""安全地移除或注销 skill。
|
||||
执行物理删除或逻辑删除操作,并妥善清理相关的关联数据及占用资源。
|
||||
Args: skill_name (str): 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""从注册表中移除一个 Skill。"""
|
||||
return self._global_skill_manager.remove_skill(skill_name)
|
||||
|
||||
# Individual Manager Methods
|
||||
def add_individual(self, agent_id: str, config):
|
||||
"""创建并持久化新的 individual 实体。
|
||||
接收构建参数,执行必要的数据校验与默认值填充后,将新记录安全地写入底层存储或系统注册表中。
|
||||
Args: agent_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 agent 实例。 config: 驱动该模块运行的核心配置字典或 Pydantic 数据模型,定义了重试策略、超时时间及模型参数等选项。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""把一个 Worker Individual 的运行期配置加入注册表。"""
|
||||
return self._global_individual_manager.add_individual(agent_id, config)
|
||||
|
||||
def get_individual(self, agent_id: str):
|
||||
"""检索并获取特定的 individual 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Args: agent_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 agent 实例。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""按 agent_id 取出某个 Worker Individual 的配置。"""
|
||||
return self._global_individual_manager.get_individual(agent_id)
|
||||
|
||||
def remove_individual(self, agent_id: str):
|
||||
"""安全地移除或注销 individual。
|
||||
执行物理删除或逻辑删除操作,并妥善清理相关的关联数据及占用资源。
|
||||
Args: agent_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 agent 实例。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""从注册表中移除一个 Worker Individual。"""
|
||||
return self._global_individual_manager.remove_individual(agent_id)
|
||||
|
||||
def list_individuals(self):
|
||||
"""执行与 list individuals 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""返回当前注册的全部 Worker Individual 列表。"""
|
||||
return self._global_individual_manager.list_individuals()
|
||||
|
||||
@@ -19,17 +19,17 @@ logger = get_logger("individual_manager")
|
||||
|
||||
|
||||
class GlobalIndividualManager:
|
||||
"""GlobalIndividualManager 核心组件类。
|
||||
这是一个管理器类,职责集中在维护整个系统内有关 GlobalIndividual 资源的全局生命周期。它提供了注册机制、状态同步以及跨组件的统一查询入口,确保系统中该类型资源的实例一致性与可控性。"""
|
||||
"""Worker Individual 的内存注册表,按 agent_id 索引其配置字典。"""
|
||||
|
||||
def __init__(self):
|
||||
self._individuals: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
async def init_individual_register(self, postgres) -> None:
|
||||
"""完成 individual register 模块的启动与依赖初始化。
|
||||
在系统引导或服务拉起阶段被调用,负责建立网络连接、分配基础内存资源及注册核心服务组件。
|
||||
Args: postgres: 参与 init individual register 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: (None): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""从 Postgres 拉取已存的全部 Worker Individual 配置写入内存。
|
||||
|
||||
若底层数据库尚未实现 ``get_all_worker_individual``,会以警告形式跳过,
|
||||
而不是直接抛出,以便老库平滑升级。
|
||||
"""
|
||||
try:
|
||||
try:
|
||||
individuals = await postgres.get_all_worker_individual.remote()
|
||||
@@ -74,15 +74,10 @@ class GlobalIndividualManager:
|
||||
return self._individuals.get(agent_id, None)
|
||||
|
||||
def remove_individual(self, agent_id: str) -> None:
|
||||
"""安全地移除或注销 individual。
|
||||
执行物理删除或逻辑删除操作,并妥善清理相关的关联数据及占用资源。
|
||||
Args: agent_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 agent 实例。
|
||||
Returns: (None): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""从注册表中删除指定 agent_id;不存在时静默返回。"""
|
||||
if agent_id in self._individuals:
|
||||
del self._individuals[agent_id]
|
||||
|
||||
def list_individuals(self) -> Dict[str, Dict[str, Any]]:
|
||||
"""执行与 list individuals 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Returns: (Dict[str, Dict[str, Any]]): 高度聚合的字典结构数据,将多维度的属性特征或统计指标组合后一并返回。"""
|
||||
"""返回 agent_id -> config 的全量映射。"""
|
||||
return self._individuals
|
||||
|
||||
@@ -19,16 +19,14 @@ from enum import Enum
|
||||
|
||||
|
||||
class ProviderStatus(str, Enum):
|
||||
"""ProviderStatus 核心组件类。
|
||||
这是一个模型/服务提供商适配器类,屏蔽了外部不同供应商(如 OpenAI、Anthropic 等)的底层 API 差异。它负责标准化参数组装、网络请求发送、鉴权处理以及响应结构的反序列化。"""
|
||||
"""Provider 健康状态枚举:``UP`` 表示可用,``DOWN`` 表示已被探测为不可用。"""
|
||||
|
||||
UP = "up"
|
||||
DOWN = "down"
|
||||
|
||||
|
||||
class Provider(BaseModel):
|
||||
"""Provider 核心组件类。
|
||||
这是一个模型/服务提供商适配器类,屏蔽了外部不同供应商(如 OpenAI、Anthropic 等)的底层 API 差异。它负责标准化参数组装、网络请求发送、鉴权处理以及响应结构的反序列化。"""
|
||||
"""模型 Provider 的运行期表示,包含基础信息以及当前健康状态。"""
|
||||
|
||||
provider_title: str
|
||||
provider_url: str
|
||||
@@ -40,8 +38,7 @@ class Provider(BaseModel):
|
||||
|
||||
|
||||
class ProviderArgs(BaseModel):
|
||||
"""ProviderArgs 核心组件类。
|
||||
这是一个模型/服务提供商适配器类,屏蔽了外部不同供应商(如 OpenAI、Anthropic 等)的底层 API 差异。它负责标准化参数组装、网络请求发送、鉴权处理以及响应结构的反序列化。"""
|
||||
"""新增 Provider 时的入参集合,由 API 层拼装后传给具体 Provider 的工厂。"""
|
||||
|
||||
provider_title: str
|
||||
provider_url: str
|
||||
@@ -50,8 +47,7 @@ class ProviderArgs(BaseModel):
|
||||
|
||||
|
||||
class BaseProvider(ABC):
|
||||
"""BaseProvider 核心组件类。
|
||||
这是一个模型/服务提供商适配器类,屏蔽了外部不同供应商(如 OpenAI、Anthropic 等)的底层 API 差异。它负责标准化参数组装、网络请求发送、鉴权处理以及响应结构的反序列化。"""
|
||||
"""所有具体 Provider 适配器的抽象基类,约定 ``create_provider`` 工厂三段式。"""
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
|
||||
@@ -24,15 +24,11 @@ from typing import List
|
||||
|
||||
|
||||
class ClaudeProvider(BaseProvider):
|
||||
"""ClaudeProvider 核心组件类。
|
||||
这是一个模型/服务提供商适配器类,屏蔽了外部不同供应商(如 OpenAI、Anthropic 等)的底层 API 差异。它负责标准化参数组装、网络请求发送、鉴权处理以及响应结构的反序列化。"""
|
||||
"""Anthropic Claude Provider:使用 ``x-api-key`` + ``anthropic-version`` 头拉模型列表。"""
|
||||
|
||||
@staticmethod
|
||||
async def create_provider(provider_args: ProviderArgs) -> Provider:
|
||||
"""创建并持久化新的 provider 实体。
|
||||
接收构建参数,执行必要的数据校验与默认值填充后,将新记录安全地写入底层存储或系统注册表中。
|
||||
Args: provider_args (ProviderArgs): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_args 实例。
|
||||
Returns: (Provider): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""工厂入口:拉取 Claude 模型列表后包装成 Provider。"""
|
||||
provider_models: List[str] = await ClaudeProvider._load_models(provider_args)
|
||||
provider: Provider = ClaudeProvider._return_provider(
|
||||
provider_args, provider_models
|
||||
@@ -43,10 +39,7 @@ class ClaudeProvider(BaseProvider):
|
||||
@retry_on_retryable_error()
|
||||
async def _load_models(provider_args: ProviderArgs) -> List[str]:
|
||||
# Anthropic 官方需要 version 头
|
||||
"""执行与 load models 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: provider_args (ProviderArgs): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_args 实例。
|
||||
Returns: (List[str]): 经过筛选、排序或分页处理后的实体对象列表集合。"""
|
||||
"""从 ``/v1/models`` 拉取模型列表;接口不可用时回落到一组已知的 Claude 3.x 模型。"""
|
||||
headers = {
|
||||
"x-api-key": provider_args.provider_apikey,
|
||||
"anthropic-version": "2023-06-01",
|
||||
@@ -78,10 +71,7 @@ class ClaudeProvider(BaseProvider):
|
||||
def _return_provider(
|
||||
provider_args: ProviderArgs, provider_models: List[str]
|
||||
) -> Provider:
|
||||
"""执行与 return provider 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: provider_args (ProviderArgs): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_args 实例。 provider_models (List[str]): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_models 实例。
|
||||
Returns: (Provider): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""把 ProviderArgs + 模型清单包装成 ``provider_type="claude"`` 的 Provider。"""
|
||||
return Provider(
|
||||
provider_title=provider_args.provider_title,
|
||||
provider_apikey=provider_args.provider_apikey,
|
||||
|
||||
@@ -23,15 +23,11 @@ from typing import List
|
||||
|
||||
|
||||
class DeepseekProvider(BaseProvider):
|
||||
"""DeepseekProvider 核心组件类。
|
||||
这是一个模型/服务提供商适配器类,屏蔽了外部不同供应商(如 OpenAI、Anthropic 等)的底层 API 差异。它负责标准化参数组装、网络请求发送、鉴权处理以及响应结构的反序列化。"""
|
||||
"""Deepseek Provider:API 兼容 OpenAI 协议,复用 ``GET /v1/models`` 拉取模型清单。"""
|
||||
|
||||
@staticmethod
|
||||
async def create_provider(provider_args: ProviderArgs) -> Provider:
|
||||
"""创建并持久化新的 provider 实体。
|
||||
接收构建参数,执行必要的数据校验与默认值填充后,将新记录安全地写入底层存储或系统注册表中。
|
||||
Args: provider_args (ProviderArgs): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_args 实例。
|
||||
Returns: (Provider): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""工厂入口:拉取 Deepseek 模型列表后包装成 Provider。"""
|
||||
provider_models: List[str] = await DeepseekProvider._load_models(provider_args)
|
||||
provider: Provider = DeepseekProvider._return_provider(
|
||||
provider_args, provider_models
|
||||
@@ -41,10 +37,7 @@ class DeepseekProvider(BaseProvider):
|
||||
@staticmethod
|
||||
@retry_on_retryable_error()
|
||||
async def _load_models(provider_args: ProviderArgs) -> List[str]:
|
||||
"""执行与 load models 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: provider_args (ProviderArgs): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_args 实例。
|
||||
Returns: (List[str]): 经过筛选、排序或分页处理后的实体对象列表集合。"""
|
||||
"""从 ``{base_url}/v1/models`` 拉取模型 ID 列表;网络异常会被包装为 RetryableError。"""
|
||||
headers = {
|
||||
"Authorization": f"Bearer {provider_args.provider_apikey}",
|
||||
"Content-Type": "application/json",
|
||||
@@ -81,10 +74,7 @@ class DeepseekProvider(BaseProvider):
|
||||
def _return_provider(
|
||||
provider_args: ProviderArgs, provider_models: List[str]
|
||||
) -> Provider:
|
||||
"""执行与 return provider 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: provider_args (ProviderArgs): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_args 实例。 provider_models (List[str]): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_models 实例。
|
||||
Returns: (Provider): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""把 ProviderArgs + 模型清单包装成 ``provider_type="deepseek"`` 的 Provider。"""
|
||||
return Provider(
|
||||
provider_title=provider_args.provider_title,
|
||||
provider_apikey=provider_args.provider_apikey,
|
||||
|
||||
@@ -23,15 +23,11 @@ from typing import List
|
||||
|
||||
|
||||
class GeminiProvider(BaseProvider):
|
||||
"""GeminiProvider 核心组件类。
|
||||
这是一个模型/服务提供商适配器类,屏蔽了外部不同供应商(如 Google Gemini)的底层 API 差异。它负责标准化参数组装、网络请求发送、鉴权处理以及响应结构的反序列化。"""
|
||||
"""Google Gemini Provider:调用 ``/v1beta/models`` 接口获取模型清单。"""
|
||||
|
||||
@staticmethod
|
||||
async def create_provider(provider_args: ProviderArgs) -> Provider:
|
||||
"""创建并持久化新的 provider 实体。
|
||||
接收构建参数,执行必要的数据校验与默认值填充后,将新记录安全地写入底层存储或系统注册表中。
|
||||
Args: provider_args (ProviderArgs): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_args 实例。
|
||||
Returns: (Provider): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""工厂入口:拉取 Gemini 模型列表后包装成 Provider。"""
|
||||
provider_models: List[str] = await GeminiProvider._load_models(provider_args)
|
||||
provider: Provider = GeminiProvider._return_provider(
|
||||
provider_args, provider_models
|
||||
@@ -41,10 +37,7 @@ class GeminiProvider(BaseProvider):
|
||||
@staticmethod
|
||||
@retry_on_retryable_error()
|
||||
async def _load_models(provider_args: ProviderArgs) -> List[str]:
|
||||
"""执行与 load models 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: provider_args (ProviderArgs): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_args 实例。
|
||||
Returns: (List[str]): 经过筛选、排序或分页处理后的实体对象列表集合。"""
|
||||
"""从 ``/v1beta/models`` 拉取模型列表,去掉 ``models/`` 前缀;网络异常会被包装为 RetryableError。"""
|
||||
headers = {
|
||||
"Authorization": f"Bearer {provider_args.provider_apikey}",
|
||||
"Content-Type": "application/json",
|
||||
@@ -78,10 +71,7 @@ class GeminiProvider(BaseProvider):
|
||||
def _return_provider(
|
||||
provider_args: ProviderArgs, provider_models: List[str]
|
||||
) -> Provider:
|
||||
"""执行与 return provider 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: provider_args (ProviderArgs): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_args 实例。 provider_models (List[str]): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_models 实例。
|
||||
Returns: (Provider): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""把 ProviderArgs + 模型清单包装成 ``provider_type="gemini"`` 的 Provider。"""
|
||||
return Provider(
|
||||
provider_title=provider_args.provider_title,
|
||||
provider_apikey=provider_args.provider_apikey,
|
||||
|
||||
@@ -23,15 +23,11 @@ from typing import List
|
||||
|
||||
|
||||
class OpenAIProvider(BaseProvider):
|
||||
"""OpenAIProvider 核心组件类。
|
||||
这是一个模型/服务提供商适配器类,屏蔽了外部不同供应商(如 OpenAI、Anthropic 等)的底层 API 差异。它负责标准化参数组装、网络请求发送、鉴权处理以及响应结构的反序列化。"""
|
||||
"""OpenAI 兼容 Provider:通过 ``GET /v1/models`` 拉取模型清单,包装为 Provider 对象。"""
|
||||
|
||||
@staticmethod
|
||||
async def create_provider(provider_args: ProviderArgs) -> Provider:
|
||||
"""创建并持久化新的 provider 实体。
|
||||
接收构建参数,执行必要的数据校验与默认值填充后,将新记录安全地写入底层存储或系统注册表中。
|
||||
Args: provider_args (ProviderArgs): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_args 实例。
|
||||
Returns: (Provider): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""工厂入口:拉取模型列表后包装成 Provider。"""
|
||||
provider_models: List[str] = await OpenAIProvider._load_models(provider_args)
|
||||
provider: Provider = OpenAIProvider._return_provider(
|
||||
provider_args, provider_models
|
||||
@@ -41,10 +37,7 @@ class OpenAIProvider(BaseProvider):
|
||||
@staticmethod
|
||||
@retry_on_retryable_error()
|
||||
async def _load_models(provider_args: ProviderArgs) -> List[str]:
|
||||
"""执行与 load models 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: provider_args (ProviderArgs): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_args 实例。
|
||||
Returns: (List[str]): 经过筛选、排序或分页处理后的实体对象列表集合。"""
|
||||
"""从 ``{base_url}/v1/models`` 拉取模型 ID 列表;网络异常会被包装为 RetryableError。"""
|
||||
headers = {
|
||||
"Authorization": f"Bearer {provider_args.provider_apikey}",
|
||||
"Content-Type": "application/json",
|
||||
@@ -81,10 +74,7 @@ class OpenAIProvider(BaseProvider):
|
||||
def _return_provider(
|
||||
provider_args: ProviderArgs, provider_models: List[str]
|
||||
) -> Provider:
|
||||
"""执行与 return provider 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: provider_args (ProviderArgs): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_args 实例。 provider_models (List[str]): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_models 实例。
|
||||
Returns: (Provider): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""把 ProviderArgs + 模型清单包装成 ``provider_type="openai"`` 的 Provider。"""
|
||||
return Provider(
|
||||
provider_title=provider_args.provider_title,
|
||||
provider_apikey=provider_args.provider_apikey,
|
||||
|
||||
@@ -45,10 +45,7 @@ class ProviderManager:
|
||||
self.provider_register = {}
|
||||
|
||||
async def init_provider_register(self, postgres) -> None:
|
||||
"""完成 provider register 模块的启动与依赖初始化。
|
||||
在系统引导或服务拉起阶段被调用,负责建立网络连接、分配基础内存资源及注册核心服务组件。
|
||||
Args: postgres: 参与 init provider register 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: (None): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""从 Postgres 读取已存的 Provider 列表,按 provider_title 装入内存注册表。"""
|
||||
providers = await postgres.get_provider.remote()
|
||||
for provider in providers:
|
||||
self.provider_register[provider.provider_title] = provider
|
||||
@@ -62,10 +59,13 @@ class ProviderManager:
|
||||
provider_owner,
|
||||
postgres_database,
|
||||
) -> None:
|
||||
"""创建并持久化新的 provider 实体。
|
||||
接收构建参数,执行必要的数据校验与默认值填充后,将新记录安全地写入底层存储或系统注册表中。
|
||||
Args: provider_type: 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_type 实例。 provider_title: 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_title 实例。 provider_url: 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_url 实例。 provider_apikey: 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_apikey 实例。 provider_owner: 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_owner 实例。 postgres_database: 从客户端传递过来或由上游组件生成的核心业务数据体,通常需要进一步的清洗和结构化解析。
|
||||
Returns: (None): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""新增并落库一个 Provider:
|
||||
|
||||
- 按 ``provider_type`` 选择具体适配器(openai/claude/deepseek/gemini);
|
||||
- 适配器调用其 ``create_provider`` 拉取模型清单;
|
||||
- 写入内存注册表,并通过 ``postgres_database`` 持久化。
|
||||
网络异常会包装成 RetryableError;不支持的类型记 warning 后返回 None。
|
||||
"""
|
||||
from kilostar.core.global_state_machine.model_provider import ProviderArgs
|
||||
from kilostar.utils.logger import get_logger
|
||||
|
||||
@@ -112,23 +112,15 @@ class ProviderManager:
|
||||
)
|
||||
|
||||
def get_provider_list(self):
|
||||
"""检索并获取特定的 provider list 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""返回 provider_title -> Provider 的全量映射。"""
|
||||
return self.provider_register
|
||||
|
||||
def get_provider(self, provider_title):
|
||||
"""检索并获取特定的 provider 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Args: provider_title: 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_title 实例。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""按 provider_title 取出单个 Provider;不存在返回 None。"""
|
||||
return self.provider_register.get(provider_title)
|
||||
|
||||
async def delete_provider(self, provider_title: str, postgres_database) -> None:
|
||||
"""安全地移除或注销 provider。
|
||||
执行物理删除或逻辑删除操作,并妥善清理相关的关联数据及占用资源。
|
||||
Args: provider_title (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_title 实例。 postgres_database: 从客户端传递过来或由上游组件生成的核心业务数据体,通常需要进一步的清洗和结构化解析。
|
||||
Returns: (None): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""从内存注册表 + Postgres 中一并删除指定 Provider;不存在时静默返回。"""
|
||||
if provider_title in self.provider_register:
|
||||
provider = self.provider_register[provider_title]
|
||||
await postgres_database.delete_provider_db.remote(
|
||||
|
||||
@@ -19,8 +19,7 @@ import json
|
||||
|
||||
|
||||
class GlobalSkillManager:
|
||||
"""GlobalSkillManager 核心组件类。
|
||||
这是一个管理器类,职责集中在维护整个系统内有关 GlobalSkill 资源的全局生命周期。它提供了注册机制、状态同步以及跨组件的统一查询入口,确保系统中该类型资源的实例一致性与可控性。"""
|
||||
"""Skill 注册表:从 ``kilostar/plugin/skill/<name>/skill.json`` 启动期一次性扫描加载。"""
|
||||
|
||||
skill_mapper = Dict[str, Tuple[str]]
|
||||
"""skill的存储表"""
|
||||
|
||||
@@ -24,8 +24,8 @@ logger = get_logger("tool_manager")
|
||||
|
||||
|
||||
class GlobalToolManager:
|
||||
"""GlobalToolManager 核心组件类。
|
||||
这是一个管理器类,职责集中在维护整个系统内有关 GlobalTool 资源的全局生命周期。它提供了注册机制、状态同步以及跨组件的统一查询入口,确保系统中该类型资源的实例一致性与可控性。"""
|
||||
"""工具注册表:扫描 ``kilostar/plugin/tool_plugin/`` 下所有 BaseToolData 子类,
|
||||
按 ``action_scope`` 分桶到 ``tool_mapper[scope][plugin_name]``;无 scope 的归入 ``default``。"""
|
||||
|
||||
tool_mapper: Dict[str, Dict[str, Type[BaseToolData]]]
|
||||
|
||||
|
||||
@@ -25,15 +25,13 @@ class ConsciousnessNodeResponse(ResponseModel):
|
||||
pass
|
||||
|
||||
class ConsciousnessNodeDeps(DepsModel):
|
||||
"""ConsciousnessNodeDeps 核心组件类。
|
||||
这是一个系统执行节点类,作为多智能体架构中的独立处理单元。它能够接收工作流上下文,根据内置的大模型策略进行意图理解和自主决策,从而驱动特定阶段的任务闭环。"""
|
||||
"""ConsciousnessNode 在 pydantic-ai Agent 中使用的依赖:原始指令、当前指令以及可用 Skill 列表。"""
|
||||
original_command: str
|
||||
command: str
|
||||
available_skills: Optional[List[str]]
|
||||
|
||||
class ConsciousnessNodeInput(RequestModel):
|
||||
"""ConsciousnessNodeInput 核心组件类。
|
||||
这是一个系统执行节点类,作为多智能体架构中的独立处理单元。它能够接收工作流上下文,根据内置的大模型策略进行意图理解和自主决策,从而驱动特定阶段的任务闭环。"""
|
||||
"""ConsciousnessNode 各类入参的共同基类,仅用于打 schema 标签。"""
|
||||
pass
|
||||
|
||||
|
||||
@@ -60,24 +58,21 @@ class ForregulatoryNode(ConsciousnessNodeResponse):
|
||||
)
|
||||
|
||||
class ForWorkflowEngineInput(ConsciousnessNodeInput):
|
||||
"""ForWorkflowEngineInput 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 ForWorkflowEngineInput 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。"""
|
||||
"""从 RegulatoryNode 移交过来生成 Workflow 的入参:原始指令 + 已注册的 Skill 列表。"""
|
||||
|
||||
original_command: str
|
||||
available_skills: list[dict] | None = None
|
||||
|
||||
|
||||
class ForWorkflowInput(ConsciousnessNodeInput):
|
||||
"""ForWorkflowInput 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 ForWorkflowInput 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。"""
|
||||
"""工作流执行期分配给 ConsciousnessNode 的步骤入参:当前 step + 原始指令上下文。"""
|
||||
|
||||
workflow_step: WorkflowStep
|
||||
original_command: str
|
||||
|
||||
|
||||
class ForregulatoryInput(ConsciousnessNodeInput):
|
||||
"""ForregulatoryInput 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 ForregulatoryInput 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。"""
|
||||
"""工作流跑完后回交给 RegulatoryNode 时的入参:完整 workflow 对象 + 原始指令。"""
|
||||
|
||||
workflow: KiloStarWorkflow
|
||||
original_command: str
|
||||
|
||||
@@ -26,8 +26,11 @@ from kilostar.core.individual.control_node.template import (
|
||||
|
||||
@ray.remote
|
||||
class ControlNode:
|
||||
"""ControlNode 核心组件类。
|
||||
这是一个系统执行节点类,作为多智能体架构中的独立处理单元。它能够接收工作流上下文,根据内置的大模型策略进行意图理解和自主决策,从而驱动特定阶段的任务闭环。"""
|
||||
"""ControlNode(控制节点):工作流中具体子任务的执行 Actor。
|
||||
|
||||
它把 ConsciousnessNode 编排出的 ``workflow_step`` 拿来当作输入,借助
|
||||
pydantic-ai Agent + 已绑定的工具集合产出 ``ForWorkflow`` 结构化输出。
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
from kilostar.utils.logger import get_logger
|
||||
@@ -85,10 +88,7 @@ class ControlNode:
|
||||
|
||||
@self.agent.system_prompt
|
||||
async def dynamic_prompt(ctx: RunContext[ControlNodeDeps]):
|
||||
"""执行与 dynamic prompt 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: ctx (RunContext[ControlNodeDeps]): 参与 dynamic prompt 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""运行期动态拼接 system prompt:把当前 workflow_step 的关键字段塞进去。"""
|
||||
prompt = system_prompt + "\n\n"
|
||||
prompt += (
|
||||
f"=== 当前任务步骤上下文 ===\n"
|
||||
@@ -99,10 +99,7 @@ class ControlNode:
|
||||
return prompt
|
||||
|
||||
async def working(self, payload: ForWorkflowInput) -> str:
|
||||
"""执行与 working 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: payload (ForWorkflowInput): 从客户端传递过来或由上游组件生成的核心业务数据体,通常需要进一步的清洗和结构化解析。
|
||||
Returns: (str): 处理流程所输出的具体字符串产物,可能是新生成的 ID 序列、格式化好的文本片段或 LLM 推理的回答内容。"""
|
||||
"""对外入口:执行一次步骤,吞掉异常并返回 ``None`` 以避免拖垮上游 Workflow。"""
|
||||
try:
|
||||
result: ForWorkflow = await self._run(payload)
|
||||
return result
|
||||
@@ -111,10 +108,7 @@ class ControlNode:
|
||||
return None
|
||||
|
||||
async def _run(self, payload: ForWorkflowInput) -> ForWorkflow:
|
||||
"""执行与 run 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: payload (ForWorkflowInput): 从客户端传递过来或由上游组件生成的核心业务数据体,通常需要进一步的清洗和结构化解析。
|
||||
Returns: (ForWorkflow): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""实际执行步骤:组装 ``ControlNodeDeps``、调用 Agent,最终把 ``ForWorkflow`` 输出取出。"""
|
||||
try:
|
||||
self.agent.retries = 3
|
||||
deps = ControlNodeDeps(workflow_step=payload.workflow_step)
|
||||
|
||||
@@ -15,34 +15,30 @@
|
||||
|
||||
from pydantic import Field
|
||||
from kilostar.core.work.workflow.workflow import WorkflowStep
|
||||
from kilostar.utils.agent_model import ResponseModel, InputModel, DepsModel
|
||||
from kilostar.utils.agent_model import ResponseModel, RequestModel, DepsModel
|
||||
|
||||
|
||||
class ControlNodeResponse(ResponseModel):
|
||||
"""控制节点回复的基类"""
|
||||
"""控制节点回复的基类。"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ControlNodeInput(InputModel):
|
||||
"""ControlNodeInput 核心组件类。
|
||||
这是一个系统执行节点类,作为多智能体架构中的独立处理单元。它能够接收工作流上下文,根据内置的大模型策略进行意图理解和自主决策,从而驱动特定阶段的任务闭环。"""
|
||||
class ControlNodeInput(RequestModel):
|
||||
"""控制节点输入的基类,承载一次调度所需的入参。"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ControlNodeDeps(DepsModel):
|
||||
"""ControlNodeDeps 核心组件类。
|
||||
这是一个系统执行节点类,作为多智能体架构中的独立处理单元。它能够接收工作流上下文,根据内置的大模型策略进行意图理解和自主决策,从而驱动特定阶段的任务闭环。"""
|
||||
"""控制节点运行期依赖,注入到 pydantic-ai Agent 的 RunContext。"""
|
||||
|
||||
workflow_step: WorkflowStep
|
||||
workflow_step: WorkflowStep
|
||||
# In the future, this can be dynamically populated with tools specific to the current task execution
|
||||
|
||||
|
||||
class ForWorkflow(ControlNodeResponse):
|
||||
"""ForWorkflow 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 ForWorkflow 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。"""
|
||||
"""控制节点执行单个工作流步骤的输出模型。"""
|
||||
|
||||
output: str = Field(
|
||||
..., description="控制节点执行特定工作流步骤的结果。包含执行细节和输出数据。"
|
||||
@@ -50,7 +46,6 @@ class ForWorkflow(ControlNodeResponse):
|
||||
|
||||
|
||||
class ForWorkflowInput(ControlNodeInput):
|
||||
"""ForWorkflowInput 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 ForWorkflowInput 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。"""
|
||||
"""控制节点针对工作流步骤的输入模型。"""
|
||||
|
||||
workflow_step: WorkflowStep
|
||||
|
||||
@@ -28,8 +28,11 @@ from pydantic_ai import RunContext, Agent
|
||||
|
||||
@ray.remote
|
||||
class RegulatoryNode:
|
||||
"""regulatoryNode 核心组件类。
|
||||
这是一个系统执行节点类,作为多智能体架构中的独立处理单元。它能够接收工作流上下文,根据内置的大模型策略进行意图理解和自主决策,从而驱动特定阶段的任务闭环。"""
|
||||
"""RegulatoryNode(监管节点):用户请求的入口路由 Actor。
|
||||
|
||||
负责对消息做意图识别:闲聊 → 直接回 ``ForUser``;复杂任务 → 走
|
||||
``ForConsciousnessNode`` 移交给意识节点;工作流回执 → 转译成对用户的总结回复。
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
from kilostar.utils.logger import get_logger
|
||||
@@ -86,10 +89,7 @@ class RegulatoryNode:
|
||||
|
||||
@self.agent.system_prompt
|
||||
async def dynamic_prompt(ctx: RunContext[RegulatoryNodeDeps]):
|
||||
"""执行与 dynamic prompt 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: ctx (RunContext[regulatoryNodeDeps]): 参与 dynamic prompt 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""运行期动态拼接 system prompt:注入平台/用户/时间/错误历史等上下文。"""
|
||||
prompt = system_prompt + "\n\n"
|
||||
prompt += (
|
||||
f"=== 当前上下文 ===\n"
|
||||
|
||||
@@ -22,15 +22,9 @@ logger = get_logger("database_exception")
|
||||
|
||||
|
||||
def database_exception(func):
|
||||
"""执行与 database exception 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: func: 参与 database exception 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""异步装饰器:把 SQLAlchemy / Pydantic / 业务异常归类记日志后再抛出。"""
|
||||
|
||||
async def wrapper(*args, **kwargs):
|
||||
"""执行与 wrapper 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
try:
|
||||
return await func(*args, **kwargs)
|
||||
except ValidationError as e:
|
||||
|
||||
@@ -32,8 +32,7 @@ _AGENT_TYPE_MODEL_MAP = {
|
||||
|
||||
|
||||
class IndividualDatabase:
|
||||
"""IndividualDatabase 核心组件类。
|
||||
这是一个数据库操作层 (DAO/Repository) 封装类,专注于处理实体模型与关系型数据库表之间的映射。它将复杂的 SQL 查询、跨表 Join 和事务回滚逻辑进行了高级抽象,向上层服务暴露简洁的数据读写接口。"""
|
||||
"""Individual 表族(Base/Specialist/Ordinary/Special)的 DAO,按 agent_type 选择具体子表。"""
|
||||
|
||||
def __init__(self, async_session_maker):
|
||||
self.async_session_maker = async_session_maker
|
||||
@@ -44,8 +43,7 @@ class IndividualDatabase:
|
||||
|
||||
@database_exception
|
||||
async def add_worker_individual(self, **kwargs):
|
||||
"""创建并持久化新的 worker individual 实体。
|
||||
接收构建参数,执行必要的数据校验与默认值填充后,将新记录安全地写入底层存储或系统注册表中。"""
|
||||
"""新建一个 Worker Individual:自动生成 ULID,按 ``agent_type`` 选择对应子表写入。"""
|
||||
async with self.async_session_maker() as session:
|
||||
agent_id = str(ULID())
|
||||
agent_type = kwargs.get("agent_type", "base")
|
||||
@@ -58,7 +56,7 @@ class IndividualDatabase:
|
||||
|
||||
@database_exception
|
||||
async def get_worker_individual(self, agent_id: str):
|
||||
"""检索并获取特定的 worker individual 数据集合或实例对象。"""
|
||||
"""按 agent_id 取单个 Individual;不存在返回 None。"""
|
||||
async with self.async_session_maker() as session:
|
||||
statement = select(BaseIndividualModel).where(
|
||||
BaseIndividualModel.agent_id == agent_id
|
||||
@@ -68,7 +66,7 @@ class IndividualDatabase:
|
||||
|
||||
@database_exception
|
||||
async def get_worker_individual_list(self, owner_id: str):
|
||||
"""检索并获取特定的 worker individual list 数据集合或实例对象。"""
|
||||
"""读取某用户名下的所有 Individual。"""
|
||||
async with self.async_session_maker() as session:
|
||||
statement = select(BaseIndividualModel).where(
|
||||
BaseIndividualModel.owner_id == owner_id
|
||||
@@ -78,7 +76,7 @@ class IndividualDatabase:
|
||||
|
||||
@database_exception
|
||||
async def update_worker_individual(self, agent_id: str, **kwargs):
|
||||
"""对现有的 worker individual 进行状态更新或属性覆盖。"""
|
||||
"""部分更新 Individual:只覆盖 kwargs 中非 None 的字段;找不到返回 None。"""
|
||||
async with self.async_session_maker() as session:
|
||||
statement = select(BaseIndividualModel).where(
|
||||
BaseIndividualModel.agent_id == agent_id
|
||||
@@ -97,7 +95,7 @@ class IndividualDatabase:
|
||||
|
||||
@database_exception
|
||||
async def delete_worker_individual(self, agent_id: str) -> bool:
|
||||
"""安全地移除或注销 worker individual。"""
|
||||
"""删除 Individual;不存在返回 False,删除成功返回 True。"""
|
||||
async with self.async_session_maker() as session:
|
||||
statement = select(BaseIndividualModel).where(
|
||||
BaseIndividualModel.agent_id == agent_id
|
||||
@@ -112,7 +110,7 @@ class IndividualDatabase:
|
||||
|
||||
@database_exception
|
||||
async def get_all_worker_individual(self):
|
||||
"""检索并获取特定的 all worker individual 数据集合或实例对象。"""
|
||||
"""返回数据库中全部 Individual。"""
|
||||
async with self.async_session_maker() as session:
|
||||
statement = select(BaseIndividualModel)
|
||||
results = await session.execute(statement)
|
||||
|
||||
@@ -20,17 +20,14 @@ from kilostar.core.postgres_database.database_exception import database_exceptio
|
||||
|
||||
|
||||
class ProviderDatabase:
|
||||
"""ProviderDatabase 核心组件类。
|
||||
这是一个模型/服务提供商适配器类,屏蔽了外部不同供应商(如 OpenAI、Anthropic 等)的底层 API 差异。它负责标准化参数组装、网络请求发送、鉴权处理以及响应结构的反序列化。"""
|
||||
"""Provider 表的 DAO:模型 Provider 的增删查改。"""
|
||||
|
||||
def __init__(self, async_session_maker):
|
||||
self.async_session_maker = async_session_maker
|
||||
|
||||
@database_exception
|
||||
async def get_provider(self) -> List[ProviderModel]:
|
||||
"""检索并获取特定的 provider 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Returns: (List[ProviderModel]): 经过筛选、排序或分页处理后的实体对象列表集合。"""
|
||||
"""返回全部 Provider,并将每行重新构造为新的 ``ProviderModel`` 实例(脱离 session)。"""
|
||||
async with self.async_session_maker() as session:
|
||||
statement = select(ProviderModel)
|
||||
results = await session.execute(statement)
|
||||
@@ -53,9 +50,7 @@ class ProviderDatabase:
|
||||
|
||||
@database_exception
|
||||
async def add_provider(self, **kwargs) -> None:
|
||||
"""创建并持久化新的 provider 实体。
|
||||
接收构建参数,执行必要的数据校验与默认值填充后,将新记录安全地写入底层存储或系统注册表中。
|
||||
Returns: (None): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""新建一条 Provider 记录;字段通过 kwargs 直接传给 ProviderModel。"""
|
||||
async with self.async_session_maker() as session:
|
||||
provider = ProviderModel(**kwargs)
|
||||
session.add(provider)
|
||||
@@ -63,10 +58,7 @@ class ProviderDatabase:
|
||||
|
||||
@database_exception
|
||||
async def delete_provider(self, provider_id: str) -> None:
|
||||
"""安全地移除或注销 provider。
|
||||
执行物理删除或逻辑删除操作,并妥善清理相关的关联数据及占用资源。
|
||||
Args: provider_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider 实例。
|
||||
Returns: (None): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""删除指定 ID 的 Provider;不存在时静默返回。"""
|
||||
async with self.async_session_maker() as session:
|
||||
provider = await session.get(ProviderModel, provider_id)
|
||||
if provider is not None:
|
||||
@@ -75,10 +67,7 @@ class ProviderDatabase:
|
||||
|
||||
@database_exception
|
||||
async def update_provider(self, provider_id: str, **kwargs) -> None:
|
||||
"""对现有的 provider 进行状态更新或属性覆盖。
|
||||
基于增量变更原则,合并最新的配置或数据,并触发相关依赖组件的缓存刷新或事件通知。
|
||||
Args: provider_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider 实例。
|
||||
Returns: (Provider): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""部分更新指定 Provider 的字段;不存在时返回 None,否则返回刷新后的对象。"""
|
||||
async with self.async_session_maker() as session:
|
||||
provider = await session.get(ProviderModel, provider_id)
|
||||
if provider is not None:
|
||||
|
||||
@@ -19,8 +19,7 @@ from kilostar.core.postgres_database.database_exception import database_exceptio
|
||||
|
||||
|
||||
class SystemNodeDatabase:
|
||||
"""SystemNodeDatabase 核心组件类。
|
||||
这是一个系统执行节点类,作为多智能体架构中的独立处理单元。它能够接收工作流上下文,根据内置的大模型策略进行意图理解和自主决策,从而驱动特定阶段的任务闭环。"""
|
||||
"""SystemNodeConfig 表的 DAO:管理 control/consciousness/regulatory 等系统节点的模型配置。"""
|
||||
|
||||
def __init__(self, async_session_maker):
|
||||
self.async_session_maker = async_session_maker
|
||||
@@ -33,10 +32,7 @@ class SystemNodeDatabase:
|
||||
model_id: str,
|
||||
tools: Optional[List[str]] = None,
|
||||
) -> SystemNodeConfigModel:
|
||||
"""执行与 upsert system node config 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: node_name (str): 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。 provider_title (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_title 实例。 model_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 model 实例。 tools (Optional[List[str]]): 控制逻辑流向的具体字符串参数,指定了期望的 tools 内容。
|
||||
Returns: (SystemNodeConfigModel): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""按 node_name 插入或更新一个系统节点的模型配置(Provider + 模型 ID + 工具列表)。"""
|
||||
async with self.async_session_maker() as session:
|
||||
statement = select(SystemNodeConfigModel).where(
|
||||
SystemNodeConfigModel.node_name == node_name
|
||||
@@ -62,9 +58,7 @@ class SystemNodeDatabase:
|
||||
|
||||
@database_exception
|
||||
async def get_all_system_node_configs(self) -> List[SystemNodeConfigModel]:
|
||||
"""检索并获取特定的 all system node configs 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Returns: (List[SystemNodeConfigModel]): 经过筛选、排序或分页处理后的实体对象列表集合。"""
|
||||
"""返回所有系统节点的模型配置列表。"""
|
||||
async with self.async_session_maker() as session:
|
||||
statement = select(SystemNodeConfigModel)
|
||||
results = await session.execute(statement)
|
||||
@@ -74,10 +68,7 @@ class SystemNodeDatabase:
|
||||
async def get_system_node_config(
|
||||
self, node_name: str
|
||||
) -> Optional[SystemNodeConfigModel]:
|
||||
"""检索并获取特定的 system node config 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Args: node_name (str): 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。
|
||||
Returns: (Optional[SystemNodeConfigModel]): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""按 node_name 取出单个系统节点的模型配置;不存在返回 None。"""
|
||||
async with self.async_session_maker() as session:
|
||||
statement = select(SystemNodeConfigModel).where(
|
||||
SystemNodeConfigModel.node_name == node_name
|
||||
|
||||
@@ -21,18 +21,14 @@ from kilostar.utils.access import Accessor
|
||||
|
||||
|
||||
class AuthDatabase:
|
||||
"""AuthDatabase 核心组件类。
|
||||
这是一个数据库操作层 (DAO/Repository) 封装类,专注于处理实体模型与关系型数据库表之间的映射。它将复杂的 SQL 查询、跨表 Join 和事务回滚逻辑进行了高级抽象,向上层服务暴露简洁的数据读写接口。"""
|
||||
"""User 表的 DAO:注册、登录、改密、删除以及权限读写。"""
|
||||
|
||||
def __init__(self, async_session_maker):
|
||||
self.async_session_maker = async_session_maker
|
||||
|
||||
@database_exception
|
||||
async def add_user(self, user_name: str, hashed_password: str) -> User:
|
||||
"""创建并持久化新的 user 实体。
|
||||
接收构建参数,执行必要的数据校验与默认值填充后,将新记录安全地写入底层存储或系统注册表中。
|
||||
Args: user_name (str): 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。 hashed_password (str): 控制逻辑流向的具体字符串参数,指定了期望的 hashed password 内容。
|
||||
Returns: (User): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""新建一名用户;若当前库中尚无任何用户,第一名将被自动赋予 SUPER_ADMINISTRATOR 权限。"""
|
||||
from ulid import ULID
|
||||
|
||||
async with self.async_session_maker() as session:
|
||||
@@ -58,10 +54,7 @@ class AuthDatabase:
|
||||
|
||||
@database_exception
|
||||
async def change_password(self, user_name, old_password, new_password) -> User:
|
||||
"""执行与 change password 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: user_name: 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。 old_password: 参与 change password 逻辑运算或数据构建的上下文依赖对象。 new_password: 参与 change password 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: (User): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""校验旧密码后将其替换为新密码;旧密码不匹配抛 UserPasswordError。"""
|
||||
async with self.async_session_maker() as session:
|
||||
statement = select(User).where(User.user_name == user_name)
|
||||
results = await session.execute(statement)
|
||||
@@ -78,10 +71,7 @@ class AuthDatabase:
|
||||
|
||||
@database_exception
|
||||
async def delete_user(self, user_name: str) -> None:
|
||||
"""安全地移除或注销 user。
|
||||
执行物理删除或逻辑删除操作,并妥善清理相关的关联数据及占用资源。
|
||||
Args: user_name (str): 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。
|
||||
Returns: (None): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""按用户名删除一名用户,不存在则抛 UserNotExistError。"""
|
||||
async with self.async_session_maker() as session:
|
||||
statement = select(User).where(User.user_name == user_name)
|
||||
results = await session.execute(statement)
|
||||
@@ -93,10 +83,7 @@ class AuthDatabase:
|
||||
|
||||
@database_exception
|
||||
async def delete_user_by_id(self, user_id: str) -> None:
|
||||
"""安全地移除或注销 user by id。
|
||||
执行物理删除或逻辑删除操作,并妥善清理相关的关联数据及占用资源。
|
||||
Args: user_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 user 实例。
|
||||
Returns: (None): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""按用户 ID 删除一名用户,不存在则抛 UserNotExistError。"""
|
||||
async with self.async_session_maker() as session:
|
||||
user = await session.get(User, user_id)
|
||||
if user is None:
|
||||
@@ -106,10 +93,7 @@ class AuthDatabase:
|
||||
|
||||
@database_exception
|
||||
async def login_user(self, user_name: str) -> str:
|
||||
"""执行与 login user 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: user_name (str): 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。
|
||||
Returns: (str): 处理流程所输出的具体字符串产物,可能是新生成的 ID 序列、格式化好的文本片段或 LLM 推理的回答内容。"""
|
||||
"""按用户名查出 User 记录返回给上层;上层再做密码校验并签发 token。"""
|
||||
async with self.async_session_maker() as session:
|
||||
statement = select(User).where(User.user_name == user_name)
|
||||
results = await session.execute(statement)
|
||||
@@ -120,9 +104,7 @@ class AuthDatabase:
|
||||
|
||||
@database_exception
|
||||
async def get_all_users(self) -> list[User]:
|
||||
"""检索并获取特定的 all users 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Returns: (list[User]): 经过筛选、排序或分页处理后的实体对象列表集合。"""
|
||||
"""返回数据库中全部用户列表。"""
|
||||
async with self.async_session_maker() as session:
|
||||
statement = select(User)
|
||||
results = await session.execute(statement)
|
||||
@@ -131,10 +113,7 @@ class AuthDatabase:
|
||||
|
||||
@database_exception
|
||||
async def get_user_authority(self, user_id: str) -> UserAuthority:
|
||||
"""检索并获取特定的 user authority 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Args: user_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 user 实例。
|
||||
Returns: (UserAuthority): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""返回指定用户的 UserAuthority 枚举;不存在抛 UserNotExistError。"""
|
||||
async with self.async_session_maker() as session:
|
||||
user = await session.get(User, user_id)
|
||||
if user is None:
|
||||
|
||||
@@ -49,8 +49,12 @@ from .module.chat_history import ChatHistoryDatabase
|
||||
|
||||
@ray.remote
|
||||
class PostgresDatabase:
|
||||
"""PostgresDatabase 核心组件类。
|
||||
这是一个数据库操作层 (DAO/Repository) 封装类,专注于处理实体模型与关系型数据库表之间的映射。它将复杂的 SQL 查询、跨表 Join 和事务回滚逻辑进行了高级抽象,向上层服务暴露简洁的数据读写接口。"""
|
||||
"""以 Ray Actor 形式暴露的统一数据库门面。
|
||||
|
||||
内部组合了 Auth / Provider / Individual / SystemNode / Workflow / ChatHistory
|
||||
六个子库,所有方法在调用前都会等待 ``ready_event``,确保 ``init_db`` 完成后
|
||||
再放行业务请求。
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
user = os.environ.get("POSTGRES_USER")
|
||||
@@ -76,6 +80,7 @@ class PostgresDatabase:
|
||||
self.ready_event = asyncio.Event()
|
||||
|
||||
async def init_db(self) -> None:
|
||||
"""根据 metadata 创建(或校验)所有 ORM 表,并置位 ready_event。"""
|
||||
try:
|
||||
async with self.async_engine.begin() as conn:
|
||||
await conn.run_sync(BaseDataModel.metadata.create_all)
|
||||
@@ -88,98 +93,65 @@ class PostgresDatabase:
|
||||
|
||||
# Auth Database Methods
|
||||
async def add_user(self, user_name: str, hashed_password: str):
|
||||
"""创建并持久化新的 user 实体。
|
||||
接收构建参数,执行必要的数据校验与默认值填充后,将新记录安全地写入底层存储或系统注册表中。
|
||||
Args: user_name (str): 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。 hashed_password (str): 控制逻辑流向的具体字符串参数,指定了期望的 hashed password 内容。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""新建一名用户。"""
|
||||
await self.ready_event.wait()
|
||||
return await self._auth_database.add_user(user_name, hashed_password)
|
||||
|
||||
async def change_password(self, user_name, old_password, new_password):
|
||||
"""执行与 change password 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: user_name: 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。 old_password: 参与 change password 逻辑运算或数据构建的上下文依赖对象。 new_password: 参与 change password 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""校验旧密码后将用户的密码替换为新密码。"""
|
||||
await self.ready_event.wait()
|
||||
return await self._auth_database.change_password(
|
||||
user_name, old_password, new_password
|
||||
)
|
||||
|
||||
async def delete_user(self, user_name: str):
|
||||
"""安全地移除或注销 user。
|
||||
执行物理删除或逻辑删除操作,并妥善清理相关的关联数据及占用资源。
|
||||
Args: user_name (str): 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""按用户名删除一名用户。"""
|
||||
await self.ready_event.wait()
|
||||
return await self._auth_database.delete_user(user_name)
|
||||
|
||||
async def delete_user_by_id(self, user_id: str):
|
||||
"""安全地移除或注销 user by id。
|
||||
执行物理删除或逻辑删除操作,并妥善清理相关的关联数据及占用资源。
|
||||
Args: user_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 user 实例。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""按用户 ID 删除一名用户。"""
|
||||
await self.ready_event.wait()
|
||||
return await self._auth_database.delete_user_by_id(user_id)
|
||||
|
||||
async def login_user(self, user_name: str):
|
||||
"""执行与 login user 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: user_name (str): 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""按用户名查询用户记录,用于上层做密码校验与签发 token。"""
|
||||
await self.ready_event.wait()
|
||||
return await self._auth_database.login_user(user_name)
|
||||
|
||||
async def get_all_users(self):
|
||||
"""检索并获取特定的 all users 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""返回全部用户列表。"""
|
||||
await self.ready_event.wait()
|
||||
return await self._auth_database.get_all_users()
|
||||
|
||||
async def get_user_authority(self, user_id: str):
|
||||
"""检索并获取特定的 user authority 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Args: user_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 user 实例。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""读取指定用户的权限/角色字段。"""
|
||||
await self.ready_event.wait()
|
||||
return await self._auth_database.get_user_authority(user_id)
|
||||
|
||||
async def change_user_authority(self, user_id: str, new_authority):
|
||||
"""执行与 change user authority 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: user_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 user 实例。 new_authority: 参与 change user authority 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""更新指定用户的权限/角色字段。"""
|
||||
await self.ready_event.wait()
|
||||
return await self._auth_database.change_user_authority(user_id, new_authority)
|
||||
|
||||
# Provider Database Methods
|
||||
async def get_provider(self):
|
||||
"""检索并获取特定的 provider 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""返回全部已登记的模型 Provider。"""
|
||||
await self.ready_event.wait()
|
||||
return await self._provider_database.get_provider()
|
||||
|
||||
async def add_provider_db(self, **kwargs):
|
||||
"""创建并持久化新的 provider db 实体。
|
||||
接收构建参数,执行必要的数据校验与默认值填充后,将新记录安全地写入底层存储或系统注册表中。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""新增一个模型 Provider 记录。"""
|
||||
await self.ready_event.wait()
|
||||
return await self._provider_database.add_provider(**kwargs)
|
||||
|
||||
async def delete_provider_db(self, provider_id: str):
|
||||
"""安全地移除或注销 provider db。
|
||||
执行物理删除或逻辑删除操作,并妥善清理相关的关联数据及占用资源。
|
||||
Args: provider_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider 实例。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""删除指定 ID 的模型 Provider 记录。"""
|
||||
await self.ready_event.wait()
|
||||
return await self._provider_database.delete_provider(provider_id)
|
||||
|
||||
async def update_provider_db(self, provider_id: str, **kwargs):
|
||||
"""对现有的 provider db 进行状态更新或属性覆盖。
|
||||
基于增量变更原则,合并最新的配置或数据,并触发相关依赖组件的缓存刷新或事件通知。
|
||||
Args: provider_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider 实例。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""部分更新指定 Provider 的字段。"""
|
||||
await self.ready_event.wait()
|
||||
return await self._provider_database.update_provider(provider_id, **kwargs)
|
||||
|
||||
@@ -191,68 +163,47 @@ class PostgresDatabase:
|
||||
model_id: str,
|
||||
tools: list[str] = None,
|
||||
):
|
||||
"""执行与 upsert system node config 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: node_name (str): 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。 provider_title (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 provider_title 实例。 model_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 model 实例。 tools (list[str]): 控制逻辑流向的具体字符串参数,指定了期望的 tools 内容。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""插入或更新某个系统节点(如 control/consciousness/regulatory)的模型配置。"""
|
||||
await self.ready_event.wait()
|
||||
return await self._system_node_database.upsert_system_node_config(
|
||||
node_name, provider_title, model_id, tools
|
||||
)
|
||||
|
||||
async def get_all_system_node_configs(self):
|
||||
"""检索并获取特定的 all system node configs 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""返回所有系统节点的模型配置。"""
|
||||
await self.ready_event.wait()
|
||||
return await self._system_node_database.get_all_system_node_configs()
|
||||
|
||||
# Individual Database Methods
|
||||
async def add_worker_individual(self, **kwargs):
|
||||
"""创建并持久化新的 worker individual 实体。
|
||||
接收构建参数,执行必要的数据校验与默认值填充后,将新记录安全地写入底层存储或系统注册表中。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""登记一个新的 Worker Individual 配置。"""
|
||||
await self.ready_event.wait()
|
||||
return await self._individual_database.add_worker_individual(**kwargs)
|
||||
|
||||
async def get_worker_individual(self, agent_id: str):
|
||||
"""检索并获取特定的 worker individual 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Args: agent_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 agent 实例。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""按 agent_id 读取单个 Worker Individual 配置。"""
|
||||
await self.ready_event.wait()
|
||||
return await self._individual_database.get_worker_individual(agent_id)
|
||||
|
||||
async def get_worker_individual_list(self, owner_id: str):
|
||||
"""检索并获取特定的 worker individual list 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Args: owner_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 owner 实例。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""读取某用户名下的所有 Worker Individual 配置。"""
|
||||
await self.ready_event.wait()
|
||||
return await self._individual_database.get_worker_individual_list(owner_id)
|
||||
|
||||
async def update_worker_individual(self, agent_id: str, **kwargs):
|
||||
"""对现有的 worker individual 进行状态更新或属性覆盖。
|
||||
基于增量变更原则,合并最新的配置或数据,并触发相关依赖组件的缓存刷新或事件通知。
|
||||
Args: agent_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 agent 实例。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""部分更新指定 Worker Individual 的字段。"""
|
||||
await self.ready_event.wait()
|
||||
return await self._individual_database.update_worker_individual(
|
||||
agent_id, **kwargs
|
||||
)
|
||||
|
||||
async def delete_worker_individual(self, agent_id: str):
|
||||
"""安全地移除或注销 worker individual。
|
||||
执行物理删除或逻辑删除操作,并妥善清理相关的关联数据及占用资源。
|
||||
Args: agent_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 agent 实例。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""删除指定的 Worker Individual。"""
|
||||
await self.ready_event.wait()
|
||||
return await self._individual_database.delete_worker_individual(agent_id)
|
||||
|
||||
async def get_all_worker_individual(self):
|
||||
"""检索并获取特定的 all worker individual 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""返回全部 Worker Individual 配置。"""
|
||||
await self.ready_event.wait()
|
||||
return await self._individual_database.get_all_worker_individual()
|
||||
|
||||
@@ -260,46 +211,56 @@ class PostgresDatabase:
|
||||
async def create_workflow(
|
||||
self, trace_id: str, user_id: str, title: str, command: str
|
||||
):
|
||||
"""新建一个工作流记录。"""
|
||||
await self.ready_event.wait()
|
||||
return await self._workflow_database.create_workflow(
|
||||
trace_id, user_id, title, command
|
||||
)
|
||||
|
||||
async def get_workflow(self, trace_id: str):
|
||||
"""按 trace_id 读取工作流记录。"""
|
||||
await self.ready_event.wait()
|
||||
return await self._workflow_database.get_workflow(trace_id)
|
||||
|
||||
async def update_workflow_status(self, trace_id: str, status: str):
|
||||
"""更新工作流的状态字段。"""
|
||||
await self.ready_event.wait()
|
||||
return await self._workflow_database.update_workflow_status(trace_id, status)
|
||||
|
||||
async def list_workflows(self, user_id: str):
|
||||
"""返回某用户名下的全部工作流。"""
|
||||
await self.ready_event.wait()
|
||||
return await self._workflow_database.list_workflows(user_id)
|
||||
|
||||
async def upsert_workflow_context(self, trace_id: str, **kwargs):
|
||||
"""插入或更新工作流的运行期上下文快照。"""
|
||||
await self.ready_event.wait()
|
||||
return await self._workflow_database.upsert_workflow_context(trace_id, **kwargs)
|
||||
|
||||
async def get_workflow_context(self, trace_id: str):
|
||||
"""读取指定工作流的上下文快照。"""
|
||||
await self.ready_event.wait()
|
||||
return await self._workflow_database.get_workflow_context(trace_id)
|
||||
|
||||
# Chat History Database Methods
|
||||
async def create_chat_session(self, user_id: str, title: str = "新对话"):
|
||||
"""新建一个聊天会话。"""
|
||||
await self.ready_event.wait()
|
||||
return await self._chat_history_database.create_chat_session(user_id, title)
|
||||
|
||||
async def list_chat_sessions(self, user_id: str):
|
||||
"""返回某用户名下的全部聊天会话。"""
|
||||
await self.ready_event.wait()
|
||||
return await self._chat_history_database.list_chat_sessions(user_id)
|
||||
|
||||
async def add_chat_message(self, chat_id: str, message: str, message_owner: str):
|
||||
"""向某个聊天会话追加一条消息。"""
|
||||
await self.ready_event.wait()
|
||||
return await self._chat_history_database.add_chat_message(
|
||||
chat_id, message, message_owner
|
||||
)
|
||||
|
||||
async def list_chat_messages(self, chat_id: str):
|
||||
"""返回某个聊天会话的全部消息。"""
|
||||
await self.ready_event.wait()
|
||||
return await self._chat_history_database.list_chat_messages(chat_id)
|
||||
|
||||
@@ -85,11 +85,7 @@ class KiloStarWorkflow(BaseModel):
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_workflow_integrity(self) -> "KiloStarWorkflow":
|
||||
"""
|
||||
执行与 validate workflow integrity 相关的核心业务流转操作。
|
||||
该方法保证了workflow中的work_step的序号为递增且跳转逻辑不会发生越界
|
||||
Returns:
|
||||
('KiloStarWorkflow'): 经过校验后的KiloStarWorkflow对象。"""
|
||||
"""校验 work_link 完整性:步骤序号连续递增,且 ``logic_gate.if_fail`` 跳转目标不越界。"""
|
||||
steps = [s.step for s in self.work_link]
|
||||
expected = list(range(1, len(steps) + 1))
|
||||
if steps != expected:
|
||||
|
||||
@@ -18,8 +18,7 @@ from typing import List, Literal, Dict
|
||||
|
||||
|
||||
class ApprovalToolData(BaseToolData):
|
||||
"""ApprovalToolData 核心组件类。
|
||||
这是一个可被智能体动态调用的外部工具组件类。它定义了清晰的输入参数 Schema 与执行契约,赋予智能体与外界真实系统(如文件、网页、API)进行交互的能力。"""
|
||||
"""``approval`` 工具的元数据:默认面向 control/consciousness 两类节点开放。"""
|
||||
|
||||
is_system: bool = True
|
||||
action_scope: List[
|
||||
|
||||
@@ -18,8 +18,7 @@ from pydantic import ConfigDict
|
||||
|
||||
|
||||
class BaseToolData(BaseModel):
|
||||
"""BaseToolData 核心组件类。
|
||||
这是一个可被智能体动态调用的外部工具组件类。它定义了清晰的输入参数 Schema 与执行契约,赋予智能体与外界真实系统(如文件、网页、API)进行交互的能力。"""
|
||||
"""所有工具插件的基类:声明工具的作用域、是否系统级以及配置参数 schema。"""
|
||||
|
||||
model_config = ConfigDict(extra="allow")
|
||||
is_system: bool
|
||||
|
||||
@@ -18,8 +18,7 @@ import os
|
||||
|
||||
|
||||
class FileReaderData(BaseToolData):
|
||||
"""FileReaderData 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 FileReaderData 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。"""
|
||||
"""``file_reader`` 工具的元数据:声明工具的名称、描述与是否系统级别。"""
|
||||
|
||||
is_system: bool = True
|
||||
name: str = "file_reader"
|
||||
|
||||
+35
-39
@@ -14,46 +14,57 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import jwt
|
||||
import os
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Optional
|
||||
from fastapi import HTTPException, status, Request
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
import jwt
|
||||
from fastapi import HTTPException, Request, status
|
||||
from pydantic import BaseModel, ValidationError
|
||||
from pwdlib import PasswordHash
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from kilostar.core.postgres_database.model.user import User
|
||||
|
||||
|
||||
ALGORITHM = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24
|
||||
_INSECURE_SECRETS = {"secret", "114514", "changethiskey12345"}
|
||||
|
||||
|
||||
class TokenData(BaseModel):
|
||||
"""TokenData 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 TokenData 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。"""
|
||||
"""JWT 解码后的用户身份载荷。"""
|
||||
|
||||
user_id: str
|
||||
username: Optional[str] = None
|
||||
exp: Optional[int] = None
|
||||
|
||||
|
||||
SECRET_KEY = os.getenv("SECRET_KEY")
|
||||
ALGORITHM = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24
|
||||
def _get_secret_key() -> str:
|
||||
"""读取并校验 SECRET_KEY 环境变量。
|
||||
|
||||
校验在首次实际使用 JWT 时进行,避免在模块导入阶段抛错,
|
||||
从而把"环境约束"和"模块加载"解耦。
|
||||
"""
|
||||
key = os.getenv("SECRET_KEY")
|
||||
if not key or key in _INSECURE_SECRETS:
|
||||
raise RuntimeError(
|
||||
"未提供有效的 SECRET_KEY 或使用了不安全的默认值,请设置一个高熵的随机字符串"
|
||||
)
|
||||
return key
|
||||
|
||||
if not SECRET_KEY or SECRET_KEY in {"secret", "114514"}:
|
||||
raise RuntimeError("未提供有效的 SECRET_KEY 或使用了不安全的默认值")
|
||||
|
||||
password_hasher = PasswordHash.recommended()
|
||||
|
||||
|
||||
class Accessor:
|
||||
"""Accessor 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 Accessor 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。"""
|
||||
"""封装认证与口令哈希相关的静态工具方法。"""
|
||||
|
||||
@staticmethod
|
||||
def _decode_token(token: str) -> TokenData:
|
||||
"""执行与 decode token 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: token (str): 由认证中心颁发的 JWT 或长期访问令牌,用于跨服务调用时的身份自证与权限校验。
|
||||
Returns: (TokenData): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""解码并校验 JWT,返回 TokenData;过期或无效时抛 401。"""
|
||||
try:
|
||||
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||||
payload = jwt.decode(token, _get_secret_key(), algorithms=[ALGORITHM])
|
||||
return TokenData(**payload)
|
||||
except jwt.ExpiredSignatureError:
|
||||
raise HTTPException(
|
||||
@@ -68,31 +79,22 @@ class Accessor:
|
||||
|
||||
@staticmethod
|
||||
def _create_access_token(data: dict) -> str:
|
||||
"""创建并持久化新的 access token 实体。
|
||||
接收构建参数,执行必要的数据校验与默认值填充后,将新记录安全地写入底层存储或系统注册表中。
|
||||
Args: data (dict): 从客户端传递过来或由上游组件生成的核心业务数据体,通常需要进一步的清洗和结构化解析。
|
||||
Returns: (str): 处理流程所输出的具体字符串产物,可能是新生成的 ID 序列、格式化好的文本片段或 LLM 推理的回答内容。"""
|
||||
"""根据 payload 生成带过期时间的 JWT 访问令牌。"""
|
||||
to_encode = data.copy()
|
||||
expire = datetime.now(timezone.utc) + timedelta(
|
||||
minutes=ACCESS_TOKEN_EXPIRE_MINUTES
|
||||
)
|
||||
to_encode.update({"exp": int(expire.timestamp())})
|
||||
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
||||
return jwt.encode(to_encode, _get_secret_key(), algorithm=ALGORITHM)
|
||||
|
||||
@staticmethod
|
||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||
"""执行与 verify password 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: plain_password (str): 控制逻辑流向的具体字符串参数,指定了期望的 plain password 内容。 hashed_password (str): 控制逻辑流向的具体字符串参数,指定了期望的 hashed password 内容。
|
||||
Returns: (bool): 一个布尔型结果标志,明确返回 True 表示该操作成功应用或条件达成,False 则表示失败或被拒绝。"""
|
||||
"""校验明文口令是否匹配数据库中存储的哈希。"""
|
||||
return password_hasher.verify(plain_password, hashed_password)
|
||||
|
||||
@staticmethod
|
||||
def get_current_user(request: Request) -> TokenData:
|
||||
"""检索并获取特定的 current user 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Args: request (Request): FastAPI 框架注入的原生 HTTP 请求对象,包含了完整的 Header 标头、查询参数和正文流。
|
||||
Returns: (TokenData): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""从 Authorization Bearer 头解析当前请求的用户身份。"""
|
||||
auth_header = request.headers.get("Authorization")
|
||||
if not auth_header or not auth_header.startswith("Bearer "):
|
||||
raise HTTPException(
|
||||
@@ -103,11 +105,8 @@ class Accessor:
|
||||
return Accessor._decode_token(token)
|
||||
|
||||
@staticmethod
|
||||
def login_hashed_password(user: User, password: str) -> str:
|
||||
"""执行与 login hashed password 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: user (User): 当前已通过鉴权流程的访问者实体对象,内部包含用户角色、权限层级及租户归属等核心元信息。 password (str): 控制逻辑流向的具体字符串参数,指定了期望的 password 内容。
|
||||
Returns: (str): 处理流程所输出的具体字符串产物,可能是新生成的 ID 序列、格式化好的文本片段或 LLM 推理的回答内容。"""
|
||||
def login_hashed_password(user: "User", password: str) -> str:
|
||||
"""完成登录核验:找不到用户或密码错误抛 401,否则签发新令牌。"""
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
@@ -123,10 +122,7 @@ class Accessor:
|
||||
|
||||
@staticmethod
|
||||
def hash_password(password: str) -> str:
|
||||
"""执行与 hash password 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: password (str): 控制逻辑流向的具体字符串参数,指定了期望的 password 内容。
|
||||
Returns: (str): 处理流程所输出的具体字符串产物,可能是新生成的 ID 序列、格式化好的文本片段或 LLM 推理的回答内容。"""
|
||||
"""对明文口令做强哈希;空值或长度不足 6 位会抛 ValueError。"""
|
||||
if not password:
|
||||
raise ValueError("密码不能为空")
|
||||
if len(password) < 6:
|
||||
|
||||
@@ -18,19 +18,17 @@ import yaml
|
||||
|
||||
|
||||
def print_banner() -> None:
|
||||
"""执行与 print banner 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Returns: (None): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""在启动阶段输出 KiloStar 的 ASCII 横幅与版本/作者元信息。"""
|
||||
with open("config/config.yml", "r") as config:
|
||||
config = yaml.load(config, Loader=yaml.FullLoader)
|
||||
version = config.get("version", "unknown")
|
||||
kilostar_banner = """
|
||||
██████╗ ██████╗ ███████╗████████╗ ██████╗ ██████╗
|
||||
██╔══██╗██╔══██╗██╔════╝╚══██╔══╝██╔═══██╗██╔══██╗
|
||||
██████╔╝██████╔╝█████╗ ██║ ██║ ██║██████╔╝
|
||||
██╔═══╝ ██╔══██╗██╔══╝ ██║ ██║ ██║██╔══██╗
|
||||
██║ ██║ ██║███████╗ ██║ ╚██████╔╝██║ ██║
|
||||
╚═╝ ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝
|
||||
kilostar_banner = r"""
|
||||
██╗ ██╗██╗██╗ ██████╗ ███████╗████████╗ █████╗ ██████╗
|
||||
██║ ██╔╝██║██║ ██╔═══██╗██╔════╝╚══██╔══╝██╔══██╗██╔══██╗
|
||||
█████╔╝ ██║██║ ██║ ██║███████╗ ██║ ███████║██████╔╝
|
||||
██╔═██╗ ██║██║ ██║ ██║╚════██║ ██║ ██╔══██║██╔══██╗
|
||||
██║ ██╗██║███████╗╚██████╔╝███████║ ██║ ██║ ██║██║ ██║
|
||||
╚═╝ ╚═╝╚═╝╚══════╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝
|
||||
"""
|
||||
console = Console()
|
||||
banner_colored = Text(kilostar_banner, style="gold3 bold")
|
||||
|
||||
@@ -19,10 +19,7 @@ from kilostar.utils.ray_hook import ray_actor_hook
|
||||
|
||||
|
||||
async def get_authority(user_id: str) -> UserAuthority:
|
||||
"""检索并获取特定的 authority 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Args: user_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 user 实例。
|
||||
Returns: (UserAuthority): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""通过 PostgresDatabase Actor 查出指定用户的 ``UserAuthority``;用户不存在时抛 401。"""
|
||||
from kilostar.utils.error import UserNotExistError
|
||||
|
||||
postgres_database = ray_actor_hook("postgres_database").postgres_database
|
||||
@@ -43,8 +40,10 @@ async def get_authority(user_id: str) -> UserAuthority:
|
||||
|
||||
|
||||
class RoleChecker:
|
||||
"""RoleChecker 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 RoleChecker 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。"""
|
||||
"""FastAPI 依赖:在路由级别按 ``UserAuthority`` 做最低权限校验。
|
||||
|
||||
例:``Depends(RoleChecker(allowed_roles=UserAuthority.ADMINISTRATOR))``。
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.allowed_roles = kwargs.get(
|
||||
@@ -54,10 +53,7 @@ class RoleChecker:
|
||||
async def __call__(
|
||||
self, token_data: Annotated[TokenData, Depends(Accessor.get_current_user)]
|
||||
):
|
||||
"""执行与 call 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: token_data (Annotated[TokenData, Depends(Accessor.get_current_user)]): 从客户端传递过来或由上游组件生成的核心业务数据体,通常需要进一步的清洗和结构化解析。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""对当前请求执行权限比较,权限不足抛 403,否则把 ``TokenData`` 透传给路由。"""
|
||||
user_authority = await get_authority(token_data.user_id)
|
||||
if user_authority < self.allowed_roles:
|
||||
raise HTTPException(
|
||||
|
||||
+11
-20
@@ -14,75 +14,66 @@
|
||||
|
||||
|
||||
class RetryableError(Exception):
|
||||
"""基类:所有可重试错误(如网络断开、抖动等临时性故障)"""
|
||||
"""基类:所有可重试错误(如网络断开、抖动等临时性故障)。"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class NonRetryableError(Exception):
|
||||
"""基类:所有不可重试错误(如数据验证失败、类型错误等业务逻辑故障)"""
|
||||
"""基类:所有不可重试错误(如数据验证失败、类型错误等业务逻辑故障)。"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class DemandError(NonRetryableError):
|
||||
"""DemandError 核心组件类。
|
||||
这是一个自定义异常类,专门用于在 Demand 相关业务流程中触发中断。它携带了精确的错误上下文与追溯代码,帮助最外层网关能够统一捕获并返回友好的前端错误提示。"""
|
||||
"""需求/任务参数不合法或不满足前置条件时抛出。"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ModelNotExistError(Exception):
|
||||
"""ModelNotExistError 核心组件类。
|
||||
这是一个自定义异常类,专门用于在 ModelNotExist 相关业务流程中触发中断。它携带了精确的错误上下文与追溯代码,帮助最外层网关能够统一捕获并返回友好的前端错误提示。"""
|
||||
"""请求了一个未在 Provider 中注册的模型 ID 时抛出。"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class UserError(Exception):
|
||||
"""UserError 核心组件类。
|
||||
这是一个自定义异常类,专门用于在 User 相关业务流程中触发中断。它携带了精确的错误上下文与追溯代码,帮助最外层网关能够统一捕获并返回友好的前端错误提示。"""
|
||||
"""用户相关错误的基类,HTTP 层会被统一映射为 4xx。"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class UserNotExistError(UserError):
|
||||
"""UserNotExistError 核心组件类。
|
||||
这是一个自定义异常类,专门用于在 UserNotExist 相关业务流程中触发中断。它携带了精确的错误上下文与追溯代码,帮助最外层网关能够统一捕获并返回友好的前端错误提示。"""
|
||||
"""按用户名/ID 查询时用户不存在。"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class UserPasswordError(UserError):
|
||||
"""UserPasswordError 核心组件类。
|
||||
这是一个自定义异常类,专门用于在 UserPassword 相关业务流程中触发中断。它携带了精确的错误上下文与追溯代码,帮助最外层网关能够统一捕获并返回友好的前端错误提示。"""
|
||||
"""口令校验失败(旧密码错误、登录密码错误等)。"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ProviderError(Exception):
|
||||
"""ProviderError 核心组件类。
|
||||
这是一个模型/服务提供商适配器类,屏蔽了外部不同供应商(如 OpenAI、Anthropic 等)的底层 API 差异。它负责标准化参数组装、网络请求发送、鉴权处理以及响应结构的反序列化。"""
|
||||
"""模型 Provider 相关错误的基类。"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ProviderNotExistError(ProviderError):
|
||||
"""ProviderNotExistError 核心组件类。
|
||||
这是一个模型/服务提供商适配器类,屏蔽了外部不同供应商(如 OpenAI、Anthropic 等)的底层 API 差异。它负责标准化参数组装、网络请求发送、鉴权处理以及响应结构的反序列化。"""
|
||||
"""请求了一个未注册的 Provider 时抛出。"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class WorkflowError(Exception):
|
||||
"""WorkflowError 核心组件类。
|
||||
这是一个自定义异常类,专门用于在 Workflow 相关业务流程中触发中断。它携带了精确的错误上下文与追溯代码,帮助最外层网关能够统一捕获并返回友好的前端错误提示。"""
|
||||
"""工作流执行期错误的基类,HTTP 层会被统一映射为 5xx。"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class WorkflowExit(WorkflowError):
|
||||
"""WorkflowExit 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 WorkflowExit 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。"""
|
||||
"""工作流被显式终止(用户取消、上游决策跳出等)时抛出,是预期内的退出信号。"""
|
||||
|
||||
pass
|
||||
|
||||
@@ -24,10 +24,11 @@ _tool_cache: Dict[str, Callable] = {}
|
||||
|
||||
|
||||
def _get_tool_func(tool_name: str) -> Callable | None:
|
||||
"""检索并获取特定的 tool func 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Args: tool_name (str): 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。
|
||||
Returns: (Callable | None): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""按名字从 ``kilostar/plugin/tool_plugin/<tool_name>/__init__.py`` 中加载工具函数。
|
||||
|
||||
加载成功后会被缓存到模块级 ``_tool_cache``;找不到目录、找不到同名函数或
|
||||
导入失败都会记录日志并返回 ``None``。
|
||||
"""
|
||||
func = _tool_cache.get(tool_name, None)
|
||||
if func:
|
||||
return func
|
||||
@@ -72,19 +73,13 @@ def _get_tool_func(tool_name: str) -> Callable | None:
|
||||
|
||||
|
||||
def del_tool_cache(tool_name: str) -> None:
|
||||
"""执行与 del tool cache 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: tool_name (str): 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。
|
||||
Returns: (None): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""从内存缓存中移除某个工具,下次调用 ``load_tools_from_list`` 会重新从磁盘加载。"""
|
||||
if tool_name in _tool_cache:
|
||||
del _tool_cache[tool_name]
|
||||
|
||||
|
||||
def load_tools_from_list(tool_names: List[str] | None) -> List[Callable]:
|
||||
"""执行与 load tools from list 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: tool_names (List[str] | None): 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。
|
||||
Returns: (List[Callable]): 经过筛选、排序或分页处理后的实体对象列表集合。"""
|
||||
"""批量加载工具:传入工具名列表,返回成功加载到的函数对象列表(失败项被跳过)。"""
|
||||
if not tool_names:
|
||||
return []
|
||||
|
||||
|
||||
@@ -18,17 +18,11 @@ from loguru._logger import Logger
|
||||
|
||||
|
||||
def setup_logger() -> Logger:
|
||||
"""对现有的 setup logger 进行状态更新或属性覆盖。
|
||||
基于增量变更原则,合并最新的配置或数据,并触发相关依赖组件的缓存刷新或事件通知。
|
||||
Returns: (Logger): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""初始化全局 loguru logger,输出格式为 ``actor:(...) | trace_id:(...) : message``。"""
|
||||
logger.remove()
|
||||
|
||||
def format_record(record):
|
||||
# Format string for rich handler
|
||||
"""执行与 format record 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: record: 参与 format record 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
actor = record["extra"].get("actor_name", "System")
|
||||
trace_id = record["extra"].get("trace_id", "")
|
||||
|
||||
@@ -57,8 +51,5 @@ global_logger = setup_logger()
|
||||
|
||||
|
||||
def get_logger(actor_name: str, trace_id: str = "") -> Logger:
|
||||
"""检索并获取特定的 logger 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Args: actor_name (str): 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。 trace_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 trace 实例。
|
||||
Returns: (Logger): 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""获取一个绑定了 actor_name 与可选 trace_id 的 logger,便于日志按 Actor/请求归类。"""
|
||||
return global_logger.bind(actor_name=actor_name, trace_id=trace_id)
|
||||
|
||||
@@ -31,9 +31,6 @@ def pickle(cls: T) -> T:
|
||||
|
||||
def __reduce__(self):
|
||||
# 1. 序列化:触发 Pydantic-core (Rust) 的极速序列化
|
||||
"""执行与 reduce 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
data = self.model_dump_json()
|
||||
# 2. 反序列化:告诉 Pickle 重建时调用 cls.model_validate_json
|
||||
return cls.model_validate_json, (data,)
|
||||
|
||||
@@ -16,31 +16,20 @@ from functools import lru_cache
|
||||
|
||||
|
||||
class ActorList:
|
||||
"""ActorList 核心组件类。
|
||||
这是一个领域数据模型或功能封装类,承载了 ActorList 相关的内聚属性定义与状态维护。它的存在隔离了局部的业务复杂性,并对外提供了类型安全的访问接口。"""
|
||||
"""属性式访问的简易容器,用 ``a.actor_name`` 取代 ``d["actor_name"]``。"""
|
||||
|
||||
def __init__(self):
|
||||
super().__setattr__("dict", {})
|
||||
|
||||
def __setattr__(self, key, value):
|
||||
"""对现有的 setattr 进行状态更新或属性覆盖。
|
||||
基于增量变更原则,合并最新的配置或数据,并触发相关依赖组件的缓存刷新或事件通知。
|
||||
Args: key: 参与 setattr 逻辑运算或数据构建的上下文依赖对象。 value: 参与 setattr 逻辑运算或数据构建的上下文依赖对象。"""
|
||||
self.dict[key] = value
|
||||
|
||||
def __getattr__(self, key):
|
||||
"""检索并获取特定的 getattr 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Args: key: 参与 getattr 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
if key in self.dict:
|
||||
return self.dict[key]
|
||||
raise AttributeError(f"ActorList 对象没有属性 '{key}'")
|
||||
|
||||
def __delattr__(self, key):
|
||||
"""执行与 delattr 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: key: 参与 delattr 逻辑运算或数据构建的上下文依赖对象。"""
|
||||
if key in self.dict:
|
||||
del self.dict[key]
|
||||
else:
|
||||
@@ -59,9 +48,11 @@ def clear_actor_cache():
|
||||
|
||||
|
||||
def ray_actor_hook(*actor_names: str):
|
||||
"""执行与 ray actor hook 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""按名字批量取出 Ray Actor 句柄,组装成一个 ``ActorList`` 返回。
|
||||
|
||||
例:``actors = ray_actor_hook("postgres_database", "global_state_machine")``,
|
||||
随后即可用 ``actors.postgres_database`` 拿到对应句柄。
|
||||
"""
|
||||
actor_list = ActorList()
|
||||
for actor_name in actor_names:
|
||||
handle = _get_cached_actor_handle(actor_name)
|
||||
|
||||
+8
-14
@@ -19,23 +19,20 @@ from kilostar.utils.error import RetryableError
|
||||
|
||||
|
||||
def retry_on_retryable_error(max_retries=3, base_delay=1):
|
||||
"""执行与 retry on retryable error 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: max_retries: 参与 retry on retryable error 逻辑运算或数据构建的上下文依赖对象。 base_delay: 参与 retry on retryable error 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""指数退避重试装饰器:仅在抛出 ``RetryableError`` 时重试。
|
||||
|
||||
同步/异步函数都支持。第 n 次重试前会 ``sleep(base_delay * 2**n)``。
|
||||
|
||||
Args:
|
||||
max_retries: 最多尝试次数(含首次),超过后会把最后一次异常重新抛出。
|
||||
base_delay: 退避基准秒数。
|
||||
"""
|
||||
|
||||
def decorator(func):
|
||||
"""执行与 decorator 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: func: 参与 decorator 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
if asyncio.iscoroutinefunction(func):
|
||||
|
||||
@wraps(func)
|
||||
async def async_wrapper(*args, **kwargs):
|
||||
"""执行与 async wrapper 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
return await func(*args, **kwargs)
|
||||
@@ -49,9 +46,6 @@ def retry_on_retryable_error(max_retries=3, base_delay=1):
|
||||
|
||||
@wraps(func)
|
||||
def sync_wrapper(*args, **kwargs):
|
||||
"""执行与 sync wrapper 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
import time
|
||||
|
||||
for attempt in range(max_retries):
|
||||
|
||||
@@ -45,8 +45,7 @@ class WorkerCluster:
|
||||
self.logger = get_logger("worker_cluster")
|
||||
|
||||
async def start(self):
|
||||
"""执行与 start 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。"""
|
||||
"""启动 runner 协程池并初始化任务队列。"""
|
||||
if self.task_queue is None:
|
||||
self.task_queue = Queue()
|
||||
self.runners = [
|
||||
@@ -84,9 +83,7 @@ class WorkerCluster:
|
||||
return worker
|
||||
|
||||
async def _runner(self, runner_id: int):
|
||||
"""执行与 runner 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: runner_id (int): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 runner 实例。"""
|
||||
"""单个 runner 协程:从任务队列取任务,按 agent_id 唤醒 Worker 执行,结果写回 future。"""
|
||||
while True:
|
||||
try:
|
||||
if self.task_queue is None:
|
||||
@@ -130,10 +127,7 @@ class WorkerCluster:
|
||||
await asyncio.sleep(1)
|
||||
|
||||
async def submit_task(self, task_id: str, agent_id: str, task_event: dict):
|
||||
"""执行与 submit task 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: task_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 task 实例。 agent_id (str): 目标对象的唯一全局标识符 (UUID/ULID),用于在数据库表或缓存结构中精准匹配该 agent 实例。 task_event (dict): 由事件总线或工作流引擎分发过来的事件载荷,封装了触发此次调用的上下文快照与任务目标指令。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""提交一个任务到队列,挂起等待 runner 处理完成后返回响应字典。"""
|
||||
if not self.runners:
|
||||
await self.start()
|
||||
|
||||
@@ -151,9 +145,7 @@ class WorkerCluster:
|
||||
self.results_futures.pop(task_id, None)
|
||||
|
||||
def get_cluster_metrics(self):
|
||||
"""检索并获取特定的 cluster metrics 数据集合或实例对象。
|
||||
根据提供的查询条件或上下文凭证,从数据库、缓存或第三方服务中读取对应的资源状态。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""返回当前内存池中 Worker 数量、容量、缓存的 agent_id 列表与队列长度等指标。"""
|
||||
return {
|
||||
"active_worker_count": len(self._active_workers),
|
||||
"max_capacity": self.max_capacity,
|
||||
|
||||
@@ -16,7 +16,7 @@ from pydantic_ai import Agent, RunContext
|
||||
from pydantic import Field
|
||||
from kilostar.adapter.model_adapter.agent_factory import AgentFactory
|
||||
from kilostar.core.global_state_machine.model_provider.base_provider import Provider
|
||||
from kilostar.utils.agent_model import ResponseModel, InputModel, DepsModel
|
||||
from kilostar.utils.agent_model import ResponseModel, RequestModel, DepsModel
|
||||
from kilostar.utils.ray_hook import ray_actor_hook
|
||||
|
||||
from kilostar.utils.logger import get_logger
|
||||
@@ -25,22 +25,19 @@ logger = get_logger("worker_individual")
|
||||
|
||||
|
||||
class WorkerIndividualResponse(ResponseModel):
|
||||
"""WorkerIndividualResponse 核心组件类。
|
||||
这是一个具体的 Worker 智能体实体类,代表着具备特定人设、领域技能或长文本处理能力的数字员工。它可以被控制器动态拉起,并在安全沙箱内执行复杂的工作流指令与多步骤推理任务。"""
|
||||
"""Worker Individual 的输出模型,承载一次任务执行后的结果文本。"""
|
||||
|
||||
output: str = Field(..., description="Worker执行任务的输出结果")
|
||||
|
||||
|
||||
class WorkerIndividualDeps(DepsModel):
|
||||
"""WorkerIndividualDeps 核心组件类。
|
||||
这是一个具体的 Worker 智能体实体类,代表着具备特定人设、领域技能或长文本处理能力的数字员工。它可以被控制器动态拉起,并在安全沙箱内执行复杂的工作流指令与多步骤推理任务。"""
|
||||
"""Worker Individual 的运行期依赖,注入到 pydantic-ai Agent 的 RunContext。"""
|
||||
|
||||
task_event: dict
|
||||
|
||||
|
||||
class WorkerIndividualInput(InputModel):
|
||||
"""WorkerIndividualInput 核心组件类。
|
||||
这是一个具体的 Worker 智能体实体类,代表着具备特定人设、领域技能或长文本处理能力的数字员工。它可以被控制器动态拉起,并在安全沙箱内执行复杂的工作流指令与多步骤推理任务。"""
|
||||
class WorkerIndividualInput(RequestModel):
|
||||
"""Worker Individual 的输入模型,承载一次任务事件的入参。"""
|
||||
|
||||
task_event: dict
|
||||
|
||||
@@ -56,10 +53,15 @@ class BaseIndividual:
|
||||
self.agent: Agent | None = None
|
||||
|
||||
async def _init_agent(self, agent_name: str, system_prompt: str):
|
||||
"""完成 agent 模块的启动与依赖初始化。
|
||||
在系统引导或服务拉起阶段被调用,负责建立网络连接、分配基础内存资源及注册核心服务组件。
|
||||
Args: agent_name (str): 赋予该实体的人类可读名称或标题字符串,主要用于前端 UI 展示、日志记录或模糊检索。 system_prompt (str): 控制逻辑流向的具体字符串参数,指定了期望的 system prompt 内容。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""根据 agent_config 拉起一个 pydantic-ai Agent 实例。
|
||||
|
||||
从 GlobalStateMachine 取出 Provider,按 agent_config 中的 provider_title
|
||||
和 model_id 选择模型,加载工具列表,并把 system_prompt 注册为动态提示词。
|
||||
|
||||
Args:
|
||||
agent_name: Agent 的人类可读名称,用于日志与展示。
|
||||
system_prompt: 该 Agent 的基础系统提示词,会和 task_event 拼接成动态提示词。
|
||||
"""
|
||||
from kilostar.utils.get_tool import load_tools_from_list
|
||||
|
||||
global_state_machine = ray_actor_hook(
|
||||
@@ -90,17 +92,11 @@ class BaseIndividual:
|
||||
|
||||
@self.agent.system_prompt
|
||||
async def dynamic_prompt(ctx: RunContext[WorkerIndividualDeps]):
|
||||
"""执行与 dynamic prompt 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: ctx (RunContext[WorkerIndividualDeps]): 参与 dynamic prompt 逻辑运算或数据构建的上下文依赖对象。
|
||||
Returns: : 经由当前业务模型加工处理后所输出的具体数据实例或领域模型对象。"""
|
||||
"""把基础 system_prompt 与本次 task_event 拼接成最终动态提示词。"""
|
||||
prompt = system_prompt + "\n\n"
|
||||
prompt += f"=== 当前任务上下文 ===\n{ctx.deps.task_event}\n"
|
||||
return prompt
|
||||
|
||||
async def run(self, task_event: dict) -> dict:
|
||||
"""执行与 run 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: task_event (dict): 由事件总线或工作流引擎分发过来的事件载荷,封装了触发此次调用的上下文快照与任务目标指令。
|
||||
Returns: (dict): 高度聚合的字典结构数据,将多维度的属性特征或统计指标组合后一并返回。"""
|
||||
"""执行一次任务,需要由子类按自身策略实现。"""
|
||||
raise NotImplementedError("子类必须实现 run 方法")
|
||||
|
||||
@@ -30,10 +30,7 @@ class OrdinaryIndividual(BaseIndividual):
|
||||
super().__init__(agent_config)
|
||||
|
||||
async def run(self, task_event: dict) -> dict:
|
||||
"""执行与 run 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: task_event (dict): 由事件总线或工作流引擎分发过来的事件载荷,封装了触发此次调用的上下文快照与任务目标指令。
|
||||
Returns: (dict): 高度聚合的字典结构数据,将多维度的属性特征或统计指标组合后一并返回。"""
|
||||
"""执行一次普通任务:首次调用时懒初始化 Agent,再用 ``WorkerIndividualDeps`` 跑出结果。"""
|
||||
if self.agent is None:
|
||||
system_prompt = self.agent_config.get(
|
||||
"prompt", "你是一个普通的AI助手,请尽力完成给定的任务。"
|
||||
|
||||
@@ -104,10 +104,7 @@ class SkillIndividual(BaseIndividual):
|
||||
return tools
|
||||
|
||||
async def run(self, task_event: dict) -> dict:
|
||||
"""执行与 run 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: task_event (dict): 由事件总线或工作流引擎分发过来的事件载荷,封装了触发此次调用的上下文快照与任务目标指令。
|
||||
Returns: (dict): 高度聚合的字典结构数据,将多维度的属性特征或统计指标组合后一并返回。"""
|
||||
"""执行一次专家任务:先按 ``bound_skill`` 动态加载工具,再驱动 Agent 运行。"""
|
||||
if self.agent is None:
|
||||
system_prompt = self.agent_config.get(
|
||||
"prompt",
|
||||
|
||||
@@ -30,10 +30,7 @@ class SpecialIndividual(BaseIndividual):
|
||||
super().__init__(agent_config)
|
||||
|
||||
async def run(self, task_event: dict) -> dict:
|
||||
"""执行与 run 相关的核心业务流转操作。
|
||||
该方法封装了具体的算法策略或状态控制逻辑,确保操作能够在事务上下文中被原子且一致地执行。
|
||||
Args: task_event (dict): 由事件总线或工作流引擎分发过来的事件载荷,封装了触发此次调用的上下文快照与任务目标指令。
|
||||
Returns: (dict): 高度聚合的字典结构数据,将多维度的属性特征或统计指标组合后一并返回。"""
|
||||
"""执行一次特殊任务(语音/视频等特殊产物):懒初始化 Agent 后跑出结果。"""
|
||||
if self.agent is None:
|
||||
system_prompt = self.agent_config.get(
|
||||
"prompt", "你是一个特殊的AI助手,负责处理特殊类型的任务。"
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
import os
|
||||
import secrets
|
||||
|
||||
_secret_key = os.getenv("SECRET_KEY")
|
||||
if not _secret_key or _secret_key in {"secret", "114514", "changethiskey12345"}:
|
||||
_secret_key = secrets.token_urlsafe(32)
|
||||
os.environ["SECRET_KEY"] = _secret_key
|
||||
print(
|
||||
"⚠️ 警告: 未提供有效的 SECRET_KEY 或使用了不安全的默认值,已生成并设置随机密钥。"
|
||||
)
|
||||
|
||||
import asyncio
|
||||
import ray
|
||||
from ray import serve
|
||||
|
||||
from kilostar.worker_cluster import WorkerCluster
|
||||
from kilostar.utils.banner import print_banner
|
||||
from kilostar.core.postgres_database import PostgresDatabase
|
||||
@@ -9,18 +22,7 @@ from kilostar.core.individual.regulatory_node import RegulatoryNode
|
||||
from kilostar.core.individual.consciousness_node import ConsciousnessNode
|
||||
from kilostar.core.individual.control_node import ControlNode
|
||||
from kilostar.core.work.workflow.workflow_engine import WorkflowRunningEngine
|
||||
from kilostar.api import kilostarGateway
|
||||
from ray import serve
|
||||
import os
|
||||
import secrets
|
||||
|
||||
_secret_key = os.getenv("SECRET_KEY")
|
||||
if not _secret_key or _secret_key in {"secret", "114514"}:
|
||||
_secret_key = secrets.token_urlsafe(32)
|
||||
os.environ["SECRET_KEY"] = _secret_key
|
||||
print(
|
||||
"⚠️ 警告: 未提供有效的 SECRET_KEY 或使用了不安全的默认值,已生成并设置随机密钥。"
|
||||
)
|
||||
from kilostar.api import KiloStarGateway
|
||||
|
||||
|
||||
async def start_system():
|
||||
@@ -97,7 +99,7 @@ async def start_system():
|
||||
|
||||
# 6. 启动 FastAPI 网关 (使用 Ray Serve)
|
||||
serve.start(http_options={"host": "0.0.0.0", "port": 8000})
|
||||
serve.run(kilostarGateway.bind())
|
||||
serve.run(KiloStarGateway.bind())
|
||||
|
||||
# 挂起主线程以保持系统运行
|
||||
while True:
|
||||
@@ -113,4 +115,4 @@ def main():
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main()
|
||||
|
||||
+1
-5
@@ -8,17 +8,14 @@ packages = ["kilostar"]
|
||||
[project]
|
||||
name = "kilostar"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
description = "A multi-agent system"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12,<4.0"
|
||||
dependencies = [
|
||||
"asyncpg>=0.31.0",
|
||||
"docker-py>=1.10.6",
|
||||
"docutils-stubs==0.0.22",
|
||||
"httpx>=0.28.1",
|
||||
"jinja2>=3.1.6",
|
||||
"loguru>=0.7.3",
|
||||
"passlib[argon2]>=1.7.4",
|
||||
"pretor-viceroy>=0.2.0",
|
||||
"pwdlib[argon2,bcrypt]>=0.3.0",
|
||||
"pydantic-ai>=1.73.0",
|
||||
@@ -28,7 +25,6 @@ dependencies = [
|
||||
"ray[default,serve]>=2.54.0",
|
||||
"rich>=14.3.3",
|
||||
"sqlalchemy>=2.0.49",
|
||||
"types-docutils==0.22.3.20260408",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
|
||||
@@ -1028,33 +1028,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "docker-py"
|
||||
version = "1.10.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "docker-pycreds" },
|
||||
{ name = "requests" },
|
||||
{ name = "six" },
|
||||
{ name = "websocket-client" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fa/2d/906afc44a833901fc6fed1a89c228e5c88fbfc6bd2f3d2f0497fdfb9c525/docker-py-1.10.6.tar.gz", hash = "sha256:4c2a75875764d38d67f87bc7d03f7443a3895704efc57962bdf6500b8d4bc415", size = 84129, upload-time = "2016-11-02T23:49:04.907Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/23/c7/1fd6d4d620809fe2f323869d719e2dd0086c939b67021303a9ec40f5a05b/docker_py-1.10.6-py2.py3-none-any.whl", hash = "sha256:35b506e95861914fa5ad57a6707e3217b4082843b883be246190f57013948aba", size = 50003, upload-time = "2016-11-02T23:49:07.683Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "docker-pycreds"
|
||||
version = "0.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "six" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c5/e6/d1f6c00b7221e2d7c4b470132c931325c8b22c51ca62417e300f5ce16009/docker-pycreds-0.4.0.tar.gz", hash = "sha256:6ce3270bcaf404cc4c3e27e4b6c70d3521deae82fb508767870fdbf772d584d4", size = 8754, upload-time = "2018-11-29T03:26:50.996Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/e8/f6bd1eee09314e7e6dee49cbe2c5e22314ccdb38db16c9fc72d2fa80d054/docker_pycreds-0.4.0-py2.py3-none-any.whl", hash = "sha256:7266112468627868005106ec19cd0d722702d2b7d5912a28e19b826c3d37af49", size = 8982, upload-time = "2018-11-29T03:26:49.575Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "docstring-parser"
|
||||
version = "0.18.0"
|
||||
@@ -1073,18 +1046,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "docutils-stubs"
|
||||
version = "0.0.22"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "docutils" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/31/fb/3eda037eed8b98d6b2169e4198a8f12a03a461c4d4dc44de1a7790d0f7c7/docutils-stubs-0.0.22.tar.gz", hash = "sha256:1736d9650cfc20cff8c72582806c33a5c642694e2df9e430717e7da7e73efbdf", size = 43699, upload-time = "2022-01-02T11:13:17.499Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/59/85/10507e1011d5370b94567e4b57f93086a2476d1da73caf98dc53aa87004b/docutils_stubs-0.0.22-py3-none-any.whl", hash = "sha256:157807309de24e8c96af9a13afe207410f1fc6e5aab5d974fd6b9191f04de327", size = 87506, upload-time = "2022-01-02T11:13:15.94Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "einops"
|
||||
version = "0.8.2"
|
||||
@@ -2164,12 +2125,9 @@ version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "asyncpg" },
|
||||
{ name = "docker-py" },
|
||||
{ name = "docutils-stubs" },
|
||||
{ name = "httpx" },
|
||||
{ name = "jinja2" },
|
||||
{ name = "loguru" },
|
||||
{ name = "passlib", extra = ["argon2"] },
|
||||
{ name = "pretor-viceroy" },
|
||||
{ name = "pwdlib", extra = ["argon2", "bcrypt"] },
|
||||
{ name = "pydantic-ai" },
|
||||
@@ -2179,7 +2137,6 @@ dependencies = [
|
||||
{ name = "ray", extra = ["default", "serve"] },
|
||||
{ name = "rich" },
|
||||
{ name = "sqlalchemy" },
|
||||
{ name = "types-docutils" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
@@ -2196,12 +2153,9 @@ dev = [
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "asyncpg", specifier = ">=0.31.0" },
|
||||
{ name = "docker-py", specifier = ">=1.10.6" },
|
||||
{ name = "docutils-stubs", specifier = "==0.0.22" },
|
||||
{ name = "httpx", specifier = ">=0.28.1" },
|
||||
{ name = "jinja2", specifier = ">=3.1.6" },
|
||||
{ name = "loguru", specifier = ">=0.7.3" },
|
||||
{ name = "passlib", extras = ["argon2"], specifier = ">=1.7.4" },
|
||||
{ name = "pretor-viceroy", specifier = ">=0.2.0" },
|
||||
{ name = "pwdlib", extras = ["argon2", "bcrypt"], specifier = ">=0.3.0" },
|
||||
{ name = "pydantic-ai", specifier = ">=1.73.0" },
|
||||
@@ -2211,7 +2165,6 @@ requires-dist = [
|
||||
{ name = "ray", extras = ["default", "serve"], specifier = ">=2.54.0" },
|
||||
{ name = "rich", specifier = ">=14.3.3" },
|
||||
{ name = "sqlalchemy", specifier = ">=2.0.49" },
|
||||
{ name = "types-docutils", specifier = "==0.22.3.20260408" },
|
||||
{ name = "vllm", marker = "extra == 'gpu'", specifier = ">=0.11.0" },
|
||||
]
|
||||
provides-extras = ["gpu"]
|
||||
@@ -3498,20 +3451,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/42/32/658973117bf0fd82a24abbfb94fe73a5e86216e49342985e10acce54775a/partial_json_parser-0.2.1.1.post7-py3-none-any.whl", hash = "sha256:145119e5eabcf80cbb13844a6b50a85c68bf99d376f8ed771e2a3c3b03e653ae", size = 10877, upload-time = "2025-11-17T07:27:40.457Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "passlib"
|
||||
version = "1.7.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b6/06/9da9ee59a67fae7761aab3ccc84fa4f3f33f125b370f1ccdb915bf967c11/passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04", size = 689844, upload-time = "2020-10-08T19:00:52.121Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/a4/ab6b7589382ca3df236e03faa71deac88cae040af60c071a78d254a62172/passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1", size = 525554, upload-time = "2020-10-08T19:00:49.856Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
argon2 = [
|
||||
{ name = "argon2-cffi" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pathable"
|
||||
version = "0.5.0"
|
||||
@@ -5568,15 +5507,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl", hash = "sha256:75caa44ed46a03fb2dab8808753ffacdbfea88495e74c85a28c5eefcf5f39c89", size = 58409, upload-time = "2026-04-30T19:32:18.271Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "types-docutils"
|
||||
version = "0.22.3.20260408"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3c/49/48a386fe15539556de085b87a69568b028cca2fa4b92596a3d4f79ac6784/types_docutils-0.22.3.20260408.tar.gz", hash = "sha256:22d5d45e4e0d65a1bc8280987a73e28669bb1cc9d16b18d0afc91713d1be26da", size = 57383, upload-time = "2026-04-08T04:27:26.924Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/08/47/1667fda6e9fcb044f8fb797f6dc4367b88dc2ab40f1a035e387f5405e870/types_docutils-0.22.3.20260408-py3-none-any.whl", hash = "sha256:2545a86966022cdf1468d430b0007eba0837be77974a7f3fafa1b04a6815d531", size = 91981, upload-time = "2026-04-08T04:27:25.934Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "types-protobuf"
|
||||
version = "6.32.1.20260221"
|
||||
@@ -5868,15 +5798,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/41/52/e465037f5375f43533d1a80b6923955201596a99142ed524d77b571a1418/wcwidth-0.7.0-py3-none-any.whl", hash = "sha256:5d69154c429a82910e241c738cd0e2976fac8a2dd47a1a805f4afed1c0f136f2", size = 110825, upload-time = "2026-05-02T16:04:11.033Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "websocket-client"
|
||||
version = "1.9.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c3b39865b12251e8e7d239f4cd0e3cc1b6c2ccde25c1/websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", size = 70576, upload-time = "2025-10-07T21:16:36.495Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "websockets"
|
||||
version = "16.0"
|
||||
|
||||
Reference in New Issue
Block a user