From 99520c69d7762dc8fbe1151def42f007970346d6 Mon Sep 17 00:00:00 2001 From: zhaoxi Date: Sun, 31 May 2026 15:39:34 +0000 Subject: [PATCH] =?UTF-8?q?feat(system):=E4=BC=98=E5=8C=96=E5=90=8E?= =?UTF-8?q?=E7=AB=AF=201.=E6=96=B0=E5=A2=9E=E5=90=8E=E7=AB=AF=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=202.=E5=A2=9E=E5=8A=A0=E4=BA=86=E5=90=8E=E7=AB=AF?= =?UTF-8?q?=E7=9A=84=E5=8A=A0=E5=AF=86=203.=E5=A2=9E=E5=8A=A0=E4=BA=86i18n?= =?UTF-8?q?=EF=BC=88=E5=9B=BD=E9=99=85=E5=8C=96=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.template | 15 +- .gitignore | 6 +- Makefile | 21 + alembic.ini | 58 ++ alembic/env.py | 97 +++ alembic/script.py.mako | 26 + ...5_31_0000-0001_initial_initial_baseline.py | 29 + alembic/versions/README.md | 20 + changelogs/ROADMAP.md | 1 - docker-compose.yml | 12 +- frontend/src/App.tsx | 9 + frontend/src/api/client.ts | 5 +- .../components/Agent/ProvidersSettings.tsx | 50 +- .../Agent/WorkerIndividualSettings.tsx | 84 +-- frontend/src/components/Auth/AuthPage.tsx | 8 +- frontend/src/components/Chat/ChatPanel.tsx | 22 +- frontend/src/components/Chat/LeftPanel.tsx | 2 +- .../src/components/Chat/NewWorkflowDialog.tsx | 8 +- .../src/components/Chat/WorkflowListView.tsx | 4 +- frontend/src/components/Layout/TopBar.tsx | 6 +- .../src/components/Plugin/SkillSettings.tsx | 42 +- .../src/components/Plugin/ToolSettings.tsx | 22 +- .../components/Settings/SystemSettings.tsx | 11 +- .../src/components/Settings/UsersSettings.tsx | 12 +- frontend/src/i18n/locales/en.json | 116 +++- frontend/src/i18n/locales/zh.json | 116 +++- frontend/src/store/useAppStore.ts | 12 + .../adapter/model_adapter/agent_factory.py | 25 +- kilostar/api/__init__.py | 131 ++-- kilostar/api/agent.py | 17 +- kilostar/api/chat.py | 41 +- kilostar/api/health.py | 54 ++ kilostar/api/platform/__init__.py | 3 +- kilostar/api/platform/frontend.py | 16 +- kilostar/api/platform/onebot.py | 279 ++++++++ kilostar/api/provider.py | 20 +- kilostar/api/resource.py | 279 +++++++- kilostar/api/workflow.py | 86 +++ .../global_state_machine.py | 275 ++++++-- .../core/global_state_machine/gsm_snapshot.py | 184 ++++++ .../core/global_state_machine/tool_manager.py | 206 +++++- .../consciousness_node/consciousness_node.py | 48 +- .../individual/control_node/control_node.py | 24 +- .../regulatory_node/regulatory_node.py | 43 +- .../individual/regulatory_node/template.py | 4 +- .../core/postgres_database/model/__init__.py | 8 + .../postgres_database/model/custom_toolset.py | 32 + .../postgres_database/model/mcp_server.py | 29 + .../postgres_database/model/tool_config.py | 22 + .../core/postgres_database/model/workflow.py | 25 + .../module/custom_toolset.py | 81 +++ .../postgres_database/module/mcp_server.py | 79 +++ .../core/postgres_database/module/provider.py | 42 +- .../postgres_database/module/tool_config.py | 58 ++ .../core/postgres_database/module/workflow.py | 56 ++ kilostar/core/postgres_database/postgres.py | 112 ++++ .../core/work/workflow/graph_persistence.py | 191 ++++++ kilostar/core/work/workflow/workflow.py | 14 +- .../core/work/workflow/workflow_engine.py | 601 ++++++++++++++---- .../plugin/tool_plugin/approval/approval.py | 2 +- kilostar/plugin/tool_plugin/base_tool.py | 3 +- .../tool_plugin/file_reader/__init__.py | 4 +- .../tool_plugin/file_reader/file_reader.py | 47 +- .../tool_plugin/tavily_search/__init__.py | 122 ++++ kilostar/utils/check_user/role_check.py | 12 +- kilostar/utils/crypto.py | 87 +++ kilostar/utils/error.py | 119 +++- kilostar/utils/get_tool.py | 6 +- kilostar/utils/i18n.py | 183 ++++++ kilostar/utils/logger.py | 81 ++- kilostar/utils/mcp_helper.py | 180 ++++++ kilostar/utils/request_context.py | 130 ++++ kilostar/worker_cluster/worker_cluster.py | 6 +- kilostar/worker_individual/base_individual.py | 18 +- main.py | 13 +- pyproject.toml | 12 + .../model_adapter/agent_factory_test.py | 61 -- tests/conftest.py | 87 +++ .../core/database/database_exception_test.py | 86 --- tests/core/database/module/user_test.py | 190 ------ .../database/table/table_provider_test.py | 15 - tests/core/database/table/table_user_test.py | 7 - .../global_state_machine_test.py | 128 ---- .../model_provider/base_provider_test.py | 32 - .../model_provider/claude_provider_test.py | 60 -- .../model_provider/openai_provider_test.py | 131 ---- .../provider_manager_test.py | 30 - .../global_state_machine/tool_manager_test.py | 6 - tests/core/postgres_database/postgres_test.py | 87 --- tests/unit/test_agent_factory.py | 185 ++++++ tests/unit/test_api_app.py | 77 +++ tests/unit/test_api_chat.py | 75 +++ tests/unit/test_api_custom_toolset_auth.py | 142 +++++ tests/unit/test_api_health.py | 81 +++ tests/unit/test_api_onebot.py | 168 +++++ tests/unit/test_api_resource.py | 50 ++ tests/unit/test_crypto.py | 117 ++++ tests/unit/test_database_exception.py | 72 +++ tests/unit/test_gsm_registries.py | 194 ++++++ tests/unit/test_gsm_snapshot.py | 359 +++++++++++ tests/unit/test_gsm_tool_manager.py | 93 +++ tests/unit/test_individual_nodes.py | 210 ++++++ tests/unit/test_plugin_metadata.py | 41 ++ tests/unit/test_provider_manager.py | 110 ++++ tests/unit/test_regulatory_template.py | 66 ++ tests/unit/test_request_context.py | 105 +++ tests/unit/test_request_id_middleware.py | 107 ++++ tests/unit/test_utils_error.py | 88 +++ tests/unit/test_utils_get_tool.py | 44 ++ tests/unit/test_utils_mcp_helper.py | 244 +++++++ tests/unit/test_utils_ray_hook.py | 46 ++ tests/unit/test_utils_retry.py | 87 +++ tests/unit/test_workflow_engine.py | 149 +++++ tests/unit/test_workflow_graph.py | 290 +++++++++ tests/unit/test_workflow_persistence.py | 386 +++++++++++ tests/unit/test_workflow_validator.py | 88 +++ tests/utils/access_test.py | 101 --- uv.lock | 46 ++ 118 files changed, 8174 insertions(+), 1491 deletions(-) create mode 100644 alembic.ini create mode 100644 alembic/env.py create mode 100644 alembic/script.py.mako create mode 100644 alembic/versions/2026_05_31_0000-0001_initial_initial_baseline.py create mode 100644 alembic/versions/README.md create mode 100644 kilostar/api/health.py create mode 100644 kilostar/api/platform/onebot.py create mode 100644 kilostar/core/global_state_machine/gsm_snapshot.py create mode 100644 kilostar/core/postgres_database/model/custom_toolset.py create mode 100644 kilostar/core/postgres_database/model/mcp_server.py create mode 100644 kilostar/core/postgres_database/model/tool_config.py create mode 100644 kilostar/core/postgres_database/module/custom_toolset.py create mode 100644 kilostar/core/postgres_database/module/mcp_server.py create mode 100644 kilostar/core/postgres_database/module/tool_config.py create mode 100644 kilostar/core/work/workflow/graph_persistence.py create mode 100644 kilostar/plugin/tool_plugin/tavily_search/__init__.py create mode 100644 kilostar/utils/crypto.py create mode 100644 kilostar/utils/i18n.py create mode 100644 kilostar/utils/mcp_helper.py create mode 100644 kilostar/utils/request_context.py delete mode 100644 tests/adapter/model_adapter/agent_factory_test.py create mode 100644 tests/conftest.py delete mode 100644 tests/core/database/database_exception_test.py delete mode 100644 tests/core/database/module/user_test.py delete mode 100644 tests/core/database/table/table_provider_test.py delete mode 100644 tests/core/database/table/table_user_test.py delete mode 100644 tests/core/global_state_machine/global_state_machine_test.py delete mode 100644 tests/core/global_state_machine/model_provider/base_provider_test.py delete mode 100644 tests/core/global_state_machine/model_provider/claude_provider_test.py delete mode 100644 tests/core/global_state_machine/model_provider/openai_provider_test.py delete mode 100644 tests/core/global_state_machine/provider_manager_test.py delete mode 100644 tests/core/global_state_machine/tool_manager_test.py delete mode 100644 tests/core/postgres_database/postgres_test.py create mode 100644 tests/unit/test_agent_factory.py create mode 100644 tests/unit/test_api_app.py create mode 100644 tests/unit/test_api_chat.py create mode 100644 tests/unit/test_api_custom_toolset_auth.py create mode 100644 tests/unit/test_api_health.py create mode 100644 tests/unit/test_api_onebot.py create mode 100644 tests/unit/test_api_resource.py create mode 100644 tests/unit/test_crypto.py create mode 100644 tests/unit/test_database_exception.py create mode 100644 tests/unit/test_gsm_registries.py create mode 100644 tests/unit/test_gsm_snapshot.py create mode 100644 tests/unit/test_gsm_tool_manager.py create mode 100644 tests/unit/test_individual_nodes.py create mode 100644 tests/unit/test_plugin_metadata.py create mode 100644 tests/unit/test_provider_manager.py create mode 100644 tests/unit/test_regulatory_template.py create mode 100644 tests/unit/test_request_context.py create mode 100644 tests/unit/test_request_id_middleware.py create mode 100644 tests/unit/test_utils_error.py create mode 100644 tests/unit/test_utils_get_tool.py create mode 100644 tests/unit/test_utils_mcp_helper.py create mode 100644 tests/unit/test_utils_ray_hook.py create mode 100644 tests/unit/test_utils_retry.py create mode 100644 tests/unit/test_workflow_engine.py create mode 100644 tests/unit/test_workflow_graph.py create mode 100644 tests/unit/test_workflow_persistence.py create mode 100644 tests/unit/test_workflow_validator.py delete mode 100644 tests/utils/access_test.py diff --git a/.env.template b/.env.template index bb5c737..2c99b59 100644 --- a/.env.template +++ b/.env.template @@ -3,7 +3,20 @@ POSTGRES_PASSWORD=postgrespassword POSTGRES_HOST=127.0.0.1 POSTGRES_PORT=5432 POSTGRES_DB=kilostar -# 必须填写一个高熵随机字符串,建议生成命令: +# JWT 签名密钥,必须填写一个高熵随机字符串,建议生成命令: # python -c "import secrets; print(secrets.token_urlsafe(32))" # 留空或填 "secret" / "114514" / "changethiskey12345" 等弱值会被拒绝。 SECRET_KEY= + +# 数据加密密钥(Fernet),用于加密 provider apikey / tool config 中的敏感字段。 +# 与 SECRET_KEY 独立,生成命令: +# python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" +KILOSTAR_SECRET_KEY= + +# CORS 允许的源(逗号分隔,默认 "*" 允许所有源但禁用 credentials)。 +# 生产建议显式列出:KILOSTAR_CORS_ORIGINS=https://app.example.com,https://admin.example.com +KILOSTAR_CORS_ORIGINS=* + +# 日志格式:开发默认彩色 console;生产可设为 json 以便 ELK/Loki 采集。 +# KILOSTAR_LOG_FORMAT=json +# KILOSTAR_LOG_LEVEL=INFO diff --git a/.gitignore b/.gitignore index 225702f..3237735 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,8 @@ wheels/ # Virtual environments .venv -.idea \ No newline at end of file +.idea +# Local runtime data (MCP registry, etc.) +data/ +tmp/ +.env diff --git a/Makefile b/Makefile index 59a74f1..8d2623f 100644 --- a/Makefile +++ b/Makefile @@ -1,2 +1,23 @@ run: uv run main.py + +clean-cache: + find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true + find . -type f -name "*.pyc" -delete 2>/dev/null || true + +# Alembic 数据库迁移:m="message" 控制 revision 描述 +db-revision: + uv run alembic revision --autogenerate -m "$(m)" + +db-upgrade: + uv run alembic upgrade head + +db-downgrade: + uv run alembic downgrade -1 + +db-history: + uv run alembic history --verbose + +db-stamp-head: + uv run alembic stamp head + diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 0000000..0929523 --- /dev/null +++ b/alembic.ini @@ -0,0 +1,58 @@ +# A generic, single database configuration. + +[alembic] +# 迁移脚本目录 +script_location = alembic + +# 默认时间戳模板 +file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s + +# 时区 +timezone = Asia/Shanghai + +# Path to alembic version_path(多环境可以分目录) +version_locations = alembic/versions + +# 数据库 URL:留空,由 env.py 从环境变量动态读取 +sqlalchemy.url = + +[post_write_hooks] +# 默认不做格式化;如需可启用 black +# hooks = black +# black.type = console_scripts +# black.entrypoint = black +# black.options = -l 88 REVISION_SCRIPT_FILENAME + +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARNING +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARNING +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/alembic/env.py b/alembic/env.py new file mode 100644 index 0000000..a1d6d46 --- /dev/null +++ b/alembic/env.py @@ -0,0 +1,97 @@ +"""Alembic 迁移环境。 + +设计要点: +1. 数据库 URL 从 ``POSTGRES_*`` 环境变量动态拼装,不污染 ``alembic.ini``; +2. 复用项目本身的 ORM metadata:``BaseDataModel.metadata`` 让 autogenerate + 能识别全部表; +3. 与运行期保持一致使用 ``asyncpg`` 异步驱动 —— 通过 ``async_engine_from_config`` + 建立 AsyncEngine,再借 ``run_sync`` 把 alembic 的 sync migration 执行入口 + 挂载进去。这样无需额外引入 psycopg 等同步驱动。 +""" + +from __future__ import annotations + +import asyncio +import os +import sys +from logging.config import fileConfig +from pathlib import Path + +from alembic import context +from sqlalchemy import pool +from sqlalchemy.engine import Connection +from sqlalchemy.ext.asyncio import async_engine_from_config + +PROJECT_ROOT = Path(__file__).resolve().parent.parent +if str(PROJECT_ROOT) not in sys.path: + sys.path.insert(0, str(PROJECT_ROOT)) + +from kilostar.core.postgres_database.model.base import BaseDataModel # noqa: E402 +from kilostar.core.postgres_database import model as _model_pkg # noqa: F401,E402 + +config = context.config + +if config.config_file_name is not None: + fileConfig(config.config_file_name) + + +def _build_db_url() -> str: + """从 ``POSTGRES_*`` 环境变量拼装一个 asyncpg URL。""" + user = os.environ.get("POSTGRES_USER", "postgres") + password = os.environ.get("POSTGRES_PASSWORD", "postgrespassword") + host = os.environ.get("POSTGRES_HOST", "127.0.0.1") + port = os.environ.get("POSTGRES_PORT", "5432") + db = os.environ.get("POSTGRES_DB", "kilostar") + return f"postgresql+asyncpg://{user}:{password}@{host}:{port}/{db}" + + +config.set_main_option("sqlalchemy.url", _build_db_url()) + +target_metadata = BaseDataModel.metadata + + +def run_migrations_offline() -> None: + """离线模式:不连库,直接把 SQL 渲染到 stdout。""" + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + compare_type=True, + compare_server_default=True, + ) + with context.begin_transaction(): + context.run_migrations() + + +def _do_run_migrations(connection: Connection) -> None: + context.configure( + connection=connection, + target_metadata=target_metadata, + compare_type=True, + compare_server_default=True, + ) + with context.begin_transaction(): + context.run_migrations() + + +async def _run_async_migrations() -> None: + connectable = async_engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + async with connectable.connect() as connection: + await connection.run_sync(_do_run_migrations) + await connectable.dispose() + + +def run_migrations_online() -> None: + asyncio.run(_run_async_migrations()) + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/alembic/script.py.mako b/alembic/script.py.mako new file mode 100644 index 0000000..fbc4b07 --- /dev/null +++ b/alembic/script.py.mako @@ -0,0 +1,26 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/alembic/versions/2026_05_31_0000-0001_initial_initial_baseline.py b/alembic/versions/2026_05_31_0000-0001_initial_initial_baseline.py new file mode 100644 index 0000000..4977d53 --- /dev/null +++ b/alembic/versions/2026_05_31_0000-0001_initial_initial_baseline.py @@ -0,0 +1,29 @@ +"""initial baseline + +Revision ID: 0001_initial +Revises: +Create Date: 2026-05-31 00:00:00 + +这是 Alembic 接入时的占位 baseline。 + +- 全新部署:``alembic upgrade head`` 会跑过这条 no-op, + 然后由后续 ``alembic revision --autogenerate`` 生成真正的建表脚本, + 或在首次部署时由应用 ``Base.metadata.create_all`` 直接建表,再 ``alembic stamp head``。 +- 已有数据库:直接 ``alembic stamp 0001_initial`` 标定基线,再做后续 autogenerate。 +""" + +from typing import Sequence, Union + + +revision: str = "0001_initial" +down_revision: Union[str, None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + pass + + +def downgrade() -> None: + pass diff --git a/alembic/versions/README.md b/alembic/versions/README.md new file mode 100644 index 0000000..b11d17c --- /dev/null +++ b/alembic/versions/README.md @@ -0,0 +1,20 @@ +# Alembic versions + +迁移脚本会被自动生成到这个目录。 + +常用命令(在项目根目录运行): + +- 生成 baseline(首次接入,已有数据库): + `alembic stamp head` + +- 自动检测 ORM 与 DB 差异并生成迁移: + `alembic revision --autogenerate -m "your message"` + +- 应用所有未执行的迁移: + `alembic upgrade head` + +- 回滚一个版本: + `alembic downgrade -1` + +- 查看历史: + `alembic history --verbose` diff --git a/changelogs/ROADMAP.md b/changelogs/ROADMAP.md index b348ed4..d34a194 100644 --- a/changelogs/ROADMAP.md +++ b/changelogs/ROADMAP.md @@ -1,4 +1,3 @@ -# Roadmap --- ## [v0.1.0Alpha] - 2026/4/28 diff --git a/docker-compose.yml b/docker-compose.yml index 4f47305..21486c3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -24,9 +24,9 @@ services: db: condition: service_healthy environment: - - POSTGRES_USER=postgres - - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-postgrespassword} - - POSTGRES_HOST=db - - POSTGRES_PORT=5432 - - POSTGRES_DB=kilostar - - SECRET_KEY=${SECRET_KEY:?SECRET_KEY must be set; generate one via: python -c \"import secrets;print(secrets.token_urlsafe(32))\"} + POSTGRES_USER: postgres + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgrespassword} + POSTGRES_HOST: db + POSTGRES_PORT: 5432 + POSTGRES_DB: kilostar + SECRET_KEY: ${SECRET_KEY} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 5228b15..b8a51be 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,4 +1,5 @@ import { useEffect } from 'react'; +import i18n from './i18n'; import { TopBar } from './components/Layout/TopBar'; import { CollapsibleSidebar } from './components/Layout/CollapsibleSidebar'; import { SettingsLayout } from './components/Settings/SettingsLayout'; @@ -22,6 +23,7 @@ function App() { workTab, agentTab, applyTheme, + locale, } = useAppStore(); const { loadSessions } = useChatStore(); @@ -35,6 +37,13 @@ function App() { return () => mediaQuery.removeEventListener('change', handler); }, [applyTheme]); + // Sync persisted locale to i18next on mount + useEffect(() => { + if (locale && i18n.language !== locale) { + i18n.changeLanguage(locale); + } + }, [locale]); + // Check auth and load sessions useEffect(() => { const token = localStorage.getItem('token'); diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index b82b652..432deff 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -10,12 +10,15 @@ export const apiClient = axios.create({ }, }); -// Interceptor to attach token to requests if we have one +// Interceptor to attach token and locale to requests apiClient.interceptors.request.use((config) => { const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } + // 把用户语言偏好透传给后端,让 Agent prompt 和错误消息都能本地化 + const lang = localStorage.getItem('i18nextLng') || navigator.language || 'zh'; + config.headers['Accept-Language'] = lang; return config; }); diff --git a/frontend/src/components/Agent/ProvidersSettings.tsx b/frontend/src/components/Agent/ProvidersSettings.tsx index 3d5e8df..e548733 100644 --- a/frontend/src/components/Agent/ProvidersSettings.tsx +++ b/frontend/src/components/Agent/ProvidersSettings.tsx @@ -1,9 +1,11 @@ import { useState, useEffect } from 'react'; -import { Box, Plus, X, Server } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; +import { Box, Plus, X, Server, Loader2, Boxes } from 'lucide-react'; import type { Provider } from '../../types'; import apiClient from '../../api/client'; export function ProvidersSettings() { + const { t } = useTranslation(); const [providers, setProviders] = useState([]); const [loading, setLoading] = useState(true); const [isModalOpen, setIsModalOpen] = useState(false); @@ -29,7 +31,7 @@ export function ProvidersSettings() { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!formData.provider_title || !formData.provider_url || !formData.provider_apikey) { - setError('Please fill in all fields.'); + setError(t('agent.providerFillAll')); return; } setSubmitLoading(true); @@ -39,7 +41,7 @@ export function ProvidersSettings() { await fetchProviders(); setIsModalOpen(false); } catch (err) { - setError('Failed to add provider. Please check your inputs and try again.'); + setError(t('agent.providerAddFailed')); } finally { setSubmitLoading(false); } @@ -49,20 +51,24 @@ export function ProvidersSettings() {
-

Provider Management

-

Configure external AI model providers

+

{t('agent.providerManagement')}

+

{t('agent.providerDesc')}

{loading ? ( -
Loading providers...
+
+ + {t('common.loading')} +
) : providers.length === 0 ? ( -
- No providers configured yet +
+ + {t('agent.noProviders')}
) : (
@@ -80,19 +86,19 @@ export function ProvidersSettings() {
{provider.status === 'Connected' && } - {provider.status || 'Unknown'} + {provider.status || t('common.unknown')}
-

Endpoint

-

{provider.provider_url || 'Default'}

+

{t('agent.endpoint')}

+

{provider.provider_url || t('common.default')}

- + + if (!confirm(t('agent.deleteProviderConfirm'))) return; + try { await apiClient.delete(`/api/v1/provider/${provider.provider_title}`); fetchProviders(); } catch { alert(t('common.deleteFailed')); } + }} 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">{t('common.delete')}
))} @@ -106,14 +112,14 @@ export function ProvidersSettings() {
-

Add New Provider

+

{t('agent.addNewProvider')}

{error &&
{error}
}
- + setFormData({...formData, [field]: e.target.value})} - placeholder={field === 'provider_title' ? 'My OpenAI' : field === 'provider_url' ? 'https://api.openai.com/v1' : 'sk-...'} + placeholder={field === 'provider_title' ? t('agent.providerTitlePlaceholder') : field === 'provider_url' ? t('agent.baseUrlPlaceholder') : t('agent.apiKeyPlaceholder')} 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" />
))}
- - + +
diff --git a/frontend/src/components/Agent/WorkerIndividualSettings.tsx b/frontend/src/components/Agent/WorkerIndividualSettings.tsx index f670bb7..0efad13 100644 --- a/frontend/src/components/Agent/WorkerIndividualSettings.tsx +++ b/frontend/src/components/Agent/WorkerIndividualSettings.tsx @@ -1,6 +1,7 @@ import { useState, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; import apiClient from '../../api/client'; -import { Save, Plus, Edit2, Trash2, X, Bot } from 'lucide-react'; +import { Save, Plus, Edit2, Trash2, X, Bot, Loader2, Users } from 'lucide-react'; import type { Provider } from '../../types'; interface WorkerIndividual { @@ -18,6 +19,7 @@ interface WorkerIndividual { } export function WorkerIndividualSettings() { + const { t } = useTranslation(); const [providers, setProviders] = useState([]); const [workers, setWorkers] = useState([]); const [systemNodes, setSystemNodes] = useState([]); @@ -62,7 +64,7 @@ export function WorkerIndividualSettings() { }; })); } catch (err: any) { - setError('Failed to load data'); + setError(t('agent.loadFailed')); } finally { setLoading(false); } @@ -93,8 +95,8 @@ export function WorkerIndividualSettings() { }; const handleDelete = async (agent_id: string) => { - if (!confirm('Delete this agent?')) return; - try { await apiClient.delete(`/api/v1/agent/worker/${agent_id}`); fetchData(); } catch { alert('Failed'); } + if (!confirm(t('agent.deleteWorkerConfirm'))) return; + try { await apiClient.delete(`/api/v1/agent/worker/${agent_id}`); fetchData(); } catch { alert(t('common.deleteFailed')); } }; const handleModalSave = async (e: React.FormEvent) => { @@ -123,31 +125,31 @@ export function WorkerIndividualSettings() { setIsEditing(false); fetchData(); } catch (err: any) { - setModalMessage(err.response?.data?.detail || err.message || 'Failed to save'); + setModalMessage(err.response?.data?.detail || err.message || t('common.saveFailed')); } finally { setSubmitLoading(false); } }; const getTypeBadge = (type: string, isSystem?: boolean) => { - if (isSystem) return System; + if (isSystem) return {t('agent.system')}; const colors: Record = { ordinary_individual: 'bg-bg-secondary text-text-muted', skill_individual: 'bg-success-bg text-success', special_individual: 'bg-warning-bg text-warning', }; - return {type.replace('_', ' ')}; + return {t(`agent.type.${type}`, type.replace('_', ' '))}; }; return (
-

Individual

-

Manage system nodes and custom workers

+

{t('agent.individual')}

+

{t('agent.individualDesc')}

@@ -155,17 +157,23 @@ export function WorkerIndividualSettings() {
{loading ? ( -
Loading...
+
+ + {t('common.loading')} +
) : (workers.length === 0 && systemNodes.length === 0) ? ( -
No individuals found.
+
+ + {t('agent.noIndividuals')} +
) : ( - - - - + + + + @@ -212,47 +220,47 @@ export function WorkerIndividualSettings() { {/* Modal */} {isEditing && (
-
+
-

{(editData as any).is_system ? 'Edit System Node' : (isNew ? 'Create Worker' : 'Edit Worker')}

+

{(editData as any).is_system ? t('agent.editSystemNode') : (isNew ? t('agent.createWorker') : t('agent.editWorker'))}

- + 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} />
- +
- +
- + {(() => { const sp = providers.find(p => p.provider_title === editData.provider_title); const models = sp?.provider_models || []; return ( ); @@ -262,40 +270,40 @@ export function WorkerIndividualSettings() { {!(editData as any).is_system && ( <>
- +
NameTypeProvider / ModelActions{t('agent.name')}{t('agent.type')}{t('agent.providerModel')}{t('common.actions')}