From 00e5587e7561ba557b8394a13e14452c72041f36 Mon Sep 17 00:00:00 2001 From: qixinbo Date: Sat, 28 Mar 2026 08:58:02 +0800 Subject: [PATCH] chore: nothing special, subagent enhanced --- backend/app/core/nanobot.py | 11 +- backend/app/core/session_alias_store.py | 16 +++ backend/app/tools/subagent.py | 27 +++- backend/main.py | 14 ++ backend/tests/test_chat_project_id.py | 100 +++++++++++++ .../tests/test_nanobot_project_resolution.py | 110 ++++++++++++++ backend/tests/test_subagent_detail_route.py | 102 +++++++++++++ .../test_subagent_tools_e2e_regression.py | 136 ++++++++++++++++++ frontend/src/api/subagents.ts | 4 +- frontend/src/components/ChatInterface.tsx | 1 + frontend/src/i18n/locales/zh.json | 18 +-- 11 files changed, 517 insertions(+), 22 deletions(-) create mode 100644 backend/tests/test_chat_project_id.py create mode 100644 backend/tests/test_nanobot_project_resolution.py create mode 100644 backend/tests/test_subagent_detail_route.py create mode 100644 backend/tests/test_subagent_tools_e2e_regression.py diff --git a/backend/app/core/nanobot.py b/backend/app/core/nanobot.py index 58e8f0f..b784ba1 100644 --- a/backend/app/core/nanobot.py +++ b/backend/app/core/nanobot.py @@ -185,9 +185,8 @@ class NanobotIntegration: agent.tools.register(NL2SQLTool()) agent.tools.register(VisualizationTool()) agent.tools.register(GetDatabaseSchemaTool()) - if project_id is not None: - agent.tools.register(ListSubagentsTool(project_id=project_id)) - agent.tools.register(InvokeSubagentTool(project_id=project_id)) + agent.tools.register(ListSubagentsTool(project_id=project_id)) + agent.tools.register(InvokeSubagentTool(project_id=project_id)) def _build_provider( self, @@ -357,9 +356,9 @@ class NanobotIntegration: if project_id is None: from app.core.session_alias_store import session_alias_store - alias_info = session_alias_store.get_alias(session_id) - if alias_info and alias_info.get("project_id"): - project_id = alias_info.get("project_id") + alias_meta = session_alias_store.get_alias_meta(session_id) + if alias_meta and alias_meta.get("project_id") is not None: + project_id = alias_meta.get("project_id") agent_to_use = self.agent need_custom_agent = False diff --git a/backend/app/core/session_alias_store.py b/backend/app/core/session_alias_store.py index 42c5ef6..2419b9e 100644 --- a/backend/app/core/session_alias_store.py +++ b/backend/app/core/session_alias_store.py @@ -173,6 +173,22 @@ class SessionAliasStore: alias = row["alias"] return str(alias) if alias else None + def get_alias_meta(self, session_key: str) -> dict[str, Any] | None: + with self._connect() as conn: + row = conn.execute( + "SELECT alias, pinned, archived, project_id FROM session_cache WHERE session_key = ?", + (session_key,), + ).fetchone() + if not row: + return None + alias = (row["alias"] or "").strip() + return { + "alias": alias or None, + "pinned": bool(row["pinned"]) if "pinned" in row.keys() else False, + "archived": bool(row["archived"]) if "archived" in row.keys() else False, + "project_id": row["project_id"] if "project_id" in row.keys() else None, + } + def delete_session(self, session_key: str) -> None: with self._connect() as conn: conn.execute("DELETE FROM session_cache WHERE session_key = ?", (session_key,)) diff --git a/backend/app/tools/subagent.py b/backend/app/tools/subagent.py index 45ad22d..87fb938 100644 --- a/backend/app/tools/subagent.py +++ b/backend/app/tools/subagent.py @@ -5,8 +5,23 @@ from nanobot.agent.tools.base import Tool from app.database import SessionLocal from app.models.subagent import Subagent from app.core.nanobot import nanobot_service +from app.core.session_alias_store import session_alias_store from app.services.llm_cache import get_llm_configs, get_active_llm_config + +def _resolve_project_id(preferred_project_id: Optional[int]) -> Optional[int]: + if preferred_project_id is not None: + return preferred_project_id + from app.context import current_session_id + session_id = (current_session_id.get() or "").strip() + if not session_id: + return None + alias_meta = session_alias_store.get_alias_meta(session_id) + if not alias_meta: + return None + project_id = alias_meta.get("project_id") + return project_id if isinstance(project_id, int) else None + class ListSubagentsTool(Tool): """ Tool to list available subagents for the current project. @@ -32,11 +47,12 @@ class ListSubagentsTool(Tool): } async def execute(self, **kwargs: Any) -> str: - if not self.project_id: + resolved_project_id = _resolve_project_id(self.project_id) + if resolved_project_id is None: return "Error: No project context available to list subagents." with SessionLocal() as db: - subagents = db.query(Subagent).filter(Subagent.project_id == self.project_id).all() + subagents = db.query(Subagent).filter(Subagent.project_id == resolved_project_id).all() if not subagents: return "No subagents found in the current project." @@ -91,8 +107,9 @@ class InvokeSubagentTool(Tool): async def execute(self, **kwargs: Any) -> str: subagent_name = kwargs.get("subagent_name") task = kwargs.get("task") + resolved_project_id = _resolve_project_id(self.project_id) - if not self.project_id: + if resolved_project_id is None: return "Error: No project context available to invoke subagent." if not subagent_name or not task: @@ -100,7 +117,7 @@ class InvokeSubagentTool(Tool): with SessionLocal() as db: subagent = db.query(Subagent).filter( - Subagent.project_id == self.project_id, + Subagent.project_id == resolved_project_id, Subagent.name == subagent_name ).first() @@ -141,7 +158,7 @@ class InvokeSubagentTool(Tool): response = await nanobot_service.process_message( message=message, session_id=subagent_session_id, - project_id=self.project_id, + project_id=resolved_project_id, model_id=resolved_model_id, ) return f"Subagent '{subagent.name}' completed the task.\nResult:\n{response}" diff --git a/backend/main.py b/backend/main.py index e7861ef..65dd2c2 100644 --- a/backend/main.py +++ b/backend/main.py @@ -214,6 +214,7 @@ def preview_web_artifact_resource(root_token: str, resource_path: str): class ChatRequest(BaseModel): message: str session_id: str = "api:default" + project_id: Optional[int] = None skill_ids: Optional[List[str]] = None model_id: Optional[str] = None source: str = "postgres" @@ -238,6 +239,15 @@ def _resolve_effective_source(request: ChatRequest) -> str: effective_source = session_source return effective_source + +def _sync_session_project(session_id: str, project_id: Optional[int]) -> None: + if project_id is None: + return + session_alias_store.update_alias_meta( + session_key=session_id, + project_id=project_id, + ) + class SessionAliasUpdateRequest(BaseModel): title: Optional[str] = None pinned: Optional[bool] = None @@ -277,6 +287,7 @@ def _persist_assistant_enrichment( @app.post("/nanobot/chat") async def nanobot_chat(request: ChatRequest): try: + _sync_session_project(request.session_id, request.project_id) resolved_source = _resolve_effective_source(request) current_data_source.set(resolved_source) current_file_url.set(request.file_url) @@ -300,6 +311,7 @@ async def nanobot_chat(request: ChatRequest): session_id=request.session_id, skill_ids=request.skill_ids, model_id=request.model_id, + project_id=request.project_id, ) text = response or "" session_messages = [] @@ -331,6 +343,7 @@ async def nanobot_chat_stream(request: ChatRequest): async def event_generator(): current_task = None try: + _sync_session_project(request.session_id, request.project_id) resolved_source = _resolve_effective_source(request) current_data_source.set(resolved_source) current_file_url.set(request.file_url) @@ -369,6 +382,7 @@ async def nanobot_chat_stream(request: ChatRequest): session_id=request.session_id, skill_ids=request.skill_ids, model_id=request.model_id, + project_id=request.project_id, on_progress=_on_progress, on_stream=_on_stream, ) diff --git a/backend/tests/test_chat_project_id.py b/backend/tests/test_chat_project_id.py new file mode 100644 index 0000000..26e470d --- /dev/null +++ b/backend/tests/test_chat_project_id.py @@ -0,0 +1,100 @@ +import asyncio +import sys +from pathlib import Path + +BACKEND_ROOT = Path(__file__).resolve().parents[1] +REPO_ROOT = BACKEND_ROOT.parent +NANOBOT_ROOT = REPO_ROOT / "nanobot" +if str(BACKEND_ROOT) not in sys.path: + sys.path.insert(0, str(BACKEND_ROOT)) +if str(NANOBOT_ROOT) not in sys.path: + sys.path.insert(0, str(NANOBOT_ROOT)) + +import main + + +def test_nanobot_chat_syncs_project_id(monkeypatch) -> None: + calls: list[dict[str, object]] = [] + process_kwargs: list[dict[str, object]] = [] + + def fake_update_alias_meta(**kwargs): + calls.append(kwargs) + return kwargs + + async def fake_process_message(*args, **kwargs): + process_kwargs.append(kwargs) + return "ok" + + monkeypatch.setattr(main.session_alias_store, "update_alias_meta", fake_update_alias_meta) + monkeypatch.setattr(main.nanobot_service, "process_message", fake_process_message) + monkeypatch.setattr(main.nanobot_service, "agent", None) + + request = main.ChatRequest(message="hello", session_id="api:test-1", project_id=101) + response = asyncio.run(main.nanobot_chat(request)) + + assert response["response"] == "ok" + assert calls == [{"session_key": "api:test-1", "project_id": 101}] + assert process_kwargs and process_kwargs[0]["project_id"] == 101 + + +def test_nanobot_chat_without_project_id_does_not_sync(monkeypatch) -> None: + calls: list[dict[str, object]] = [] + process_kwargs: list[dict[str, object]] = [] + + def fake_update_alias_meta(**kwargs): + calls.append(kwargs) + return kwargs + + async def fake_process_message(*args, **kwargs): + process_kwargs.append(kwargs) + return "ok" + + monkeypatch.setattr(main.session_alias_store, "update_alias_meta", fake_update_alias_meta) + monkeypatch.setattr(main.nanobot_service, "process_message", fake_process_message) + monkeypatch.setattr(main.nanobot_service, "agent", None) + + request = main.ChatRequest(message="hello", session_id="api:test-2") + response = asyncio.run(main.nanobot_chat(request)) + + assert response["response"] == "ok" + assert calls == [] + assert process_kwargs and process_kwargs[0]["project_id"] is None + + +def test_nanobot_chat_stream_syncs_project_id(monkeypatch) -> None: + calls: list[dict[str, object]] = [] + process_kwargs: list[dict[str, object]] = [] + + def fake_update_alias_meta(**kwargs): + calls.append(kwargs) + return kwargs + + async def fake_process_message(*args, **kwargs): + process_kwargs.append(kwargs) + on_stream = kwargs.get("on_stream") + if on_stream: + await on_stream("stream-token") + return "stream-complete" + + async def collect_stream_chunks(response) -> list[str]: + chunks: list[str] = [] + async for chunk in response.body_iterator: + if isinstance(chunk, bytes): + chunks.append(chunk.decode("utf-8")) + else: + chunks.append(chunk) + return chunks + + monkeypatch.setattr(main.session_alias_store, "update_alias_meta", fake_update_alias_meta) + monkeypatch.setattr(main.nanobot_service, "process_message", fake_process_message) + monkeypatch.setattr(main.nanobot_service, "agent", None) + + request = main.ChatRequest(message="hello", session_id="api:test-3", project_id=202) + response = asyncio.run(main.nanobot_chat_stream(request)) + chunks = asyncio.run(collect_stream_chunks(response)) + content = "".join(chunks) + + assert "stream-token" in content + assert "stream-complete" in content + assert calls == [{"session_key": "api:test-3", "project_id": 202}] + assert process_kwargs and process_kwargs[0]["project_id"] == 202 diff --git a/backend/tests/test_nanobot_project_resolution.py b/backend/tests/test_nanobot_project_resolution.py new file mode 100644 index 0000000..0fea7f7 --- /dev/null +++ b/backend/tests/test_nanobot_project_resolution.py @@ -0,0 +1,110 @@ +import asyncio +from types import SimpleNamespace + +from app.core.nanobot import NanobotIntegration +from app.context import current_session_id + + +class _DummySessions: + def __init__(self) -> None: + self.saved = [] + self._session = SimpleNamespace(messages=[]) + + def get_or_create(self, _session_id: str): + return self._session + + def save(self, session) -> None: + self.saved.append(session) + + +class _DummyAgent: + def __init__(self) -> None: + self.sessions = _DummySessions() + self.provider = SimpleNamespace(default_model="demo-model") + self.model = "demo-model" + + async def process_direct(self, *_args, **_kwargs): + return "ok" + + +def test_process_message_project_id_fallback_from_session_alias(monkeypatch) -> None: + service = NanobotIntegration() + base_agent = _DummyAgent() + custom_agent = _DummyAgent() + service.agent = base_agent + service._started = True + + captured: dict[str, object] = {} + + async def fake_get_or_create_model_agent(model_id, target_config, project_id): + captured["project_id"] = project_id + return custom_agent + + monkeypatch.setattr(service, "_get_or_create_model_agent", fake_get_or_create_model_agent) + monkeypatch.setattr("app.core.nanobot.get_llm_configs", lambda: []) + monkeypatch.setattr("app.core.nanobot.get_active_llm_config", lambda: None) + monkeypatch.setattr( + "app.core.session_alias_store.session_alias_store.get_alias_meta", + lambda _session_id: {"project_id": 77}, + ) + + response = asyncio.run(service.process_message("hello", session_id="api:s1")) + + assert response == "ok" + assert captured["project_id"] == 77 + + +def test_process_message_project_id_prefers_request_value(monkeypatch) -> None: + service = NanobotIntegration() + base_agent = _DummyAgent() + custom_agent = _DummyAgent() + service.agent = base_agent + service._started = True + + captured: dict[str, object] = {} + + async def fake_get_or_create_model_agent(model_id, target_config, project_id): + captured["project_id"] = project_id + return custom_agent + + monkeypatch.setattr(service, "_get_or_create_model_agent", fake_get_or_create_model_agent) + monkeypatch.setattr("app.core.nanobot.get_llm_configs", lambda: []) + monkeypatch.setattr("app.core.nanobot.get_active_llm_config", lambda: None) + monkeypatch.setattr( + "app.core.session_alias_store.session_alias_store.get_alias_meta", + lambda _session_id: {"project_id": 88}, + ) + + response = asyncio.run(service.process_message("hello", session_id="api:s2", project_id=9)) + + assert response == "ok" + assert captured["project_id"] == 9 + + +def test_register_custom_tools_always_contains_subagent_tools() -> None: + service = NanobotIntegration() + names: list[str] = [] + + class _ToolRegistry: + def register(self, tool) -> None: + names.append(tool.name) + + fake_agent = SimpleNamespace(tools=_ToolRegistry()) + service._register_custom_tools(fake_agent, project_id=None) + + assert "list_subagents" in names + assert "invoke_subagent" in names + + +def test_subagent_tool_resolves_project_from_session_alias(monkeypatch) -> None: + from app.tools.subagent import _resolve_project_id + + token = current_session_id.set("api:subagent-test") + try: + monkeypatch.setattr( + "app.tools.subagent.session_alias_store.get_alias_meta", + lambda _session_id: {"project_id": 66}, + ) + assert _resolve_project_id(None) == 66 + finally: + current_session_id.reset(token) diff --git a/backend/tests/test_subagent_detail_route.py b/backend/tests/test_subagent_detail_route.py new file mode 100644 index 0000000..ab6b71c --- /dev/null +++ b/backend/tests/test_subagent_detail_route.py @@ -0,0 +1,102 @@ +import sys +from collections.abc import Generator +from pathlib import Path + +BACKEND_ROOT = Path(__file__).resolve().parents[1] +REPO_ROOT = BACKEND_ROOT.parent +NANOBOT_ROOT = REPO_ROOT / "nanobot" +if str(BACKEND_ROOT) not in sys.path: + sys.path.insert(0, str(BACKEND_ROOT)) +if str(NANOBOT_ROOT) not in sys.path: + sys.path.insert(0, str(NANOBOT_ROOT)) + +from fastapi.testclient import TestClient +from sqlalchemy import create_engine +from sqlalchemy.orm import Session, sessionmaker +from sqlalchemy.pool import StaticPool + +from app.core.security import CurrentUser, get_current_user +from app.database import Base, get_db +from app.models.project import Project +from app.models.subagent import Subagent +from app.models.user import User +from main import app + + +def _seed_subagent(db: Session) -> tuple[User, Project, Subagent]: + user = User( + username="task3-owner", + email="task3-owner@example.com", + hashed_password="test", + is_admin=False, + ) + db.add(user) + db.commit() + db.refresh(user) + + project = Project( + name="task3-project", + description="task3", + owner_id=user.id, + ) + db.add(project) + db.commit() + db.refresh(project) + + subagent = Subagent( + project_id=project.id, + name="task3-subagent", + description="task3", + instructions="do task3", + model="gpt", + ) + db.add(subagent) + db.commit() + db.refresh(subagent) + return user, project, subagent + + +def test_subagent_detail_route_is_global_and_project_scoped_route_is_invalid() -> None: + engine = create_engine( + "sqlite://", + connect_args={"check_same_thread": False}, + poolclass=StaticPool, + ) + testing_session_local = sessionmaker(autocommit=False, autoflush=False, bind=engine) + Base.metadata.create_all(bind=engine) + + db = testing_session_local() + user, project, subagent = _seed_subagent(db) + user_id = user.id + username = user.username + project_id = project.id + subagent_id = subagent.id + db.close() + + def override_get_db() -> Generator[Session, None, None]: + override_db = testing_session_local() + try: + yield override_db + finally: + override_db.close() + + def override_current_user() -> CurrentUser: + return CurrentUser(id=user_id, username=username, is_admin=False) + + app.dependency_overrides[get_db] = override_get_db + app.dependency_overrides[get_current_user] = override_current_user + + try: + client = TestClient(app) + response = client.get(f"/api/v1/subagents/{subagent_id}") + assert response.status_code == 200 + body = response.json() + assert body["id"] == subagent_id + assert body["project_id"] == project_id + + legacy_path_response = client.get(f"/api/v1/projects/{project_id}/subagents/{subagent_id}") + assert legacy_path_response.status_code == 404 + finally: + app.dependency_overrides.clear() + Base.metadata.drop_all(bind=engine) + engine.dispose() diff --git a/backend/tests/test_subagent_tools_e2e_regression.py b/backend/tests/test_subagent_tools_e2e_regression.py new file mode 100644 index 0000000..ec060a5 --- /dev/null +++ b/backend/tests/test_subagent_tools_e2e_regression.py @@ -0,0 +1,136 @@ +import asyncio +import json +import sys +from collections.abc import Generator +from pathlib import Path + +from fastapi.testclient import TestClient +from sqlalchemy import create_engine +from sqlalchemy.orm import Session, sessionmaker +from sqlalchemy.pool import StaticPool + +BACKEND_ROOT = Path(__file__).resolve().parents[1] +REPO_ROOT = BACKEND_ROOT.parent +NANOBOT_ROOT = REPO_ROOT / "nanobot" +if str(BACKEND_ROOT) not in sys.path: + sys.path.insert(0, str(BACKEND_ROOT)) +if str(NANOBOT_ROOT) not in sys.path: + sys.path.insert(0, str(NANOBOT_ROOT)) + +from app.context import current_session_id +from app.core.security import CurrentUser, get_current_user +from app.database import Base, get_db +from app.models.project import Project +from app.models.user import User +from app.tools.subagent import InvokeSubagentTool, ListSubagentsTool +from main import app + + +def _seed_owner_and_project(db: Session) -> tuple[User, Project]: + user = User( + username="task4-owner", + email="task4-owner@example.com", + hashed_password="test", + is_admin=False, + ) + db.add(user) + db.commit() + db.refresh(user) + + project = Project( + name="task4-project", + description="task4", + owner_id=user.id, + ) + db.add(project) + db.commit() + db.refresh(project) + return user, project + + +def test_create_subagent_then_list_and_invoke_success(monkeypatch) -> None: + engine = create_engine( + "sqlite://", + connect_args={"check_same_thread": False}, + poolclass=StaticPool, + ) + testing_session_local = sessionmaker(autocommit=False, autoflush=False, bind=engine) + Base.metadata.create_all(bind=engine) + + db = testing_session_local() + user, project = _seed_owner_and_project(db) + user_id = user.id + username = user.username + project_id = project.id + db.close() + + def override_get_db() -> Generator[Session, None, None]: + override_db = testing_session_local() + try: + yield override_db + finally: + override_db.close() + + def override_current_user() -> CurrentUser: + return CurrentUser(id=user_id, username=username, is_admin=False) + + app.dependency_overrides[get_db] = override_get_db + app.dependency_overrides[get_current_user] = override_current_user + + token = current_session_id.set("api:task4-regression") + captured: dict[str, object] = {} + + async def fake_process_message(message, session_id, project_id, model_id): + captured["message"] = message + captured["session_id"] = session_id + captured["project_id"] = project_id + captured["model_id"] = model_id + return "invoke-ok" + + try: + monkeypatch.setattr("app.tools.subagent.SessionLocal", testing_session_local) + monkeypatch.setattr( + "app.tools.subagent.session_alias_store.get_alias_meta", + lambda _session_id: {"project_id": project_id}, + ) + monkeypatch.setattr("app.tools.subagent.get_llm_configs", lambda: []) + monkeypatch.setattr("app.tools.subagent.get_active_llm_config", lambda: None) + monkeypatch.setattr("app.tools.subagent.nanobot_service.process_message", fake_process_message) + + client = TestClient(app) + create_response = client.post( + f"/api/v1/projects/{project_id}/subagents", + json={ + "name": "task4-subagent", + "description": "task4-desc", + "instructions": "focus on regression", + "model": "gpt-x", + }, + ) + assert create_response.status_code == 200 + created = create_response.json() + assert created["project_id"] == project_id + assert created["name"] == "task4-subagent" + + listed = asyncio.run(ListSubagentsTool().execute()) + listed_payload = json.loads(listed) + assert len(listed_payload) == 1 + assert listed_payload[0]["name"] == "task4-subagent" + assert listed_payload[0]["description"] == "task4-desc" + + invoke_result = asyncio.run( + InvokeSubagentTool().execute( + subagent_name="task4-subagent", + task="run regression task", + ) + ) + assert "completed the task" in invoke_result + assert "invoke-ok" in invoke_result + assert captured["project_id"] == project_id + assert captured["session_id"] == f"api:task4-regression:subagent:{created['id']}" + assert "focus on regression" in str(captured["message"]) + finally: + current_session_id.reset(token) + app.dependency_overrides.clear() + Base.metadata.drop_all(bind=engine) + engine.dispose() diff --git a/frontend/src/api/subagents.ts b/frontend/src/api/subagents.ts index c12edbb..c70d065 100644 --- a/frontend/src/api/subagents.ts +++ b/frontend/src/api/subagents.ts @@ -29,8 +29,8 @@ export const subagentApi = { return response.data; }, - get: async (projectId: string, id: string) => { - const response = await axiosInstance.get(`${API_BASE_URL}/${projectId}/subagents/${id}`); + get: async (_projectId: string, id: string) => { + const response = await axiosInstance.get(`/api/v1/subagents/${id}`); return response.data; }, diff --git a/frontend/src/components/ChatInterface.tsx b/frontend/src/components/ChatInterface.tsx index bceb451..6c293a6 100644 --- a/frontend/src/components/ChatInterface.tsx +++ b/frontend/src/components/ChatInterface.tsx @@ -648,6 +648,7 @@ export function ChatInterface() { body: JSON.stringify({ message: messagePayload, session_id: targetSessionKey, + project_id: currentProject?.id, model_id: effectiveModelId, skill_ids: selectedSkillIds, source, diff --git a/frontend/src/i18n/locales/zh.json b/frontend/src/i18n/locales/zh.json index 50b351f..b7af86a 100644 --- a/frontend/src/i18n/locales/zh.json +++ b/frontend/src/i18n/locales/zh.json @@ -252,15 +252,15 @@ "noMcpServers": "暂无 MCP 服务器", "confirmDeleteMcpServer": "确定要删除这个 MCP 服务器吗?", "saveMcpServer": "保存 MCP 服务器", - "subagents": "子代理", - "subagentManagement": "子代理管理", - "manageSubagentsDesc": "管理该项目的子代理", - "addSubagent": "添加子代理", - "editSubagent": "编辑子代理", - "subagentName": "子代理名称", + "subagents": "子智能体", + "subagentManagement": "子智能体管理", + "manageSubagentsDesc": "管理该项目的子智能体", + "addSubagent": "添加子智能体", + "editSubagent": "编辑子智能体", + "subagentName": "子智能体名称", "selectModel": "请选择一个模型", "systemInstructionsPlaceholder": "你是一个有用的 AI 助手...", - "noSubagents": "暂无配置的子代理", - "confirmDeleteSubagent": "确定要删除这个子代理吗?", - "selectProjectToManageSubagents": "请先选择一个项目以管理其子代理" + "noSubagents": "暂无配置的子智能体", + "confirmDeleteSubagent": "确定要删除这个子智能体吗?", + "selectProjectToManageSubagents": "请先选择一个项目以管理其子智能体" }