Merge branch 'main' into fix-wa-mcp#272
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
|
@ -12,7 +12,7 @@ body:
|
|||
id: version
|
||||
attributes:
|
||||
label: What version of eigent are you using?
|
||||
placeholder: E.g., 0.0.66
|
||||
placeholder: E.g., 0.0.72
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
|
|
|||
6
.vscode/settings.json
vendored
|
|
@ -12,5 +12,11 @@
|
|||
],
|
||||
"cSpell.words": [
|
||||
"Eigent"
|
||||
],
|
||||
"i18n-ally.localesPaths": [
|
||||
"backend/lang",
|
||||
"server/lang",
|
||||
"src/i18n",
|
||||
"src/i18n/locales"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
[![Wechat][wechat-image]][wechat-url]
|
||||
[![][sponsor-shield]][sponsor-link]
|
||||
[![][built-with-camel]][camel-github]
|
||||
[![][join-us-image]][join-us]
|
||||
|
||||
</div>
|
||||
|
||||
|
|
@ -43,6 +44,8 @@ Built on [CAMEL-AI][camel-site]'s acclaimed open-source project, our system intr
|
|||
|
||||
<br/>
|
||||
|
||||
[![][image-join-us]][join-us]
|
||||
|
||||
<details>
|
||||
<summary><kbd>Table of contents</kbd></summary>
|
||||
|
||||
|
|
@ -355,6 +358,9 @@ For more information please contact info@eigent.ai
|
|||
[eigent-download]: https://www.eigent.ai/download
|
||||
[download-shield]: https://img.shields.io/badge/Download%20Eigent-363AF5?style=plastic
|
||||
|
||||
[join-us]:https://eigent-ai.notion.site/eigent-ai-careers
|
||||
[join-us-image]:https://img.shields.io/badge/Join%20Us-yellow?style=plastic
|
||||
|
||||
<!-- camel & eigent -->
|
||||
[camel-site]: https://www.camel-ai.org
|
||||
[eigent-site]: https://www.eigent.ai
|
||||
|
|
@ -368,6 +374,7 @@ For more information please contact info@eigent.ai
|
|||
[image-star-us]: https://eigent-ai.github.io/.github/assets/star-us.gif
|
||||
[image-opensource]: https://eigent-ai.github.io/.github/assets/opensource.png
|
||||
[image-wechat]: https://eigent-ai.github.io/.github/assets/wechat.png
|
||||
[image-join-us]: https://camel-ai.github.io/camel_asset/graphics/join_us.png
|
||||
|
||||
<!-- feature -->
|
||||
[image-workforce]: https://eigent-ai.github.io/.github/assets/feature_dynamic_workforce.gif
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
[![Wechat][wechat-image]][wechat-url]
|
||||
[![][sponsor-shield]][sponsor-link]
|
||||
[![][built-with-camel]][camel-github]
|
||||
[![][join-us-image]][join-us]
|
||||
|
||||
</div>
|
||||
|
||||
|
|
@ -43,6 +44,8 @@
|
|||
|
||||
<br/>
|
||||
|
||||
[![][image-join-us]][join-us]
|
||||
|
||||
<details>
|
||||
<summary><kbd>目录</kbd></summary>
|
||||
|
||||
|
|
@ -348,6 +351,9 @@ Eigent 基于 [CAMEL-AI.org][camel-ai-org-github] 的研究和基础设施构建
|
|||
[eigent-download]: https://www.eigent.ai/download
|
||||
[download-shield]: https://img.shields.io/badge/Download%20Eigent-363AF5?style=plastic
|
||||
|
||||
[join-us]:https://eigent-ai.notion.site/eigent-ai-careers
|
||||
[join-us-image]:https://img.shields.io/badge/Join%20Us-yellow?style=plastic
|
||||
|
||||
<!-- camel & eigent -->
|
||||
[camel-site]: https://www.camel-ai.org
|
||||
[eigent-site]: https://www.eigent.ai
|
||||
|
|
@ -361,6 +367,7 @@ Eigent 基于 [CAMEL-AI.org][camel-ai-org-github] 的研究和基础设施构建
|
|||
[image-star-us]: https://eigent-ai.github.io/.github/assets/star-us.gif
|
||||
[image-opensource]: https://eigent-ai.github.io/.github/assets/opensource.png
|
||||
[image-wechat]: https://eigent-ai.github.io/.github/assets/wechat.png
|
||||
[image-join-us]: https://camel-ai.github.io/camel_asset/graphics/join_us.png
|
||||
|
||||
<!-- feature -->
|
||||
[image-workforce]: https://eigent-ai.github.io/.github/assets/feature_dynamic_workforce.gif
|
||||
|
|
|
|||
30
SECURITY.md
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
The following versions of Eigent are currently being supported with security updates:
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 0.0.x | :white_check_mark: |
|
||||
| < 0.0 | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you discover a security vulnerability in Eigent, please report it responsibly:
|
||||
|
||||
### How to Report
|
||||
- **Email**: Send details to info@eigent.ai
|
||||
- **GitHub**: Use GitHub's private security advisory feature
|
||||
- **Include**: Detailed description, steps to reproduce, and potential impact
|
||||
|
||||
### What to Expect
|
||||
- **Response Time**: We aim to acknowledge reports within 48 hours
|
||||
- **Updates**: We will provide updates on the investigation progress weekly
|
||||
- **Resolution**: Critical vulnerabilities will be addressed within 7 days
|
||||
- **Credit**: We will credit security researchers in our security advisories (if desired)
|
||||
|
||||
### Security Disclosure Policy
|
||||
- We follow responsible disclosure practices
|
||||
- We request 90 days to address the vulnerability before public disclosure
|
||||
- We will coordinate disclosure timing with the reporter
|
||||
|
|
@ -6,4 +6,4 @@ def bun():
|
|||
|
||||
|
||||
def uv():
|
||||
return os.path.expanduser("~/.eigent/bin/uv")
|
||||
return os.path.expanduser("~/.local/bin/uv")
|
||||
|
|
|
|||
72
backend/app/component/error_format.py
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import json
|
||||
import re
|
||||
|
||||
|
||||
def normalize_error_to_openai_format(exception: Exception) -> tuple[str, str | None, dict | None]:
|
||||
"""
|
||||
Normalize error to OpenAI-style error structure.
|
||||
|
||||
Args:
|
||||
exception: The exception to normalize
|
||||
|
||||
Returns:
|
||||
tuple: (message, error_code, error_object)
|
||||
"""
|
||||
raw_msg = str(exception)
|
||||
error_obj = None
|
||||
error_code = None
|
||||
message = raw_msg
|
||||
|
||||
# Match "Error code: <code> - {json}"
|
||||
m = re.search(r"Error code:\s*(\d+)\s*-\s*(\{.*\})", raw_msg, re.DOTALL)
|
||||
if m:
|
||||
error_code = m.group(1)
|
||||
try:
|
||||
parsed = json.loads(m.group(2))
|
||||
err = parsed.get("error") or parsed
|
||||
if isinstance(err, dict):
|
||||
error_obj = {
|
||||
"message": err.get("message"),
|
||||
"type": err.get("type"),
|
||||
"param": err.get("param"),
|
||||
"code": err.get("code"),
|
||||
}
|
||||
if err.get("message"):
|
||||
message = err.get("message")
|
||||
if err.get("code"):
|
||||
error_code = err.get("code")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Heuristics if not parsed
|
||||
if error_obj is None:
|
||||
lower = raw_msg.lower()
|
||||
if "invalid_api_key" in lower or "incorrect api key" in lower or "unauthorized" in lower or " 401" in lower:
|
||||
error_code = "invalid_api_key"
|
||||
message = "Invalid key. Validation failed."
|
||||
error_obj = {
|
||||
"message": message,
|
||||
"type": "invalid_request_error",
|
||||
"param": None,
|
||||
"code": "invalid_api_key",
|
||||
}
|
||||
elif "model_not_found" in lower or "does not exist" in lower or " 404" in lower:
|
||||
error_code = "model_not_found"
|
||||
message = "Invalid model name. Validation failed."
|
||||
error_obj = {
|
||||
"message": message,
|
||||
"type": "invalid_request_error",
|
||||
"param": None,
|
||||
"code": "model_not_found",
|
||||
}
|
||||
elif "insufficient_quota" in lower or "quota" in lower or " 429" in lower:
|
||||
error_code = "insufficient_quota"
|
||||
message = "You exceeded your current quota, please check your plan and billing details."
|
||||
error_obj = {
|
||||
"message": message,
|
||||
"type": "insufficient_quota",
|
||||
"param": None,
|
||||
"code": "insufficient_quota",
|
||||
}
|
||||
|
||||
return message, error_code, error_obj
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel, Field
|
||||
from app.component.model_validation import create_agent
|
||||
from camel.types import ModelType
|
||||
from app.component.error_format import normalize_error_to_openai_format
|
||||
|
||||
|
||||
router = APIRouter(tags=["model"])
|
||||
|
|
@ -18,12 +20,29 @@ class ValidateModelRequest(BaseModel):
|
|||
class ValidateModelResponse(BaseModel):
|
||||
is_valid: bool = Field(..., description="Is valid")
|
||||
is_tool_calls: bool = Field(..., description="Is tool call used")
|
||||
error_code: str | None = Field(None, description="Error code")
|
||||
error: dict | None = Field(None, description="OpenAI-style error object")
|
||||
message: str = Field(..., description="Message")
|
||||
|
||||
|
||||
@router.post("/model/validate")
|
||||
async def validate_model(request: ValidateModelRequest):
|
||||
try:
|
||||
# API key validation
|
||||
if request.api_key is not None and str(request.api_key).strip() == "":
|
||||
return ValidateModelResponse(
|
||||
is_valid=False,
|
||||
is_tool_calls=False,
|
||||
message="Invalid key. Validation failed.",
|
||||
error_code="invalid_api_key",
|
||||
error={
|
||||
"message": "Invalid key. Validation failed.",
|
||||
"type": "invalid_request_error",
|
||||
"param": None,
|
||||
"code": "invalid_api_key",
|
||||
},
|
||||
)
|
||||
|
||||
extra = request.extra_params or {}
|
||||
|
||||
agent = create_agent(
|
||||
|
|
@ -43,17 +62,33 @@ async def validate_model(request: ValidateModelRequest):
|
|||
"""
|
||||
)
|
||||
except Exception as e:
|
||||
return ValidateModelResponse(is_valid=False, is_tool_calls=False, message=str(e))
|
||||
# Normalize error to OpenAI-style error structure
|
||||
message, error_code, error_obj = normalize_error_to_openai_format(e)
|
||||
|
||||
return ValidateModelResponse(
|
||||
is_valid=False,
|
||||
is_tool_calls=False,
|
||||
message=message,
|
||||
error_code=error_code,
|
||||
error=error_obj,
|
||||
)
|
||||
is_valid = bool(response)
|
||||
is_tool_calls = False
|
||||
|
||||
if response and hasattr(response, 'info') and response.info:
|
||||
|
||||
if response and hasattr(response, "info") and response.info:
|
||||
tool_calls = response.info.get("tool_calls", [])
|
||||
if tool_calls and len(tool_calls) > 0:
|
||||
is_tool_calls = tool_calls[0].result == "Tool execution completed successfully for https://www.camel-ai.org, Website Content: Welcome to CAMEL AI!"
|
||||
|
||||
is_tool_calls = (
|
||||
tool_calls[0].result
|
||||
== "Tool execution completed successfully for https://www.camel-ai.org, Website Content: Welcome to CAMEL AI!"
|
||||
)
|
||||
|
||||
return ValidateModelResponse(
|
||||
is_valid=is_valid,
|
||||
is_tool_calls=is_tool_calls,
|
||||
message="",
|
||||
message="Validation Success"
|
||||
if is_tool_calls
|
||||
else "This model doesn't support tool calls. please try with another model.",
|
||||
error_code=None,
|
||||
error=None,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from fastapi import APIRouter
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from loguru import logger
|
||||
from app.utils.toolkit.notion_mcp_toolkit import NotionMCPToolkit
|
||||
from app.utils.toolkit.google_calendar_toolkit import GoogleCalendarToolkit
|
||||
|
||||
|
||||
router = APIRouter(tags=["task"])
|
||||
|
|
@ -8,11 +9,107 @@ router = APIRouter(tags=["task"])
|
|||
|
||||
@router.post("/install/tool/{tool}", name="install tool")
|
||||
async def install_tool(tool: str):
|
||||
"""
|
||||
Install and pre-instantiate a specific MCP tool for authentication
|
||||
|
||||
Args:
|
||||
tool: Tool name to install (notion)
|
||||
|
||||
Returns:
|
||||
Installation result with tool information
|
||||
"""
|
||||
if tool == "notion":
|
||||
toolkit = NotionMCPToolkit(tool)
|
||||
await toolkit.connect()
|
||||
try:
|
||||
# Use a dummy task_id for installation, as this is just for pre-authentication
|
||||
toolkit = NotionMCPToolkit("install_auth")
|
||||
|
||||
try:
|
||||
# Pre-instantiate by connecting (this completes authentication)
|
||||
await toolkit.connect()
|
||||
|
||||
# Get available tools to verify connection
|
||||
tools = [tool_func.func.__name__ for tool_func in toolkit.get_tools()]
|
||||
logger.info(f"Successfully pre-instantiated {tool} toolkit with {len(tools)} tools")
|
||||
|
||||
# Disconnect, authentication info is saved
|
||||
await toolkit.disconnect()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"tools": tools,
|
||||
"message": f"Successfully installed and authenticated {tool} toolkit",
|
||||
"count": len(tools),
|
||||
"toolkit_name": "NotionMCPToolkit"
|
||||
}
|
||||
except Exception as connect_error:
|
||||
logger.warning(f"Could not connect to {tool} MCP server: {connect_error}")
|
||||
# Even if connection fails, mark as installed so user can use it later
|
||||
return {
|
||||
"success": True,
|
||||
"tools": [],
|
||||
"message": f"{tool} toolkit installed but not connected. Will connect when needed.",
|
||||
"count": 0,
|
||||
"toolkit_name": "NotionMCPToolkit",
|
||||
"warning": "Could not connect to Notion MCP server. You may need to authenticate when using the tool."
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to install {tool} toolkit: {e}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Failed to install {tool}: {str(e)}"
|
||||
)
|
||||
elif tool == "google_calendar":
|
||||
try:
|
||||
# Use a dummy task_id for installation, as this is just for pre-authentication
|
||||
toolkit = GoogleCalendarToolkit("install_auth")
|
||||
|
||||
# Get available tools to verify connection
|
||||
tools = [tool_func.func.__name__ for tool_func in toolkit.get_tools()]
|
||||
logger.info(f"Successfully pre-instantiated {tool} toolkit with {len(tools)} tools")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"tools": tools,
|
||||
"message": f"Successfully installed {tool} toolkit",
|
||||
"count": len(tools),
|
||||
"toolkit_name": "GoogleCalendarToolkit"
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to install {tool} toolkit: {e}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Failed to install {tool}: {str(e)}"
|
||||
)
|
||||
else:
|
||||
return {"error": "Tool not found"}
|
||||
tools = [tool.func.__name__ for tool in toolkit.get_tools()]
|
||||
await toolkit.disconnect()
|
||||
return tools
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Tool '{tool}' not found. Available tools: ['notion', 'google_calendar']"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/tools/available", name="list available tools")
|
||||
async def list_available_tools():
|
||||
"""
|
||||
List all available MCP tools that can be installed
|
||||
|
||||
Returns:
|
||||
List of available tools with their information
|
||||
"""
|
||||
return {
|
||||
"tools": [
|
||||
{
|
||||
"name": "notion",
|
||||
"display_name": "Notion MCP",
|
||||
"description": "Notion workspace integration for reading and managing Notion pages",
|
||||
"toolkit_class": "NotionMCPToolkit",
|
||||
"requires_auth": True
|
||||
},
|
||||
{
|
||||
"name": "google_calendar",
|
||||
"display_name": "Google Calendar",
|
||||
"description": "Google Calendar integration for managing events and schedules",
|
||||
"toolkit_class": "GoogleCalendarToolkit",
|
||||
"requires_auth": True
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -725,7 +725,7 @@ def search_agent(options: Chat):
|
|||
"browser_enter",
|
||||
"browser_visit_page",
|
||||
"browser_scroll",
|
||||
# "browser_get_som_screenshot",
|
||||
"browser_get_som_screenshot",
|
||||
],
|
||||
)
|
||||
|
||||
|
|
@ -1441,7 +1441,7 @@ async def get_toolkits(tools: list[str], agent_name: str, api_task_id: str):
|
|||
"image_analysis_toolkit": ImageAnalysisToolkit,
|
||||
"linkedin_toolkit": LinkedInToolkit,
|
||||
"mcp_search_toolkit": McpSearchToolkit,
|
||||
"notion_toolkit": NotionMCPToolkit,
|
||||
"notion_mcp_toolkit": NotionMCPToolkit,
|
||||
"pptx_toolkit": PPTXToolkit,
|
||||
"reddit_toolkit": RedditToolkit,
|
||||
"search_toolkit": SearchToolkit,
|
||||
|
|
@ -1457,9 +1457,9 @@ async def get_toolkits(tools: list[str], agent_name: str, api_task_id: str):
|
|||
if item in toolkits:
|
||||
toolkit: AbstractToolkit = toolkits[item]
|
||||
toolkit.agent_name = agent_name
|
||||
res = toolkit.get_can_use_tools(api_task_id)
|
||||
res = await res if asyncio.iscoroutine(res) else res
|
||||
res.extend(res)
|
||||
toolkit_tools = toolkit.get_can_use_tools(api_task_id)
|
||||
toolkit_tools = await toolkit_tools if asyncio.iscoroutine(toolkit_tools) else toolkit_tools
|
||||
res.extend(toolkit_tools)
|
||||
else:
|
||||
logger.warning(f"Toolkit {item} not found, please check your configuration.")
|
||||
traceroot_logger.warning(f"Toolkit {item} not found for agent {agent_name}")
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ def listen_toolkit(
|
|||
|
||||
if iscoroutinefunction(func):
|
||||
# async function wrapper
|
||||
@wraps(func)
|
||||
@wraps(wrap)
|
||||
async def async_wrapper(*args, **kwargs):
|
||||
toolkit: AbstractToolkit = args[0]
|
||||
task_lock = get_task_lock(toolkit.api_task_id)
|
||||
|
|
@ -93,7 +93,7 @@ def listen_toolkit(
|
|||
|
||||
else:
|
||||
# sync function wrapper
|
||||
@wraps(func)
|
||||
@wraps(wrap)
|
||||
def sync_wrapper(*args, **kwargs):
|
||||
toolkit: AbstractToolkit = args[0]
|
||||
task_lock = get_task_lock(toolkit.api_task_id)
|
||||
|
|
|
|||
|
|
@ -326,9 +326,9 @@ class HybridBrowserPythonToolkit(BaseHybridBrowserToolkit, AbstractToolkit):
|
|||
async def browser_get_page_snapshot(self) -> str:
|
||||
return await super().browser_get_page_snapshot()
|
||||
|
||||
# @listen_toolkit(BaseHybridBrowserToolkit.browser_get_som_screenshot)
|
||||
# async def browser_get_som_screenshot(self):
|
||||
# return await super().browser_get_som_screenshot()
|
||||
@listen_toolkit(BaseHybridBrowserToolkit.browser_get_som_screenshot)
|
||||
async def browser_get_som_screenshot(self):
|
||||
return await super().browser_get_som_screenshot()
|
||||
|
||||
@listen_toolkit(BaseHybridBrowserToolkit.browser_get_page_links)
|
||||
async def browser_get_page_links(self, *, ref: List[str]) -> Dict[str, Any]:
|
||||
|
|
@ -362,7 +362,7 @@ class HybridBrowserPythonToolkit(BaseHybridBrowserToolkit, AbstractToolkit):
|
|||
FunctionTool(browser.browser_enter),
|
||||
FunctionTool(browser.browser_visit_page),
|
||||
FunctionTool(browser.browser_scroll),
|
||||
# FunctionTool(browser.browser_get_som_screenshot),
|
||||
FunctionTool(browser.browser_get_som_screenshot),
|
||||
# FunctionTool(browser.select),
|
||||
# FunctionTool(browser.wait_user),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -78,102 +78,9 @@ class WebSocketBrowserWrapper(BaseWebSocketBrowserWrapper):
|
|||
self.websocket = None
|
||||
|
||||
async def start(self):
|
||||
# Check if node_modules exists (dependencies installed)
|
||||
node_modules_path = os.path.join(self.ts_dir, "node_modules")
|
||||
if not os.path.exists(node_modules_path):
|
||||
logger.warning("Node modules not found. Running npm install...")
|
||||
install_result = subprocess.run(
|
||||
[uv(), "run", "npm", "install"],
|
||||
cwd=self.ts_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if install_result.returncode != 0:
|
||||
logger.error(f"npm install failed: {install_result.stderr}")
|
||||
raise RuntimeError(
|
||||
f"Failed to install npm dependencies: {install_result.stderr}\n" # noqa:E501
|
||||
f"Please run 'npm install' in {self.ts_dir} manually."
|
||||
)
|
||||
logger.info("npm dependencies installed successfully")
|
||||
|
||||
# Ensure the TypeScript code is built
|
||||
build_result = subprocess.run(
|
||||
[uv(), "run", "npm", "run", "build"],
|
||||
cwd=self.ts_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if build_result.returncode != 0:
|
||||
logger.error(f"TypeScript build failed: {build_result.stderr}")
|
||||
raise RuntimeError(f"TypeScript build failed: {build_result.stderr}")
|
||||
else:
|
||||
# Log warnings but don't fail on them
|
||||
if build_result.stderr:
|
||||
logger.warning(f"TypeScript build warnings: {build_result.stderr}")
|
||||
logger.info("TypeScript build completed successfully")
|
||||
|
||||
# Start the WebSocket server
|
||||
self.process = subprocess.Popen(
|
||||
[uv(), "run", "node", "websocket-server.js"], # bun not support playwright, use uv nodejs-bin
|
||||
cwd=self.ts_dir,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
|
||||
# Wait for server to output the port
|
||||
server_ready = False
|
||||
timeout = 10 # 10 seconds timeout
|
||||
start_time = time.time()
|
||||
|
||||
while not server_ready and time.time() - start_time < timeout:
|
||||
if self.process.poll() is not None:
|
||||
# Process died
|
||||
stderr = self.process.stderr.read() # type: ignore
|
||||
raise RuntimeError(f"WebSocket server failed to start: {stderr}")
|
||||
|
||||
try:
|
||||
line = self.process.stdout.readline() # type: ignore
|
||||
logger.debug(f"WebSocket server output: {line}")
|
||||
if line.startswith("SERVER_READY:"):
|
||||
self.server_port = int(line.split(":")[1].strip())
|
||||
server_ready = True
|
||||
logger.info(f"WebSocket server ready on port {self.server_port}")
|
||||
except (ValueError, IndexError):
|
||||
continue
|
||||
|
||||
if not server_ready:
|
||||
self.process.kill()
|
||||
raise RuntimeError("WebSocket server failed to start within timeout")
|
||||
|
||||
# Connect to the WebSocket server
|
||||
try:
|
||||
self.websocket = await websockets.connect(
|
||||
f"ws://localhost:{self.server_port}",
|
||||
ping_interval=30,
|
||||
ping_timeout=10,
|
||||
max_size=50 * 1024 * 1024, # 50MB limit to match server
|
||||
)
|
||||
logger.info("Connected to WebSocket server")
|
||||
except Exception as e:
|
||||
self.process.kill()
|
||||
raise RuntimeError(f"Failed to connect to WebSocket server: {e}") from e
|
||||
|
||||
# Start the background receiver task - THIS WAS MISSING!
|
||||
self._receive_task = asyncio.create_task(self._receive_loop())
|
||||
logger.debug("Started WebSocket receiver task")
|
||||
|
||||
# Initialize the browser toolkit
|
||||
logger.debug(f"send init {self.config}")
|
||||
try:
|
||||
await self._send_command("init", self.config)
|
||||
logger.debug("WebSocket server initialized successfully")
|
||||
except RuntimeError as e:
|
||||
if "Timeout waiting for response to command: init" in str(e):
|
||||
logger.warning("Init timeout - continuing anyway (CDP connection may be slow)")
|
||||
# Continue without error - the WebSocket server is likely still initializing
|
||||
else:
|
||||
raise
|
||||
# Simply use the parent implementation which uses system npm/node
|
||||
logger.info("Starting WebSocket server using parent implementation (system npm/node)")
|
||||
await super().start()
|
||||
|
||||
async def _send_command(self, command: str, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Send a command to the WebSocket server with enhanced error handling."""
|
||||
|
|
@ -461,9 +368,9 @@ class HybridBrowserToolkit(BaseHybridBrowserToolkit, AbstractToolkit):
|
|||
async def browser_get_page_snapshot(self) -> str:
|
||||
return await super().browser_get_page_snapshot()
|
||||
|
||||
# @listen_toolkit(BaseHybridBrowserToolkit.browser_get_som_screenshot)
|
||||
# async def browser_get_som_screenshot(self, read_image: bool = False, instruction: str | None = None) -> str:
|
||||
# return await super().browser_get_som_screenshot(read_image, instruction)
|
||||
@listen_toolkit(BaseHybridBrowserToolkit.browser_get_som_screenshot)
|
||||
async def browser_get_som_screenshot(self, read_image: bool = False, instruction: str | None = None) -> str:
|
||||
return await super().browser_get_som_screenshot(read_image, instruction)
|
||||
|
||||
@listen_toolkit(BaseHybridBrowserToolkit.browser_click)
|
||||
async def browser_click(self, *, ref: str) -> Dict[str, Any]:
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
import os
|
||||
from camel.toolkits import FunctionTool, NotionMCPToolkit as BaseNotionMCPToolkit
|
||||
from app.component.command import bun
|
||||
from typing import Any, Dict, List
|
||||
from loguru import logger
|
||||
from camel.toolkits import FunctionTool
|
||||
from app.component.environment import env
|
||||
from app.service.task import Agents
|
||||
from app.utils.toolkit.abstract_toolkit import AbstractToolkit
|
||||
from camel.toolkits.mcp_toolkit import MCPToolkit
|
||||
|
||||
|
||||
class NotionMCPToolkit(BaseNotionMCPToolkit, AbstractToolkit):
|
||||
agent_name: str = Agents.social_medium_agent
|
||||
class NotionMCPToolkit(MCPToolkit, AbstractToolkit):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -18,29 +17,96 @@ class NotionMCPToolkit(BaseNotionMCPToolkit, AbstractToolkit):
|
|||
self.api_task_id = api_task_id
|
||||
if timeout is None:
|
||||
timeout = 120.0
|
||||
super().__init__(timeout)
|
||||
self._mcp_toolkit = MCPToolkit(
|
||||
config_dict={
|
||||
"mcpServers": {
|
||||
"notionMCP": {
|
||||
"command": bun(),
|
||||
"args": ["x", "-y", "eigent-mcp-remote@0.1.22", "https://mcp.notion.com/mcp"],
|
||||
"env": {
|
||||
"MCP_REMOTE_CONFIG_DIR": env("MCP_REMOTE_CONFIG_DIR", os.path.expanduser("~/.mcp-auth")),
|
||||
},
|
||||
}
|
||||
|
||||
config_dict={
|
||||
"mcpServers": {
|
||||
"notionMCP": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"mcp-remote",
|
||||
"https://mcp.notion.com/mcp",
|
||||
],
|
||||
"env": {
|
||||
"MCP_REMOTE_CONFIG_DIR": env("MCP_REMOTE_CONFIG_DIR", os.path.expanduser("~/.mcp-auth")),
|
||||
},
|
||||
}
|
||||
},
|
||||
timeout=timeout,
|
||||
)
|
||||
}
|
||||
}
|
||||
super().__init__(config_dict=config_dict, timeout=timeout)
|
||||
|
||||
def get_tools(self) -> List[FunctionTool]:
|
||||
r"""Returns a list of tools provided by the NotionMCPToolkit.
|
||||
|
||||
Returns:
|
||||
List[FunctionTool]: List of available tools.
|
||||
"""
|
||||
all_tools = []
|
||||
for client in self.clients:
|
||||
try:
|
||||
original_build_schema = client._build_tool_schema
|
||||
|
||||
def create_wrapper(orig_func):
|
||||
def wrapper(mcp_tool):
|
||||
return self._build_custom_tool_schema(
|
||||
mcp_tool, orig_func
|
||||
)
|
||||
|
||||
return wrapper
|
||||
|
||||
client._build_tool_schema = create_wrapper( # type: ignore[method-assign]
|
||||
original_build_schema
|
||||
)
|
||||
|
||||
client_tools = client.get_tools()
|
||||
all_tools.extend(client_tools)
|
||||
|
||||
client._build_tool_schema = original_build_schema # type: ignore[method-assign]
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get tools from client: {e}")
|
||||
return all_tools
|
||||
|
||||
def _build_custom_tool_schema(self, mcp_tool, original_build_schema):
|
||||
r"""Build tool schema with custom modifications."""
|
||||
schema = original_build_schema(mcp_tool)
|
||||
self._customize_function_parameters(schema)
|
||||
return schema
|
||||
|
||||
def _customize_function_parameters(self, schema: Dict[str, Any]) -> None:
|
||||
r"""Customize function parameters for specific functions.
|
||||
|
||||
This method allows modifying parameter descriptions or other schema
|
||||
attributes for specific functions.
|
||||
"""
|
||||
function_info = schema.get("function", {})
|
||||
function_name = function_info.get("name", "")
|
||||
parameters = function_info.get("parameters", {})
|
||||
properties = parameters.get("properties", {})
|
||||
|
||||
# Modify the notion-create-pages function to make parent optional
|
||||
if function_name == "notion-create-pages":
|
||||
if "parent" in properties:
|
||||
# Update the parent parameter description
|
||||
properties["parent"]["description"] = (
|
||||
"Optional. The parent under which the new pages will be created. "
|
||||
"This can be a page (page_id), a database page (database_id), or "
|
||||
"a data source/collection under a database (data_source_id). "
|
||||
"If omitted, the new pages will be created as private pages at the workspace level. "
|
||||
"Use data_source_id when you have a collection:// URL from the fetch tool."
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def get_can_use_tools(cls, api_task_id: str) -> list[FunctionTool]:
|
||||
tools = []
|
||||
if env("MCP_REMOTE_CONFIG_DIR"):
|
||||
toolkit = cls(api_task_id)
|
||||
toolkit = cls(api_task_id)
|
||||
try:
|
||||
await toolkit.connect()
|
||||
for item in toolkit.get_tools():
|
||||
# Use subclass implementation that inlines upstream processing
|
||||
all_tools = toolkit.get_tools()
|
||||
for item in all_tools:
|
||||
setattr(item, "_toolkit_name", cls.__name__)
|
||||
tools.append(item)
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not connect to Notion MCP server: {e}")
|
||||
return tools
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
import asyncio
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
from camel.toolkits.terminal_toolkit import TerminalToolkit as BaseTerminalToolkit
|
||||
from app.component.command import uv
|
||||
from camel.toolkits.terminal_toolkit.terminal_toolkit import _to_plain
|
||||
from app.component.environment import env
|
||||
from app.service.task import Action, ActionTerminalData, Agents, get_task_lock
|
||||
from app.utils.listen.toolkit_listen import listen_toolkit
|
||||
|
|
@ -19,14 +17,13 @@ class TerminalToolkit(BaseTerminalToolkit, AbstractToolkit):
|
|||
api_task_id: str,
|
||||
agent_name: str | None = None,
|
||||
timeout: float | None = None,
|
||||
shell_sessions: Dict[str, Any] | None = None,
|
||||
working_directory: str | None = None,
|
||||
need_terminal: bool = True,
|
||||
use_shell_mode: bool = True,
|
||||
clone_current_env: bool = False,
|
||||
use_docker_backend: bool = False,
|
||||
docker_container_name: str | None = None,
|
||||
session_logs_dir: str | None = None,
|
||||
safe_mode: bool = True,
|
||||
interactive: bool = False,
|
||||
log_dir: str | None = None,
|
||||
allowed_commands: list[str] | None = None,
|
||||
clone_current_env: bool = False,
|
||||
):
|
||||
self.api_task_id = api_task_id
|
||||
if agent_name is not None:
|
||||
|
|
@ -35,16 +32,26 @@ class TerminalToolkit(BaseTerminalToolkit, AbstractToolkit):
|
|||
working_directory = env("file_save_path", os.path.expanduser("~/.eigent/terminal/"))
|
||||
super().__init__(
|
||||
timeout=timeout,
|
||||
shell_sessions=shell_sessions,
|
||||
working_directory=working_directory,
|
||||
need_terminal=False, # Override the code that creates GUI output logs, use queue for SSE output instead
|
||||
use_shell_mode=use_shell_mode,
|
||||
clone_current_env=clone_current_env,
|
||||
use_docker_backend=use_docker_backend,
|
||||
docker_container_name=docker_container_name,
|
||||
session_logs_dir=session_logs_dir,
|
||||
safe_mode=safe_mode,
|
||||
interactive=interactive,
|
||||
log_dir=log_dir,
|
||||
allowed_commands=allowed_commands,
|
||||
clone_current_env=clone_current_env,
|
||||
)
|
||||
|
||||
def _write_to_log(self, log_file: str, content: str) -> None:
|
||||
r"""Write content to log file with optional ANSI stripping.
|
||||
|
||||
Args:
|
||||
log_file (str): Path to the log file
|
||||
content (str): Content to write
|
||||
"""
|
||||
# Convert ANSI escape sequences to plain text
|
||||
super()._write_to_log(log_file, content)
|
||||
self._update_terminal_output(_to_plain(content))
|
||||
|
||||
def _update_terminal_output(self, output: str):
|
||||
task_lock = get_task_lock(self.api_task_id)
|
||||
# This method will be called during init. At that time, the process_task_id parameter does not exist, so it is set to be empty default
|
||||
|
|
@ -61,16 +68,12 @@ class TerminalToolkit(BaseTerminalToolkit, AbstractToolkit):
|
|||
if hasattr(task_lock, "add_background_task"):
|
||||
task_lock.add_background_task(task)
|
||||
|
||||
def _ensure_uv_available(self) -> bool:
|
||||
self.uv_path = uv()
|
||||
return True
|
||||
|
||||
@listen_toolkit(
|
||||
BaseTerminalToolkit.shell_exec,
|
||||
lambda _, id, command: f"id: {id}, command: {command}",
|
||||
lambda _, id, command, block=True: f"id: {id}, command: {command}, block: {block}",
|
||||
)
|
||||
def shell_exec(self, id: str, command: str) -> str:
|
||||
return super().shell_exec(id=id, command=command)
|
||||
def shell_exec(self, id: str, command: str, block: bool = True) -> str:
|
||||
return super().shell_exec(id=id, command=command, block=block)
|
||||
|
||||
@listen_toolkit(
|
||||
BaseTerminalToolkit.shell_view,
|
||||
|
|
@ -81,17 +84,17 @@ class TerminalToolkit(BaseTerminalToolkit, AbstractToolkit):
|
|||
|
||||
@listen_toolkit(
|
||||
BaseTerminalToolkit.shell_wait,
|
||||
lambda _, id, seconds: f"id: {id}, seconds: {seconds}",
|
||||
lambda _, id, wait_seconds=None: f"id: {id}, wait_seconds: {wait_seconds}",
|
||||
)
|
||||
def shell_wait(self, id: str, seconds: int | None = None) -> str:
|
||||
return super().shell_wait(id=id, seconds=seconds)
|
||||
def shell_wait(self, id: str, wait_seconds: float = 5.0) -> str:
|
||||
return super().shell_wait(id=id, wait_seconds=wait_seconds)
|
||||
|
||||
@listen_toolkit(
|
||||
BaseTerminalToolkit.shell_write_to_process,
|
||||
lambda _, id, input, press_enter: f"id: {id}, input: {input}, press_enter: {press_enter}",
|
||||
lambda _, id, command: f"id: {id}, command: {command}",
|
||||
)
|
||||
def shell_write_to_process(self, id: str, input: str, press_enter: bool) -> str:
|
||||
return super().shell_write_to_process(id=id, input=input, press_enter=press_enter)
|
||||
def shell_write_to_process(self, id: str, command: str) -> str:
|
||||
return super().shell_write_to_process(id=id, command=command)
|
||||
|
||||
@listen_toolkit(
|
||||
BaseTerminalToolkit.shell_kill_process,
|
||||
|
|
@ -101,8 +104,8 @@ class TerminalToolkit(BaseTerminalToolkit, AbstractToolkit):
|
|||
return super().shell_kill_process(id=id)
|
||||
|
||||
@listen_toolkit(
|
||||
BaseTerminalToolkit.ask_user_for_help,
|
||||
lambda _, id: f"id: {id}",
|
||||
BaseTerminalToolkit.shell_ask_user_for_help,
|
||||
lambda _, id, prompt: f"id: {id}, prompt: {prompt}",
|
||||
)
|
||||
def ask_user_for_help(self, id: str) -> str:
|
||||
return super().ask_user_for_help(id=id)
|
||||
def shell_ask_user_for_help(self, id: str, prompt: str) -> str:
|
||||
return super().shell_ask_user_for_help(id=id, prompt=prompt)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ description = "Add your description here"
|
|||
readme = "README.md"
|
||||
requires-python = "==3.10.16"
|
||||
dependencies = [
|
||||
"camel-ai[eigent]>=0.2.76a3",
|
||||
"camel-ai[eigent]==0.2.76a13",
|
||||
"fastapi>=0.115.12",
|
||||
"fastapi-babel>=1.0.0",
|
||||
"uvicorn[standard]>=0.34.2",
|
||||
|
|
@ -17,8 +17,9 @@ dependencies = [
|
|||
"inflection>=0.5.1",
|
||||
"aiofiles>=24.1.0",
|
||||
"openai>=1.99.3,<2",
|
||||
"traceroot>=0.0.4a9",
|
||||
"traceroot>=0.0.5a2",
|
||||
"nodejs-wheel>=22.18.0",
|
||||
"numpy>=1.23.0,<2.0.0",
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -8,66 +8,63 @@ from app.controller.model_controller import validate_model, ValidateModelRequest
|
|||
@pytest.mark.unit
|
||||
class TestModelController:
|
||||
"""Test cases for model controller endpoints."""
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_model_success(self):
|
||||
"""Test successful model validation."""
|
||||
request_data = ValidateModelRequest(
|
||||
model_platform="OPENAI",
|
||||
model_type="GPT_4O_MINI",
|
||||
model_platform="openai",
|
||||
model_type="gpt-4o",
|
||||
api_key="test_key",
|
||||
url="https://api.openai.com/v1",
|
||||
model_config_dict={"temperature": 0.7},
|
||||
extra_params={"max_tokens": 1000}
|
||||
extra_params={"max_tokens": 1000},
|
||||
)
|
||||
|
||||
|
||||
mock_agent = MagicMock()
|
||||
mock_response = MagicMock()
|
||||
tool_call = MagicMock()
|
||||
tool_call.result = "Tool execution completed successfully for https://www.camel-ai.org, Website Content: Welcome to CAMEL AI!"
|
||||
tool_call.result = (
|
||||
"Tool execution completed successfully for https://www.camel-ai.org, Website Content: Welcome to CAMEL AI!"
|
||||
)
|
||||
mock_response.info = {"tool_calls": [tool_call]}
|
||||
mock_agent.step.return_value = mock_response
|
||||
|
||||
|
||||
with patch("app.controller.model_controller.create_agent", return_value=mock_agent):
|
||||
response = await validate_model(request_data)
|
||||
|
||||
|
||||
assert isinstance(response, ValidateModelResponse)
|
||||
assert response.is_valid is True
|
||||
assert response.is_tool_calls is True
|
||||
assert response.message == ""
|
||||
assert response.message == "Validation Success"
|
||||
assert response.error_code is None
|
||||
assert response.error is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_model_creation_failure(self):
|
||||
"""Test model validation when agent creation fails."""
|
||||
request_data = ValidateModelRequest(
|
||||
model_platform="INVALID",
|
||||
model_type="INVALID_MODEL",
|
||||
api_key="invalid_key"
|
||||
)
|
||||
|
||||
with patch("app.controller.model_controller.create_agent", side_effect=Exception("Invalid model configuration")):
|
||||
request_data = ValidateModelRequest(model_platform="INVALID", model_type="INVALID_MODEL", api_key="invalid_key")
|
||||
|
||||
with patch(
|
||||
"app.controller.model_controller.create_agent", side_effect=Exception("Invalid model configuration")
|
||||
):
|
||||
response = await validate_model(request_data)
|
||||
|
||||
assert isinstance(response, ValidateModelResponse)
|
||||
assert response.is_valid is False
|
||||
assert response.is_tool_calls is False
|
||||
assert "Invalid model configuration" in response.message
|
||||
assert "Invalid model name" in response.message
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_model_step_failure(self):
|
||||
"""Test model validation when agent step fails."""
|
||||
request_data = ValidateModelRequest(
|
||||
model_platform="OPENAI",
|
||||
model_type="GPT_4O_MINI",
|
||||
api_key="test_key"
|
||||
)
|
||||
|
||||
request_data = ValidateModelRequest(model_platform="openai", model_type="gpt-4o", api_key="test_key")
|
||||
|
||||
mock_agent = MagicMock()
|
||||
mock_agent.step.side_effect = Exception("API call failed")
|
||||
|
||||
|
||||
with patch("app.controller.model_controller.create_agent", return_value=mock_agent):
|
||||
response = await validate_model(request_data)
|
||||
|
||||
|
||||
assert isinstance(response, ValidateModelResponse)
|
||||
assert response.is_valid is False
|
||||
assert response.is_tool_calls is False
|
||||
|
|
@ -76,125 +73,116 @@ class TestModelController:
|
|||
@pytest.mark.asyncio
|
||||
async def test_validate_model_tool_calls_false(self):
|
||||
"""Test model validation when tool calls fail."""
|
||||
request_data = ValidateModelRequest(
|
||||
model_platform="OPENAI",
|
||||
model_type="GPT_4O_MINI",
|
||||
api_key="test_key"
|
||||
)
|
||||
|
||||
request_data = ValidateModelRequest(model_platform="openai", model_type="gpt-4o", api_key="test_key")
|
||||
|
||||
mock_agent = MagicMock()
|
||||
mock_response = MagicMock()
|
||||
tool_call = MagicMock()
|
||||
tool_call.result = "Different response"
|
||||
mock_response.info = {"tool_calls": [tool_call]}
|
||||
mock_agent.step.return_value = mock_response
|
||||
|
||||
|
||||
with patch("app.controller.model_controller.create_agent", return_value=mock_agent):
|
||||
response = await validate_model(request_data)
|
||||
|
||||
|
||||
assert isinstance(response, ValidateModelResponse)
|
||||
assert response.is_valid is True
|
||||
assert response.is_tool_calls is False
|
||||
assert response.message == ""
|
||||
assert response.message == "This model doesn't support tool calls. please try with another model."
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_model_with_minimal_parameters(self):
|
||||
"""Test model validation with minimal parameters."""
|
||||
request_data = ValidateModelRequest() # Uses default values
|
||||
|
||||
|
||||
mock_agent = MagicMock()
|
||||
mock_response = MagicMock()
|
||||
tool_call = MagicMock()
|
||||
tool_call.result = "Tool execution completed successfully for https://www.camel-ai.org, Website Content: Welcome to CAMEL AI!"
|
||||
tool_call.result = (
|
||||
"Tool execution completed successfully for https://www.camel-ai.org, Website Content: Welcome to CAMEL AI!"
|
||||
)
|
||||
mock_response.info = {"tool_calls": [tool_call]}
|
||||
mock_agent.step.return_value = mock_response
|
||||
|
||||
|
||||
with patch("app.controller.model_controller.create_agent", return_value=mock_agent):
|
||||
response = await validate_model(request_data)
|
||||
|
||||
assert isinstance(response, ValidateModelResponse)
|
||||
assert response.is_valid is True
|
||||
assert response.is_tool_calls is True
|
||||
assert response.is_valid is False
|
||||
assert response.is_tool_calls is False
|
||||
assert response.error_code is not None
|
||||
assert response.error is not None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_model_no_response(self):
|
||||
"""Test model validation when no response is returned."""
|
||||
request_data = ValidateModelRequest(
|
||||
model_platform="OPENAI",
|
||||
model_type="GPT_4O_MINI"
|
||||
)
|
||||
|
||||
request_data = ValidateModelRequest(model_platform="openai", model_type="gpt-4o")
|
||||
|
||||
mock_agent = MagicMock()
|
||||
mock_agent.step.return_value = None
|
||||
|
||||
|
||||
# When response is None, should return False
|
||||
with patch("app.controller.model_controller.create_agent", return_value=mock_agent):
|
||||
result = await validate_model(request_data)
|
||||
assert result.is_valid is False
|
||||
assert result.is_tool_calls is False
|
||||
assert result.error_code is None
|
||||
assert result.error is None
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestModelControllerIntegration:
|
||||
"""Integration tests for model controller."""
|
||||
|
||||
|
||||
def test_validate_model_endpoint_integration(self, client: TestClient):
|
||||
"""Test validate model endpoint through FastAPI test client."""
|
||||
request_data = {
|
||||
"model_platform": "OPENAI",
|
||||
"model_type": "GPT_4O_MINI",
|
||||
"model_platform": "openai",
|
||||
"model_type": "gpt-4o",
|
||||
"api_key": "test_key",
|
||||
"url": "https://api.openai.com/v1",
|
||||
"model_config_dict": {"temperature": 0.7},
|
||||
"extra_params": {"max_tokens": 1000}
|
||||
"extra_params": {"max_tokens": 1000},
|
||||
}
|
||||
|
||||
|
||||
mock_agent = MagicMock()
|
||||
mock_response = MagicMock()
|
||||
tool_call = MagicMock()
|
||||
tool_call.result = "Tool execution completed successfully for https://www.camel-ai.org, Website Content: Welcome to CAMEL AI!"
|
||||
tool_call.result = (
|
||||
"Tool execution completed successfully for https://www.camel-ai.org, Website Content: Welcome to CAMEL AI!"
|
||||
)
|
||||
mock_response.info = {"tool_calls": [tool_call]}
|
||||
mock_agent.step.return_value = mock_response
|
||||
|
||||
|
||||
with patch("app.controller.model_controller.create_agent", return_value=mock_agent):
|
||||
response = client.post("/model/validate", json=request_data)
|
||||
|
||||
|
||||
assert response.status_code == 200
|
||||
response_data = response.json()
|
||||
assert response_data["is_valid"] is True
|
||||
assert response_data["is_tool_calls"] is True
|
||||
assert response_data["message"] == ""
|
||||
assert response_data["message"] == "Validation Success"
|
||||
|
||||
def test_validate_model_endpoint_error_integration(self, client: TestClient):
|
||||
"""Test validate model endpoint error handling through FastAPI test client."""
|
||||
request_data = {
|
||||
"model_platform": "INVALID",
|
||||
"model_type": "INVALID_MODEL"
|
||||
}
|
||||
|
||||
request_data = {"model_platform": "INVALID", "model_type": "INVALID_MODEL"}
|
||||
|
||||
with patch("app.controller.model_controller.create_agent", side_effect=Exception("Test error")):
|
||||
response = client.post("/model/validate", json=request_data)
|
||||
|
||||
|
||||
assert response.status_code == 200 # Returns 200 with error in response body
|
||||
response_data = response.json()
|
||||
assert response_data["is_valid"] is False
|
||||
assert response_data["is_tool_calls"] is False
|
||||
assert "Test error" in response_data["message"]
|
||||
assert "Invalid model name" in response_data["message"]
|
||||
|
||||
|
||||
@pytest.mark.model_backend
|
||||
class TestModelControllerWithRealModels:
|
||||
"""Tests that require real model backends (marked for selective running)."""
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_model_with_real_openai_model(self):
|
||||
"""Test model validation with real OpenAI model (requires API key)."""
|
||||
request_data = ValidateModelRequest(
|
||||
model_platform="OPENAI",
|
||||
model_type="GPT_4O_MINI",
|
||||
api_key=None, # Would need real API key from environment
|
||||
)
|
||||
|
||||
# This test would validate against real OpenAI API
|
||||
# Marked as model_backend for selective execution
|
||||
assert True # Placeholder
|
||||
|
|
@ -210,55 +198,48 @@ class TestModelControllerWithRealModels:
|
|||
@pytest.mark.unit
|
||||
class TestModelControllerErrorCases:
|
||||
"""Test error cases and edge conditions for model controller."""
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_model_with_invalid_json_config(self):
|
||||
"""Test model validation with invalid JSON configuration."""
|
||||
request_data = ValidateModelRequest(
|
||||
model_platform="OPENAI",
|
||||
model_type="GPT_4O_MINI",
|
||||
model_config_dict={"invalid": float('inf')} # Invalid JSON value
|
||||
model_platform="openai",
|
||||
model_type="gpt-4o",
|
||||
model_config_dict={"invalid": float("inf")}, # Invalid JSON value
|
||||
)
|
||||
|
||||
|
||||
with patch("app.controller.model_controller.create_agent", side_effect=ValueError("Invalid configuration")):
|
||||
response = await validate_model(request_data)
|
||||
|
||||
|
||||
assert response.is_valid is False
|
||||
assert "Invalid configuration" in response.message
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_model_with_network_error(self):
|
||||
"""Test model validation with network connectivity issues."""
|
||||
request_data = ValidateModelRequest(
|
||||
model_platform="OPENAI",
|
||||
model_type="GPT_4O_MINI",
|
||||
url="https://invalid-url.com"
|
||||
)
|
||||
|
||||
request_data = ValidateModelRequest(model_platform="openai", model_type="gpt-4o", url="https://invalid-url.com")
|
||||
|
||||
mock_agent = MagicMock()
|
||||
mock_agent.step.side_effect = ConnectionError("Network unreachable")
|
||||
|
||||
|
||||
with patch("app.controller.model_controller.create_agent", return_value=mock_agent):
|
||||
response = await validate_model(request_data)
|
||||
|
||||
|
||||
assert response.is_valid is False
|
||||
assert "Network unreachable" in response.message
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_model_with_malformed_tool_calls_response(self):
|
||||
"""Test model validation with malformed tool calls in response."""
|
||||
request_data = ValidateModelRequest(
|
||||
model_platform="OPENAI",
|
||||
model_type="GPT_4O_MINI"
|
||||
)
|
||||
|
||||
request_data = ValidateModelRequest(model_platform="openai", model_type="gpt-4o")
|
||||
|
||||
mock_agent = MagicMock()
|
||||
mock_response = MagicMock()
|
||||
mock_response.info = {
|
||||
"tool_calls": [] # Empty tool calls
|
||||
}
|
||||
mock_agent.step.return_value = mock_response
|
||||
|
||||
|
||||
with patch("app.controller.model_controller.create_agent", return_value=mock_agent):
|
||||
# Should handle empty tool calls gracefully
|
||||
result = await validate_model(request_data)
|
||||
|
|
@ -268,18 +249,53 @@ class TestModelControllerErrorCases:
|
|||
@pytest.mark.asyncio
|
||||
async def test_validate_model_with_missing_info_field(self):
|
||||
"""Test model validation with missing info field in response."""
|
||||
request_data = ValidateModelRequest(
|
||||
model_platform="OPENAI",
|
||||
model_type="GPT_4O_MINI"
|
||||
)
|
||||
|
||||
request_data = ValidateModelRequest(model_platform="openai", model_type="gpt-4o")
|
||||
|
||||
mock_agent = MagicMock()
|
||||
mock_response = MagicMock()
|
||||
mock_response.info = {} # Missing tool_calls
|
||||
mock_agent.step.return_value = mock_response
|
||||
|
||||
|
||||
with patch("app.controller.model_controller.create_agent", return_value=mock_agent):
|
||||
# Should handle missing tool_calls key gracefully
|
||||
result = await validate_model(request_data)
|
||||
assert result.is_valid is True # Response exists
|
||||
assert result.is_tool_calls is False # No tool_calls key
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_model_empty_api_key(self):
|
||||
"""Test model validation with empty API key."""
|
||||
request_data = ValidateModelRequest(
|
||||
model_platform="openai",
|
||||
model_type="gpt-4o",
|
||||
api_key="", # Empty API key
|
||||
)
|
||||
|
||||
response = await validate_model(request_data)
|
||||
|
||||
assert response.is_valid is False
|
||||
assert response.is_tool_calls is False
|
||||
assert response.message == "Invalid key. Validation failed."
|
||||
assert response.error_code == "invalid_api_key"
|
||||
assert response.error is not None
|
||||
assert response.error["message"] == "Invalid key. Validation failed."
|
||||
assert response.error["type"] == "invalid_request_error"
|
||||
assert response.error["code"] == "invalid_api_key"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_model_invalid_model_type(self):
|
||||
"""Test model validation with invalid model type."""
|
||||
request_data = ValidateModelRequest(
|
||||
model_platform="openai", model_type="INVALID_MODEL_TYPE", api_key="test_key"
|
||||
)
|
||||
|
||||
response = await validate_model(request_data)
|
||||
assert response.is_valid is False
|
||||
assert response.is_tool_calls is False
|
||||
assert response.message == "Invalid model name. Validation failed."
|
||||
assert response.error_code is not None
|
||||
assert "model_not_found" in response.error_code
|
||||
assert response.error is not None
|
||||
assert response.error["message"] == "Invalid model name. Validation failed."
|
||||
assert response.error["type"] == "invalid_request_error"
|
||||
assert response.error["code"] == "model_not_found"
|
||||
|
|
|
|||
|
|
@ -672,7 +672,7 @@ class TestAgentFactoryFunctions:
|
|||
patch('app.utils.agent.TwitterToolkit') as mock_twitter_toolkit, \
|
||||
patch('app.utils.agent.LinkedInToolkit') as mock_linkedin_toolkit, \
|
||||
patch('app.utils.agent.RedditToolkit') as mock_reddit_toolkit, \
|
||||
patch('app.utils.agent.NotionMCPToolkit') as mock_notion_toolkit, \
|
||||
patch('app.utils.agent.NotionMCPToolkit') as mock_notion_mcp_toolkit, \
|
||||
patch('app.utils.agent.GoogleGmailMCPToolkit') as mock_gmail_toolkit, \
|
||||
patch('app.utils.agent.GoogleCalendarToolkit') as mock_calendar_toolkit, \
|
||||
patch('app.utils.agent.HumanToolkit') as mock_human_toolkit, \
|
||||
|
|
@ -684,7 +684,7 @@ class TestAgentFactoryFunctions:
|
|||
mock_twitter_toolkit.get_can_use_tools.return_value = []
|
||||
mock_linkedin_toolkit.get_can_use_tools.return_value = []
|
||||
mock_reddit_toolkit.get_can_use_tools.return_value = []
|
||||
mock_notion_toolkit.get_can_use_tools = AsyncMock(return_value=[])
|
||||
mock_notion_mcp_toolkit.get_can_use_tools = AsyncMock(return_value=[])
|
||||
mock_gmail_toolkit.get_can_use_tools = AsyncMock(return_value=[])
|
||||
mock_calendar_toolkit.get_can_use_tools.return_value = []
|
||||
mock_human_toolkit.get_can_use_tools.return_value = []
|
||||
|
|
|
|||
866
backend/uv.lock
generated
|
|
@ -445,10 +445,10 @@ export class FileReader {
|
|||
if (type === 'md') {
|
||||
const content = fs.readFileSync(filePath, 'utf-8')
|
||||
resolve(content)
|
||||
} else if (isShowSourceCode && type === 'html') {
|
||||
} else if (type === 'html') {
|
||||
const content = fs.readFileSync(filePath, 'utf-8')
|
||||
resolve(content)
|
||||
} else if (["pdf", "html"].includes(type)) {
|
||||
} else if (["pdf"].includes(type)) {
|
||||
resolve(filePath)
|
||||
} else if (type === "csv") {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import path from 'node:path'
|
|||
import os, { homedir } from 'node:os'
|
||||
import log from 'electron-log'
|
||||
import { update, registerUpdateIpcHandlers } from './update'
|
||||
import { checkToolInstalled, installDependencies, killProcessOnPort, startBackend } from './init'
|
||||
import { checkToolInstalled, killProcessOnPort, startBackend } from './init'
|
||||
import { WebViewManager } from './webview'
|
||||
import { FileReader } from './fileReader'
|
||||
import { ChildProcessWithoutNullStreams } from 'node:child_process'
|
||||
|
|
@ -18,9 +18,10 @@ import kill from 'tree-kill';
|
|||
import { zipFolder } from './utils/log'
|
||||
import axios from 'axios';
|
||||
import FormData from 'form-data';
|
||||
import { checkAndInstallDepsOnUpdate, PromiseReturnType, getInstallationStatus } from './install-deps'
|
||||
import { isBinaryExists, getBackendPath, getVenvPath } from './utils/process'
|
||||
|
||||
const userData = app.getPath('userData');
|
||||
const versionFile = path.join(userData, 'version.txt');
|
||||
|
||||
// ==================== constants ====================
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
|
@ -50,69 +51,6 @@ findAvailablePort(browser_port).then(port => {
|
|||
app.commandLine.appendSwitch('remote-debugging-port', port + '');
|
||||
});
|
||||
|
||||
// Read last run version and install dependencies on update
|
||||
async function checkAndInstallDepsOnUpdate(): Promise<boolean> {
|
||||
const currentVersion = app.getVersion();
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
log.info(' start check version', { currentVersion });
|
||||
|
||||
// Check if version file exists
|
||||
const versionExists = fs.existsSync(versionFile);
|
||||
let savedVersion = '';
|
||||
|
||||
if (versionExists) {
|
||||
savedVersion = fs.readFileSync(versionFile, 'utf-8').trim();
|
||||
log.info(' read saved version', { savedVersion });
|
||||
} else {
|
||||
log.info(' version file not exist, will create new file');
|
||||
}
|
||||
|
||||
// If version file does not exist or version does not match, reinstall dependencies
|
||||
if (!versionExists || savedVersion !== currentVersion) {
|
||||
log.info(' version changed, prepare to reinstall uv dependencies...', {
|
||||
currentVersion,
|
||||
savedVersion: versionExists ? savedVersion : 'none',
|
||||
reason: !versionExists ? 'version file not exist' : 'version not match'
|
||||
});
|
||||
|
||||
// Notify frontend to update
|
||||
if (win && !win.isDestroyed()) {
|
||||
win.webContents.send('update-notification', {
|
||||
type: 'version-update',
|
||||
currentVersion,
|
||||
previousVersion: versionExists ? savedVersion : 'none',
|
||||
reason: !versionExists ? 'version file not exist' : 'version not match'
|
||||
});
|
||||
}
|
||||
|
||||
// Update version file
|
||||
fs.writeFileSync(versionFile, currentVersion);
|
||||
log.info(' version file updated', { currentVersion });
|
||||
|
||||
// Install dependencies
|
||||
const result = await installDependencies();
|
||||
if (!result) {
|
||||
log.error(' install dependencies failed');
|
||||
resolve(false);
|
||||
return
|
||||
}
|
||||
resolve(true);
|
||||
log.info(' install dependencies complete');
|
||||
return
|
||||
} else {
|
||||
log.info(' version not changed, skip install dependencies', { currentVersion });
|
||||
resolve(true);
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(' check version and install dependencies error:', error);
|
||||
resolve(false);
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== app config ====================
|
||||
process.env.APP_ROOT = MAIN_DIST;
|
||||
process.env.VITE_PUBLIC = VITE_PUBLIC;
|
||||
|
|
@ -259,51 +197,6 @@ const checkManagerInstance = (manager: any, name: string) => {
|
|||
return manager;
|
||||
};
|
||||
|
||||
export const handleDependencyInstallation = async () => {
|
||||
try {
|
||||
log.info(' start install dependencies...');
|
||||
|
||||
const isSuccess = await installDependencies();
|
||||
if (!isSuccess) {
|
||||
log.error(' install dependencies failed');
|
||||
return { success: false, error: 'install dependencies failed' };
|
||||
}
|
||||
|
||||
log.info(' install dependencies success, check tool installed status...');
|
||||
const isToolInstalled = await checkToolInstalled();
|
||||
log.info('isToolInstalled && !python_process', isToolInstalled && !python_process);
|
||||
if (isToolInstalled && !python_process) {
|
||||
log.info(' tool installed, start backend service...');
|
||||
python_process = await startBackend((port) => {
|
||||
backendPort = port;
|
||||
log.info(' backend service start success', { port });
|
||||
});
|
||||
|
||||
// Notify frontend to install success
|
||||
if (win && !win.isDestroyed()) {
|
||||
win.webContents.send('install-dependencies-complete', { success: true, code: 0 });
|
||||
}
|
||||
|
||||
python_process?.on('exit', (code, signal) => {
|
||||
log.info(' python process exit', { code, signal });
|
||||
});
|
||||
} else if (!isToolInstalled) {
|
||||
log.warn(' tool not installed, skip backend start');
|
||||
} else {
|
||||
log.info(' backend process already exist, skip start');
|
||||
}
|
||||
|
||||
log.info(' install dependencies complete');
|
||||
return { success: true };
|
||||
} catch (error: any) {
|
||||
log.error(' install dependencies error:', error);
|
||||
if (win && !win.isDestroyed()) {
|
||||
win.webContents.send('install-dependencies-complete', { success: false, code: 2 });
|
||||
}
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
};
|
||||
|
||||
function registerIpcHandlers() {
|
||||
// ==================== basic info handler ====================
|
||||
ipcMain.handle('get-browser-port', () => {
|
||||
|
|
@ -934,13 +827,35 @@ function registerIpcHandlers() {
|
|||
});
|
||||
|
||||
// ==================== dependency install handler ====================
|
||||
ipcMain.handle('install-dependencies', handleDependencyInstallation);
|
||||
ipcMain.handle('frontend-ready', handleDependencyInstallation);
|
||||
ipcMain.handle('install-dependencies', async () => {
|
||||
try {
|
||||
if(win === null) throw new Error("Window is null");
|
||||
//Force installation even if versionFile exists
|
||||
const isInstalled = await checkAndInstallDepsOnUpdate({win, forceInstall: true});
|
||||
return { success: true, isInstalled };
|
||||
} catch (error) {
|
||||
return { success: false, error: (error as Error).message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('check-tool-installed', async () => {
|
||||
try {
|
||||
const isInstalled = await checkToolInstalled();
|
||||
return { success: true, isInstalled };
|
||||
return { success: true, isInstalled: isInstalled.success };
|
||||
} catch (error) {
|
||||
return { success: false, error: (error as Error).message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('get-installation-status', async () => {
|
||||
try {
|
||||
const { isInstalling, hasLockFile } = await getInstallationStatus();
|
||||
return {
|
||||
success: true,
|
||||
isInstalling,
|
||||
hasLockFile,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
} catch (error) {
|
||||
return { success: false, error: (error as Error).message };
|
||||
}
|
||||
|
|
@ -950,10 +865,34 @@ function registerIpcHandlers() {
|
|||
registerUpdateIpcHandlers();
|
||||
}
|
||||
|
||||
// ==================== ensure eigent directories ====================
|
||||
const ensureEigentDirectories = () => {
|
||||
const eigentBase = path.join(os.homedir(), '.eigent');
|
||||
const requiredDirs = [
|
||||
eigentBase,
|
||||
path.join(eigentBase, 'bin'),
|
||||
path.join(eigentBase, 'cache'),
|
||||
path.join(eigentBase, 'venvs'),
|
||||
path.join(eigentBase, 'runtime'),
|
||||
];
|
||||
|
||||
for (const dir of requiredDirs) {
|
||||
if (!fs.existsSync(dir)) {
|
||||
log.info(`Creating directory: ${dir}`);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
log.info('.eigent directory structure ensured');
|
||||
};
|
||||
|
||||
// ==================== window create ====================
|
||||
async function createWindow() {
|
||||
const isMac = process.platform === 'darwin';
|
||||
|
||||
// Ensure .eigent directories exist before anything else
|
||||
ensureEigentDirectories();
|
||||
|
||||
win = new BrowserWindow({
|
||||
title: 'Eigent',
|
||||
width: 1200,
|
||||
|
|
@ -988,14 +927,6 @@ async function createWindow() {
|
|||
webViewManager.createWebview(i === 1 ? undefined : i.toString());
|
||||
}
|
||||
|
||||
// ==================== load content ====================
|
||||
if (VITE_DEV_SERVER_URL) {
|
||||
win.loadURL(VITE_DEV_SERVER_URL);
|
||||
win.webContents.openDevTools();
|
||||
} else {
|
||||
win.loadFile(indexHtml);
|
||||
}
|
||||
|
||||
// ==================== set event listeners ====================
|
||||
setupWindowEventListeners();
|
||||
setupDevToolsShortcuts();
|
||||
|
|
@ -1005,13 +936,175 @@ async function createWindow() {
|
|||
// ==================== auto update ====================
|
||||
update(win);
|
||||
|
||||
// ==================== check tool installed ====================
|
||||
let res = await checkAndInstallDepsOnUpdate();
|
||||
if (!res) {
|
||||
log.info('checkAndInstallDepsOnUpdate,install dependencies failed');
|
||||
win.webContents.send('install-dependencies-complete', { success: false, code: 2 });
|
||||
// ==================== CHECK IF INSTALLATION IS NEEDED BEFORE LOADING CONTENT ====================
|
||||
log.info('Pre-checking if dependencies need to be installed...');
|
||||
|
||||
// Check version and tools status synchronously
|
||||
const currentVersion = app.getVersion();
|
||||
const versionFile = path.join(app.getPath('userData'), 'version.txt');
|
||||
const versionExists = fs.existsSync(versionFile);
|
||||
let savedVersion = '';
|
||||
if (versionExists) {
|
||||
savedVersion = fs.readFileSync(versionFile, 'utf-8').trim();
|
||||
}
|
||||
|
||||
const uvExists = await isBinaryExists('uv');
|
||||
const bunExists = await isBinaryExists('bun');
|
||||
|
||||
// Check if installation was previously completed
|
||||
const backendPath = getBackendPath();
|
||||
const installedLockPath = path.join(backendPath, 'uv_installed.lock');
|
||||
const installationCompleted = fs.existsSync(installedLockPath);
|
||||
|
||||
// Check if venv path exists for current version
|
||||
const venvPath = getVenvPath(currentVersion);
|
||||
const venvExists = fs.existsSync(venvPath);
|
||||
|
||||
const needsInstallation = !versionExists || savedVersion !== currentVersion || !uvExists || !bunExists || !installationCompleted || !venvExists;
|
||||
|
||||
log.info('Installation check result:', {
|
||||
needsInstallation,
|
||||
versionExists,
|
||||
versionMatch: savedVersion === currentVersion,
|
||||
uvExists,
|
||||
bunExists,
|
||||
installationCompleted,
|
||||
venvExists,
|
||||
venvPath
|
||||
});
|
||||
|
||||
// Handle localStorage based on installation state
|
||||
if (needsInstallation) {
|
||||
log.info('Installation needed - clearing auth storage to force carousel state');
|
||||
|
||||
// Clear the persisted auth storage file to force fresh initialization with carousel
|
||||
const localStoragePath = path.join(app.getPath('userData'), 'Local Storage');
|
||||
const leveldbPath = path.join(localStoragePath, 'leveldb');
|
||||
|
||||
try {
|
||||
// Delete the localStorage database to force fresh init
|
||||
if (fs.existsSync(leveldbPath)) {
|
||||
log.info('Removing localStorage database to force fresh state...');
|
||||
fs.rmSync(leveldbPath, { recursive: true, force: true });
|
||||
log.info('Successfully cleared localStorage');
|
||||
}
|
||||
} catch (error) {
|
||||
log.error('Error clearing localStorage:', error);
|
||||
}
|
||||
|
||||
// Set up the injection for when page loads
|
||||
win.webContents.once('dom-ready', () => {
|
||||
if (!win || win.isDestroyed()) {
|
||||
log.warn('Window destroyed before DOM ready - skipping localStorage injection');
|
||||
return;
|
||||
}
|
||||
log.info('DOM ready - creating auth-storage with carousel state');
|
||||
win.webContents.executeJavaScript(`
|
||||
(function() {
|
||||
try {
|
||||
// Create fresh auth storage with carousel state
|
||||
const newAuthStorage = {
|
||||
state: {
|
||||
token: null,
|
||||
username: null,
|
||||
email: null,
|
||||
user_id: null,
|
||||
appearance: 'light',
|
||||
language: 'system',
|
||||
isFirstLaunch: true,
|
||||
modelType: 'cloud',
|
||||
cloud_model_type: 'gpt-4.1',
|
||||
initState: 'carousel',
|
||||
share_token: null,
|
||||
workerListData: {}
|
||||
},
|
||||
version: 0
|
||||
};
|
||||
localStorage.setItem('auth-storage', JSON.stringify(newAuthStorage));
|
||||
console.log('[ELECTRON PRE-INJECT] Created fresh auth-storage with carousel state');
|
||||
} catch (e) {
|
||||
console.error('[ELECTRON PRE-INJECT] Failed to create storage:', e);
|
||||
}
|
||||
})();
|
||||
`).catch(err => {
|
||||
log.error('Failed to inject script:', err);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Installation is complete - ensure initState is set to 'done'
|
||||
log.info('Installation already complete - ensuring initState is done');
|
||||
|
||||
win.webContents.once('dom-ready', () => {
|
||||
if (!win || win.isDestroyed()) {
|
||||
log.warn('Window destroyed before DOM ready - skipping localStorage update');
|
||||
return;
|
||||
}
|
||||
log.info('DOM ready - checking and updating auth-storage to done state');
|
||||
win.webContents.executeJavaScript(`
|
||||
(function() {
|
||||
try {
|
||||
const authStorage = localStorage.getItem('auth-storage');
|
||||
if (authStorage) {
|
||||
const parsed = JSON.parse(authStorage);
|
||||
if (parsed.state && parsed.state.initState !== 'done') {
|
||||
console.log('[ELECTRON] Updating initState from', parsed.state.initState, 'to done');
|
||||
// Only update the initState field, preserve all other data
|
||||
const updatedStorage = {
|
||||
...parsed,
|
||||
state: {
|
||||
...parsed.state,
|
||||
initState: 'done'
|
||||
}
|
||||
};
|
||||
localStorage.setItem('auth-storage', JSON.stringify(updatedStorage));
|
||||
console.log('[ELECTRON] initState updated to done, reloading page...');
|
||||
return true; // Signal that we need to reload
|
||||
}
|
||||
}
|
||||
return false; // No reload needed
|
||||
} catch (e) {
|
||||
console.error('[ELECTRON] Failed to update initState:', e);
|
||||
// Don't modify localStorage if there's an error to prevent data corruption
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
`).then(needsReload => {
|
||||
if (needsReload) {
|
||||
log.info('Reloading window after localStorage update');
|
||||
win!.reload();
|
||||
}
|
||||
}).catch(err => {
|
||||
log.error('Failed to inject script:', err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Load content
|
||||
if (VITE_DEV_SERVER_URL) {
|
||||
win.loadURL(VITE_DEV_SERVER_URL);
|
||||
win.webContents.openDevTools();
|
||||
} else {
|
||||
win.loadFile(indexHtml);
|
||||
}
|
||||
|
||||
// Wait for window to be ready
|
||||
await new Promise<void>(resolve => {
|
||||
win!.webContents.once('did-finish-load', () => {
|
||||
log.info('Window content loaded, starting dependency check immediately...');
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
// Now check and install dependencies
|
||||
let res:PromiseReturnType = await checkAndInstallDepsOnUpdate({ win });
|
||||
if (!res.success) {
|
||||
log.info("[DEPS INSTALL] Dependency Error: ", res.message);
|
||||
win.webContents.send('install-dependencies-complete', { success: false, code: 2, error: res.message });
|
||||
return;
|
||||
}
|
||||
log.info("[DEPS INSTALL] Dependency Success: ", res.message);
|
||||
|
||||
// Start backend after dependencies are ready
|
||||
await checkAndStartBackend();
|
||||
}
|
||||
|
||||
|
|
@ -1069,27 +1162,30 @@ const setupExternalLinkHandling = () => {
|
|||
// ==================== check and start backend ====================
|
||||
const checkAndStartBackend = async () => {
|
||||
log.info('Checking and starting backend service...');
|
||||
try {
|
||||
const isToolInstalled = await checkToolInstalled();
|
||||
if (isToolInstalled.success) {
|
||||
log.info('Tool installed, starting backend service...');
|
||||
|
||||
const isToolInstalled = await checkToolInstalled();
|
||||
if (isToolInstalled) {
|
||||
log.info('Tool installed, starting backend service...');
|
||||
// Notify frontend installation success
|
||||
if (win && !win.isDestroyed()) {
|
||||
win.webContents.send('install-dependencies-complete', { success: true, code: 0 });
|
||||
}
|
||||
|
||||
// Notify frontend installation success
|
||||
if (win && !win.isDestroyed()) {
|
||||
win.webContents.send('install-dependencies-complete', { success: true, code: 0 });
|
||||
python_process = await startBackend((port) => {
|
||||
backendPort = port;
|
||||
log.info('Backend service started successfully', { port });
|
||||
});
|
||||
|
||||
python_process?.on('exit', (code, signal) => {
|
||||
|
||||
log.info('Python process exited', { code, signal });
|
||||
});
|
||||
} else {
|
||||
log.warn('Tool not installed, cannot start backend service');
|
||||
}
|
||||
|
||||
python_process = await startBackend((port) => {
|
||||
backendPort = port;
|
||||
log.info('Backend service started successfully', { port });
|
||||
});
|
||||
|
||||
python_process?.on('exit', (code, signal) => {
|
||||
|
||||
log.info('Python process exited', { code, signal });
|
||||
});
|
||||
} else {
|
||||
log.warn('Tool not installed, cannot start backend service');
|
||||
} catch (error) {
|
||||
log.debug("Cannot Start Backend due to ", error)
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { getBackendPath, getBinaryPath, getCachePath, isBinaryExists, runInstallScript } from "./utils/process";
|
||||
import { getBackendPath, getBinaryPath, getCachePath, getVenvPath, isBinaryExists, runInstallScript } from "./utils/process";
|
||||
import { spawn, exec } from 'child_process'
|
||||
import log from 'electron-log'
|
||||
import fs from 'fs'
|
||||
|
|
@ -6,72 +6,30 @@ import path from 'path'
|
|||
import * as net from "net";
|
||||
import { ipcMain, BrowserWindow, app } from 'electron'
|
||||
import { promisify } from 'util'
|
||||
import { detectInstallationLogs, PromiseReturnType } from "./install-deps";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
// helper function to get main window
|
||||
function getMainWindow(): BrowserWindow | null {
|
||||
export function getMainWindow(): BrowserWindow | null {
|
||||
const windows = BrowserWindow.getAllWindows();
|
||||
return windows.length > 0 ? windows[0] : null;
|
||||
}
|
||||
|
||||
|
||||
export async function checkToolInstalled() {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
return new Promise<PromiseReturnType>(async (resolve, reject) => {
|
||||
if (!(await isBinaryExists('uv'))) {
|
||||
resolve(false)
|
||||
resolve({success: false, message: "uv doesn't exist"})
|
||||
return
|
||||
}
|
||||
|
||||
if (!(await isBinaryExists('bun'))) {
|
||||
resolve(false)
|
||||
resolve({success: false, message: "Bun doesn't exist"})
|
||||
return
|
||||
}
|
||||
|
||||
resolve(true)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if command line tools are installed, install if not
|
||||
*/
|
||||
export async function installCommandTool() {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const ensureInstalled = async (toolName: 'uv' | 'bun', scriptName: string): Promise<boolean> => {
|
||||
if (await isBinaryExists(toolName)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log(`start install ${toolName}`);
|
||||
await runInstallScript(scriptName);
|
||||
const installed = await isBinaryExists(toolName);
|
||||
|
||||
const mainWindow = getMainWindow();
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
if (installed) {
|
||||
mainWindow.webContents.send('install-dependencies-log', {
|
||||
type: 'stdout',
|
||||
data: `${toolName} installed successfully`,
|
||||
});
|
||||
} else {
|
||||
mainWindow.webContents.send('install-dependencies-complete', {
|
||||
success: false,
|
||||
code: 2,
|
||||
error: `${toolName} installation failed (script exit code 2)`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return installed;
|
||||
};
|
||||
|
||||
if (!(await ensureInstalled('uv', 'install-uv.js'))) {
|
||||
return reject("uv install failed");
|
||||
}
|
||||
if (!(await ensureInstalled('bun', 'install-bun.js'))) {
|
||||
return reject("bun install failed");
|
||||
}
|
||||
|
||||
return resolve(true);
|
||||
resolve({success: true, message: "Tools exist already"})
|
||||
})
|
||||
|
||||
}
|
||||
|
|
@ -157,131 +115,16 @@ export async function installCommandTool() {
|
|||
// })
|
||||
// })
|
||||
// }
|
||||
export async function installDependencies() {
|
||||
return new Promise<boolean>(async (resolve, reject) => {
|
||||
console.log('start install dependencies')
|
||||
|
||||
const mainWindow = getMainWindow();
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send('install-dependencies-start');
|
||||
}
|
||||
|
||||
const isInstalCommandTool = await installCommandTool()
|
||||
if (!isInstalCommandTool) {
|
||||
resolve(false)
|
||||
return
|
||||
}
|
||||
|
||||
const uv_path = await getBinaryPath('uv')
|
||||
const backendPath = getBackendPath()
|
||||
|
||||
if (!fs.existsSync(backendPath)) {
|
||||
fs.mkdirSync(backendPath, { recursive: true })
|
||||
}
|
||||
|
||||
const installingLockPath = path.join(backendPath, 'uv_installing.lock')
|
||||
fs.writeFileSync(installingLockPath, '')
|
||||
|
||||
const installedLockPath = path.join(backendPath, 'uv_installed.lock')
|
||||
// const proxyArgs = ['--default-index', 'https://pypi.tuna.tsinghua.edu.cn/simple']
|
||||
const proxyArgs = ['--default-index', 'https://mirrors.aliyun.com/pypi/simple/']
|
||||
const runInstall = (extraArgs: string[]) => {
|
||||
return new Promise<boolean>((resolveInner, rejectInner) => {
|
||||
try {
|
||||
const node_process = spawn(uv_path, [
|
||||
'sync',
|
||||
'--no-dev',
|
||||
'--cache-dir', getCachePath('uv_cache'),
|
||||
...extraArgs], {
|
||||
cwd: backendPath,
|
||||
env: {
|
||||
...process.env,
|
||||
UV_TOOL_DIR: getCachePath('uv_tool'),
|
||||
UV_PYTHON_INSTALL_DIR: getCachePath('uv_python'),
|
||||
}
|
||||
})
|
||||
console.log('start install dependencies', extraArgs)
|
||||
node_process.stdout.on('data', (data) => {
|
||||
|
||||
log.info(`Script output: ${data}`)
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send('install-dependencies-log', { type: 'stdout', data: data.toString() });
|
||||
}
|
||||
})
|
||||
|
||||
node_process.stderr.on('data', (data) => {
|
||||
log.error(`Script error: ${data}`)
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send('install-dependencies-log', { type: 'stderr', data: data.toString() });
|
||||
}
|
||||
})
|
||||
|
||||
node_process.on('close', (code) => {
|
||||
console.log('install dependencies end', code === 0)
|
||||
resolveInner(code === 0)
|
||||
})
|
||||
}catch(err) {
|
||||
log.error('run install failed', err)
|
||||
// Clean up uv_installing.lock file if installation fails
|
||||
if (fs.existsSync(installingLockPath)) {
|
||||
fs.unlinkSync(installingLockPath);
|
||||
}
|
||||
rejectInner(err)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
// try default install
|
||||
const installSuccess = await runInstall([])
|
||||
|
||||
if (installSuccess) {
|
||||
fs.unlinkSync(installingLockPath)
|
||||
fs.writeFileSync(installedLockPath, '')
|
||||
log.info('Script completed successfully')
|
||||
console.log('end install dependencies')
|
||||
spawn(uv_path, ['run', 'task', 'babel'], { cwd: backendPath })
|
||||
resolve(true)
|
||||
return
|
||||
}
|
||||
|
||||
// try mirror install
|
||||
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
let mirrorInstallSuccess = false
|
||||
|
||||
if (timezone === 'Asia/Shanghai') {
|
||||
mirrorInstallSuccess = await runInstall(proxyArgs)
|
||||
} else {
|
||||
mirrorInstallSuccess = await runInstall([])
|
||||
}
|
||||
|
||||
|
||||
fs.existsSync(installingLockPath) && fs.unlinkSync(installingLockPath)
|
||||
|
||||
if (mirrorInstallSuccess) {
|
||||
fs.writeFileSync(installedLockPath, '')
|
||||
log.info('Mirror script completed successfully')
|
||||
console.log('end install dependencies (mirror)')
|
||||
spawn(uv_path, ['run', 'task', 'babel'], { cwd: backendPath })
|
||||
resolve(true)
|
||||
} else {
|
||||
log.error('Both default and mirror install failed')
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send('install-dependencies-complete', { success: false, error: 'Both default and mirror install failed' });
|
||||
}
|
||||
resolve(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
export async function startBackend(setPort?: (port: number) => void): Promise<any> {
|
||||
console.log('start fastapi')
|
||||
const uv_path = await getBinaryPath('uv')
|
||||
const backendPath = getBackendPath()
|
||||
const userData = app.getPath('userData');
|
||||
const currentVersion = app.getVersion();
|
||||
const venvPath = getVenvPath(currentVersion);
|
||||
console.log('userData', userData)
|
||||
console.log('Using venv path:', venvPath)
|
||||
// Try to find an available port, with aggressive cleanup if needed
|
||||
let port: number;
|
||||
const portFile = path.join(userData, 'port.txt');
|
||||
|
|
@ -310,16 +153,26 @@ export async function startBackend(setPort?: (port: number) => void): Promise<an
|
|||
setPort(port);
|
||||
}
|
||||
|
||||
const npmCacheDir = path.join(venvPath, '.npm-cache');
|
||||
if (!fs.existsSync(npmCacheDir)) {
|
||||
fs.mkdirSync(npmCacheDir, { recursive: true });
|
||||
}
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
SERVER_URL: "https://dev.eigent.ai/api",
|
||||
PYTHONIOENCODING: 'utf-8'
|
||||
PYTHONIOENCODING: 'utf-8',
|
||||
UV_PROJECT_ENVIRONMENT: venvPath,
|
||||
npm_config_cache: npmCacheDir,
|
||||
}
|
||||
|
||||
//Redirect output
|
||||
const displayFilteredLogs = (data:String) => {
|
||||
const displayFilteredLogs = (data: String) => {
|
||||
if (!data) return;
|
||||
const msg = data.toString().trimEnd();
|
||||
//Detect if uv sync is run
|
||||
detectInstallationLogs(msg);
|
||||
|
||||
if (msg.toLowerCase().includes("error") || msg.toLowerCase().includes("traceback")) {
|
||||
log.error(`BACKEND: ${msg}`);
|
||||
} else if (msg.toLowerCase().includes("warn")) {
|
||||
|
|
@ -333,6 +186,7 @@ export async function startBackend(setPort?: (port: number) => void): Promise<an
|
|||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
//Implicitly runs uv sync
|
||||
const node_process = spawn(
|
||||
uv_path,
|
||||
["run", "uvicorn", "main:api", "--port", port.toString(), "--loop", "asyncio"],
|
||||
|
|
|
|||
672
electron/main/install-deps.ts
Normal file
|
|
@ -0,0 +1,672 @@
|
|||
import { app, BrowserWindow } from 'electron'
|
||||
import path from 'node:path'
|
||||
import log from 'electron-log'
|
||||
import { getMainWindow } from './init'
|
||||
import fs from 'node:fs'
|
||||
import { getBackendPath, getBinaryPath, getCachePath, getVenvPath, cleanupOldVenvs, isBinaryExists, runInstallScript } from './utils/process'
|
||||
import { spawn } from 'child_process'
|
||||
import { safeMainWindowSend } from './utils/safeWebContentsSend'
|
||||
|
||||
const userData = app.getPath('userData');
|
||||
const versionFile = path.join(userData, 'version.txt');
|
||||
|
||||
export type PromiseReturnType = {
|
||||
message: string;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
interface checkInstallProps {
|
||||
win:BrowserWindow|null;
|
||||
forceInstall?:boolean
|
||||
}
|
||||
// Read last run version and install dependencies on update
|
||||
export const checkAndInstallDepsOnUpdate = async ({win, forceInstall=false}:checkInstallProps):
|
||||
Promise<PromiseReturnType> => {
|
||||
const currentVersion = app.getVersion();
|
||||
let savedVersion = '';
|
||||
const checkInstallOperations = {
|
||||
getSavedVersion: ():boolean => {
|
||||
// Check if version file exists
|
||||
const versionExists = fs.existsSync(versionFile);
|
||||
if (versionExists) {
|
||||
log.info('[DEPS INSTALL] start check version', { currentVersion });
|
||||
savedVersion = fs.readFileSync(versionFile, 'utf-8').trim();
|
||||
log.info('[DEPS INSTALL] read saved version', { savedVersion });
|
||||
} else {
|
||||
log.info('[DEPS INSTALL] version file not exist, will create new file');
|
||||
}
|
||||
return versionExists;
|
||||
},
|
||||
handleUpdateNotification: (versionExists:boolean) => {
|
||||
if (win && !win.isDestroyed()) {
|
||||
win.webContents.send('update-notification', {
|
||||
type: 'version-update',
|
||||
currentVersion,
|
||||
previousVersion: versionExists ? savedVersion : 'none',
|
||||
reason: !versionExists ? 'version file not exist' : 'version not match'
|
||||
});
|
||||
} else {
|
||||
log.warn('[DEPS INSTALL] Cannot send update notification - window not available');
|
||||
}
|
||||
},
|
||||
createVersionFile: () => {
|
||||
fs.writeFileSync(versionFile, currentVersion);
|
||||
log.info('[DEPS INSTALL] version file updated', { currentVersion });
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const versionExists:boolean = checkInstallOperations.getSavedVersion();
|
||||
|
||||
// Check if command tools are installed
|
||||
const uvExists = await isBinaryExists('uv');
|
||||
const bunExists = await isBinaryExists('bun');
|
||||
const toolsMissing = !uvExists || !bunExists;
|
||||
|
||||
// If version file does not exist or version does not match, reinstall dependencies
|
||||
// Or if command tools are missing, need to install them
|
||||
if (forceInstall || !versionExists || savedVersion !== currentVersion || toolsMissing) {
|
||||
if (toolsMissing) {
|
||||
log.info('[DEPS INSTALL] Command tools missing, starting installation...', {
|
||||
uvExists,
|
||||
bunExists
|
||||
});
|
||||
} else {
|
||||
log.info('[DEPS INSTALL] version changed, prepare to reinstall uv dependencies...', {
|
||||
currentVersion,
|
||||
savedVersion: versionExists ? savedVersion : 'none',
|
||||
reason: !versionExists ? 'version file not exist' : 'version not match'
|
||||
});
|
||||
}
|
||||
|
||||
// Notify frontend to update
|
||||
checkInstallOperations.handleUpdateNotification(versionExists);
|
||||
|
||||
// Install dependencies (version.txt will be updated AFTER successful install)
|
||||
const result = await installDependencies(currentVersion);
|
||||
if (!result.success) {
|
||||
log.error(' install dependencies failed');
|
||||
resolve({ message: `Install dependencies failed, msg ${result.message}`, success: false });
|
||||
return
|
||||
}
|
||||
|
||||
// Update version file ONLY after successful installation
|
||||
checkInstallOperations.createVersionFile();
|
||||
|
||||
resolve({ message: "Dependencies installed successfully after update", success: true });
|
||||
log.info('[DEPS INSTALL] install dependencies complete');
|
||||
return
|
||||
} else {
|
||||
log.info('[DEPS INSTALL] version not changed and tools installed, skip install dependencies', { currentVersion });
|
||||
resolve({ message: "Version not changed and tools installed, skipped installation", success: true });
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(' check version and install dependencies error:', error);
|
||||
resolve({ message: `Error checking version: ${error}`, success: false });
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if command line tools are installed, install if not
|
||||
*/
|
||||
export async function installCommandTool(): Promise<PromiseReturnType> {
|
||||
try {
|
||||
const ensureInstalled = async (toolName: 'uv' | 'bun', scriptName: string): Promise<PromiseReturnType> => {
|
||||
if (await isBinaryExists(toolName)) {
|
||||
return { message: `${toolName} already installed`, success: true };
|
||||
}
|
||||
|
||||
console.log(`start install ${toolName}`);
|
||||
await runInstallScript(scriptName);
|
||||
const installed = await isBinaryExists(toolName);
|
||||
|
||||
if (installed) {
|
||||
safeMainWindowSend('install-dependencies-log', {
|
||||
type: 'stdout',
|
||||
data: `${toolName} installed successfully`,
|
||||
});
|
||||
} else {
|
||||
safeMainWindowSend('install-dependencies-complete', {
|
||||
success: false,
|
||||
code: 2,
|
||||
error: `${toolName} installation failed (script exit code 2)`,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
message: installed ? `${toolName} installed successfully` : `${toolName} installation failed`,
|
||||
success: installed
|
||||
};
|
||||
};
|
||||
|
||||
const uvResult = await ensureInstalled('uv', 'install-uv.js');
|
||||
if (!uvResult.success) {
|
||||
return { message: uvResult.message, success: false };
|
||||
}
|
||||
|
||||
const bunResult = await ensureInstalled('bun', 'install-bun.js');
|
||||
if (!bunResult.success) {
|
||||
return { message: bunResult.message, success: false };
|
||||
}
|
||||
|
||||
return { message: "Command tools installed successfully", success: true };
|
||||
} catch (error) {
|
||||
return { message: `Command tool installation failed: ${error}`, success: false };
|
||||
}
|
||||
}
|
||||
|
||||
let uv_path:string;
|
||||
const mainWindow = getMainWindow();
|
||||
const backendPath = getBackendPath();
|
||||
|
||||
// Ensure backend directory exists
|
||||
if (!fs.existsSync(backendPath)) {
|
||||
log.info(`Creating backend directory: ${backendPath}`);
|
||||
fs.mkdirSync(backendPath, { recursive: true });
|
||||
}
|
||||
|
||||
const installingLockPath = path.join(backendPath, 'uv_installing.lock')
|
||||
const installedLockPath = path.join(backendPath, 'uv_installed.lock')
|
||||
// const proxyArgs = ['--default-index', 'https://pypi.tuna.tsinghua.edu.cn/simple']
|
||||
const proxyArgs = ['--default-index', 'https://mirrors.aliyun.com/pypi/simple/']
|
||||
|
||||
/**
|
||||
* Get current installation status by checking lock files
|
||||
* @returns Object with installation status information
|
||||
*/
|
||||
export async function getInstallationStatus(): Promise<{
|
||||
isInstalling: boolean;
|
||||
hasLockFile: boolean;
|
||||
installedExists: boolean;
|
||||
}> {
|
||||
try {
|
||||
const installingExists = fs.existsSync(installingLockPath);
|
||||
const installedExists = fs.existsSync(installedLockPath);
|
||||
|
||||
// If installing lock exists, installation is in progress
|
||||
// If installed lock exists, installation completed previously
|
||||
return {
|
||||
isInstalling: installingExists,
|
||||
hasLockFile: installingExists || installedExists,
|
||||
installedExists: installedExists
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[getInstallationStatus] Error checking installation status:', error);
|
||||
return {
|
||||
isInstalling: false,
|
||||
hasLockFile: false,
|
||||
installedExists: false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class InstallLogs {
|
||||
private node_process;
|
||||
private version: string;
|
||||
|
||||
constructor(extraArgs:string[], version: string) {
|
||||
console.log('start install dependencies', extraArgs, 'version:', version)
|
||||
const venvPath = getVenvPath(version);
|
||||
this.version = version;
|
||||
|
||||
this.node_process = spawn(uv_path, [
|
||||
'sync',
|
||||
'--no-dev',
|
||||
'--cache-dir', getCachePath('uv_cache'),
|
||||
...extraArgs], {
|
||||
cwd: backendPath,
|
||||
env: {
|
||||
...process.env,
|
||||
UV_TOOL_DIR: getCachePath('uv_tool'),
|
||||
UV_PYTHON_INSTALL_DIR: getCachePath('uv_python'),
|
||||
UV_PROJECT_ENVIRONMENT: venvPath,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**Display filtered logs based on severity */
|
||||
displayFilteredLogs(data:String) {
|
||||
if (!data) return;
|
||||
const msg = data.toString().trimEnd();
|
||||
//Detect if uv sync is run
|
||||
detectInstallationLogs(msg);
|
||||
if (msg.toLowerCase().includes("error") || msg.toLowerCase().includes("traceback")) {
|
||||
log.error(`BACKEND: [DEPS INSTALL] ${msg}`);
|
||||
safeMainWindowSend('install-dependencies-log', { type: 'stderr', data: data.toString() });
|
||||
} else {
|
||||
log.info(`BACKEND: [DEPS INSTALL] ${msg}`);
|
||||
safeMainWindowSend('install-dependencies-log', { type: 'stdout', data: data.toString() });
|
||||
}
|
||||
}
|
||||
|
||||
/**Handle stdout data */
|
||||
onStdout() {
|
||||
this.node_process.stdout.on('data', (data:any) => {
|
||||
this.displayFilteredLogs(data);
|
||||
})
|
||||
}
|
||||
|
||||
/**Handle stderr data */
|
||||
onStderr() {
|
||||
this.node_process.stderr.on('data', (data:any) => {
|
||||
this.displayFilteredLogs(data);
|
||||
})
|
||||
}
|
||||
|
||||
/**Handle process close event */
|
||||
onClose(resolveInner:(code: number | null) => void) {
|
||||
this.node_process.on('close', resolveInner);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set installing Lock Path
|
||||
* Creates uv_installing.lock file to indicate installation in progress
|
||||
* Creates backend directory if not exists
|
||||
*/
|
||||
static setLockPath() {
|
||||
if (!fs.existsSync(backendPath)) {
|
||||
fs.mkdirSync(backendPath, { recursive: true })
|
||||
}
|
||||
fs.writeFileSync(installingLockPath, '')
|
||||
}
|
||||
|
||||
/**Clean installing Lock Path */
|
||||
static cleanLockPath() {
|
||||
if (fs.existsSync(installingLockPath)) {
|
||||
fs.unlinkSync(installingLockPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const runInstall = (extraArgs: string[], version: string) => {
|
||||
const installLogs = new InstallLogs(extraArgs, version);
|
||||
return new Promise<PromiseReturnType>((resolveInner, rejectInner) => {
|
||||
try {
|
||||
installLogs.onStdout();
|
||||
installLogs.onStderr();
|
||||
installLogs.onClose((code) => {
|
||||
console.log('install dependencies end', code === 0)
|
||||
InstallLogs.cleanLockPath()
|
||||
resolveInner({
|
||||
message: code === 0 ? "Installation completed successfully" : `Installation failed with code ${code}`,
|
||||
success: code === 0
|
||||
})
|
||||
})
|
||||
} catch (err) {
|
||||
log.error('run install failed', err)
|
||||
// Clean up uv_installing.lock file if installation fails
|
||||
InstallLogs.cleanLockPath();
|
||||
rejectInner({ message: `Installation failed: ${err}`, success: false })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function installDependencies(version: string): Promise<PromiseReturnType> {
|
||||
uv_path = await getBinaryPath('uv');
|
||||
const venvPath = getVenvPath(version);
|
||||
|
||||
const handleInstallOperations = {
|
||||
spawnBabel: (message:"mirror"|"main"="main") => {
|
||||
fs.writeFileSync(installedLockPath, '')
|
||||
log.info('[DEPS INSTALL] Script completed successfully')
|
||||
console.log(`Install Dependencies completed ${message} for version ${version}`)
|
||||
console.log(`Virtual environment path: ${venvPath}`)
|
||||
spawn(uv_path, ['run', 'task', 'babel'], {
|
||||
cwd: backendPath,
|
||||
env: {
|
||||
...process.env,
|
||||
UV_PROJECT_ENVIRONMENT: venvPath,
|
||||
}
|
||||
})
|
||||
},
|
||||
notifyInstallDependenciesPage: ():boolean => {
|
||||
const success = safeMainWindowSend('install-dependencies-start');
|
||||
if (!success) {
|
||||
log.warn('[DEPS INSTALL] Main window not available, continuing installation without UI updates');
|
||||
}
|
||||
return success;
|
||||
},
|
||||
installHybridBrowserDependencies: async (): Promise<boolean> => {
|
||||
try {
|
||||
// Find the hybrid_browser_toolkit ts directory in the virtual environment
|
||||
// Need to determine the Python version to construct the correct path
|
||||
let sitePackagesPath: string | null = null;
|
||||
const libPath = path.join(venvPath, 'lib');
|
||||
|
||||
// Try to find the site-packages directory (it varies by Python version)
|
||||
if (fs.existsSync(libPath)) {
|
||||
const libContents = fs.readdirSync(libPath);
|
||||
const pythonDir = libContents.find(name => name.startsWith('python'));
|
||||
if (pythonDir) {
|
||||
sitePackagesPath = path.join(libPath, pythonDir, 'site-packages');
|
||||
}
|
||||
}
|
||||
|
||||
if (!sitePackagesPath || !fs.existsSync(sitePackagesPath)) {
|
||||
log.warn('[DEPS INSTALL] site-packages directory not found in venv, skipping npm install');
|
||||
return true; // Not an error if the venv structure is different
|
||||
}
|
||||
|
||||
const toolkitPath = path.join(sitePackagesPath, 'camel', 'toolkits', 'hybrid_browser_toolkit', 'ts');
|
||||
|
||||
if (!fs.existsSync(toolkitPath)) {
|
||||
log.warn('[DEPS INSTALL] hybrid_browser_toolkit ts directory not found at ' + toolkitPath + ', skipping npm install');
|
||||
return true; // Not an error if the toolkit isn't installed
|
||||
}
|
||||
|
||||
log.info('[DEPS INSTALL] Installing hybrid_browser_toolkit npm dependencies...');
|
||||
safeMainWindowSend('install-dependencies-log', {
|
||||
type: 'stdout',
|
||||
data: 'Installing browser toolkit dependencies...\n'
|
||||
});
|
||||
|
||||
// Try to find npm - first try system npm, then try uv run npm
|
||||
let npmCommand: string[];
|
||||
const testNpm = spawn('npm', ['--version'], { shell: true });
|
||||
const npmExists = await new Promise<boolean>(resolve => {
|
||||
testNpm.on('close', (code) => resolve(code === 0));
|
||||
testNpm.on('error', () => resolve(false));
|
||||
});
|
||||
|
||||
if (npmExists) {
|
||||
// Use system npm directly
|
||||
npmCommand = ['npm'];
|
||||
log.info('[DEPS INSTALL] Using system npm for installation');
|
||||
} else {
|
||||
// Try uv run npm (might not work if nodejs-wheel isn't properly set up)
|
||||
npmCommand = [uv_path, 'run', 'npm'];
|
||||
log.info('[DEPS INSTALL] Attempting to use uv run npm');
|
||||
}
|
||||
|
||||
// Run npm install
|
||||
const npmCacheDir = path.join(venvPath, '.npm-cache');
|
||||
if (!fs.existsSync(npmCacheDir)) {
|
||||
fs.mkdirSync(npmCacheDir, { recursive: true });
|
||||
}
|
||||
|
||||
const npmInstall = spawn(npmCommand[0], [...npmCommand.slice(1), 'install'], {
|
||||
cwd: toolkitPath,
|
||||
env: {
|
||||
...process.env,
|
||||
UV_PROJECT_ENVIRONMENT: venvPath,
|
||||
npm_config_cache: npmCacheDir,
|
||||
},
|
||||
shell: true // Important for Windows
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
if (npmInstall.stdout) {
|
||||
npmInstall.stdout.on('data', (data) => {
|
||||
log.info(`[DEPS INSTALL] npm install: ${data}`);
|
||||
safeMainWindowSend('install-dependencies-log', { type: 'stdout', data: data.toString() });
|
||||
});
|
||||
}
|
||||
|
||||
if (npmInstall.stderr) {
|
||||
npmInstall.stderr.on('data', (data) => {
|
||||
log.warn(`[DEPS INSTALL] npm install stderr: ${data}`);
|
||||
safeMainWindowSend('install-dependencies-log', { type: 'stderr', data: data.toString() });
|
||||
});
|
||||
}
|
||||
|
||||
npmInstall.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
log.info('[DEPS INSTALL] npm install completed successfully');
|
||||
resolve();
|
||||
} else {
|
||||
log.error(`[DEPS INSTALL] npm install failed with code ${code}`);
|
||||
reject(new Error(`npm install failed with code ${code}`));
|
||||
}
|
||||
});
|
||||
|
||||
npmInstall.on('error', (err) => {
|
||||
log.error(`[DEPS INSTALL] npm install process error: ${err}`);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
|
||||
// Run npm build (use the same npm command as install)
|
||||
log.info('[DEPS INSTALL] Building hybrid_browser_toolkit TypeScript...');
|
||||
safeMainWindowSend('install-dependencies-log', {
|
||||
type: 'stdout',
|
||||
data: 'Building browser toolkit TypeScript...\n'
|
||||
});
|
||||
|
||||
const buildArgs = npmCommand[0] === 'npm' ? ['run', 'build'] : [...npmCommand.slice(1), 'run', 'build'];
|
||||
const npmBuild = spawn(npmCommand[0], buildArgs, {
|
||||
cwd: toolkitPath,
|
||||
env: {
|
||||
...process.env,
|
||||
UV_PROJECT_ENVIRONMENT: venvPath,
|
||||
npm_config_cache: npmCacheDir,
|
||||
},
|
||||
shell: true // Important for Windows
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
if (npmBuild.stdout) {
|
||||
npmBuild.stdout.on('data', (data) => {
|
||||
log.info(`[DEPS INSTALL] npm build: ${data}`);
|
||||
safeMainWindowSend('install-dependencies-log', { type: 'stdout', data: data.toString() });
|
||||
});
|
||||
}
|
||||
|
||||
if (npmBuild.stderr) {
|
||||
npmBuild.stderr.on('data', (data) => {
|
||||
// TypeScript build warnings are common, don't treat as errors
|
||||
log.info(`[DEPS INSTALL] npm build output: ${data}`);
|
||||
safeMainWindowSend('install-dependencies-log', { type: 'stdout', data: data.toString() });
|
||||
});
|
||||
}
|
||||
|
||||
npmBuild.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
log.info('[DEPS INSTALL] TypeScript build completed successfully');
|
||||
resolve();
|
||||
} else {
|
||||
log.error(`[DEPS INSTALL] TypeScript build failed with code ${code}`);
|
||||
reject(new Error(`TypeScript build failed with code ${code}`));
|
||||
}
|
||||
});
|
||||
|
||||
npmBuild.on('error', (err) => {
|
||||
log.error(`[DEPS INSTALL] npm build process error: ${err}`);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
|
||||
// Optionally install Playwright browsers
|
||||
try {
|
||||
log.info('[DEPS INSTALL] Installing Playwright browsers...');
|
||||
const npxCommand = npmCommand[0] === 'npm' ? ['npx'] : [uv_path, 'run', 'npx'];
|
||||
const playwrightInstall = spawn(npxCommand[0], [...npxCommand.slice(1), 'playwright', 'install'], {
|
||||
cwd: toolkitPath,
|
||||
env: {
|
||||
...process.env,
|
||||
UV_PROJECT_ENVIRONMENT: venvPath,
|
||||
},
|
||||
shell: true
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
playwrightInstall.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
log.info('[DEPS INSTALL] Playwright browsers installed successfully');
|
||||
// Create marker file
|
||||
const markerPath = path.join(toolkitPath, '.playwright_installed');
|
||||
fs.writeFileSync(markerPath, 'installed');
|
||||
} else {
|
||||
log.warn('[DEPS INSTALL] Playwright installation failed, but continuing anyway');
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
|
||||
playwrightInstall.on('error', (err) => {
|
||||
log.warn('[DEPS INSTALL] Playwright installation process error:', err);
|
||||
resolve(); // Non-critical, continue
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
log.warn('[DEPS INSTALL] Failed to install Playwright browsers:', error);
|
||||
// Non-critical, continue
|
||||
}
|
||||
|
||||
log.info('[DEPS INSTALL] hybrid_browser_toolkit dependencies installed successfully');
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.error('[DEPS INSTALL] Failed to install hybrid_browser_toolkit dependencies:', error);
|
||||
// Don't fail the entire installation if this fails
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise<PromiseReturnType>(async (resolve, reject) => {
|
||||
console.log('start install dependencies')
|
||||
const mainWindowAvailable = handleInstallOperations.notifyInstallDependenciesPage();
|
||||
|
||||
if (!mainWindowAvailable) {
|
||||
log.info('[DEPS INSTALL] Proceeding with installation without UI notifications');
|
||||
}
|
||||
|
||||
const isInstalCommandTool = await installCommandTool()
|
||||
if (!isInstalCommandTool.success) {
|
||||
resolve({ message: "Command tool installation failed", success: false })
|
||||
return
|
||||
}
|
||||
|
||||
// Set Installing Lock Files
|
||||
InstallLogs.setLockPath();
|
||||
|
||||
// try default install
|
||||
const installSuccess = await runInstall([], version)
|
||||
if (installSuccess.success) {
|
||||
// Install hybrid_browser_toolkit npm dependencies after Python packages are installed
|
||||
log.info('[DEPS INSTALL] Installing hybrid_browser_toolkit dependencies...')
|
||||
await handleInstallOperations.installHybridBrowserDependencies()
|
||||
|
||||
handleInstallOperations.spawnBabel()
|
||||
|
||||
// Clean up old venvs after successful installation
|
||||
log.info('[DEPS INSTALL] Cleaning up old virtual environments...')
|
||||
await cleanupOldVenvs(version)
|
||||
log.info('[DEPS INSTALL] Old venvs cleanup completed')
|
||||
|
||||
resolve({ message: "Dependencies installed successfully", success: true })
|
||||
return
|
||||
}
|
||||
|
||||
// try mirror install
|
||||
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
let mirrorInstallSuccess: PromiseReturnType = { message: "", success: false }
|
||||
mirrorInstallSuccess = (timezone === 'Asia/Shanghai')? await runInstall(proxyArgs, version) :await runInstall([], version)
|
||||
|
||||
if (mirrorInstallSuccess.success) {
|
||||
// Install hybrid_browser_toolkit npm dependencies after Python packages are installed
|
||||
log.info('[DEPS INSTALL] Installing hybrid_browser_toolkit dependencies...')
|
||||
await handleInstallOperations.installHybridBrowserDependencies()
|
||||
|
||||
handleInstallOperations.spawnBabel("mirror")
|
||||
|
||||
// Clean up old venvs after successful installation
|
||||
log.info('[DEPS INSTALL] Cleaning up old virtual environments...')
|
||||
await cleanupOldVenvs(version)
|
||||
log.info('[DEPS INSTALL] Old venvs cleanup completed')
|
||||
|
||||
resolve({ message: "Dependencies installed successfully with mirror", success: true })
|
||||
} else {
|
||||
log.error('Both default and mirror install failed')
|
||||
safeMainWindowSend('install-dependencies-complete', {
|
||||
success: false,
|
||||
error: 'Both default and mirror install failed'
|
||||
});
|
||||
resolve({ message: "Both default and mirror install failed", success: false })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let dependencyInstallationDetected = false;
|
||||
let installationNotificationSent = false;
|
||||
export function detectInstallationLogs(msg:string) {
|
||||
// Check for UV dependency installation patterns
|
||||
const installPatterns = [
|
||||
"Resolved", // UV resolving dependencies
|
||||
"Downloaded", // UV downloading packages
|
||||
"Installing", // UV installing packages
|
||||
"Built", // UV building packages
|
||||
"Prepared", // UV preparing virtual environment
|
||||
"Syncing", // UV sync process
|
||||
"Creating virtualenv", // Virtual environment creation
|
||||
"Updating", // UV updating packages
|
||||
"× No solution found when resolving dependencies", // Dependency resolution issues
|
||||
"Audited" // UV auditing dependencies
|
||||
];
|
||||
|
||||
// Detect if UV is installing dependencies
|
||||
if (!dependencyInstallationDetected && installPatterns.some(pattern =>
|
||||
msg.includes(pattern) && !msg.includes("Uvicorn running on")
|
||||
)) {
|
||||
dependencyInstallationDetected = true;
|
||||
log.info('[BACKEND STARTUP] UV dependency installation detected during uvicorn startup');
|
||||
|
||||
// Create installing lock file to maintain consistency with install-deps.ts
|
||||
InstallLogs.setLockPath();
|
||||
log.info('[BACKEND STARTUP] Created uv_installing.lock file');
|
||||
|
||||
// Notify frontend that installation has started (only once)
|
||||
if (!installationNotificationSent) {
|
||||
installationNotificationSent = true;
|
||||
const notificationSent = safeMainWindowSend('install-dependencies-start');
|
||||
if (notificationSent) {
|
||||
log.info('[BACKEND STARTUP] Notified frontend of dependency installation start');
|
||||
} else {
|
||||
log.warn('[BACKEND STARTUP] Failed to notify frontend of dependency installation start');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Send installation logs to frontend if installation was detected
|
||||
if (dependencyInstallationDetected && !msg.includes("Uvicorn running on")) {
|
||||
safeMainWindowSend('install-dependencies-log', {
|
||||
type: msg.toLowerCase().includes("error") || msg.toLowerCase().includes("traceback") ? 'stderr' : 'stdout',
|
||||
data: msg
|
||||
});
|
||||
}
|
||||
|
||||
// Check if installation is complete (uvicorn starts successfully)
|
||||
if (dependencyInstallationDetected && msg.includes("Uvicorn running on")) {
|
||||
log.info('[BACKEND STARTUP] UV dependency installation completed, uvicorn started successfully');
|
||||
|
||||
// Clean up installing lock and create installed lock
|
||||
InstallLogs.cleanLockPath();
|
||||
fs.writeFileSync(installedLockPath, '');
|
||||
log.info('[BACKEND STARTUP] Created uv_installed.lock file');
|
||||
|
||||
safeMainWindowSend('install-dependencies-complete', {
|
||||
success: true,
|
||||
message: 'Dependencies installed successfully during backend startup'
|
||||
});
|
||||
}
|
||||
|
||||
// Handle installation failures
|
||||
if (dependencyInstallationDetected && (
|
||||
msg.toLowerCase().includes("failed to resolve dependencies") ||
|
||||
msg.toLowerCase().includes("installation failed") ||
|
||||
msg.includes("× No solution found when resolving dependencies")
|
||||
)) {
|
||||
log.error('[BACKEND STARTUP] UV dependency installation failed');
|
||||
|
||||
// Clean up installing lock file
|
||||
InstallLogs.cleanLockPath();
|
||||
log.info('[BACKEND STARTUP] Cleaned up uv_installing.lock file after failure');
|
||||
|
||||
safeMainWindowSend('install-dependencies-complete', {
|
||||
success: false,
|
||||
error: 'Dependency installation failed during backend startup'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,14 @@ export const ENV_END = '# === MCP INTEGRATION ENV END ===';
|
|||
|
||||
export function getEnvPath(email: string) {
|
||||
const tempEmail = email.split("@")[0].replace(/[\\/*?:"<>|\s]/g, "_").replace(".", "_")
|
||||
const envPath = path.join(os.homedir(), '.eigent', '.env.' + tempEmail)
|
||||
const eigentDir = path.join(os.homedir(), '.eigent')
|
||||
|
||||
// Ensure .eigent directory exists
|
||||
if (!fs.existsSync(eigentDir)) {
|
||||
fs.mkdirSync(eigentDir, { recursive: true });
|
||||
}
|
||||
|
||||
const envPath = path.join(eigentDir, '.env.' + tempEmail)
|
||||
const defaultEnv = path.join(process.resourcesPath, 'backend', '.env');
|
||||
if (!fs.existsSync(envPath) && fs.existsSync(defaultEnv)) {
|
||||
fs.copyFileSync(defaultEnv, envPath);
|
||||
|
|
|
|||
|
|
@ -57,24 +57,81 @@ export async function getBinaryName(name: string): Promise<string> {
|
|||
}
|
||||
|
||||
export async function getBinaryPath(name?: string): Promise<string> {
|
||||
if (!name) {
|
||||
return path.join(os.homedir(), '.eigent', 'bin')
|
||||
}
|
||||
const binaryName = await getBinaryName(name)
|
||||
const binariesDir = path.join(os.homedir(), '.eigent', 'bin')
|
||||
const binariesDirExists = await fs.existsSync(binariesDir)
|
||||
|
||||
return binariesDirExists ? path.join(binariesDir, binaryName) : binaryName
|
||||
// Ensure .eigent/bin directory exists
|
||||
if (!fs.existsSync(binariesDir)) {
|
||||
fs.mkdirSync(binariesDir, { recursive: true })
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
return binariesDir
|
||||
}
|
||||
|
||||
const binaryName = await getBinaryName(name)
|
||||
return path.join(binariesDir, binaryName)
|
||||
}
|
||||
|
||||
export function getCachePath(folder: string): string {
|
||||
const cacheDir = path.join(os.homedir(), '.eigent', 'cache', folder)
|
||||
console.log('cacheDir+++++++++++++++++++++++++++')
|
||||
console.log('Cache directory:', cacheDir)
|
||||
console.log('cacheDir--------------------')
|
||||
|
||||
// Ensure cache directory exists
|
||||
if (!fs.existsSync(cacheDir)) {
|
||||
fs.mkdirSync(cacheDir, { recursive: true })
|
||||
}
|
||||
|
||||
return cacheDir
|
||||
}
|
||||
|
||||
export function getVenvPath(version: string): string {
|
||||
const venvDir = path.join(os.homedir(), '.eigent', 'venvs', `backend-${version}`)
|
||||
|
||||
// Ensure venvs directory exists (parent of the actual venv)
|
||||
const venvsBaseDir = path.dirname(venvDir)
|
||||
if (!fs.existsSync(venvsBaseDir)) {
|
||||
fs.mkdirSync(venvsBaseDir, { recursive: true })
|
||||
}
|
||||
|
||||
return venvDir
|
||||
}
|
||||
|
||||
export function getVenvsBaseDir(): string {
|
||||
return path.join(os.homedir(), '.eigent', 'venvs')
|
||||
}
|
||||
|
||||
export async function cleanupOldVenvs(currentVersion: string): Promise<void> {
|
||||
const venvsBaseDir = getVenvsBaseDir()
|
||||
|
||||
// Check if venvs directory exists
|
||||
if (!fs.existsSync(venvsBaseDir)) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const entries = fs.readdirSync(venvsBaseDir, { withFileTypes: true })
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory() && entry.name.startsWith('backend-')) {
|
||||
const versionMatch = entry.name.match(/^backend-(.+)$/)
|
||||
if (versionMatch && versionMatch[1] !== currentVersion) {
|
||||
const oldVenvPath = path.join(venvsBaseDir, entry.name)
|
||||
console.log(`Cleaning up old venv: ${oldVenvPath}`)
|
||||
|
||||
try {
|
||||
// Remove old venv directory recursively
|
||||
fs.rmSync(oldVenvPath, { recursive: true, force: true })
|
||||
console.log(`Successfully removed old venv: ${entry.name}`)
|
||||
} catch (err) {
|
||||
console.error(`Failed to remove old venv ${entry.name}:`, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error during venv cleanup:', err)
|
||||
}
|
||||
}
|
||||
|
||||
export async function isBinaryExists(name: string): Promise<boolean> {
|
||||
const cmd = await getBinaryPath(name)
|
||||
|
||||
|
|
|
|||
20
electron/main/utils/safeWebContentsSend.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import log from 'electron-log'
|
||||
import { getMainWindow } from "../init";
|
||||
|
||||
/**
|
||||
* Safely send message to main window if it exists and is not destroyed
|
||||
* @param channel - The IPC channel to send message to
|
||||
* @param data - The data to send
|
||||
*/
|
||||
function safeMainWindowSend(channel: string, data?: any) {
|
||||
const mainWindow = getMainWindow();
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send(channel, data);
|
||||
return true;
|
||||
} else {
|
||||
log.warn(`[WEBCONTENTS SEND] Cannot send message to main window: ${channel}`, data);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export {safeMainWindowSend}
|
||||
|
|
@ -62,9 +62,9 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||
deleteFolder: (email: string) => ipcRenderer.invoke('delete-folder', email),
|
||||
getMcpConfigPath: (email: string) => ipcRenderer.invoke('get-mcp-config-path', email),
|
||||
// install dependencies related API
|
||||
installDependencies: () => ipcRenderer.invoke('install-dependencies'),
|
||||
frontendReady: () => ipcRenderer.invoke('frontend-ready'),
|
||||
checkAndInstallDepsOnUpdate: () => ipcRenderer.invoke('install-dependencies'),
|
||||
checkInstallBrowser: () => ipcRenderer.invoke('check-install-browser'),
|
||||
getInstallationStatus: () => ipcRenderer.invoke('get-installation-status'),
|
||||
onInstallDependenciesStart: (callback: () => void) => {
|
||||
ipcRenderer.on('install-dependencies-start', callback);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "eigent",
|
||||
"version": "0.0.66",
|
||||
"version": "0.0.72",
|
||||
"main": "dist-electron/main/index.js",
|
||||
"description": "Eigent",
|
||||
"author": "Eigent.AI",
|
||||
|
|
@ -59,6 +59,7 @@
|
|||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"csv-parser": "^3.2.0",
|
||||
"dompurify": "^3.2.7",
|
||||
"electron-log": "^5.4.0",
|
||||
"electron-updater": "^6.3.9",
|
||||
"embla-carousel-autoplay": "^8.6.0",
|
||||
|
|
@ -92,6 +93,7 @@
|
|||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/archiver": "^6.0.3",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/papaparse": "^5.3.16",
|
||||
"@types/react": "^18.3.12",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,84 @@
|
|||
"""drop_mcp_user_foreign_key_constraint
|
||||
|
||||
Revision ID: d74ab2a44600
|
||||
Revises: 0001_init
|
||||
Create Date: 2025-09-28 16:21:06.930093
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import sqlmodel.sql.sqltypes
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "d74ab2a44600"
|
||||
down_revision: Union[str, None] = "0001_init"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# Drop the foreign key constraint for mcp_id in mcp_user table
|
||||
# Use try-catch to handle cases where constraint might not exist or have different name
|
||||
|
||||
try:
|
||||
op.drop_constraint("mcp_user_mcp_id_fkey", "mcp_user", type_="foreignkey")
|
||||
except Exception as e:
|
||||
# If the expected constraint name doesn't work, try to find it dynamically
|
||||
try:
|
||||
connection = op.get_bind()
|
||||
inspector = sa.inspect(connection)
|
||||
fk_constraints = inspector.get_foreign_keys("mcp_user")
|
||||
|
||||
# Find the constraint that references mcp_id -> mcp.id
|
||||
target_constraint = None
|
||||
for fk in fk_constraints:
|
||||
if (fk.get("constrained_columns") == ["mcp_id"] and
|
||||
fk.get("referred_table") == "mcp" and
|
||||
fk.get("referred_columns") == ["id"]):
|
||||
target_constraint = fk.get("name")
|
||||
break
|
||||
|
||||
if target_constraint:
|
||||
op.drop_constraint(target_constraint, "mcp_user", type_="foreignkey")
|
||||
else:
|
||||
print("Warning: No foreign key constraint found for mcp_user.mcp_id -> mcp.id")
|
||||
except Exception as e2:
|
||||
print(f"Warning: Could not drop foreign key constraint: {e2}")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# Re-add the foreign key constraint for mcp_id in mcp_user table
|
||||
# Check if the constraint already exists before creating it
|
||||
|
||||
try:
|
||||
connection = op.get_bind()
|
||||
inspector = sa.inspect(connection)
|
||||
fk_constraints = inspector.get_foreign_keys("mcp_user")
|
||||
|
||||
# Check if the constraint already exists
|
||||
constraint_exists = False
|
||||
for fk in fk_constraints:
|
||||
if (fk.get("constrained_columns") == ["mcp_id"] and
|
||||
fk.get("referred_table") == "mcp" and
|
||||
fk.get("referred_columns") == ["id"]):
|
||||
constraint_exists = True
|
||||
break
|
||||
|
||||
if not constraint_exists:
|
||||
op.create_foreign_key(
|
||||
"mcp_user_mcp_id_fkey",
|
||||
"mcp_user",
|
||||
"mcp",
|
||||
["mcp_id"],
|
||||
["id"]
|
||||
)
|
||||
else:
|
||||
print("Info: Foreign key constraint for mcp_user.mcp_id -> mcp.id already exists")
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not check or create foreign key constraint: {e}")
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import os
|
||||
from typing import Dict
|
||||
from fastapi import Depends, HTTPException, APIRouter
|
||||
from fastapi_babel import _
|
||||
|
|
@ -11,6 +12,8 @@ from app.model.mcp.mcp import Mcp, McpOut, McpType
|
|||
from app.model.mcp.mcp_env import McpEnv, Status as McpEnvStatus
|
||||
from app.model.mcp.mcp_user import McpImportType, McpUser, Status
|
||||
from loguru import logger
|
||||
from camel.toolkits.mcp_toolkit import MCPToolkit
|
||||
from app.component.environment import env
|
||||
|
||||
from app.component.validator.McpServer import (
|
||||
McpRemoteServer,
|
||||
|
|
@ -22,6 +25,45 @@ from app.component.validator.McpServer import (
|
|||
router = APIRouter(tags=["Mcp Servers"])
|
||||
|
||||
|
||||
async def pre_instantiate_mcp_toolkit(config_dict: dict) -> bool:
|
||||
"""
|
||||
Pre-instantiate MCP toolkit to complete authentication process
|
||||
|
||||
Args:
|
||||
config_dict: MCP server configuration dictionary
|
||||
|
||||
Returns:
|
||||
bool: Whether successfully instantiated and connected
|
||||
"""
|
||||
try:
|
||||
# Ensure unified auth directory for all mcp servers
|
||||
for server_config in config_dict.get("mcpServers", {}).values():
|
||||
if "env" not in server_config:
|
||||
server_config["env"] = {}
|
||||
# Set global auth directory to persist authentication across tasks
|
||||
if "MCP_REMOTE_CONFIG_DIR" not in server_config["env"]:
|
||||
server_config["env"]["MCP_REMOTE_CONFIG_DIR"] = env(
|
||||
"MCP_REMOTE_CONFIG_DIR",
|
||||
os.path.expanduser("~/.mcp-auth")
|
||||
)
|
||||
|
||||
# Create MCP toolkit and attempt to connect
|
||||
mcp_toolkit = MCPToolkit(config_dict=config_dict, timeout=30)
|
||||
await mcp_toolkit.connect()
|
||||
|
||||
# Get tools list to ensure connection is successful
|
||||
tools = mcp_toolkit.get_tools()
|
||||
logger.info(f"Successfully pre-instantiated MCP toolkit with {len(tools)} tools")
|
||||
|
||||
# Disconnect, authentication info is already saved
|
||||
await mcp_toolkit.disconnect()
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to pre-instantiate MCP toolkit: {e!r}")
|
||||
return False
|
||||
|
||||
|
||||
@router.get("/mcps", name="mcp list")
|
||||
async def gets(
|
||||
keyword: str | None = None,
|
||||
|
|
@ -71,7 +113,23 @@ async def install(mcp_id: int, session: Session = Depends(session), auth: Auth =
|
|||
exists = session.exec(select(McpUser).where(McpUser.mcp_id == mcp.id, McpUser.user_id == auth.user.id)).first()
|
||||
if exists:
|
||||
raise HTTPException(status_code=400, detail=_("mcp is installed"))
|
||||
|
||||
install_command: dict = mcp.install_command
|
||||
|
||||
# Pre-instantiate MCP toolkit for authentication
|
||||
config_dict = {
|
||||
"mcpServers": {
|
||||
mcp.key: install_command
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
success = await pre_instantiate_mcp_toolkit(config_dict)
|
||||
if not success:
|
||||
logger.warning(f"Pre-instantiation failed for MCP {mcp.key}, but continuing with installation")
|
||||
except Exception as e:
|
||||
logger.warning(f"Exception during pre-instantiation for MCP {mcp.key}: {e}")
|
||||
|
||||
mcp_user = McpUser(
|
||||
mcp_id=mcp.id,
|
||||
user_id=auth.user.id,
|
||||
|
|
@ -100,7 +158,26 @@ async def import_mcp(
|
|||
if not is_valid:
|
||||
raise HTTPException(status_code=400, detail=res)
|
||||
mcp_data: Dict[str, McpServerItem] = res.mcpServers
|
||||
|
||||
for name, data in mcp_data.items():
|
||||
# Pre-instantiate MCP toolkit for authentication
|
||||
config_dict = {
|
||||
"mcpServers": {
|
||||
name: {
|
||||
"command": data.command,
|
||||
"args": data.args,
|
||||
"env": data.env or {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
success = await pre_instantiate_mcp_toolkit(config_dict)
|
||||
if not success:
|
||||
logger.warning(f"Pre-instantiation failed for local MCP {name}, but continuing with installation")
|
||||
except Exception as e:
|
||||
logger.warning(f"Exception during pre-instantiation for local MCP {name}: {e}")
|
||||
|
||||
mcp_user = McpUser(
|
||||
mcp_id=0,
|
||||
user_id=auth.user.id,
|
||||
|
|
@ -114,12 +191,17 @@ async def import_mcp(
|
|||
env=data.env,
|
||||
server_url=None,
|
||||
)
|
||||
break
|
||||
mcp_user.save()
|
||||
return {"message": "Local MCP servers imported successfully", "count": len(mcp_data)}
|
||||
elif mcp_type == McpImportType.Remote:
|
||||
is_valid, res = validate_mcp_remote_servers(mcp_data)
|
||||
if not is_valid:
|
||||
raise HTTPException(status_code=400, detail=res)
|
||||
data: McpRemoteServer = res
|
||||
|
||||
# For remote servers, we don't need to pre-instantiate as they typically don't require authentication
|
||||
# but we can still try to validate the connection if needed
|
||||
|
||||
mcp_user = McpUser(
|
||||
mcp_id=0,
|
||||
user_id=auth.user.id,
|
||||
|
|
@ -128,5 +210,5 @@ async def import_mcp(
|
|||
mcp_name=data.server_name,
|
||||
server_url=data.server_url,
|
||||
)
|
||||
mcp_user.save()
|
||||
return mcp_user
|
||||
mcp_user.save()
|
||||
return mcp_user
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import os
|
||||
from typing import List, Optional
|
||||
from fastapi import Depends, HTTPException, Query, Response, APIRouter
|
||||
from sqlmodel import Session, select
|
||||
|
|
@ -5,11 +6,53 @@ from app.component.database import session
|
|||
from app.component.auth import Auth, auth_must
|
||||
from fastapi_babel import _
|
||||
from app.model.mcp.mcp_user import McpUser, McpUserIn, McpUserOut, McpUserUpdate, Status
|
||||
from app.model.mcp.mcp import Mcp
|
||||
from loguru import logger
|
||||
from camel.toolkits.mcp_toolkit import MCPToolkit
|
||||
from app.component.environment import env
|
||||
|
||||
router = APIRouter(tags=["McpUser Management"])
|
||||
|
||||
|
||||
async def pre_instantiate_mcp_toolkit(config_dict: dict) -> bool:
|
||||
"""
|
||||
Pre-instantiate MCP toolkit to complete authentication process
|
||||
|
||||
Args:
|
||||
config_dict: MCP server configuration dictionary
|
||||
|
||||
Returns:
|
||||
bool: Whether successfully instantiated and connected
|
||||
"""
|
||||
try:
|
||||
# Ensure unified auth directory for all mcp servers
|
||||
for server_config in config_dict.get("mcpServers", {}).values():
|
||||
if "env" not in server_config:
|
||||
server_config["env"] = {}
|
||||
# Set global auth directory to persist authentication across tasks
|
||||
if "MCP_REMOTE_CONFIG_DIR" not in server_config["env"]:
|
||||
server_config["env"]["MCP_REMOTE_CONFIG_DIR"] = env(
|
||||
"MCP_REMOTE_CONFIG_DIR",
|
||||
os.path.expanduser("~/.mcp-auth")
|
||||
)
|
||||
|
||||
# Create MCP toolkit and attempt to connect
|
||||
mcp_toolkit = MCPToolkit(config_dict=config_dict, timeout=30)
|
||||
await mcp_toolkit.connect()
|
||||
|
||||
# Get tools list to ensure connection is successful
|
||||
tools = mcp_toolkit.get_tools()
|
||||
logger.info(f"Successfully pre-instantiated MCP toolkit with {len(tools)} tools")
|
||||
|
||||
# Disconnect, authentication info is already saved
|
||||
await mcp_toolkit.disconnect()
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to pre-instantiate MCP toolkit: {e!r}")
|
||||
return False
|
||||
|
||||
|
||||
@router.get("/mcp/users", name="list mcp users", response_model=List[McpUserOut])
|
||||
async def list_mcp_users(
|
||||
mcp_id: Optional[int] = None,
|
||||
|
|
@ -42,6 +85,24 @@ async def create_mcp_user(mcp_user: McpUserIn, session: Session = Depends(sessio
|
|||
).first()
|
||||
if exists:
|
||||
raise HTTPException(status_code=400, detail=_("mcp is installed"))
|
||||
|
||||
# Get MCP configuration from the main Mcp table
|
||||
mcp = session.get(Mcp, mcp_user.mcp_id)
|
||||
if mcp and mcp.install_command:
|
||||
# Pre-instantiate MCP toolkit for authentication
|
||||
config_dict = {
|
||||
"mcpServers": {
|
||||
mcp.key: mcp.install_command
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
success = await pre_instantiate_mcp_toolkit(config_dict)
|
||||
if not success:
|
||||
logger.warning(f"Pre-instantiation failed for MCP {mcp.key}, but continuing with user creation")
|
||||
except Exception as e:
|
||||
logger.warning(f"Exception during pre-instantiation for MCP {mcp.key}: {e}")
|
||||
|
||||
db_mcp_user = McpUser(mcp_id=mcp_user.mcp_id, user_id=auth.user.id, env=mcp_user.env)
|
||||
session.add(db_mcp_user)
|
||||
session.commit()
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ class ConfigInfo:
|
|||
},
|
||||
ConfigGroup.NOTION.value: {
|
||||
"env_vars": ["MCP_REMOTE_CONFIG_DIR"],
|
||||
"toolkit": "notion_toolkit",
|
||||
"toolkit": "notion_mcp_toolkit",
|
||||
},
|
||||
ConfigGroup.TWITTER.value: {
|
||||
"env_vars": [
|
||||
|
|
|
|||
|
|
@ -71,7 +71,6 @@ class McpUserUpdate(BaseModel):
|
|||
server_url: Optional[str] = None
|
||||
command: Optional[str] = None
|
||||
args: Optional[str] = None
|
||||
env: Optional[dict] = None
|
||||
mcp_key: Optional[str] = None
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
version: '3.8'
|
||||
|
||||
services:
|
||||
# PostgreSQL Database
|
||||
postgres:
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 161 KiB After Width: | Height: | Size: 159 KiB |
|
Before Width: | Height: | Size: 162 KiB After Width: | Height: | Size: 162 KiB |
|
Before Width: | Height: | Size: 162 KiB After Width: | Height: | Size: 160 KiB |
|
Before Width: | Height: | Size: 162 KiB After Width: | Height: | Size: 159 KiB |
|
|
@ -1,9 +1,5 @@
|
|||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { TooltipSimple } from "@/components/ui/tooltip";
|
||||
import { CircleAlert } from "lucide-react";
|
||||
import { proxyFetchGet, proxyFetchPost, proxyFetchDelete } from "@/api/http";
|
||||
|
||||
|
|
@ -19,7 +15,8 @@ interface IntegrationItem {
|
|||
name: string;
|
||||
desc: string;
|
||||
env_vars: string[];
|
||||
onInstall: () => void;
|
||||
toolkit?: string; // Add toolkit field
|
||||
onInstall: () => void | Promise<void>;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -243,6 +240,8 @@ export default function IntegrationList({
|
|||
}
|
||||
if (installed[item.key]) return;
|
||||
await item.onInstall();
|
||||
// refresh configs after install to update installed state indicator
|
||||
await fetchInstalled();
|
||||
},
|
||||
[installed]
|
||||
);
|
||||
|
|
@ -302,7 +301,7 @@ export default function IntegrationList({
|
|||
onConnect={onConnect}
|
||||
activeMcp={activeMcp}
|
||||
></MCPEnvDialog>
|
||||
{items.filter((item) => item.name !== "Notion").map((item) => {
|
||||
{items.map((item) => {
|
||||
const isInstalled = !!installed[item.key];
|
||||
return (
|
||||
<div
|
||||
|
|
@ -318,8 +317,11 @@ export default function IntegrationList({
|
|||
"Github",
|
||||
].includes(item.name)
|
||||
) {
|
||||
if (item.env_vars.length === 0 || isInstalled) {
|
||||
addOption(item, true);
|
||||
if (item.env_vars.length === 0 || isInstalled) {
|
||||
// Ensure toolkit field is passed and normalized for known cases
|
||||
const normalizedToolkit =
|
||||
item.name === "Notion" ? "notion_mcp_toolkit" : item.toolkit;
|
||||
addOption({ ...item, toolkit: normalizedToolkit }, true);
|
||||
} else {
|
||||
handleInstall(item);
|
||||
}
|
||||
|
|
@ -341,14 +343,9 @@ export default function IntegrationList({
|
|||
{item.name}
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<TooltipSimple content={item.desc}>
|
||||
<CircleAlert className="w-4 h-4 text-icon-secondary" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{item.desc}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipSimple>
|
||||
</div>
|
||||
</div>
|
||||
{item.env_vars.length !== 0 && (
|
||||
|
|
|
|||
|
|
@ -7,11 +7,11 @@ import React, {
|
|||
} from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { CircleAlert, Store, X } from "lucide-react";
|
||||
import { proxyFetchGet, proxyFetchPost } from "@/api/http";
|
||||
import { proxyFetchGet, proxyFetchPost, fetchPost } from "@/api/http";
|
||||
import { Input } from "../ui/input";
|
||||
import { Button } from "../ui/button";
|
||||
import githubIcon from "@/assets/github.svg";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
||||
import { TooltipSimple } from "../ui/tooltip";
|
||||
import IntegrationList from "./IntegrationList";
|
||||
import { getProxyBaseURL } from "@/lib";
|
||||
import { capitalizeFirstLetter } from "@/lib";
|
||||
|
|
@ -65,8 +65,73 @@ const ToolSelect = forwardRef<
|
|||
.map(([key, value]: [string, any]) => {
|
||||
let onInstall = null;
|
||||
|
||||
onInstall = () =>
|
||||
(window.location.href = `${baseURL}/api/oauth/${key.toLowerCase()}/login`);
|
||||
// Special handling for Notion MCP
|
||||
if (key.toLowerCase() === 'notion') {
|
||||
onInstall = async () => {
|
||||
try {
|
||||
const response = await fetchPost("/install/tool/notion");
|
||||
if (response.success) {
|
||||
// Save to config to mark as installed
|
||||
await proxyFetchPost("/api/configs", {
|
||||
config_group: "Notion",
|
||||
config_name: "MCP_REMOTE_CONFIG_DIR",
|
||||
config_value: response.toolkit_name || "NotionMCPToolkit",
|
||||
});
|
||||
console.log("Notion MCP installed successfully");
|
||||
// After successful installation, add to selected tools
|
||||
const notionItem = {
|
||||
id: 0, // Use 0 for integration items
|
||||
key: key,
|
||||
name: key,
|
||||
description: "Notion workspace integration for reading and managing Notion pages",
|
||||
toolkit: "notion_mcp_toolkit", // Add the toolkit name
|
||||
isLocal: true
|
||||
};
|
||||
addOption(notionItem, true);
|
||||
} else {
|
||||
console.error("Failed to install Notion MCP:", response.error || "Unknown error");
|
||||
throw new Error(response.error || "Failed to install Notion MCP");
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("Failed to install Notion MCP:", error.message);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
} else if (key.toLowerCase() === 'google calendar') {
|
||||
onInstall = async () => {
|
||||
try {
|
||||
const response = await fetchPost("/install/tool/google_calendar");
|
||||
if (response.success) {
|
||||
// Save to config to mark as installed
|
||||
await proxyFetchPost("/api/configs", {
|
||||
config_group: "Google Calendar",
|
||||
config_name: "GOOGLE_CLIENT_ID",
|
||||
config_value: response.toolkit_name || "GoogleCalendarToolkit",
|
||||
});
|
||||
console.log("Google Calendar installed successfully");
|
||||
// After successful installation, add to selected tools
|
||||
const calendarItem = {
|
||||
id: 0, // Use 0 for integration items
|
||||
key: key,
|
||||
name: key,
|
||||
description: "Google Calendar integration for managing events and schedules",
|
||||
toolkit: "google_calendar_toolkit", // Add the toolkit name
|
||||
isLocal: true
|
||||
};
|
||||
addOption(calendarItem, true);
|
||||
} else {
|
||||
console.error("Failed to install Google Calendar:", response.error || "Unknown error");
|
||||
throw new Error(response.error || "Failed to install Google Calendar");
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("Failed to install Google Calendar:", error.message);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
} else {
|
||||
onInstall = () =>
|
||||
(window.location.href = `${baseURL}/api/oauth/${key.toLowerCase()}/login`);
|
||||
}
|
||||
|
||||
return {
|
||||
key: key,
|
||||
|
|
@ -78,6 +143,10 @@ const ToolSelect = forwardRef<
|
|||
? `Environmental variables required: ${value.env_vars.join(
|
||||
", "
|
||||
)}`
|
||||
: key.toLowerCase() === 'notion'
|
||||
? "Notion workspace integration for reading and managing Notion pages"
|
||||
: key.toLowerCase() === 'google calendar'
|
||||
? "Google Calendar integration for managing events and schedules"
|
||||
: "",
|
||||
onInstall,
|
||||
};
|
||||
|
|
@ -157,7 +226,7 @@ const ToolSelect = forwardRef<
|
|||
envValue?: { [key: string]: any },
|
||||
activeMcp?: any
|
||||
) => {
|
||||
// is exa search
|
||||
// is exa search or google calendar
|
||||
if (activeMcp && envValue) {
|
||||
const env: { [key: string]: string } = {};
|
||||
Object.keys(envValue).map((key) => {
|
||||
|
|
@ -171,6 +240,19 @@ const ToolSelect = forwardRef<
|
|||
activeMcp.install_command.env[key]
|
||||
);
|
||||
});
|
||||
|
||||
// Add to selected tools after saving config
|
||||
if (activeMcp.key === "Google Calendar") {
|
||||
const calendarItem = {
|
||||
id: activeMcp.id,
|
||||
key: activeMcp.key,
|
||||
name: activeMcp.name,
|
||||
description: "Google Calendar integration for managing events and schedules",
|
||||
toolkit: "google_calendar_toolkit",
|
||||
isLocal: true
|
||||
};
|
||||
addOption(calendarItem, true);
|
||||
}
|
||||
return;
|
||||
// async function fetchInstalled() {
|
||||
// try {
|
||||
|
|
@ -368,19 +450,12 @@ const ToolSelect = forwardRef<
|
|||
<div className="text-sm font-bold leading-17 text-text-action overflow-hidden text-ellipsis break-words line-clamp-1">
|
||||
{item.name}
|
||||
</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<CircleAlert
|
||||
<TooltipSimple content={item.description}>
|
||||
<CircleAlert
|
||||
className="w-4 h-4 text-icon-primary cursor-pointer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<div className="text-xs font-bold leading-17 text-text-body">
|
||||
{item.description}
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipSimple>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{getGithubRepoName(item.home_page) && (
|
||||
|
|
@ -434,19 +509,12 @@ const ToolSelect = forwardRef<
|
|||
<div className="text-sm font-bold leading-17 text-text-action overflow-hidden text-ellipsis break-words line-clamp-1">
|
||||
{item.mcp_name}
|
||||
</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<CircleAlert
|
||||
className="w-4 h-4 text-icon-primary cursor-pointer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<div className="text-xs font-bold leading-17 text-text-body">
|
||||
{item.mcp_desc}
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<TooltipSimple content={item.mcp_desc}>
|
||||
<CircleAlert
|
||||
className="w-4 h-4 text-icon-primary cursor-pointer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</TooltipSimple>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import { fetchPost } from "@/api/http";
|
|||
import { useChatStore } from "@/store/chatStore";
|
||||
import { useAuthStore, useWorkerList } from "@/store/authStore";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TooltipSimple } from "../ui/tooltip";
|
||||
|
||||
interface EnvValue {
|
||||
value: string;
|
||||
|
|
@ -57,7 +58,8 @@ export function AddWorker({
|
|||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const chatStore = useChatStore();
|
||||
const activeTaskId = useChatStore((state) => state.activeTaskId);
|
||||
const tasks = useChatStore((state) => state.tasks);
|
||||
const [showEnvConfig, setShowEnvConfig] = useState(false);
|
||||
const [activeMcp, setActiveMcp] = useState<McpItem | null>(null);
|
||||
const [envValues, setEnvValues] = useState<{ [key: string]: EnvValue }>({});
|
||||
|
|
@ -79,7 +81,7 @@ export function AddWorker({
|
|||
console.log(mcp);
|
||||
if (mcp?.install_command?.env) {
|
||||
const initialValues: { [key: string]: EnvValue } = {};
|
||||
Object.keys(mcp.install_command.env).forEach((key) => {
|
||||
for(const key of Object.keys(mcp.install_command.env)) {
|
||||
initialValues[key] = {
|
||||
value: "",
|
||||
required: true,
|
||||
|
|
@ -88,7 +90,7 @@ export function AddWorker({
|
|||
?.replace(/{{/g, "")
|
||||
?.replace(/}}/g, "") || "",
|
||||
};
|
||||
});
|
||||
}
|
||||
setEnvValues(initialValues);
|
||||
}
|
||||
};
|
||||
|
|
@ -115,7 +117,7 @@ export function AddWorker({
|
|||
|
||||
// call ToolSelect's install method
|
||||
if (toolSelectRef.current) {
|
||||
if (activeMcp.key === "EXA Search") {
|
||||
if (activeMcp.key === "EXA Search" || activeMcp.key === "Google Calendar") {
|
||||
await toolSelectRef.current.installMcp(
|
||||
activeMcp.id,
|
||||
{ ...envValues },
|
||||
|
|
@ -189,21 +191,19 @@ export function AddWorker({
|
|||
}
|
||||
const localTool: string[] = [];
|
||||
const mcpList: string[] = [];
|
||||
selectedTools.map((tool: any) => {
|
||||
selectedTools.forEach((tool: any) => {
|
||||
if (tool.isLocal) {
|
||||
localTool.push(tool.toolkit as string);
|
||||
} else {
|
||||
mcpList.push(tool?.key || tool?.mcp_name);
|
||||
}
|
||||
});
|
||||
Object.keys(mcpLocal.mcpServers).map((key) => {
|
||||
console.log("mcpList", mcpList);
|
||||
console.log("mcpLocal.mcpServers", mcpLocal.mcpServers);
|
||||
|
||||
console.log("mcpLocal.mcpServers", mcpLocal.mcpServers);
|
||||
for(const key of Object.keys(mcpLocal.mcpServers)) {
|
||||
if (!mcpList.includes(key)) {
|
||||
delete mcpLocal.mcpServers[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
if (edit) {
|
||||
const newWorkerList = workerList.map((worker) => {
|
||||
if (worker.type === workerInfo?.type) {
|
||||
|
|
@ -232,8 +232,7 @@ export function AddWorker({
|
|||
});
|
||||
setWorkerList(newWorkerList);
|
||||
} else if (
|
||||
chatStore.activeTaskId &&
|
||||
chatStore.tasks[chatStore.activeTaskId].messages.length === 0
|
||||
activeTaskId && tasks[activeTaskId].messages.length === 0
|
||||
) {
|
||||
const worker: Agent = {
|
||||
tasks: [],
|
||||
|
|
@ -255,7 +254,7 @@ export function AddWorker({
|
|||
};
|
||||
setWorkerList([...workerList, worker]);
|
||||
} else {
|
||||
fetchPost(`/task/${chatStore.activeTaskId}/add-agent`, {
|
||||
fetchPost(`/task/${activeTaskId}/add-agent`, {
|
||||
name: workerName,
|
||||
description: workerDescription,
|
||||
tools: localTool,
|
||||
|
|
@ -329,9 +328,11 @@ export function AddWorker({
|
|||
<div className="text-base font-bold leading-10 text-text-action">
|
||||
{showEnvConfig
|
||||
? t("workforce.configure-mcp-server")
|
||||
: t("workforce.add-your-mcp-server")}
|
||||
: t("workforce.add-your-agent")}
|
||||
</div>
|
||||
<CircleAlert size={16} />
|
||||
<TooltipSimple content="Configure your MCP worker node here.">
|
||||
<CircleAlert size={16} />
|
||||
</TooltipSimple>
|
||||
</div>
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
|
@ -398,7 +399,7 @@ export function AddWorker({
|
|||
{t("workforce.cancel")}
|
||||
</Button>
|
||||
<Button size="sm" onClick={handleConfigureMcpEnvSetting}>
|
||||
<span>{t("workforce.configure")}</span>
|
||||
<span>{t("Connect")}</span>
|
||||
<ArrowRight size={16} />
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
|
@ -420,7 +421,7 @@ export function AddWorker({
|
|||
<div className="flex items-center gap-sm pb-md border-[0px] border-b border-solid border-border-secondary">
|
||||
<Bot size={32} className="text-icon-primary" />
|
||||
<Input
|
||||
placeholder=""
|
||||
placeholder="Agent Name"
|
||||
value={workerName}
|
||||
onChange={(e) => {
|
||||
setWorkerName(e.target.value);
|
||||
|
|
@ -430,6 +431,7 @@ export function AddWorker({
|
|||
className={`!border-none !bg-transparent !shadow-none text-xl leading-2xl font-bold !ring-0 !ring-offset-0 ${
|
||||
nameError ? "border-red-500" : ""
|
||||
}`}
|
||||
required
|
||||
/>
|
||||
<RefreshCw
|
||||
size={16}
|
||||
|
|
@ -447,7 +449,7 @@ export function AddWorker({
|
|||
{t("workforce.description-optional")}
|
||||
</div>
|
||||
<Textarea
|
||||
placeholder=""
|
||||
placeholder="I'm an agent specially designed for..."
|
||||
value={workerDescription}
|
||||
onChange={(e) => setWorkerDescription(e.target.value)}
|
||||
className="rounded-sm border border-solid border-input-border-default bg-input-bg-default !shadow-none text-sm leading-normal !ring-0 !ring-offset-0 resize-none"
|
||||
|
|
@ -458,7 +460,9 @@ export function AddWorker({
|
|||
<div className="text-text-body text-sm leading-normal font-bold">
|
||||
{t("workforce.agent-tool")}
|
||||
</div>
|
||||
<CircleAlert size={16} />
|
||||
<TooltipSimple content="Select MCP tools for your worker node.">
|
||||
<CircleAlert size={16} />
|
||||
</TooltipSimple>
|
||||
</div>
|
||||
</div>
|
||||
<ToolSelect
|
||||
|
|
|
|||
|
|
@ -13,16 +13,19 @@ import {
|
|||
Play,
|
||||
Image,
|
||||
FileText,
|
||||
UploadCloud,
|
||||
} from "lucide-react";
|
||||
import { useChatStore } from "@/store/chatStore";
|
||||
|
||||
import racPause from "@/assets/rac-pause.svg";
|
||||
import { fetchDelete, proxyFetchDelete } from "@/api/http";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { fetchPut } from "@/api/http";
|
||||
import { Tag } from "../ui/tag";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TooltipSimple } from "../ui/tooltip";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export const BottomInput = ({
|
||||
message,
|
||||
|
|
@ -86,6 +89,8 @@ export const BottomInput = ({
|
|||
}, [chatStore]);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const dragCounter = useRef(0);
|
||||
const handleTakeControl = (type: "pause" | "resume") => {
|
||||
setIsLoading(true);
|
||||
if (type === "pause") {
|
||||
|
|
@ -133,6 +138,64 @@ export const BottomInput = ({
|
|||
}
|
||||
};
|
||||
|
||||
// drag & drop files
|
||||
const isFileDrag = (e: React.DragEvent) => {
|
||||
try {
|
||||
return Array.from(e.dataTransfer?.types || []).includes("Files");
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
|
||||
if (!privacy || isPending || useCloudModelInDev) return;
|
||||
if (!isFileDrag(e)) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.dataTransfer.dropEffect = "copy";
|
||||
setIsDragging(true);
|
||||
};
|
||||
|
||||
const handleDragEnter = (e: React.DragEvent<HTMLDivElement>) => {
|
||||
if (!privacy || isPending || useCloudModelInDev) return;
|
||||
if (!isFileDrag(e)) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
dragCounter.current += 1;
|
||||
setIsDragging(true);
|
||||
};
|
||||
|
||||
const handleDragLeave = (e: React.DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
dragCounter.current = Math.max(0, dragCounter.current - 1);
|
||||
if (dragCounter.current === 0) setIsDragging(false);
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragging(false);
|
||||
dragCounter.current = 0;
|
||||
if (!privacy || isPending || useCloudModelInDev) return;
|
||||
try {
|
||||
const dropped = Array.from(e.dataTransfer?.files || []);
|
||||
if (dropped.length === 0) return;
|
||||
const current = chatStore.tasks[chatStore.activeTaskId as string].attaches;
|
||||
const mapped = dropped.map((f: File) => ({
|
||||
fileName: f.name,
|
||||
filePath: (f as any).path || f.name,
|
||||
}));
|
||||
const files = [
|
||||
...current.filter((f: File) => !mapped.find((m) => m.filePath === f.filePath)),
|
||||
...mapped.filter((m) => !current.find((f) => f.filePath === m.filePath)),
|
||||
];
|
||||
chatStore.setAttaches(chatStore.activeTaskId as string, files as File[]);
|
||||
} catch (error) {
|
||||
console.error("Drop File Error:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditQuery = () => {
|
||||
fetchDelete(`/chat/${chatStore.activeTaskId}`);
|
||||
const tempTaskId = chatStore.activeTaskId;
|
||||
|
|
@ -310,7 +373,22 @@ export const BottomInput = ({
|
|||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="mr-2 relative z-10 h-auto min-h-[82px] rounded-2xl bg-input-bg-default !px-2 !pb-2 gap-0 space-x-1 shadow-none border-solid border border-zinc-300">
|
||||
<div
|
||||
className={`mr-2 relative z-10 h-auto min-h-[82px] rounded-2xl bg-input-bg-default !px-2 !pb-2 gap-0 space-x-1 shadow-none border-solid border border-zinc-300 transition-colors ${isDragging ? 'border-blue-400 bg-blue-50/40' : ''}`}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
{isDragging && (
|
||||
<div className="pointer-events-none absolute inset-0 z-20 flex flex-col items-center justify-center gap-2 rounded-2xl border-2 border-dashed border-blue-400 bg-blue-50/70 text-blue-700 backdrop-blur-sm">
|
||||
<UploadCloud className="w-8 h-8" />
|
||||
<div className="text-sm font-semibold">
|
||||
Drop files to attach
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)}
|
||||
<Textarea
|
||||
disabled={!privacy || isPending}
|
||||
ref={textareaRef}
|
||||
|
|
@ -385,74 +463,82 @@ export const BottomInput = ({
|
|||
)}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
disabled={!privacy || isPending}
|
||||
onClick={handleFileSelect}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="rounded"
|
||||
title="Select File"
|
||||
>
|
||||
<Paperclip
|
||||
size={16}
|
||||
className="text-button-transparent-icon-disabled"
|
||||
/>
|
||||
</Button>
|
||||
<TooltipSimple content="Select File">
|
||||
<Button
|
||||
disabled={!privacy || isPending || useCloudModelInDev}
|
||||
onClick={handleFileSelect}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="rounded"
|
||||
>
|
||||
<Paperclip
|
||||
size={16}
|
||||
className="text-button-transparent-icon-disabled"
|
||||
/>
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</div>
|
||||
<Button
|
||||
disabled={!privacy || isPending}
|
||||
onClick={() => {
|
||||
if (isPending) {
|
||||
if (isTakeControl) {
|
||||
handleTakeControl("resume");
|
||||
setIsTakeControl && setIsTakeControl(false);
|
||||
<TooltipSimple content={message.trim().length > 0 ? "Send Message" : "Enter message to send first"}>
|
||||
<Button
|
||||
disabled={!privacy || isPending || useCloudModelInDev}
|
||||
onClick={() => {
|
||||
if (isPending) {
|
||||
if (isTakeControl) {
|
||||
handleTakeControl("resume");
|
||||
setIsTakeControl && setIsTakeControl(false);
|
||||
} else {
|
||||
setIsTakeControl && setIsTakeControl(true);
|
||||
handleTakeControl("pause");
|
||||
}
|
||||
} else if(message.trim().length > 0) {
|
||||
onSend();
|
||||
onPendingChange(true);
|
||||
} else {
|
||||
setIsTakeControl && setIsTakeControl(true);
|
||||
handleTakeControl("pause");
|
||||
console.log("Message is empty ", message);
|
||||
toast.error("Message cannot be empty", {
|
||||
closeButton: true,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
onSend();
|
||||
onPendingChange(true);
|
||||
}
|
||||
}}
|
||||
size="icon"
|
||||
variant={
|
||||
isPending
|
||||
? isTakeControl
|
||||
}}
|
||||
size="icon"
|
||||
variant={
|
||||
isPending
|
||||
? isTakeControl
|
||||
? "success"
|
||||
: "cuation"
|
||||
: message.trim().length > 0
|
||||
? "success"
|
||||
: "cuation"
|
||||
: message.length > 0
|
||||
? "success"
|
||||
: "primary"
|
||||
}
|
||||
className={`rounded-full transition-all w-6`}
|
||||
>
|
||||
{isPending ? (
|
||||
// <CircleLoader className="w-4 h-4" />
|
||||
<>
|
||||
{isTakeControl ? (
|
||||
<Play
|
||||
color="white"
|
||||
className="w-4 h-4 text-button-primary-icon-default"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={racPause}
|
||||
alt="racPause"
|
||||
className="w-4 h-4 text-text-inverse-primary"
|
||||
: "primary"
|
||||
}
|
||||
className={`rounded-full transition-all w-6`}
|
||||
>
|
||||
{isPending ? (
|
||||
// <CircleLoader className="w-4 h-4" />
|
||||
<>
|
||||
{isTakeControl ? (
|
||||
<Play
|
||||
color="white"
|
||||
className="w-4 h-4 text-button-primary-icon-default"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={racPause}
|
||||
alt="racPause"
|
||||
className="w-4 h-4 text-text-inverse-primary"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<ArrowRight
|
||||
size={16}
|
||||
style={{
|
||||
transform: message ? "rotate(-90deg)" : "rotate(0deg)",
|
||||
}}
|
||||
className="transition-all text-button-primary-icon-default"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<ArrowRight
|
||||
size={16}
|
||||
style={{
|
||||
transform: message ? "rotate(-90deg)" : "rotate(0deg)",
|
||||
}}
|
||||
className="transition-all text-button-primary-icon-default"
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ export function TaskCard({
|
|||
});
|
||||
setFilterTasks(newFiltered);
|
||||
}
|
||||
}, [selectedState, taskInfo]);
|
||||
}, [selectedState, taskInfo, taskRunning]);
|
||||
|
||||
const isAllTaskFinished = useMemo(() => {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -50,16 +50,18 @@ export default function ChatBox(): JSX.Element {
|
|||
})
|
||||
.catch((err) => console.error("Failed to fetch settings:", err));
|
||||
|
||||
proxyFetchGet("/api/configs").then((configsRes) => {
|
||||
const configs = Array.isArray(configsRes) ? configsRes : [];
|
||||
const _hasApiKey = configs.find(
|
||||
(item) => item.config_name === "GOOGLE_API_KEY"
|
||||
);
|
||||
const _hasApiId = configs.find(
|
||||
(item) => item.config_name === "SEARCH_ENGINE_ID"
|
||||
);
|
||||
if (_hasApiKey && _hasApiId) setHasSearchKey(true);
|
||||
});
|
||||
proxyFetchGet("/api/configs")
|
||||
.then((configsRes) => {
|
||||
const configs = Array.isArray(configsRes) ? configsRes : [];
|
||||
const _hasApiKey = configs.find(
|
||||
(item) => item.config_name === "GOOGLE_API_KEY"
|
||||
);
|
||||
const _hasApiId = configs.find(
|
||||
(item) => item.config_name === "SEARCH_ENGINE_ID"
|
||||
);
|
||||
if (_hasApiKey && _hasApiId) setHasSearchKey(true);
|
||||
})
|
||||
.catch((err) => console.error("Failed to fetch configs:", err));
|
||||
}, []);
|
||||
|
||||
// Refresh privacy status when dialog closes
|
||||
|
|
|
|||
112
src/components/Folder/FolderComponent.tsx
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
import React, { useMemo } from "react";
|
||||
import DOMPurify from "dompurify";
|
||||
|
||||
type Props = {
|
||||
selectedFile: {
|
||||
content?: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
export default function FolderComponent({ selectedFile }: Props) {
|
||||
const sanitizedHtml = useMemo(() => {
|
||||
const raw = selectedFile?.content || "";
|
||||
if (!raw) return "";
|
||||
|
||||
// Strict dangerous content detection to prevent various bypass techniques
|
||||
const dangerousPatterns = [
|
||||
/ipcRenderer/gi,
|
||||
/window\s*\[\s*['"`]ipcRenderer['"`]\s*\]/gi,
|
||||
/parent\s*\.\s*ipcRenderer/gi,
|
||||
/top\s*\.\s*ipcRenderer/gi,
|
||||
/frames\s*\[\s*\d+\s*\]\s*\.\s*ipcRenderer/gi,
|
||||
/require\s*\(\s*['"`]electron['"`]\s*\)/gi,
|
||||
/process\s*\.\s*versions\s*\.\s*electron/gi,
|
||||
/nodeIntegration/gi,
|
||||
/webSecurity/gi,
|
||||
/contextIsolation/gi,
|
||||
];
|
||||
|
||||
for (const pattern of dangerousPatterns) {
|
||||
if (pattern.test(raw)) {
|
||||
console.warn("Detected forbidden content:", pattern);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
return DOMPurify.sanitize(raw, {
|
||||
USE_PROFILES: { html: true },
|
||||
ALLOWED_TAGS: [
|
||||
"a",
|
||||
"b",
|
||||
"i",
|
||||
"u",
|
||||
"strong",
|
||||
"em",
|
||||
"p",
|
||||
"br",
|
||||
"ul",
|
||||
"ol",
|
||||
"li",
|
||||
"img",
|
||||
"div",
|
||||
"span",
|
||||
"table",
|
||||
"thead",
|
||||
"tbody",
|
||||
"tr",
|
||||
"td",
|
||||
"th",
|
||||
"pre",
|
||||
"code",
|
||||
"h1",
|
||||
"h2",
|
||||
"h3",
|
||||
"h4",
|
||||
"h5",
|
||||
"h6",
|
||||
],
|
||||
ALLOWED_ATTR: [
|
||||
"href",
|
||||
"src",
|
||||
"alt",
|
||||
"title",
|
||||
"width",
|
||||
"height",
|
||||
"target",
|
||||
"rel",
|
||||
"colspan",
|
||||
"rowspan",
|
||||
"class",
|
||||
"id",
|
||||
],
|
||||
FORBID_ATTR: [
|
||||
"onerror",
|
||||
"onload",
|
||||
"onclick",
|
||||
"onmouseover",
|
||||
"onfocus",
|
||||
"onblur",
|
||||
"onchange",
|
||||
"onsubmit",
|
||||
"onreset",
|
||||
"onselect",
|
||||
"onabort",
|
||||
"onkeydown",
|
||||
"onkeypress",
|
||||
"onkeyup",
|
||||
"onunload",
|
||||
],
|
||||
FORBID_TAGS: ["script", "iframe", "object", "embed", "form", "input", "button"],
|
||||
ADD_ATTR: ["target"],
|
||||
SANITIZE_DOM: true,
|
||||
KEEP_CONTENT: false,
|
||||
});
|
||||
}, [selectedFile?.content]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full overflow-auto"
|
||||
dangerouslySetInnerHTML={{ __html: sanitizedHtml }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ import {
|
|||
ChevronDown,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import FolderComponent from "./FolderComponent";
|
||||
|
||||
import { useChatStore } from "@/store/chatStore";
|
||||
import { MarkDown } from "@/components/ChatBox/MarkDown";
|
||||
|
|
@ -543,30 +544,20 @@ export default function Folder({ data }: { data?: Agent }) {
|
|||
) : ["csv", "doc", "docx", "pptx", "xlsx"].includes(
|
||||
selectedFile.type
|
||||
) ? (
|
||||
<div
|
||||
className="w-full overflow-auto"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: selectedFile.content || "",
|
||||
}}
|
||||
/>
|
||||
<FolderComponent selectedFile={selectedFile} />
|
||||
) : selectedFile.type === "html" ? (
|
||||
isShowSourceCode ? (
|
||||
<>{selectedFile.content}</>
|
||||
) : (
|
||||
<iframe
|
||||
src={
|
||||
"localfile://" +
|
||||
encodeURIComponent(selectedFile.content as string)
|
||||
}
|
||||
className="w-full h-full border-0"
|
||||
title={selectedFile.name}
|
||||
/>
|
||||
<FolderComponent selectedFile={selectedFile} />
|
||||
)
|
||||
) : selectedFile.type === "zip" ? (
|
||||
<div className="flex items-center justify-center h-full text-zinc-500">
|
||||
<div className="text-center">
|
||||
<FileText className="w-12 h-12 mx-auto mb-4 text-zinc-300" />
|
||||
<p className="text-sm">{t("folder.zip-file-is-not-supported-yet")}</p>
|
||||
<p className="text-sm">
|
||||
{t("folder.zip-file-is-not-supported-yet")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : [
|
||||
|
|
@ -609,7 +600,9 @@ export default function Folder({ data }: { data?: Agent }) {
|
|||
<div className="flex items-center justify-center h-full text-zinc-500">
|
||||
<div className="text-center">
|
||||
<FileText className="w-12 h-12 mx-auto mb-4 text-zinc-300" />
|
||||
<p className="text-sm">{t("chat.select-a-file-to-view-its-contents")}</p>
|
||||
<p className="text-sm">
|
||||
{t("chat.select-a-file-to-view-its-contents")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import React from "react";
|
||||
import { useAuthStore } from "@/store/authStore";
|
||||
import { ProgressInstall } from "@/components/ui/progress-install";
|
||||
import { FileDown, RefreshCcw } from "lucide-react";
|
||||
|
|
@ -14,158 +14,21 @@ import {
|
|||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useInstallationUI } from "@/store/installationStore";
|
||||
import { TooltipSimple } from "../ui/tooltip";
|
||||
|
||||
interface InstallLog {
|
||||
type: "stdout" | "stderr";
|
||||
data: string;
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
export const InstallDependencies: React.FC<{
|
||||
isInstalling: boolean;
|
||||
setIsInstalling: (isInstalling: boolean) => void;
|
||||
}> = ({ isInstalling, setIsInstalling }) => {
|
||||
const { initState, setInitState } = useAuthStore();
|
||||
const {t} = useTranslation()
|
||||
const [logs, setLogs] = useState<InstallLog[]>([]);
|
||||
const [status, setStatus] = useState<
|
||||
"idle" | "installing" | "success" | "error"
|
||||
>("idle");
|
||||
const [showInstallScreen, setShowInstallScreen] = useState(true);
|
||||
const [progress, setProgress] = useState(20);
|
||||
useEffect(() => {
|
||||
// listen to install start event
|
||||
window.electronAPI.onInstallDependenciesStart(() => {
|
||||
setIsInstalling(true);
|
||||
setStatus("installing");
|
||||
setShowInstallScreen(true);
|
||||
setLogs([]);
|
||||
console.log("start installing dependencies...");
|
||||
setProgress(20);
|
||||
});
|
||||
|
||||
// listen to install log
|
||||
window.electronAPI.onInstallDependenciesLog(
|
||||
(data: { type: string; data: string }) => {
|
||||
console.log("data", data);
|
||||
const newLog: InstallLog = {
|
||||
type: data?.type as "stdout" | "stderr",
|
||||
data: data?.data,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
setProgress((prev) => {
|
||||
const progress = prev + 5;
|
||||
if (progress >= 90) {
|
||||
return 90;
|
||||
}
|
||||
return progress;
|
||||
});
|
||||
console.log(`install log [${data?.type}]:`, data?.data);
|
||||
setLogs((prev) => [...prev, newLog]);
|
||||
}
|
||||
);
|
||||
|
||||
// listen to install complete event
|
||||
window.electronAPI.onInstallDependenciesComplete(
|
||||
(data: { success: boolean; code?: number; error?: string }) => {
|
||||
setIsInstalling(false);
|
||||
if (data?.success) {
|
||||
setStatus("success");
|
||||
console.log("dependencies installed successfully!");
|
||||
setProgress(100);
|
||||
setInitState("done");
|
||||
} else {
|
||||
setStatus("error");
|
||||
console.error("dependencies installation failed:", data?.code);
|
||||
console.error("dependencies installation failed:", data?.error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// after component mounted, notify main process frontend is ready
|
||||
const notifyFrontendReady = async () => {
|
||||
try {
|
||||
// check if there is frontend-ready API
|
||||
if (window.electronAPI.frontendReady) {
|
||||
await window.electronAPI.frontendReady();
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(
|
||||
"frontend ready notification failed, maybe manual install mode:",
|
||||
error
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// delay notification, ensure component is fully initialized
|
||||
setTimeout(notifyFrontendReady, 500);
|
||||
|
||||
// clean up listeners
|
||||
return () => {
|
||||
window.electronAPI.removeAllListeners("install-dependencies-start");
|
||||
window.electronAPI.removeAllListeners("install-dependencies-log");
|
||||
window.electronAPI.removeAllListeners("install-dependencies-complete");
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleInstall = async () => {
|
||||
try {
|
||||
setStatus("installing");
|
||||
setIsInstalling(true);
|
||||
setLogs([]);
|
||||
setShowInstallScreen(true);
|
||||
|
||||
const result = await window.electronAPI.installDependencies();
|
||||
console.log("result", result);
|
||||
if (!result.success) {
|
||||
setStatus("error");
|
||||
setIsInstalling(false);
|
||||
return;
|
||||
}
|
||||
setStatus("success");
|
||||
setProgress(100);
|
||||
setIsInstalling(false);
|
||||
setInitState("done");
|
||||
} catch (error) {
|
||||
console.error("install start failed:", error);
|
||||
setStatus("error");
|
||||
setIsInstalling(false);
|
||||
}
|
||||
};
|
||||
|
||||
const exportLog = async () => {
|
||||
try {
|
||||
const response = await window.electronAPI.exportLog();
|
||||
|
||||
if (!response.success) {
|
||||
alert("Export cancelled:" + response.error);
|
||||
return;
|
||||
}
|
||||
if (response.savedPath) {
|
||||
window.location.href =
|
||||
"https://github.com/eigent-ai/eigent/issues/new/choose";
|
||||
alert("log saved:" + response.savedPath);
|
||||
}
|
||||
} catch (e: any) {
|
||||
alert("export error:" + e.message);
|
||||
}
|
||||
};
|
||||
|
||||
// if not show install interface, return null
|
||||
if (initState === "done" && !isInstalling) {
|
||||
return (
|
||||
<Dialog open={status === "error"}>
|
||||
<DialogContent className="bg-white-100%">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("layout.installation-failed")}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button onClick={handleInstall}>{t("layout.retry")}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
export const InstallDependencies: React.FC = () => {
|
||||
const { initState } = useAuthStore();
|
||||
const {t} = useTranslation();
|
||||
|
||||
const {
|
||||
progress,
|
||||
latestLog,
|
||||
error,
|
||||
isInstalling,
|
||||
retryInstallation,
|
||||
exportLog,
|
||||
} = useInstallationUI();
|
||||
|
||||
return (
|
||||
<div className="fixed !z-[100] inset-0 !bg-bg-page bg-opacity-80 h-full w-full flex items-center justify-center backdrop-blur-sm">
|
||||
|
|
@ -180,16 +43,18 @@ export const InstallDependencies: React.FC<{
|
|||
<div className="flex items-center gap-2 justify-between">
|
||||
<div className="text-text-label text-xs font-normal leading-tight ">
|
||||
{isInstalling ? "System Installing ..." : ""}
|
||||
<span className="pl-2">{logs.at(-1)?.data}</span>
|
||||
<span className="pl-2">{latestLog?.data}</span>
|
||||
</div>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
className="mt-1"
|
||||
onClick={handleInstall}
|
||||
>
|
||||
<RefreshCcw className="w-4 h-4" />
|
||||
</Button>
|
||||
<TooltipSimple content={`Cannot retry because state is ${error}`} hidden={true}>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
className="mt-1"
|
||||
onClick={retryInstallation}
|
||||
>
|
||||
<RefreshCcw className="w-4 h-4" />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -214,7 +79,7 @@ export const InstallDependencies: React.FC<{
|
|||
<FileDown className="w-4 h-4" />
|
||||
{t("layout.report-bug")}
|
||||
</Button>
|
||||
<Button size="sm" onClick={handleInstall}>
|
||||
<Button size="sm" onClick={retryInstallation}>
|
||||
{t("layout.retry")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { error } from "electron-log";
|
||||
import { t } from "i18next";
|
||||
import React from "react";
|
||||
|
||||
interface InstallationErrorDialogProps {
|
||||
error: string;
|
||||
installationState: string;
|
||||
latestLog: any;
|
||||
retryInstallation: () => void;
|
||||
}
|
||||
|
||||
const InstallationErrorDialog = ({
|
||||
error,
|
||||
installationState,
|
||||
latestLog,
|
||||
retryInstallation,
|
||||
}:InstallationErrorDialogProps) => {
|
||||
return (
|
||||
<Dialog open={installationState == "error"}>
|
||||
<DialogContent className="bg-white-100%">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("layout.installation-failed")}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="text-text-label text-xs font-normal leading-tight mb-4">
|
||||
{
|
||||
<div className="mb-1">
|
||||
<span className="text-text-label/60">
|
||||
Error: {error} <br />
|
||||
Log: {latestLog?.data}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button onClick={retryInstallation}>{t("layout.retry")}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default InstallationErrorDialog;
|
||||
|
|
@ -8,17 +8,48 @@ import { AnimationJson } from "@/components/AnimationJson";
|
|||
import animationData from "@/assets/animation/onboarding_success.json";
|
||||
import CloseNoticeDialog from "../Dialog/CloseNotice";
|
||||
import { useChatStore } from "@/store/chatStore";
|
||||
import { useInstallationUI } from "@/store/installationStore";
|
||||
import { useInstallationSetup } from "@/hooks/useInstallationSetup";
|
||||
import InstallationErrorDialog from "../InstallStep/InstallationErrorDialog/InstallationErrorDialog";
|
||||
const Layout = () => {
|
||||
const { initState, setInitState, isFirstLaunch, setIsFirstLaunch } =
|
||||
useAuthStore();
|
||||
const [isInstalling, setIsInstalling] = useState(false);
|
||||
const { initState, isFirstLaunch, setIsFirstLaunch, setInitState } = useAuthStore();
|
||||
const [noticeOpen, setNoticeOpen] = useState(false);
|
||||
const chatStore = useChatStore();
|
||||
|
||||
const {
|
||||
installationState,
|
||||
latestLog,
|
||||
error,
|
||||
isInstalling,
|
||||
shouldShowInstallScreen,
|
||||
retryInstallation,
|
||||
} = useInstallationUI();
|
||||
|
||||
// Setup installation IPC listeners and state synchronization
|
||||
useInstallationSetup();
|
||||
|
||||
// Additional check: If initState is carousel but tools are installed, skip to done
|
||||
useEffect(() => {
|
||||
const checkAndSkipCarousel = async () => {
|
||||
if (initState === 'carousel' && !isInstalling) {
|
||||
try {
|
||||
const result = await window.ipcRenderer.invoke("check-tool-installed");
|
||||
if (result.success && result.isInstalled) {
|
||||
console.log('[Layout] Tools installed, skipping carousel and setting initState to done');
|
||||
setInitState('done');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Layout] Failed to check tool installation:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
checkAndSkipCarousel();
|
||||
}, [initState, isInstalling, setInitState]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleBeforeClose = () => {
|
||||
const currentStatus = chatStore.tasks[chatStore.activeTaskId as string]?.status;
|
||||
if(["pending", "running", "pause"].includes(currentStatus)) {
|
||||
if(["running", "pause"].includes(currentStatus)) {
|
||||
setNoticeOpen(true);
|
||||
} else {
|
||||
window.electronAPI.closeWindow(true);
|
||||
|
|
@ -32,43 +63,46 @@ const Layout = () => {
|
|||
};
|
||||
}, [chatStore.tasks, chatStore.activeTaskId]);
|
||||
|
||||
useEffect(() => {
|
||||
const checkToolInstalled = async () => {
|
||||
// in render process
|
||||
const result = await window.ipcRenderer.invoke("check-tool-installed");
|
||||
if (result.success) {
|
||||
if (initState === "done" && !result.isInstalled) {
|
||||
setInitState("carousel");
|
||||
}
|
||||
console.log("tool is installed:");
|
||||
} else {
|
||||
console.error("check failed:", result.error);
|
||||
}
|
||||
};
|
||||
checkToolInstalled();
|
||||
}, []);
|
||||
// Determine what to show based on states
|
||||
const shouldShowOnboarding = initState === "done" && isFirstLaunch && !isInstalling;
|
||||
// Show install screen if either:
|
||||
// 1. The installation store says to show it (isVisible && not completed)
|
||||
// 2. OR if initState is not 'done' (meaning permissions or carousel should show)
|
||||
const actualShouldShowInstallScreen = shouldShowInstallScreen || initState !== 'done';
|
||||
const shouldShowMainContent = !actualShouldShowInstallScreen;
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
|
||||
<TopBar />
|
||||
<div className="flex-1 h-full p-2">
|
||||
{initState === "done" && isFirstLaunch && !isInstalling && (
|
||||
{/* Onboarding animation */}
|
||||
{shouldShowOnboarding && (
|
||||
<AnimationJson
|
||||
onComplete={() => {
|
||||
setIsFirstLaunch(false);
|
||||
}}
|
||||
onComplete={() => setIsFirstLaunch(false)}
|
||||
animationData={animationData}
|
||||
/>
|
||||
)}
|
||||
{(initState !== "done" || isInstalling) && (
|
||||
<InstallDependencies
|
||||
isInstalling={isInstalling}
|
||||
setIsInstalling={setIsInstalling}
|
||||
/>
|
||||
|
||||
{/* Installation screen */}
|
||||
{actualShouldShowInstallScreen && <InstallDependencies />}
|
||||
|
||||
{/* Main app content */}
|
||||
{shouldShowMainContent && (
|
||||
<>
|
||||
<Outlet />
|
||||
<HistorySidebar />
|
||||
</>
|
||||
)}
|
||||
<Outlet />
|
||||
<HistorySidebar />
|
||||
|
||||
{(error != "" && error !=undefined) &&
|
||||
<InstallationErrorDialog
|
||||
error={error}
|
||||
installationState={installationState}
|
||||
latestLog={latestLog}
|
||||
retryInstallation={retryInstallation}/>
|
||||
}
|
||||
|
||||
<CloseNoticeDialog
|
||||
onOpenChange={setNoticeOpen}
|
||||
open={noticeOpen}
|
||||
|
|
|
|||
|
|
@ -1,23 +1,23 @@
|
|||
import { toast } from "sonner";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import i18n from "@/i18n";
|
||||
|
||||
export function showCreditsToast() {
|
||||
toast.dismiss();
|
||||
const { t } = useTranslation();
|
||||
toast(
|
||||
<div>
|
||||
{t("chat.you-ve-reached-the-limit-of-your-current-plan")}
|
||||
{i18n.t("chat.you-ve-reached-the-limit-of-your-current-plan")}
|
||||
<a
|
||||
className="underline cursor-pointer"
|
||||
onClick={() => (window.location.href = "https://www.eigent.ai/pricing")}
|
||||
>
|
||||
{t("chat.upgrade")}
|
||||
{i18n.t("chat.upgrade")}
|
||||
</a>{" "}
|
||||
{t("chat.your-account-or-switch-to-a-self-hosted-model-and-api-in")}{" "}
|
||||
{i18n.t("chat.your-account-or-switch-to-a-self-hosted-model-and-api-in")}{" "}
|
||||
<a
|
||||
className="underline cursor-pointer"
|
||||
onClick={() => (window.location.href = "#/setting/general")}
|
||||
>
|
||||
{t("chat.settings")}
|
||||
{i18n.t("chat.settings")}
|
||||
</a>{" "}
|
||||
.
|
||||
</div>,
|
||||
|
|
|
|||
|
|
@ -18,8 +18,10 @@ import { useSidebarStore } from "@/store/sidebarStore";
|
|||
import chevron_left from "@/assets/chevron_left.svg";
|
||||
import { getAuthStore } from "@/store/authStore";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { proxyFetchGet } from "@/api/http";
|
||||
import { toast } from "sonner";
|
||||
function HeaderWin() {
|
||||
const {t} = useTranslation();
|
||||
const { t } = useTranslation();
|
||||
const titlebarRef = useRef<HTMLDivElement>(null);
|
||||
const controlsRef = useRef<HTMLDivElement>(null);
|
||||
const [platform, setPlatform] = useState<string>("");
|
||||
|
|
@ -110,6 +112,22 @@ function HeaderWin() {
|
|||
chatStore.tasks[chatStore.activeTaskId as string]?.summaryTask,
|
||||
]);
|
||||
|
||||
const getReferFriendsLink = async () => {
|
||||
try {
|
||||
const res: any = await proxyFetchGet("/api/user/invite_code");
|
||||
if (res?.invite_code) {
|
||||
const inviteLink = `https://www.eigent.ai/signup?invite_code=${res.invite_code}`;
|
||||
await navigator.clipboard.writeText(inviteLink);
|
||||
toast.success("Invitation link copied!");
|
||||
} else {
|
||||
toast.error("Failed to get invite code");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to get referral link:", error);
|
||||
toast.error("Failed to get invitation link");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex !h-9 items-center justify-between pl-2 py-1 z-50"
|
||||
|
|
@ -151,7 +169,7 @@ function HeaderWin() {
|
|||
</Button>
|
||||
{location.pathname !== "/history" && (
|
||||
<>
|
||||
{activeTaskTitle === "New Project" ? (
|
||||
{activeTaskTitle === t("chat.new-project") ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
|
|
@ -162,7 +180,7 @@ function HeaderWin() {
|
|||
</Button>
|
||||
) : (
|
||||
<div className="font-bold leading-10 text-base min-w-10 max-w-56 truncate">
|
||||
{t("chat.new-project")}
|
||||
{activeTaskTitle}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
|
@ -185,9 +203,7 @@ function HeaderWin() {
|
|||
{t("layout.report-bug")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
window.location.href = "https://www.eigent.ai/dashboard";
|
||||
}}
|
||||
onClick={getReferFriendsLink}
|
||||
variant="primary"
|
||||
size="xs"
|
||||
className="no-drag text-button-primary-text-default leading-tight"
|
||||
|
|
@ -239,4 +255,4 @@ function HeaderWin() {
|
|||
);
|
||||
}
|
||||
|
||||
export default HeaderWin;
|
||||
export default HeaderWin;
|
||||
|
|
@ -379,7 +379,7 @@ export default function Workflow({
|
|||
handleShare(chatStore.activeTaskId as string);
|
||||
}}
|
||||
>
|
||||
Share {t("workforce.share")}
|
||||
{t("workforce.share")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -27,4 +27,41 @@ const TooltipContent = React.forwardRef<
|
|||
))
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||
/**
|
||||
* A simpler interface for Tooltip when you just need a trigger and content.
|
||||
*
|
||||
* Usage:
|
||||
* ```jsx
|
||||
* <TooltipSimple content="This is a tooltip">
|
||||
* <button>Hover me</button>
|
||||
* </TooltipSimple>
|
||||
* ```
|
||||
*/
|
||||
interface TooltipSimpleProps extends Omit<React.ComponentPropsWithoutRef<typeof TooltipContent>, 'children' | 'content'> {
|
||||
children: React.ReactNode;
|
||||
content: React.ReactNode;
|
||||
}
|
||||
|
||||
const TooltipSimple = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipContent>,
|
||||
TooltipSimpleProps
|
||||
>(({ children, content, className, sideOffset = 4, ...props }, ref) => {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
{children}
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(className)}
|
||||
{...props}>
|
||||
{content}
|
||||
</TooltipContent >
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
})
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider, TooltipSimple }
|
||||
|
|
|
|||
102
src/hooks/useInstallationSetup.ts
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
import { useEffect } from 'react';
|
||||
import { useInstallationStore } from '@/store/installationStore';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
|
||||
/**
|
||||
* Hook that sets up Electron IPC listeners and handles installation state synchronization
|
||||
* This should be called once in your App component or Layout component
|
||||
*/
|
||||
export const useInstallationSetup = () => {
|
||||
const { initState, setInitState } = useAuthStore();
|
||||
|
||||
// Extract only the functions we need to avoid dependency issues
|
||||
const startInstallation = useInstallationStore(state => state.startInstallation);
|
||||
const addLog = useInstallationStore(state => state.addLog);
|
||||
const setSuccess = useInstallationStore(state => state.setSuccess);
|
||||
const setError = useInstallationStore(state => state.setError);
|
||||
|
||||
// Check tool installation status on mount
|
||||
useEffect(() => {
|
||||
const checkToolInstalled = async () => {
|
||||
try {
|
||||
console.log('[useInstallationSetup] Checking tool installation status...');
|
||||
const result = await window.ipcRenderer.invoke("check-tool-installed");
|
||||
|
||||
console.log('[useInstallationSetup] Tool check result:', result, 'initState:', initState);
|
||||
|
||||
// If tools are NOT installed and we're in done state, go back to carousel
|
||||
if (result.success && initState === "done" && !result.isInstalled) {
|
||||
console.log('[useInstallationSetup] Tool not installed, setting initState to carousel');
|
||||
setInitState("carousel");
|
||||
}
|
||||
|
||||
// If tools ARE installed and we're in carousel state, go to done
|
||||
if (result.success && initState === "carousel" && result.isInstalled) {
|
||||
console.log('[useInstallationSetup] Tools installed but initState is carousel, setting to done');
|
||||
setInitState("done");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[useInstallationSetup] Tool installation check failed:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const checkBackendStatus = async() => {
|
||||
try {
|
||||
// Also check if installation is currently in progress
|
||||
const installationStatus = await window.electronAPI.getInstallationStatus();
|
||||
console.log('[useInstallationSetup] Installation status check:', installationStatus);
|
||||
|
||||
if (installationStatus.success && installationStatus.isInstalling) {
|
||||
console.log('[useInstallationSetup] Installation in progress, starting frontend state');
|
||||
startInstallation();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[useInstallationSetup] Failed to check installation status:', err);
|
||||
}
|
||||
}
|
||||
|
||||
checkToolInstalled();
|
||||
checkBackendStatus();
|
||||
}, [initState, setInitState, startInstallation]);
|
||||
|
||||
// Setup Electron IPC listeners (only once)
|
||||
useEffect(() => {
|
||||
// Electron IPC event handlers
|
||||
const handleInstallStart = () => {
|
||||
startInstallation();
|
||||
};
|
||||
|
||||
const handleInstallLog = (data: { type: string; data: string }) => {
|
||||
addLog({
|
||||
type: data.type as 'stdout' | 'stderr',
|
||||
data: data.data,
|
||||
timestamp: new Date(),
|
||||
});
|
||||
};
|
||||
|
||||
const handleInstallComplete = (data: { success: boolean; code?: number; error?: string }) => {
|
||||
console.log('[useInstallationSetup] Install complete event received:', data);
|
||||
|
||||
if (data.success) {
|
||||
setSuccess();
|
||||
setInitState('done');
|
||||
} else {
|
||||
setError(data.error || 'Installation failed');
|
||||
}
|
||||
};
|
||||
|
||||
// Register Electron IPC listeners
|
||||
window.electronAPI.onInstallDependenciesStart(handleInstallStart);
|
||||
window.electronAPI.onInstallDependenciesLog(handleInstallLog);
|
||||
window.electronAPI.onInstallDependenciesComplete(handleInstallComplete);
|
||||
|
||||
console.log('[useInstallationSetup] Installation listeners registered');
|
||||
|
||||
// Cleanup listeners on unmount
|
||||
return () => {
|
||||
window.electronAPI.removeAllListeners('install-dependencies-start');
|
||||
window.electronAPI.removeAllListeners('install-dependencies-log');
|
||||
window.electronAPI.removeAllListeners('install-dependencies-complete');
|
||||
};
|
||||
}, [startInstallation, addLog, setSuccess, setError, setInitState]);
|
||||
};
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
"you-are-using-self-hosted-mode": ".أنت في وضع الاستضافة الذاتية. لا يمكن استخدام النماذج السحابية هنا - قم بإعداد نموذج سحابي محلي خاص بك للحفاظ على سير الأمور",
|
||||
"you-are-using-self-hosted-mode-mcp": ".أنت تستخدم وضع الاستضافة الذاتية. أدخل مفاتيح بحث Google في \"MCP والأدوات\" لضمان عمل Eigent بشكل صحيح",
|
||||
"palm-springs-tennis-trip-planner": "مخطط رحلة تنس في بالم سبرينغس",
|
||||
"palm-springs-tennis-trip-planner-message": ".نحن من عشاق التنس ونرغب في حضور بطولة تنس في بالم سبرينغز. أعيش في سان فرانسيسكو. يُرجى إعداد برنامج رحلة مفصل يتضمن الرحلات الجوية والفنادق والأنشطة التي يمكن القيام بها لمدة 3 أيام - تقريبًا عند موعد نصف النهائي/النهائي. نحب المشي لمسافات طويلة والطعام النباتي والمنتجعات الصحية. ميزانيتنا 5000 دولار أمريكي. يجب أن يتضمن برنامج الرحلة جدولًا زمنيًا مفصلاً للوقت والأنشطة والتكلفة وتفاصيل أخرى، وإذا أمكن، رابطًا لشراء التذاكر/الحجز، وما إلى ذلك لهذا العنصر. بعض التفضيلات: 1. الدخول إلى المنتجع الصحي سيكون جيدًا ولكنه ليس ضروريًا. 2. عند الانتهاء من هذه المهمة، يُرجى إنشاء تقرير هتمل حول هذه الرحلة",
|
||||
"palm-springs-tennis-trip-planner-message": "أنا من محبي التنس وأرغب في مشاهدة بطولة التنس في بالم سبرينغز. أعيش في سان فرانسيسكو (SF). يرجى إعداد خط سير مفصل لمدة ثلاثة أيام مع رحلات الطيران والفنادق والأنشطة للفترة المحيطة بنصف النهائي/النهائي. أحب التنزه والطعام النباتي والمنتجعات الصحية. ميزانيتي هي 5000 دولار. يجب أن يكون خط السير جدولًا زمنيًا مفصلاً يوضح الأوقات والأنشطة والتكاليف وتفاصيل أخرى، وحيثما ينطبق، روابط لشراء التذاكر أو إجراء الحجوزات. التفضيلات: 1. سيكون الوصول إلى منتجع صحي أمرًا لطيفًا ولكنه ليس ضروريًا. 2. عند الانتهاء من هذه المهمة، يرجى إنشاء تقرير HTML حول الرحلة.",
|
||||
"bank-transfer-csv-analysis-and-visualization": "تحليل سيسفي والتصور للتحويل البنكي",
|
||||
"bank-transfer-csv-analysis-and-visualization-message": ".تفضل بزيارة ملف سيسفي تجريبي لنماذج بنكية يحتوي على ١٠ أفكار و ١٠ صفوف. اقرأ ملف سيسفي الناتج ولخّص البيانات. تتوفر رسوم بيانية لعرض الاتجاهات أو الرؤى ذات الصلة من البيانات",
|
||||
"find-duplicate-files-in-downloads-folder": "رسالة لبحث عن الملفات المكررة في مجلد التنزيلات",
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
"data-privacy-description": "يعتمد أيجنت على الوضع المحلي الجديد ويضمن خصوصيتك. تبقى بياناتك على جهازك افتراضيًا. أذونات السحابة اختيارية، ويُستخدم حد البيانات لأغراض العمل فقط",
|
||||
"privacy-policy": "سياسة الخصوصية",
|
||||
"how-we-handle-your-data": "كيف نتعامل مع بياناتك",
|
||||
"how-we-handle-your-data-line-1": ":نحن نستخدم فقط البيانات الأساسية اللازمة لتنفيذ مهامك",
|
||||
"how-we-handle-your-data-line-1": "نحن نستخدم فقط البيانات الأساسية اللازمة لتنفيذ مهامك",
|
||||
"how-we-handle-your-data-line-1-line-1": ".يمكنك اختيار لقطات شاشة أيجنت لتحليل عناصر واجهة المستخدم وقراءة النص وتحديد الإجراء التالي، تمامًا كما تفعل",
|
||||
"how-we-handle-your-data-line-1-line-2": ".يمكن استخدام الماوس ولوحة المفاتيح المحلية للوصول إلى البرامج والملفات المحلية التي تم تنشيطها",
|
||||
"how-we-handle-your-data-line-1-line-3": "لا يتم توفير سوى الحد الأدنى من البيانات ذات الصلة لنماذج الذكاء الاصطناعي أو عمليات التكامل مع جهات خارجية التي تمكنها؛ وليس لدينا أي احتفاظ بالبيانات",
|
||||
|
|
@ -89,7 +89,7 @@
|
|||
"coming-soon": "قريبًا",
|
||||
"uninstall": "إلغاء التثبيت",
|
||||
"install": "تثبيت",
|
||||
"add-your-mcp-server": "الخاص بك MCP أضف خادم",
|
||||
"add-your-agent": "أضف وكيلك",
|
||||
"add-a-local-mcp-server-by-providing-a-valid-json-configuration": ".أضف مضيف مسيبي محليًا عن طريق توفير جسون صارم صالح",
|
||||
"learn-more": "اتعلم اكثر",
|
||||
"installing": "جارٍ التثبيت...",
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"new-worker": "عامل جديد",
|
||||
"edit": "تعديل",
|
||||
"configure-mcp-server": "تكوين خادم مسيبي",
|
||||
"add-your-mcp-server": "إضافة خادم مسيبي",
|
||||
"add-your-agent": "إضافة خادم مسيبي",
|
||||
"cancel": "إلغاء",
|
||||
"save-changes": "حفظ التغييرات",
|
||||
"description-optional": "الوصف (اختياري)",
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
"you-are-using-self-hosted-mode": "Sie befinden sich im Self-hosted-Modus. Cloud-Modelle können hier nicht verwendet werden – richten Sie Ihr eigenes lokales Cloud-Modell ein, um den Betrieb aufrechtzuerhalten.",
|
||||
"you-are-using-self-hosted-mode-mcp": "Sie verwenden den Self-hosted-Modus. Geben Sie die Google Search Keys in „MCP und Tools“ ein, um sicherzustellen, dass Eigent ordnungsgemäß funktioniert.",
|
||||
"palm-springs-tennis-trip-planner": "Palm Springs Tennis-Reiseplaner",
|
||||
"palm-springs-tennis-trip-planner-message": "Wir sind zwei Tennis-Fans und möchten das Tennisturnier in Palm Springs besuchen. Ich wohne in SF – bitte erstellen Sie einen detaillierten Reiseplan mit Flügen, Hotels und Aktivitäten für 3 Tage – ungefähr zur Zeit der Halbfinal-/Finalspiele. Wir mögen Wandern, veganes Essen und Spas. Unser Budget beträgt 5.000 $. Der Reiseplan sollte eine detaillierte Zeitleiste mit Uhrzeit, Aktivität, Kosten, weiteren Details und, falls zutreffend, einem Link zum Kauf von Tickets/Reservierungen usw. für den jeweiligen Punkt enthalten. Einige Präferenzen: 1. Spa-Zugang wäre schön, ist aber nicht notwendig. 2. Wenn Sie diese Aufgabe abgeschlossen haben, erstellen Sie bitte einen HTML-Bericht über diese Reise.",
|
||||
"palm-springs-tennis-trip-planner-message": "Ich bin ein Tennisfan und möchte das Tennisturnier in Palm Springs sehen. Ich lebe in San Francisco (SF). Bitte erstellen Sie einen detaillierten dreitägigen Reiseplan mit Flügen, Hotels und Aktivitäten für den Zeitraum um die Halbfinals/Finals. Ich mag Wandern, veganes Essen und Spas. Mein Budget beträgt 5.000 $. Der Reiseplan sollte ein detaillierter Zeitplan sein, der Zeiten, Aktivitäten, Kosten und andere Details zeigt und – wo zutreffend – Links zum Kauf von Tickets oder zur Vornahme von Reservierungen enthält. Vorlieben: 1. Zugang zu einem Spa wäre schön, ist aber nicht notwendig. 2. Wenn Sie diese Aufgabe abgeschlossen haben, erstellen Sie bitte einen HTML-Bericht über die Reise.",
|
||||
"bank-transfer-csv-analysis-and-visualization": "Banküberweisung CSV-Analyse und Visualisierung",
|
||||
"bank-transfer-csv-analysis-and-visualization-message": "Erstellen Sie eine Mock-CSV-Datei für Banküberweisungen mit 10 Spalten und 10 Zeilen. Lesen Sie die generierte CSV-Datei und fassen Sie die Daten zusammen. Erstellen Sie ein Diagramm, um relevante Trends oder Erkenntnisse aus den Daten zu visualisieren.",
|
||||
"find-duplicate-files-in-downloads-folder": "Doppelte Dateien im Download-Ordner finden",
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
"data-privacy-description": "Eigent basiert auf einem Local-First-Prinzip, um Ihre Privatsphäre zu gewährleisten. Ihre Daten verbleiben standardmäßig auf Ihrem Gerät. Cloud-Funktionen sind optional und verwenden nur die minimal erforderlichen Daten, um zu funktionieren. Für vollständige Details besuchen Sie bitte unsere",
|
||||
"privacy-policy": "Datenschutzrichtlinie",
|
||||
"how-we-handle-your-data": "Wie wir Ihre Daten behandeln",
|
||||
"how-we-handle-your-data-line-1": "Wir verwenden nur die für die Ausführung Ihrer Aufgaben erforderlichen Daten:",
|
||||
"how-we-handle-your-data-line-1": "Wir verwenden nur die für die Ausführung Ihrer Aufgaben erforderlichen Daten",
|
||||
"how-we-handle-your-data-line-1-line-1": "Eigent kann Screenshots erfassen, um UI-Elemente zu analysieren, Text zu lesen und die nächste Aktion zu bestimmen, genau wie Sie es tun würden.",
|
||||
"how-we-handle-your-data-line-1-line-2": "Eigent kann Ihre Maus und Tastatur verwenden, um auf von Ihnen angegebene lokale Software und Dateien zuzugreifen.",
|
||||
"how-we-handle-your-data-line-1-line-3": "Nur minimale Aufgabendaten werden an KI-Modellanbieter oder von Ihnen aktivierte Drittanbieter-Integrationen gesendet; wir speichern keine Daten.",
|
||||
|
|
@ -89,7 +89,7 @@
|
|||
"coming-soon": "Demnächst verfügbar",
|
||||
"uninstall": "Deinstallieren",
|
||||
"install": "Installieren",
|
||||
"add-your-mcp-server": "Fügen Sie Ihren MCP-Server hinzu",
|
||||
"add-your-agent": "Ihren Agenten hinzufügen",
|
||||
"add-a-local-mcp-server-by-providing-a-valid-json-configuration": "Fügen Sie einen lokalen MCP-Server hinzu, indem Sie eine gültige JSON-Konfiguration bereitstellen.",
|
||||
"learn-more": "Mehr erfahren",
|
||||
"installing": "Wird installiert...",
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"new-worker": "Neuer Mitarbeiter",
|
||||
"edit": "Bearbeiten",
|
||||
"configure-mcp-server": "MCP-Server konfigurieren",
|
||||
"add-your-mcp-server": "Fügen Sie Ihren MCP-Server hinzu",
|
||||
"add-your-agent": "Ihren Agenten hinzufügen",
|
||||
"cancel": "Abbrechen",
|
||||
"save-changes": "Änderungen speichern",
|
||||
"description-optional": "Beschreibung (Optional)",
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
"you-are-using-self-hosted-mode": "You're in Self-hosted mode. Cloud models can't be used here — set up your own local cloud model to keep things running.",
|
||||
"you-are-using-self-hosted-mode-mcp": "You're using Self-hosted mode. Enter the Google Search Keys in “MCP and Tools” to ensure Eigent works properly.",
|
||||
"palm-springs-tennis-trip-planner": "Palm Springs Tennis Trip Planner",
|
||||
"palm-springs-tennis-trip-planner-message": "I am two tennis fans and want to go see the tennis tournament in palm springs. l live in SF - please prepare a detailed itinerary with flights, hotels, things to do for 3 days - around the time semifinal/finals are happening. We like hiking, vegan food and spas. Our budget is $5K. The itinerary should be a detailed timeline of time, activity, cost, other details and if applicable a link to buy tickets/make reservations etc. for the item. Some preferences 1.Spa access would be nice but not necessary 2. When you finnish this task, please generate a html report about this trip.",
|
||||
"palm-springs-tennis-trip-planner-message": "I am a tennis fan and want to see the tennis tournament in Palm Springs. I live in San Francisco (SF). Please prepare a detailed, three-day itinerary with flights, hotels, and activities for the period around the semifinals/finals. I like hiking, vegan food, and spas. My budget is $5,000. The itinerary should be a detailed timeline showing times, activities, costs, and other details, and — where applicable — links to buy tickets or make reservations. Preferences: 1. Spa access would be nice but is not necessary. 2. When you finish this task, please generate an HTML report about the trip.",
|
||||
"bank-transfer-csv-analysis-and-visualization": "Bank Transfer CSV Analysis and Visualization",
|
||||
"bank-transfer-csv-analysis-and-visualization-message": "Create a mock bank transfer CSV file include 10 columns and 10 rows. Read the generated CSV file and summarize the data, generate a chart to visualize relevant trends or insights from the data.",
|
||||
"find-duplicate-files-in-downloads-folder": "Find Duplicate Files in Downloads Folder",
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
"data-privacy-description": "Eigent is built on a local-first principle to ensure your privacy. Your data remains on your device by default. Cloud features are optional and only use the minimum data necessary to function. For full details, visit our",
|
||||
"privacy-policy": "Privacy Policy",
|
||||
"how-we-handle-your-data": "How we handle your data",
|
||||
"how-we-handle-your-data-line-1": "We only use the essential data needed to run your tasks:",
|
||||
"how-we-handle-your-data-line-1": "We only use the essential data needed to run your tasks",
|
||||
"how-we-handle-your-data-line-1-line-1": "Eigent may capture screenshots to analyze UI elements, read text, and determine the next action, just as you would.",
|
||||
"how-we-handle-your-data-line-1-line-2": "Eigent may use your mouse and keyboard to access local software and files you specify.",
|
||||
"how-we-handle-your-data-line-1-line-3": "Only the minimum task data is sent to AI model providers or the third-party integrations you enable; we have zero data-retention",
|
||||
|
|
@ -89,7 +89,7 @@
|
|||
"coming-soon": "Coming Soon",
|
||||
"uninstall": "Uninstall",
|
||||
"install": "Install",
|
||||
"add-your-mcp-server": "Add your MCP server",
|
||||
"add-your-agent": "Add Your Agent",
|
||||
"add-a-local-mcp-server-by-providing-a-valid-json-configuration": "Add a local MCP server by providing a valid JSON configuration.",
|
||||
"learn-more": "Learn more",
|
||||
"installing": "Installing...",
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"your-ai-workforce": "Your AI Workforce",
|
||||
"new-worker": "New Worker",
|
||||
"new-worker": "New Worker Agent",
|
||||
"edit": "Edit",
|
||||
"configure-mcp-server": "Configure MCP Server",
|
||||
"add-your-mcp-server": "Add Your MCP Server",
|
||||
"add-your-agent": "Add Your Agent",
|
||||
"cancel": "Cancel",
|
||||
"save-changes": "Save changes",
|
||||
"description-optional": "Description (Optional)",
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
"you-are-using-self-hosted-mode": "Estás en modo autohospedado. No se pueden usar modelos en la nube aquí; configura tu propio modelo en la nube local para que todo siga funcionando.",
|
||||
"you-are-using-self-hosted-mode-mcp": "Estás usando el modo autohospedado. Ingresa las claves de búsqueda de Google en “MCP y herramientas” para asegurar que Eigent funcione correctamente.",
|
||||
"palm-springs-tennis-trip-planner": "Planificador de viaje de tenis a Palm Springs",
|
||||
"palm-springs-tennis-trip-planner-message": "Somos dos aficionados al tenis y queremos ir a ver el torneo en Palm Springs. Vivo en San Francisco; por favor, prepara un itinerario detallado con vuelos, hoteles y actividades para 3 días, alrededor de las semifinales/finales. Nos gustan el senderismo, la comida vegana y los spas. Nuestro presupuesto es de 5.000 dólares. El itinerario debe ser una línea de tiempo detallada con hora, actividad, costo, otros detalles y, si aplica, un enlace para comprar entradas/hacer reservas, etc. Preferencias: 1) Acceso a spa sería agradable pero no imprescindible. 2) Cuando termines esta tarea, genera un informe HTML sobre este viaje.",
|
||||
"palm-springs-tennis-trip-planner-message": "Soy un aficionado al tenis y quiero ver el torneo de tenis en Palm Springs. Vivo en San Francisco (SF). Por favor, prepare un itinerario detallado de tres días con vuelos, hoteles y actividades para el período de las semifinales/finales. Me gusta el senderismo, la comida vegana y los spas. Mi presupuesto es de 5,000 $. El itinerario debe ser un cronograma detallado que muestre horarios, actividades, costos y otros detalles, y, cuando corresponda, enlaces para comprar boletos o hacer reservaciones. Preferencias: 1. El acceso a un spa estaría bien, pero no es necesario. 2. Cuando termine esta tarea, por favor, genere un informe en HTML sobre el viaje.",
|
||||
"bank-transfer-csv-analysis-and-visualization": "Análisis y visualización de transferencias bancarias en CSV",
|
||||
"bank-transfer-csv-analysis-and-visualization-message": "Crea un archivo CSV simulado de transferencias bancarias que incluya 10 columnas y 10 filas. Lee el CSV generado y resume los datos; genera un gráfico para visualizar tendencias o ideas relevantes a partir de los datos.",
|
||||
"find-duplicate-files-in-downloads-folder": "Encontrar archivos duplicados en la carpeta Descargas",
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
"data-privacy-description": "Eigent está construido sobre un principio local-first para garantizar tu privacidad. Tus datos permanecen en tu dispositivo por defecto. Las características en la nube son opcionales y solo utilizan los datos mínimos necesarios para funcionar. Para obtener más detalles, visita nuestra",
|
||||
"privacy-policy": "Política de privacidad",
|
||||
"how-we-handle-your-data": "Cómo manejamos tus datos",
|
||||
"how-we-handle-your-data-line-1": "Solo utilizamos los datos esenciales necesarios para ejecutar tus tareas:",
|
||||
"how-we-handle-your-data-line-1": "Solo utilizamos los datos esenciales necesarios para ejecutar tus tareas",
|
||||
"how-we-handle-your-data-line-1-line-1": "Eigent puede capturar capturas de pantalla para analizar elementos de la interfaz de usuario, leer texto y determinar la siguiente acción, como tú.",
|
||||
"how-we-handle-your-data-line-1-line-2": "Eigent puede usar tu mouse y teclado para acceder a software y archivos locales que especifiques.",
|
||||
"how-we-handle-your-data-line-1-line-3": "Solo se envían los datos mínimos de las tareas a proveedores de modelos de IA o a las integraciones de terceros que activas; no tenemos ninguna retención de datos",
|
||||
|
|
@ -89,7 +89,7 @@
|
|||
"coming-soon": "Próximamente",
|
||||
"uninstall": "Desinstalar",
|
||||
"install": "Instalar",
|
||||
"add-your-mcp-server": "Agregar tu servidor MCP",
|
||||
"add-your-agent": "Añade tu Agente",
|
||||
"add-a-local-mcp-server-by-providing-a-valid-json-configuration": "Agregar un servidor MCP local proporcionando una configuración JSON válida.",
|
||||
"learn-more": "Aprender más",
|
||||
"installing": "Instalando...",
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"new-worker": "Nuevo Worker",
|
||||
"edit": "Editar",
|
||||
"configure-mcp-server": "Configurar MCP Server",
|
||||
"add-your-mcp-server": "Agregar tu MCP Server",
|
||||
"add-your-agent": "Añade tu Agente",
|
||||
"cancel": "Cancelar",
|
||||
"save-changes": "Guardar cambios",
|
||||
"description-optional": "Descripción (Opcional)",
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
"you-are-using-self-hosted-mode": "Vous êtes en mode auto-hébergé. Les modèles cloud ne peuvent pas être utilisés ici — configurez votre propre modèle cloud local pour que tout continue de fonctionner.",
|
||||
"you-are-using-self-hosted-mode-mcp": "Vous utilisez le mode auto-hébergé. Entrez les clés de recherche Google dans « MCP et Outils » pour garantir le bon fonctionnement d'Eigent.",
|
||||
"palm-springs-tennis-trip-planner": "Planificateur de voyage tennis à Palm Springs",
|
||||
"palm-springs-tennis-trip-planner-message": "Nous sommes deux fans de tennis et voulons aller voir le tournoi de tennis à Palm Springs. J'habite à SF - veuillez préparer un itinéraire détaillé avec vols, hôtels, choses à faire pendant 3 jours - autour de la période des demi-finales/finales. Nous aimons la randonnée, la nourriture végétalienne et les spas. Notre budget est de 5 000 $. L'itinéraire doit être un calendrier détaillé de l'heure, de l'activité, du coût, d'autres détails et, le cas échéant, un lien pour acheter des billets/faire des réservations, etc. pour l'article. Certaines préférences 1. L'accès au spa serait agréable mais pas nécessaire 2. Lorsque vous aurez terminé cette tâche, veuillez générer un rapport HTML sur ce voyage.",
|
||||
"palm-springs-tennis-trip-planner-message": "Je suis un fan de tennis et je souhaite voir le tournoi de tennis à Palm Springs. Je vis à San Francisco (SF). Veuillez préparer un itinéraire détaillé de trois jours avec les vols, les hôtels et les activités pour la période des demi-finales/finales. J'aime la randonnée, la nourriture végétalienne et les spas. Mon budget est de 5 000 $. L'itinéraire doit être un calendrier détaillé indiquant les heures, les activités, les coûts et d'autres détails, et — le cas échéant — des liens pour acheter des billets ou faire des réservations. Préférences : 1. L'accès à un spa serait appréciable mais n'est pas nécessaire. 2. Lorsque vous aurez terminé cette tâche, veuillez générer un rapport HTML sur le voyage.",
|
||||
"bank-transfer-csv-analysis-and-visualization": "Analyse et visualisation CSV des virements bancaires",
|
||||
"bank-transfer-csv-analysis-and-visualization-message": "Créez un fichier CSV de virements bancaires fictifs comprenant 10 colonnes et 10 lignes. Lisez le fichier CSV généré et résumez les données, générez un graphique pour visualiser les tendances ou les informations pertinentes à partir des données.",
|
||||
"find-duplicate-files-in-downloads-folder": "Rechercher les fichiers en double dans le dossier Téléchargements",
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
"data-privacy-description": "Eigent is built on a local-first principle to ensure your privacy. Your data remains on your device by default. Cloud features are optional and only use the minimum data necessary to function. For full details, visit our",
|
||||
"privacy-policy": "Privacy Policy",
|
||||
"how-we-handle-your-data": "How we handle your data",
|
||||
"how-we-handle-your-data-line-1": "We only use the essential data needed to run your tasks:",
|
||||
"how-we-handle-your-data-line-1": "We only use the essential data needed to run your tasks",
|
||||
"how-we-handle-your-data-line-1-line-1": "Eigent may capture screenshots to analyze UI elements, read text, and determine the next action, just as you would.",
|
||||
"how-we-handle-your-data-line-1-line-2": "Eigent may use your mouse and keyboard to access local software and files you specify.",
|
||||
"how-we-handle-your-data-line-1-line-3": "Only the minimum task data is sent to AI model providers or the third-party integrations you enable; we have zero data-retention",
|
||||
|
|
@ -88,10 +88,10 @@
|
|||
"invalid-json": "Invalid JSON",
|
||||
"coming-soon": "Coming Soon",
|
||||
"uninstall": "Uninstall",
|
||||
"install": "Install",
|
||||
"add-your-mcp-server": "Add your MCP server",
|
||||
"add-a-local-mcp-server-by-providing-a-valid-json-configuration": "Add a local MCP server by providing a valid JSON configuration.",
|
||||
"learn-more": "Learn more",
|
||||
"install": "Installer",
|
||||
"add-your-agent": "Ajoutez votre Agent",
|
||||
"add-a-local-mcp-server-by-providing-a-valid-json-configuration": "Ajoutez un serveur MCP local en fournissant une configuration JSON valide.",
|
||||
"learn-more": "En savoir plus",
|
||||
"installing": "Installing...",
|
||||
"edit-mcp-config": "Edit MCP Config",
|
||||
"name": "Name",
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"new-worker": "Nouveau travailleur",
|
||||
"edit": "Modifier",
|
||||
"configure-mcp-server": "Configurer le serveur MCP",
|
||||
"add-your-mcp-server": "Ajouter votre serveur MCP",
|
||||
"add-your-agent": "Ajoutez votre Agent",
|
||||
"cancel": "Annuler",
|
||||
"save-changes": "Enregistrer les modifications",
|
||||
"description-optional": "Description (facultatif)",
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
"you-are-using-self-hosted-mode": "Sei in modalità Self-hosted. I modelli cloud non possono essere utilizzati qui — configura il tuo modello cloud locale per mantenere tutto in funzione.",
|
||||
"you-are-using-self-hosted-mode-mcp": "Stai utilizzando la modalità Self-hosted. Inserisci le Chiavi di Ricerca Google in \"MCP e Strumenti\" per garantire che Eigent funzioni correttamente.",
|
||||
"palm-springs-tennis-trip-planner": "Pianificatore di Viaggio per il Torneo di Tennis di Palm Springs",
|
||||
"palm-springs-tennis-trip-planner-message": "Siamo due appassionati di tennis e vogliamo andare a vedere il torneo di tennis a Palm Springs. Vivo a SF - per favore prepara un itinerario dettagliato con voli, hotel, cose da fare per 3 giorni - intorno al periodo delle semifinali/finali. Ci piace fare escursioni, cibo vegano e spa. Il nostro budget è di 5.000$. L'itinerario dovrebbe essere una timeline dettagliata di orario, attività, costo, altri dettagli e, se applicabile, un link per acquistare biglietti/effettuare prenotazioni, ecc. per l'articolo. Alcune preferenze: 1. L'accesso alla spa sarebbe bello ma non necessario 2. Quando avrai finito questo compito, genera un report HTML su questo viaggio.",
|
||||
"palm-springs-tennis-trip-planner-message": "Sono un appassionato di tennis e vorrei vedere il torneo di tennis a Palm Springs. Vivo a San Francisco (SF). Per favore, prepari un itinerario dettagliato di tre giorni con voli, hotel e attività per il periodo delle semifinali/finali. Mi piacciono le escursioni, il cibo vegano e le spa. Il mio budget è di 5.000 $. L'itinerario dovrebbe essere una cronologia dettagliata che mostri orari, attività, costi e altri dettagli e, ove applicabile, i link per acquistare i biglietti o effettuare prenotazioni. Preferenze: 1. L'accesso a una spa sarebbe gradito ma non è necessario. 2. Al termine di questo compito, la prego di generare un report in HTML sul viaggio.",
|
||||
"bank-transfer-csv-analysis-and-visualization": "Analisi e Visualizzazione CSV di Bonifici Bancari",
|
||||
"bank-transfer-csv-analysis-and-visualization-message": "Crea un file CSV di bonifici bancari fittizio con 10 colonne e 10 righe. Leggi il file CSV generato e riassumi i dati, genera un grafico per visualizzare tendenze o intuizioni pertinenti dai dati.",
|
||||
"find-duplicate-files-in-downloads-folder": "Trova File Duplicati nella Cartella Download",
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
"data-privacy-description": "Eigent è costruito su un principio local-first per garantire la tua privacy. I tuoi dati rimangono sul tuo dispositivo per impostazione predefinita. Le funzionalità cloud sono opzionali e utilizzano solo i dati minimi necessari per funzionare. Per tutti i dettagli, visita la nostra",
|
||||
"privacy-policy": "Informativa sulla privacy",
|
||||
"how-we-handle-your-data": "Come gestiamo i tuoi dati",
|
||||
"how-we-handle-your-data-line-1": "Utilizziamo solo i dati essenziali necessari per eseguire le tue attività:",
|
||||
"how-we-handle-your-data-line-1": "Utilizziamo solo i dati essenziali necessari per eseguire le tue attività",
|
||||
"how-we-handle-your-data-line-1-line-1": "Eigent può acquisire screenshot per analizzare elementi dell'interfaccia utente, leggere testo e determinare l'azione successiva, proprio come faresti tu.",
|
||||
"how-we-handle-your-data-line-1-line-2": "Eigent può utilizzare il tuo mouse e la tua tastiera per accedere a software e file locali da te specificati.",
|
||||
"how-we-handle-your-data-line-1-line-3": "Solo i dati minimi necessari per l'attività vengono inviati ai fornitori di modelli AI o alle integrazioni di terze parti da te abilitate; non abbiamo alcuna conservazione dei dati",
|
||||
|
|
@ -89,7 +89,7 @@
|
|||
"coming-soon": "Prossimamente",
|
||||
"uninstall": "Disinstalla",
|
||||
"install": "Installa",
|
||||
"add-your-mcp-server": "Aggiungi il tuo server MCP",
|
||||
"add-your-agent": "Aggiungi il tuo Agente",
|
||||
"add-a-local-mcp-server-by-providing-a-valid-json-configuration": "Aggiungi un server MCP locale fornendo una configurazione JSON valida.",
|
||||
"learn-more": "Scopri di più",
|
||||
"installing": "Installazione in corso...",
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"new-worker": "Nuovo Worker",
|
||||
"edit": "Modifica",
|
||||
"configure-mcp-server": "Configura server MCP",
|
||||
"add-your-mcp-server": "Aggiungi il tuo server MCP",
|
||||
"add-your-agent": "Aggiungi il tuo Agente",
|
||||
"cancel": "Annulla",
|
||||
"save-changes": "Salva modifiche",
|
||||
"description-optional": "Descrizione (Opzionale)",
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
"you-are-using-self-hosted-mode": "セルフホストモードを使用しています。クラウドモデルはここでは使用できません。続行するには、独自のローカルクラウドモデルを設定してください。",
|
||||
"you-are-using-self-hosted-mode-mcp": "セルフホストモードを使用しています。Eigentが正しく機能するように、「MCPとツール」にGoogle検索キーを入力してください。",
|
||||
"palm-springs-tennis-trip-planner": "パームスプリングステニス旅行プランナー",
|
||||
"palm-springs-tennis-trip-planner-message": "私は2人のテニスファンで、パームスプリングスのテニストーナメントを見に行きたいです。SFに住んでいます。準決勝/決勝の頃の3日間の詳細な旅程(フライト、ホテル、アクティビティ込み)を作成してください。ハイキング、ビーガンフード、スパが好きです。予算は5,000ドルです。旅程には、時間、アクティビティ、費用、その他の詳細、および可能な場合はチケット購入/予約などのリンクを含む詳細なタイムラインを含めてください。いくつか希望があります。1.スパへのアクセスがあれば良いですが、必須ではありません。2.このタスクが完了したら、この旅行に関するHTMLレポートを生成してください。",
|
||||
"palm-springs-tennis-trip-planner-message": "私はテニスファンで、パームスプリングスで開催されるテニストーナメントを見に行きたいです。私はサンフランシスコ(SF)に住んでいます。準決勝・決勝の期間に合わせた、航空券、ホテル、アクティビティを含む3日間の詳細な旅程を作成してください。ハイキング、ヴィーガンフード、スパが好きです。予算は5,000ドルです。旅程には、時間、アクティビティ、費用、その他の詳細を示す詳細なタイムラインを含め、該当する場合にはチケットの購入や予約を行うためのリンクも記載してください。希望:1. スパの利用は必須ではありませんが、できれば嬉しいです。2. このタスクが完了したら、旅行に関するHTMLレポートを生成してください。",
|
||||
"bank-transfer-csv-analysis-and-visualization": "銀行振込CSV分析と可視化",
|
||||
"bank-transfer-csv-analysis-and-visualization-message": "10列10行のモック銀行振込CSVファイルを作成してください。生成されたCSVファイルを読み込み、データを要約し、データから関連する傾向や洞察を可視化するためのグラフを生成してください。",
|
||||
"find-duplicate-files-in-downloads-folder": "ダウンロードフォルダ内の重複ファイルを検索",
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
"data-privacy-description": "Eigentはプライバシーを確保するためにローカルファーストの原則に基づいて構築されています。デフォルトでは、データはお客様のデバイスに残ります。クラウド機能はオプションであり、機能するために必要な最小限のデータのみを使用します。詳細については、当社の",
|
||||
"privacy-policy": "プライバシーポリシー",
|
||||
"how-we-handle-your-data": "データの取り扱い方法",
|
||||
"how-we-handle-your-data-line-1": "タスクを実行するために必要な最小限のデータのみを使用します。",
|
||||
"how-we-handle-your-data-line-1": "タスクを実行するために必要な最小限のデータのみを使用します",
|
||||
"how-we-handle-your-data-line-1-line-1": "Eigentは、UI要素を分析し、テキストを読み取り、次のアクションを決定するために、お客様が行うのと同じようにスクリーンショットをキャプチャする場合があります。",
|
||||
"how-we-handle-your-data-line-1-line-2": "Eigentは、指定したローカルソフトウェアおよびファイルにアクセスするために、マウスとキーボードを使用する場合があります。",
|
||||
"how-we-handle-your-data-line-1-line-3": "有効にしたAIモデルプロバイダーまたはサードパーティ統合に送信されるのは、タスクの最小限のデータのみです。データ保持はありません。",
|
||||
|
|
@ -89,7 +89,7 @@
|
|||
"coming-soon": "近日公開",
|
||||
"uninstall": "アンインストール",
|
||||
"install": "インストール",
|
||||
"add-your-mcp-server": "MCPサーバーを追加",
|
||||
"add-your-agent": "エージェントを追加",
|
||||
"add-a-local-mcp-server-by-providing-a-valid-json-configuration": "有効なJSON構成を提供して、ローカルMCPサーバーを追加します。",
|
||||
"learn-more": "詳細はこちら",
|
||||
"installing": "インストール中...",
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"new-worker": "新規ワーカー",
|
||||
"edit": "編集",
|
||||
"configure-mcp-server": "MCPサーバーの設定",
|
||||
"add-your-mcp-server": "MCPサーバーを追加",
|
||||
"add-your-agent": "エージェントを追加",
|
||||
"cancel": "キャンセル",
|
||||
"save-changes": "変更を保存",
|
||||
"description-optional": "説明(任意)",
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
"you-are-using-self-hosted-mode": "셀프 호스팅 모드를 사용 중입니다. 클라우드 모델은 여기서 사용할 수 없습니다. 계속 진행하려면 자체 로컬 클라우드 모델을 설정하세요.",
|
||||
"you-are-using-self-hosted-mode-mcp": "셀프 호스팅 모드를 사용 중입니다. Eigent가 제대로 작동하도록 \"MCP 및 도구\"에 Google 검색 키를 입력하세요.",
|
||||
"palm-springs-tennis-trip-planner": "팜스프링스 테니스 여행 플래너",
|
||||
"palm-springs-tennis-trip-planner-message": "저는 두 명의 테니스 팬이며 팜스프링스에서 테니스 토너먼트를 보러 가고 싶습니다. 저는 샌프란시스코에 거주합니다. 준결승/결승이 열리는 시기 주변의 3일간의 항공편, 호텔, 할 일에 대한 자세한 일정 계획을 세워주세요. 하이킹, 비건 음식, 스파를 좋아합니다. 저희 예산은 5,000달러입니다. 일정에는 시간, 활동, 비용, 기타 세부 정보 및 가능한 경우 티켓 구매/예약 등을 위한 링크가 포함된 자세한 타임라인이 있어야 합니다. 몇 가지 선호 사항: 1. 스파 이용이 가능하면 좋지만 필수는 아닙니다. 2. 이 작업을 마치면 이 여행에 대한 HTML 보고서를 생성해 주세요.",
|
||||
"palm-springs-tennis-trip-planner-message": "저는 테니스 팬이며 팜스프링스에서 열리는 테니스 토너먼트를 보고 싶습니다. 저는 샌프란시스코(SF)에 살고 있습니다. 준결승/결승 기간에 맞춘 항공편, 호텔, 활동이 포함된 상세한 3일 여행 일정을 준비해 주세요. 저는 하이킹, 비건 음식, 스파를 좋아합니다. 제 예산은 5,000달러입니다. 일정에는 시간, 활동, 비용 및 기타 세부 정보가 포함된 상세한 타임라인이 있어야 하며, 해당하는 경우 티켓 구매 또는 예약을 위한 링크도 포함되어야 합니다. 선호 사항: 1. 스파 이용은 필수 사항은 아니지만 가능하면 좋겠습니다. 2. 이 작업을 마치면 여행에 대한 HTML 보고서를 생성해 주세요.",
|
||||
"bank-transfer-csv-analysis-and-visualization": "은행 송금 CSV 분석 및 시각화",
|
||||
"bank-transfer-csv-analysis-and-visualization-message": "10개의 열과 10개의 행으로 구성된 모의 은행 송금 CSV 파일을 생성하세요. 생성된 CSV 파일을 읽고 데이터를 요약하고 데이터에서 관련 추세 또는 통찰력을 시각화할 차트를 생성하세요.",
|
||||
"find-duplicate-files-in-downloads-folder": "다운로드 폴더에서 중복 파일 찾기",
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
"data-privacy-description": "Eigent는 개인정보 보호를 위해 로컬 우선 원칙을 기반으로 구축되었습니다. 기본적으로 데이터는 사용자의 기기에 남아 있습니다. 클라우드 기능은 선택 사항이며 작동에 필요한 최소한의 데이터만 사용합니다. 자세한 내용은 당사의",
|
||||
"privacy-policy": "개인정보 처리방침",
|
||||
"how-we-handle-your-data": "데이터 처리 방식",
|
||||
"how-we-handle-your-data-line-1": "작업 실행에 필요한 필수 데이터만 사용합니다:",
|
||||
"how-we-handle-your-data-line-1": "작업 실행에 필요한 필수 데이터만 사용합니다",
|
||||
"how-we-handle-your-data-line-1-line-1": "Eigent는 UI 요소를 분석하고 텍스트를 읽고 다음 작업을 결정하기 위해 스크린샷을 캡처할 수 있습니다. 사용자가 하는 방식과 같습니다.",
|
||||
"how-we-handle-your-data-line-1-line-2": "Eigent는 사용자가 지정한 로컬 소프트웨어 및 파일에 액세스하기 위해 마우스와 키보드를 사용할 수 있습니다.",
|
||||
"how-we-handle-your-data-line-1-line-3": "AI 모델 제공업체 또는 사용자가 활성화한 타사 통합에 전송되는 데이터는 최소한의 작업 데이터뿐이며, 데이터 보존은 전혀 없습니다.",
|
||||
|
|
@ -89,7 +89,7 @@
|
|||
"coming-soon": "출시 예정",
|
||||
"uninstall": "제거",
|
||||
"install": "설치",
|
||||
"add-your-mcp-server": "MCP 서버 추가",
|
||||
"add-your-agent": "에이전트 추가",
|
||||
"add-a-local-mcp-server-by-providing-a-valid-json-configuration": "유효한 JSON 구성을 제공하여 로컬 MCP 서버를 추가하세요.",
|
||||
"learn-more": "더 알아보기",
|
||||
"installing": "설치 중...",
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"new-worker": "새 작업자",
|
||||
"edit": "편집",
|
||||
"configure-mcp-server": "MCP 서버 구성",
|
||||
"add-your-mcp-server": "MCP 서버 추가",
|
||||
"add-your-agent": "에이전트 추가",
|
||||
"cancel": "취소",
|
||||
"save-changes": "변경 사항 저장",
|
||||
"description-optional": "설명 (선택 사항)",
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
"you-are-using-self-hosted-mode": "Вы используете режим самостоятельного хостинга. Облачные модели здесь использовать нельзя — настройте собственную локальную облачную модель, чтобы все работало.",
|
||||
"you-are-using-self-hosted-mode-mcp": "Вы используете режим самостоятельного хостинга. Введите ключи Google Search в разделе «MCP и инструменты», чтобы Eigent работал должным образом.",
|
||||
"palm-springs-tennis-trip-planner": "Планировщик поездки на теннисный турнир в Палм-Спрингс",
|
||||
"palm-springs-tennis-trip-planner-message": "Мы два теннисных фаната и хотим поехать на теннисный турнир в Палм-Спрингс. Мы живем в Сан-Франциско — пожалуйста, составьте подробный маршрут с указанием авиабилетов, отелей, развлечений на 3 дня — примерно во время полуфинала/финала. Нам нравится хайкинг, веганская еда и спа. Наш бюджет — 5000 долларов. Маршрут должен быть подробным графиком времени, мероприятий, стоимости, других деталей и, если применимо, ссылкой для покупки билетов/бронирования и т.д. для каждого пункта. Некоторые предпочтения: 1. Доступ к спа был бы желателен, но не обязателен. 2. По завершении этой задачи, пожалуйста, сгенерируйте HTML-отчет об этой поездке.",
|
||||
"palm-springs-tennis-trip-planner-message": "Я фанат тенниса и хочу посмотреть теннисный турнир в Палм-Спрингс. Я живу в Сан-Франциско (SF). Пожалуйста, подготовьте подробный трехдневный маршрут с перелетами, отелями и мероприятиями на период полуфиналов/финалов. Мне нравятся пешие прогулки, веганская еда и спа. Мой бюджет составляет 5000 долларов. Маршрут должен представлять собой подробный график с указанием времени, мероприятий, затрат и других деталей, а также, где это применимо, ссылок для покупки билетов или бронирования. Предпочтения: 1. Доступ в спа был бы неплох, но не обязателен. 2. Когда вы закончите эту задачу, пожалуйста, сгенерируйте HTML-отчет о поездке.",
|
||||
"bank-transfer-csv-analysis-and-visualization": "Анализ и визуализация CSV-файлов банковских переводов",
|
||||
"bank-transfer-csv-analysis-and-visualization-message": "Создайте пример CSV-файла банковских переводов, включающий 10 столбцов и 10 строк. Прочитайте сгенерированный CSV-файл и обобщите данные, сгенерируйте диаграмму для визуализации соответствующих тенденций или выводов из данных.",
|
||||
"find-duplicate-files-in-downloads-folder": "Поиск дубликатов файлов в папке загрузок",
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
"data-privacy-description": "Eigent построен по принципу \"сначала локально\", чтобы обеспечить вашу конфиденциальность. Ваши данные по умолчанию остаются на вашем устройстве. Облачные функции являются необязательными и используют только минимальные необходимые данные для функционирования. Полные сведения см. в нашей",
|
||||
"privacy-policy": "Политике конфиденциальности",
|
||||
"how-we-handle-your-data": "Как мы обрабатываем ваши данные",
|
||||
"how-we-handle-your-data-line-1": "Мы используем только необходимые данные для выполнения ваших задач:",
|
||||
"how-we-handle-your-data-line-1": "Мы используем только необходимые данные для выполнения ваших задач",
|
||||
"how-we-handle-your-data-line-1-line-1": "Eigent может делать снимки экрана для анализа элементов пользовательского интерфейса, чтения текста и определения следующего действия, точно так же, как это сделали бы вы.",
|
||||
"how-we-handle-your-data-line-1-line-2": "Eigent может использовать вашу мышь и клавиатуру для доступа к локальному программному обеспечению и файлам, которые вы указываете.",
|
||||
"how-we-handle-your-data-line-1-line-3": "Только минимальные данные задачи отправляются поставщикам моделей ИИ или сторонним интеграциям, которые вы включаете; мы не храним данные.",
|
||||
|
|
@ -89,7 +89,7 @@
|
|||
"coming-soon": "Скоро",
|
||||
"uninstall": "Удалить",
|
||||
"install": "Установить",
|
||||
"add-your-mcp-server": "Добавьте свой MCP-сервер",
|
||||
"add-your-agent": "Добавьте своего агента",
|
||||
"add-a-local-mcp-server-by-providing-a-valid-json-configuration": "Добавьте локальный MCP-сервер, предоставив допустимую JSON-конфигурацию.",
|
||||
"learn-more": "Подробнее",
|
||||
"installing": "Установка...",
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"new-worker": "Новый сотрудник",
|
||||
"edit": "Редактировать",
|
||||
"configure-mcp-server": "Настроить MCP-сервер",
|
||||
"add-your-mcp-server": "Добавить ваш MCP-сервер",
|
||||
"add-your-agent": "Добавьте своего агента",
|
||||
"cancel": "Отмена",
|
||||
"save-changes": "Сохранить изменения",
|
||||
"description-optional": "Описание (необязательно)",
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
"you-are-using-self-hosted-mode": "您正在使用自部署模式。云端模型不能在这里使用 — 设置您自己的本地云模型以保持运行。",
|
||||
"you-are-using-self-hosted-mode-mcp": "您正在使用自部署模式。在“MCP和工具”中输入 Google 搜索密钥以确保 Eigent 正常工作。",
|
||||
"palm-springs-tennis-trip-planner": "棕榈泉网球之旅规划师",
|
||||
"palm-springs-tennis-trip-planner-message": "我是两个网球爱好者,想去观看2026年棕榈泉的网球比赛。我住在旧金山——请准备一个详细的行程,包括航班、酒店、为期3天的活动安排——围绕半决赛/决赛的时间。我们喜欢徒步、素食和 SPA。预算为5,000美元。行程应是一个详细的时间表,包括时间、活动、费用、其他细节,以及购买门票/预订的链接(如适用)。完成任务后,请生成一份关于此次旅行的 HTML 报告;编写此计划的摘要,并将文本摘要和报告 HTML 链接发送到 Slack #tennis-trip-sf 频道。",
|
||||
"palm-springs-tennis-trip-planner-message": "我是一名网球迷,想去棕榈泉看网球锦标赛。我住在旧金山(SF)。请为我准备一份为期三天的详细行程,包括半决赛/决赛期间的航班、酒店和活动。我喜欢徒步、素食和水疗。我的预算是5000美元。行程应为详细的时间表,显示时间、活动、费用和其他细节,并在适用的情况下提供购买门票或进行预订的链接。偏好:1. 如果能有水疗中心就更好了,但不是必须的。2. 完成此任务后,请生成一份关于此次旅行的HTML报告。",
|
||||
"bank-transfer-csv-analysis-and-visualization": "银行转账 CSV 分析和可视化",
|
||||
"bank-transfer-csv-analysis-and-visualization-message": "创建一个包含10列和10行的模拟银行转账 CSV 文件。读取生成的 CSV 文件并总结数据,生成一个图表来可视化相关趋势或洞察数据。",
|
||||
"find-duplicate-files-in-downloads-folder": "查找下载文件夹中的重复文件",
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@
|
|||
"data-privacy-description": "Eigent 基于本地优先原则确保您的隐私。您的数据默认存储在您的设备上。云功能是可选的,仅使用最小的数据来实现功能。详细信息请访问我们的",
|
||||
"privacy-policy": "隐私政策",
|
||||
"how-we-handle-your-data": "我们如何处理您的数据",
|
||||
"we-only-use-the-essential-data-needed-to-run-your-tasks": "我们仅使用运行您的任务所需的关键数据:",
|
||||
"how-we-handle-your-data-line-1": "我们仅使用运行您的任务所需的关键数据:",
|
||||
"we-only-use-the-essential-data-needed-to-run-your-tasks": "我们仅使用运行您的任务所需的关键数据",
|
||||
"how-we-handle-your-data-line-1": "我们仅使用运行您的任务所需的关键数据",
|
||||
"how-we-handle-your-data-line-1-line-1": "Eigent 可能捕获您的屏幕截图以分析 UI 元素、读取文本并确定下一步操作,就像您一样。",
|
||||
"how-we-handle-your-data-line-1-line-2": "Eigent 可能使用您的鼠标和键盘访问您指定的本地软件和文件。",
|
||||
"how-we-handle-your-data-line-1-line-3": "只有最少的任务数据被发送到 AI 模型提供商或您启用的第三方集成;我们与这些提供商没有数据保留协议",
|
||||
|
|
@ -89,7 +89,7 @@
|
|||
"coming-soon": "即将推出",
|
||||
"uninstall": "卸载",
|
||||
"install": "安装",
|
||||
"add-your-mcp-server": "添加您的 MCP 服务器",
|
||||
"add-your-agent": "添加您的智能体",
|
||||
"add-a-local-mcp-server-by-providing-a-valid-json-configuration": "通过提供有效的 JSON 配置添加本地 MCP 服务器。",
|
||||
"learn-more": "了解更多",
|
||||
"installing": "安装中...",
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"new-worker": "增加 Worker",
|
||||
"edit": "编辑",
|
||||
"configure-mcp-server": "配置 MCP 服务器",
|
||||
"add-your-mcp-server": "添加你的 MCP 服务器",
|
||||
"add-your-agent": "添加您的智能体",
|
||||
"cancel": "取消",
|
||||
"save-changes": "保存更改",
|
||||
"description-optional": "描述(可选)",
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
"you-are-using-self-hosted-mode": "您正在使用自部署模式。雲端模型不能在這裡使用 — 設置您自己的本地雲模型以保持運行。",
|
||||
"you-are-using-self-hosted-mode-mcp": "您正在使用自部署模式。在“MCP和工具”中輸入 Google 搜索密鑰以確保 Eigent 正常工作。",
|
||||
"palm-springs-tennis-trip-planner": "棕榈泉網球之旅規劃師",
|
||||
"palm-springs-tennis-trip-planner-message": "我是两个网球爱好者,想去观看2026年棕榈泉的网球比赛。我住在旧金山——请准备一个详细的行程,包括航班、酒店、为期3天的活动安排——围绕半决赛/决赛的时间。我们喜欢徒步、素食和 SPA。预算为5,000美元。行程应是一个详细的时间表,包括时间、活动、费用、其他细节,以及购买门票/预订的链接(如适用)。完成任务后,请生成一份关于此次旅行的 HTML 报告;編寫此計劃的摘要,並將文本摘要和报告 HTML 鏈接發送到 Slack #tennis-trip-sf 頻道。",
|
||||
"palm-springs-tennis-trip-planner-message": "我是一名網球迷,想去棕櫚泉看網球錦標賽。我住在舊金山(SF)。請為我準備一份為期三天的詳細行程,包括半決賽/決賽期間的航班、酒店和活動。我喜歡徒步、素食和水療。我的預算是5000美元。行程應為詳細的時間表,顯示時間、活動、費用和其他細節,並在適用的情況下提供購買門票或進行預訂的連結。偏好:1. 如果能有水療中心就更好了,但不是必須的。2. 完成此任務後,請生成一份關於此次旅行的HTML報告。",
|
||||
"bank-transfer-csv-analysis-and-visualization": "銀行轉賬 CSV 分析和視覺化",
|
||||
"bank-transfer-csv-analysis-and-visualization-message": "創建一個包含10列和10行的模擬銀行轉賬 CSV 文件。讀取生成的 CSV 文件並總結數據,生成一個圖表來視覺化相關趨勢或洞察數據。",
|
||||
"find-duplicate-files-in-downloads-folder": "查找下载文件夹中的重复文件",
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@
|
|||
"data-privacy-description": "Eigent 以本地優先原則確保您的隱私。您的數據預設儲存在您的設備上。雲端功能是可選的,僅使用最少量的數據來實現功能。詳細資訊請參閱我們的",
|
||||
"privacy-policy": "隱私政策",
|
||||
"how-we-handle-your-data": "我們如何處理您的數據",
|
||||
"we-only-use-the-essential-data-needed-to-run-your-tasks": "我們僅使用運行您的任務所需的關鍵數據:",
|
||||
"how-we-handle-your-data-line-1": "我們僅使用運行您的任務所需的關鍵數據:",
|
||||
"we-only-use-the-essential-data-needed-to-run-your-tasks": "我們僅使用運行您的任務所需的關鍵數據",
|
||||
"how-we-handle-your-data-line-1": "我們僅使用運行您的任務所需的關鍵數據",
|
||||
"how-we-handle-your-data-line-1-line-1": "Eigent 可能會擷取您的螢幕截圖以分析 UI 元素、讀取文字並判斷下一步操作,就像您一樣。",
|
||||
"how-we-handle-your-data-line-1-line-2": "Eigent 可能會使用您的滑鼠和鍵盤存取您指定的本地軟體和檔案。",
|
||||
"how-we-handle-your-data-line-1-line-3": "僅有最少量的任務數據會傳送至 AI 模型供應商或您啟用的第三方整合;我們與這些供應商沒有數據保留協議。",
|
||||
|
|
@ -89,7 +89,7 @@
|
|||
"coming-soon": "即將推出",
|
||||
"uninstall": "解除安裝",
|
||||
"install": "安裝",
|
||||
"add-your-mcp-server": "新增您的 MCP 伺服器",
|
||||
"add-your-agent": "新增您的智能體",
|
||||
"add-a-local-mcp-server-by-providing-a-valid-json-configuration": "透過提供有效的 JSON 設定新增本地 MCP 伺服器。",
|
||||
"learn-more": "了解更多",
|
||||
"installing": "安裝中...",
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"new-worker": "新增 Worker",
|
||||
"edit": "編輯",
|
||||
"configure-mcp-server": "設定 MCP 伺服器",
|
||||
"add-your-mcp-server": "新增您的 MCP 伺服器",
|
||||
"add-your-agent": "新增您的智能體",
|
||||
"cancel": "取消",
|
||||
"save-changes": "儲存變更",
|
||||
"description-optional": "描述(可選)",
|
||||
|
|
|
|||
|
|
@ -295,7 +295,7 @@ export default function Home() {
|
|||
}}
|
||||
className={`${
|
||||
chatStore.activeTaskId === taskId ? "!bg-white-100%" : ""
|
||||
} relative cursor-pointer transition-all duration-300 bg-white-30% hover:bg-white-100% rounded-3xl flex justify-between items-center gap-md flex-1 w-[calc(33%-48px)] min-w-[300px] max-w-[500px] h-[180px] px-6 shadow-history-item`}
|
||||
} relative cursor-pointer transition-all duration-300 bg-white-30% hover:bg-white-100% rounded-3xl flex justify-between items-center gap-md flex-initial w-[calc(33%-48px)] min-w-[300px] max-w-[500px] h-[180px] px-6 shadow-history-item`}
|
||||
>
|
||||
<div className="w-[133px] py-md h-full flex flex-col gap-1">
|
||||
<div className="flex-1 flex justify-start items-end">
|
||||
|
|
@ -555,7 +555,7 @@ export default function Home() {
|
|||
chatStore.activeTaskId === task.task_id
|
||||
? "!bg-white-100%"
|
||||
: ""
|
||||
} relative cursor-pointer transition-all duration-300 bg-white-30% hover:bg-white-100% rounded-3xl flex justify-between items-center flex-wrap gap-md flex-1 w-[calc(33%-48px)] min-w-[300px] max-w-[500px] h-[180px] p-6 shadow-history-item border border-solid border-border-disabled`}
|
||||
} relative cursor-pointer transition-all duration-300 bg-white-30% hover:bg-white-100% rounded-3xl flex justify-between items-center flex-wrap gap-md flex-initial w-[calc(33%-48px)] min-w-[300px] max-w-[500px] h-[180px] p-6 shadow-history-item border border-solid border-border-disabled`}
|
||||
>
|
||||
<div
|
||||
className="flex justify-between items-end gap-1 w-full"
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ export default function SettingGeneral() {
|
|||
<div className="flex items-center gap-sm">
|
||||
<Button
|
||||
onClick={() => {
|
||||
window.location.href = `https://www.eigent.ai/dashboard`;
|
||||
window.location.href = `https://www.eigent.ai/dashboard?email=${authStore.email}`;
|
||||
}}
|
||||
variant="primary"
|
||||
size="xs"
|
||||
|
|
@ -216,4 +216,4 @@ export default function SettingGeneral() {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -61,6 +61,7 @@ export default function SettingMCP() {
|
|||
|
||||
// add: integrations list
|
||||
const [integrations, setIntegrations] = useState<any[]>([]);
|
||||
const [refreshKey, setRefreshKey] = useState<number>(0);
|
||||
const [essentialIntegrations, setEssentialIntegrations] = useState<any[]>([
|
||||
{
|
||||
key: "Search",
|
||||
|
|
@ -111,8 +112,57 @@ export default function SettingMCP() {
|
|||
const list = Object.entries(res).map(([key, value]: [string, any]) => {
|
||||
let onInstall = null;
|
||||
|
||||
onInstall = () =>
|
||||
(window.location.href = `${baseURL}/api/oauth/${key.toLowerCase()}/login`);
|
||||
// Special handling for Notion MCP
|
||||
if (key.toLowerCase() === 'notion') {
|
||||
onInstall = async () => {
|
||||
try {
|
||||
const response = await fetchPost("/install/tool/notion");
|
||||
if (response.success) {
|
||||
toast.success("Notion MCP installed successfully");
|
||||
// Save to config to mark as installed
|
||||
await proxyFetchPost("/api/configs", {
|
||||
config_group: "Notion",
|
||||
config_name: "MCP_REMOTE_CONFIG_DIR",
|
||||
config_value: response.toolkit_name || "NotionMCPToolkit",
|
||||
});
|
||||
// Refresh the integrations list to show the installed state
|
||||
fetchList();
|
||||
// Force refresh IntegrationList component
|
||||
setRefreshKey(prev => prev + 1);
|
||||
} else {
|
||||
toast.error(response.error || "Failed to install Notion MCP");
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || "Failed to install Notion MCP");
|
||||
}
|
||||
};
|
||||
} else if (key.toLowerCase() === 'google calendar') {
|
||||
onInstall = async () => {
|
||||
try {
|
||||
const response = await fetchPost("/install/tool/google_calendar");
|
||||
if (response.success) {
|
||||
toast.success("Google Calendar installed successfully");
|
||||
// Save to config to mark as installed
|
||||
await proxyFetchPost("/api/configs", {
|
||||
config_group: "Google Calendar",
|
||||
config_name: "GOOGLE_CLIENT_ID",
|
||||
config_value: response.toolkit_name || "GoogleCalendarToolkit",
|
||||
});
|
||||
// Refresh the integrations list to show the installed state
|
||||
fetchList();
|
||||
// Force refresh IntegrationList component
|
||||
setRefreshKey(prev => prev + 1);
|
||||
} else {
|
||||
toast.error(response.error || "Failed to install Google Calendar");
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || "Failed to install Google Calendar");
|
||||
}
|
||||
};
|
||||
} else {
|
||||
onInstall = () =>
|
||||
(window.location.href = `${baseURL}/api/oauth/${key.toLowerCase()}/login`);
|
||||
}
|
||||
|
||||
return {
|
||||
key,
|
||||
|
|
@ -123,11 +173,17 @@ export default function SettingMCP() {
|
|||
? `${t("setting.environmental-variables-required")}: ${value.env_vars.join(
|
||||
", "
|
||||
)}`
|
||||
: key.toLowerCase() === 'notion'
|
||||
? "Notion workspace integration for reading and managing Notion pages"
|
||||
: key.toLowerCase() === 'google calendar'
|
||||
? "Google Calendar integration for managing events and schedules"
|
||||
: "",
|
||||
onInstall,
|
||||
};
|
||||
});
|
||||
console.log("list", list);
|
||||
console.log("API response:", res);
|
||||
console.log("Generated list:", list);
|
||||
console.log("Essential integrations:", essentialIntegrations);
|
||||
|
||||
setIntegrations(
|
||||
list.filter(
|
||||
|
|
@ -351,7 +407,7 @@ export default function SettingMCP() {
|
|||
</div>
|
||||
<IntegrationList items={essentialIntegrations} />
|
||||
<div className="text-text-body font-bold text-base leading-snug">MCP</div>
|
||||
<IntegrationList items={integrations} />
|
||||
<IntegrationList key={refreshKey} items={integrations} />
|
||||
|
||||
<div className="pt-4">
|
||||
<div className="self-stretch inline-flex justify-start items-center gap-1">
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipSimple,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { MCPEnvDialog } from "./components/MCPEnvDialog";
|
||||
|
|
@ -370,14 +371,9 @@ export default function MCPMarket({ onBack }: { onBack?: () => void }) {
|
|||
<span className="text-base leading-9 font-bold text-text-primary truncate ">
|
||||
{item.name}
|
||||
</span>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<CircleAlert className="w-4 h-4 text-icon-secondary" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<div>{item.description}</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<TooltipSimple content={item.description}>
|
||||
<CircleAlert className="w-4 h-4 text-icon-secondary" />
|
||||
</TooltipSimple>
|
||||
</div>
|
||||
<Button
|
||||
variant={
|
||||
|
|
|
|||
|
|
@ -213,37 +213,22 @@ export default function SettingModels() {
|
|||
if (res.is_tool_calls && res.is_valid) {
|
||||
console.log("success");
|
||||
toast(t("setting.validate-success"), {
|
||||
description:
|
||||
t("setting.the-model-has-been-verified-to-support-function-calling-which-is-required-to-use-eigent"),
|
||||
description: t(
|
||||
"setting.the-model-has-been-verified-to-support-function-calling-which-is-required-to-use-eigent"
|
||||
),
|
||||
closeButton: true,
|
||||
});
|
||||
} else {
|
||||
console.log("failed", res.message);
|
||||
toast(t("setting.validate-failed"), {
|
||||
description: (
|
||||
<div
|
||||
style={{
|
||||
maxHeight: "120px",
|
||||
overflowY: "auto",
|
||||
}}
|
||||
>
|
||||
<div>{res.message}</div>
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
size="xs"
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(res.message);
|
||||
toast.success(t("setting.copied-to-clipboard"));
|
||||
}}
|
||||
>
|
||||
{t("setting.copy")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
closeButton: true,
|
||||
});
|
||||
const toastId = toast(t("setting.validate-failed"), {
|
||||
description: res.message,
|
||||
action: {
|
||||
label: t("setting.close"),
|
||||
onClick: () => {
|
||||
toast.dismiss(toastId);
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return;
|
||||
}
|
||||
|
|
@ -370,36 +355,21 @@ export default function SettingModels() {
|
|||
if (res.is_tool_calls && res.is_valid) {
|
||||
console.log("success");
|
||||
toast(t("setting.validate-success"), {
|
||||
description:
|
||||
t("setting.the-model-has-been-verified-to-support-function-calling-which-is-required-to-use-eigent"),
|
||||
description: t(
|
||||
"setting.the-model-has-been-verified-to-support-function-calling-which-is-required-to-use-eigent"
|
||||
),
|
||||
closeButton: true,
|
||||
});
|
||||
} else {
|
||||
console.log("failed", res.message);
|
||||
toast(t("setting.validate-failed"), {
|
||||
description: (
|
||||
<div
|
||||
style={{
|
||||
maxHeight: "120px",
|
||||
overflowY: "auto",
|
||||
}}
|
||||
>
|
||||
<div>{res.message}</div>
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
size="xs"
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(res.message);
|
||||
toast.success(t("setting.copied-to-clipboard"));
|
||||
}}
|
||||
>
|
||||
{t("setting.copy")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
closeButton: true,
|
||||
const toastId = toast(t("setting.validate-failed"), {
|
||||
description: res.message,
|
||||
action: {
|
||||
label: t("setting.close"),
|
||||
onClick: () => {
|
||||
toast.dismiss(toastId);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
|
|
@ -474,7 +444,9 @@ export default function SettingModels() {
|
|||
if (!hasSearchKey) {
|
||||
// Show warning toast instead of blocking
|
||||
toast(t("setting.warning-google-search-not-configured"), {
|
||||
description: t("setting.search-functionality-may-be-limited-without-google-api"),
|
||||
description: t(
|
||||
"setting.search-functionality-may-be-limited-without-google-api"
|
||||
),
|
||||
closeButton: true,
|
||||
});
|
||||
}
|
||||
|
|
@ -501,7 +473,9 @@ export default function SettingModels() {
|
|||
if (!hasSearchKey) {
|
||||
// Show warning toast instead of blocking
|
||||
toast(t("setting.warning-google-search-not-configured"), {
|
||||
description: t("setting.search-functionality-may-be-limited-without-google-api"),
|
||||
description: t(
|
||||
"setting.search-functionality-may-be-limited-without-google-api"
|
||||
),
|
||||
closeButton: true,
|
||||
});
|
||||
}
|
||||
|
|
@ -646,10 +620,8 @@ export default function SettingModels() {
|
|||
? t("setting.gpt-4.1-mini")
|
||||
: cloud_model_type === "gpt-4.1"
|
||||
? t("setting.gpt-4.1")
|
||||
: cloud_model_type === "claude-opus-4-1-20250805"
|
||||
? t("setting.claude-opus-4.1")
|
||||
: cloud_model_type === "claude-sonnet-4-20250514"
|
||||
? t("setting.claude-sonnet-4")
|
||||
: cloud_model_type === "claude-sonnet-4-5"
|
||||
? t("setting.claude-sonnet-4-5")
|
||||
: cloud_model_type === "claude-3-5-haiku-20241022"
|
||||
? t("setting.claude-3.5-haiku")
|
||||
: cloud_model_type === "gpt-5"
|
||||
|
|
@ -683,8 +655,8 @@ export default function SettingModels() {
|
|||
<SelectItem value="gpt-5">GPT-5</SelectItem>
|
||||
<SelectItem value="gpt-5-mini">GPT-5 mini</SelectItem>
|
||||
<SelectItem value="gpt-5-nano">GPT-5 nano</SelectItem>
|
||||
<SelectItem value="claude-sonnet-4-20250514">
|
||||
Claude Sonnet 4
|
||||
<SelectItem value="claude-sonnet-4-5">
|
||||
Claude Sonnet 4-5
|
||||
</SelectItem>
|
||||
<SelectItem value="claude-3-5-haiku-20241022">
|
||||
Claude 3.5 Haiku
|
||||
|
|
@ -760,7 +732,9 @@ export default function SettingModels() {
|
|||
<Input
|
||||
id={`apiKey-${item.id}`}
|
||||
type={showApiKey[idx] ? "text" : "password"}
|
||||
placeholder={` ${t("setting.enter-your-api-key")} ${item.name} ${t("setting.key")}`}
|
||||
placeholder={` ${t("setting.enter-your-api-key")} ${
|
||||
item.name
|
||||
} ${t("setting.key")}`}
|
||||
className="w-full pr-10"
|
||||
value={form[idx].apiKey}
|
||||
onChange={(e) => {
|
||||
|
|
@ -805,7 +779,9 @@ export default function SettingModels() {
|
|||
<div>
|
||||
<Input
|
||||
id={`apiHost-${item.id}`}
|
||||
placeholder={`${t("setting.enter-your-api-host")} ${item.name} ${t("setting.url")}`}
|
||||
placeholder={`${t("setting.enter-your-api-host")} ${
|
||||
item.name
|
||||
} ${t("setting.url")}`}
|
||||
className="w-full"
|
||||
value={form[idx].apiHost}
|
||||
onChange={(e) => {
|
||||
|
|
@ -833,7 +809,9 @@ export default function SettingModels() {
|
|||
<div>
|
||||
<Input
|
||||
id={`modelType-${item.id}`}
|
||||
placeholder={`${t("setting.enter-your-model-type")} ${item.name} ${t("setting.model-type")}`}
|
||||
placeholder={`${t("setting.enter-your-model-type")} ${
|
||||
item.name
|
||||
} ${t("setting.model-type")}`}
|
||||
className="w-full"
|
||||
value={form[idx].model_type}
|
||||
onChange={(e) => {
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ interface IntegrationItem {
|
|||
name: string;
|
||||
desc: string | React.ReactNode;
|
||||
env_vars: string[];
|
||||
onInstall: () => void;
|
||||
onInstall: () => void | Promise<void>;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -312,7 +312,7 @@ export default function IntegrationList({
|
|||
onConnect={onConnect}
|
||||
activeMcp={activeMcp}
|
||||
></MCPEnvDialog>
|
||||
{items.filter((item) => item.name !== "Notion").map((item) => {
|
||||
{items.map((item) => {
|
||||
const isInstalled = !!installed[item.key];
|
||||
return (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ export default function MCPAddDialog({
|
|||
<DialogContent className="min-w-[340px] w-[600px] max-w-[95vw] p-0">
|
||||
<DialogHeader className=" bg-gray-100 rounded-t-xl px-6 ">
|
||||
<DialogTitle className="font-bold text-lg text-gray-800 ">
|
||||
{t("setting.add-your-mcp-server")}
|
||||
{t("setting.add-your-agent")}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="px-md py-md bg-white-100% rounded-b-xl">
|
||||
|
|
|
|||
|
|
@ -12,19 +12,73 @@ import githubIcon from "@/assets/github.svg";
|
|||
import { Input } from "@/components/ui/input";
|
||||
import { useState, FC, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface EnvValue {
|
||||
value: string;
|
||||
required: boolean;
|
||||
tip: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface MCPEnvDialogProps {
|
||||
showEnvConfig: boolean;
|
||||
onClose: () => void;
|
||||
onConnect: (mcp:any) => void;
|
||||
onConnect: (mcp: any) => void;
|
||||
activeMcp?: any;
|
||||
}
|
||||
|
||||
export async function google_check(apiKey: string, searchEngineId: string) {
|
||||
const query = "hello"; // rand word
|
||||
const url = `https://www.googleapis.com/customsearch/v1?key=${apiKey}&cx=${searchEngineId}&q=${encodeURIComponent(
|
||||
query
|
||||
)}&num=1`;
|
||||
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) {
|
||||
throw new Error(`Google API error: ${res.status}`);
|
||||
}
|
||||
const data = await res.json();
|
||||
|
||||
if ("items" in data) {
|
||||
return { success: true, message: "Google key is valid ✅", sample: data.items[0] };
|
||||
} else {
|
||||
return { success: false, message: "Google key invalid ❌", error: data.error };
|
||||
}
|
||||
} catch (err: any) {
|
||||
return { success: false, message: `Google check failed: ${err.message}` };
|
||||
}
|
||||
}
|
||||
|
||||
export async function exa_check(apiKey: string) {
|
||||
const query = "hello"; // rand search word
|
||||
|
||||
try {
|
||||
const res = await fetch("https://api.exa.ai/search", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ query }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
throw new Error(`Exa API error: ${res.status} ${data.error}`);
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
if ("results" in data) {
|
||||
return { success: true, message: "Exa key is valid ✅", sample: data.results[0] };
|
||||
} else {
|
||||
return { success: false, message: "Exa key invalid ❌", error: data };
|
||||
}
|
||||
} catch (err: any) {
|
||||
return { success: false, message: `Exa check failed: ${err.message}` };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const MCPEnvDialog: FC<MCPEnvDialogProps> = ({
|
||||
showEnvConfig,
|
||||
onClose,
|
||||
|
|
@ -33,6 +87,7 @@ export const MCPEnvDialog: FC<MCPEnvDialogProps> = ({
|
|||
}) => {
|
||||
const [envValues, setEnvValues] = useState<{ [key: string]: EnvValue }>({});
|
||||
const [showKeys, setShowKeys] = useState<{ [key: string]: boolean }>({});
|
||||
const [isValidating, setIsValidating] = useState<boolean>(false);
|
||||
const { t } = useTranslation();
|
||||
useEffect(() => {
|
||||
initializeEnvValues(activeMcp);
|
||||
|
|
@ -42,7 +97,7 @@ export const MCPEnvDialog: FC<MCPEnvDialogProps> = ({
|
|||
if (mcp?.install_command?.env) {
|
||||
const initialValues: { [key: string]: EnvValue } = {};
|
||||
Object.keys(mcp.install_command.env).forEach((key) => {
|
||||
|
||||
|
||||
initialValues[key] = {
|
||||
value: "",
|
||||
required: true,
|
||||
|
|
@ -51,11 +106,11 @@ export const MCPEnvDialog: FC<MCPEnvDialogProps> = ({
|
|||
?.replace(/{{/g, "")
|
||||
?.replace(/}}/g, "") || "",
|
||||
};
|
||||
if(key==='EXA_API_KEY'){
|
||||
initialValues[key].required=false;
|
||||
if (key === 'EXA_API_KEY') {
|
||||
initialValues[key].required = false;
|
||||
}
|
||||
if(key==='GOOGLE_REFRESH_TOKEN'){
|
||||
initialValues[key].required=false;
|
||||
if (key === 'GOOGLE_REFRESH_TOKEN') {
|
||||
initialValues[key].required = false;
|
||||
}
|
||||
});
|
||||
setEnvValues(initialValues);
|
||||
|
|
@ -88,15 +143,63 @@ export const MCPEnvDialog: FC<MCPEnvDialogProps> = ({
|
|||
onClose();
|
||||
};
|
||||
|
||||
const handleConfigureMcpEnvSetting = () => {
|
||||
setEnvValues({});
|
||||
setShowKeys({});
|
||||
const mcp = { ...activeMcp };
|
||||
const setFieldError = (key: string, error: string) => {
|
||||
setEnvValues((prev) => ({
|
||||
...prev,
|
||||
[key]: {
|
||||
...prev[key],
|
||||
error,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const clearFieldErrors = () => {
|
||||
setEnvValues((prev) => {
|
||||
const updated: typeof prev = {};
|
||||
Object.keys(prev).forEach((key) => {
|
||||
updated[key] = { ...prev[key], error: "" };
|
||||
});
|
||||
return updated;
|
||||
});
|
||||
};
|
||||
|
||||
const handleConfigureMcpEnvSetting = async () => {
|
||||
if (isValidating) return;
|
||||
|
||||
setIsValidating(true);
|
||||
clearFieldErrors();
|
||||
|
||||
const env: { [key: string]: string } = {};
|
||||
Object.keys(envValues).map((key) => {
|
||||
Object.keys(envValues).forEach((key) => {
|
||||
env[key] = envValues[key]?.value;
|
||||
});
|
||||
mcp.install_command.env = env;
|
||||
|
||||
// Validate Google API key
|
||||
if (env["GOOGLE_API_KEY"] && env["SEARCH_ENGINE_ID"]) {
|
||||
const result = await google_check(env["GOOGLE_API_KEY"], env["SEARCH_ENGINE_ID"]);
|
||||
if (!result.success) {
|
||||
setFieldError("GOOGLE_API_KEY", result.message);
|
||||
setFieldError("SEARCH_ENGINE_ID", result.message);
|
||||
setIsValidating(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate Exa API key
|
||||
if (env["EXA_API_KEY"]) {
|
||||
const result = await exa_check(env["EXA_API_KEY"]);
|
||||
if (!result.success) {
|
||||
setFieldError("EXA_API_KEY", result.message);
|
||||
setIsValidating(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Save only if all validations succeed
|
||||
const mcp = { ...activeMcp, install_command: { ...activeMcp.install_command, env } };
|
||||
setEnvValues({});
|
||||
setShowKeys({});
|
||||
setIsValidating(false);
|
||||
onConnect(mcp);
|
||||
};
|
||||
return (
|
||||
|
|
@ -112,7 +215,7 @@ export const MCPEnvDialog: FC<MCPEnvDialogProps> = ({
|
|||
<DialogTitle className="m-0">
|
||||
<div className="flex gap-xs items-center justify-start">
|
||||
<div className="text-base font-bold leading-10 text-text-action">
|
||||
{t("setting.configure {name} Toolkit", {name: activeMcp?.name})}
|
||||
{t("setting.configure {name} Toolkit", { name: activeMcp?.name })}
|
||||
</div>
|
||||
<CircleAlert size={16} />
|
||||
</div>
|
||||
|
|
@ -152,7 +255,7 @@ export const MCPEnvDialog: FC<MCPEnvDialogProps> = ({
|
|||
{Object.keys(activeMcp?.install_command?.env || {}).map((key) => (
|
||||
<div key={key}>
|
||||
<div className="text-text-body text-sm leading-normal font-bold">
|
||||
{key}{envValues[key]?.required&&'*'}
|
||||
{key}{envValues[key]?.required && '*'}
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Input
|
||||
|
|
@ -175,23 +278,26 @@ export const MCPEnvDialog: FC<MCPEnvDialogProps> = ({
|
|||
</div>
|
||||
<div className="text-input-label-default text-xs leading-normal">
|
||||
{envValues[key]?.tip}
|
||||
{envValues[key]?.error && (
|
||||
<div className="text-red-500 text-xs mt-1">{envValues[key]?.error}</div>
|
||||
)}
|
||||
{key === 'SEARCH_ENGINE_ID' && (
|
||||
<div className="mt-1">
|
||||
{t("setting.get-it-from")}: <a onClick={()=>{
|
||||
{t("setting.get-it-from")}: <a onClick={() => {
|
||||
window.location.href = "https://developers.google.com/custom-search/v1/overview";
|
||||
}} className="underline text-blue-500">{t("setting.google-custom-search-api")}</a>
|
||||
</div>
|
||||
)}
|
||||
{key === 'GOOGLE_API_KEY' && (
|
||||
<div className="mt-1">
|
||||
{t("setting.get-it-from")}: <a onClick={()=>{
|
||||
{t("setting.get-it-from")}: <a onClick={() => {
|
||||
window.location.href = "https://console.cloud.google.com/apis/credentials";
|
||||
}} className="underline text-blue-500">{t("setting.google-cloud-console")}</a>
|
||||
</div>
|
||||
)}
|
||||
{key === 'EXA_API_KEY' && (
|
||||
<div className="mt-1">
|
||||
{t("setting.get-it-from")}: <a onClick={()=>{
|
||||
{t("setting.get-it-from")}: <a onClick={() => {
|
||||
window.location.href = "https://exa.ai";
|
||||
}} className="underline text-blue-500">Exa.ai</a> (Optional)
|
||||
</div>
|
||||
|
|
@ -213,8 +319,9 @@ export const MCPEnvDialog: FC<MCPEnvDialogProps> = ({
|
|||
onClick={handleConfigureMcpEnvSetting}
|
||||
variant="primary"
|
||||
size="md"
|
||||
disabled={isValidating}
|
||||
>
|
||||
{t("setting.connect")}
|
||||
{isValidating ? "Validating..." : t("setting.connect")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
|
|
|||
|
|
@ -2,9 +2,7 @@ import { Button } from "@/components/ui/button";
|
|||
import { Switch } from "@/components/ui/switch";
|
||||
import { Ellipsis, Settings, Trash2, CircleAlert } from "lucide-react";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
TooltipSimple
|
||||
} from "@/components/ui/tooltip";
|
||||
import {
|
||||
Popover,
|
||||
|
|
@ -41,14 +39,9 @@ export default function MCPListItem({
|
|||
{item.mcp_name}
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<TooltipSimple content={item.mcp_desc}>
|
||||
<CircleAlert className="w-4 h-4 text-icon-secondary" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<div>{item.mcp_desc}</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipSimple>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { persist } from 'zustand/middleware';
|
|||
// type definition
|
||||
type InitState = 'permissions' | 'carousel' | 'done';
|
||||
type ModelType = 'cloud' | 'local' | 'custom';
|
||||
type CloudModelType = 'gemini/gemini-2.5-pro' | 'gemini-2.5-flash' | 'gpt-4.1-mini' | 'gpt-4.1' | 'claude-opus-4-1-20250805' | 'claude-sonnet-4-20250514' | 'claude-3-5-haiku-20241022' | 'gpt-5' | 'gpt-5-mini' | 'gpt-5-nano';
|
||||
type CloudModelType = 'gemini/gemini-2.5-pro' | 'gemini-2.5-flash' | 'gpt-4.1-mini' | 'gpt-4.1' | 'claude-sonnet-4-5' | 'claude-3-5-haiku-20241022' | 'gpt-5' | 'gpt-5-mini' | 'gpt-5-nano';
|
||||
|
||||
// auth info interface
|
||||
interface AuthInfo {
|
||||
|
|
@ -169,7 +169,7 @@ const EMPTY_LIST: Agent[] = [];
|
|||
|
||||
// worker list Hook
|
||||
export const useWorkerList = (): Agent[] => {
|
||||
const { email } = getAuthStore();
|
||||
const workerList = getAuthStore().workerListData[email as string];
|
||||
const { email, workerListData } = getAuthStore();
|
||||
const workerList = workerListData[email as string];
|
||||
return workerList ?? EMPTY_LIST;
|
||||
};
|
||||
|
|
@ -489,7 +489,7 @@ const chatStore = create<ChatStore>()(
|
|||
|
||||
}
|
||||
if (targetTaskIndex !== -1) {
|
||||
console.log("targetTaskIndex", targetTaskIndex,state)
|
||||
console.log("targetTaskIndex", targetTaskIndex, state)
|
||||
taskRunning[targetTaskIndex].status = state === "DONE" ? "completed" : "failed";
|
||||
}
|
||||
setTaskRunning(taskId, taskRunning)
|
||||
|
|
@ -612,17 +612,16 @@ const chatStore = create<ChatStore>()(
|
|||
}
|
||||
|
||||
|
||||
// If the state is "waiting", only mark it in the agent's task list and do not add it to taskRunning
|
||||
// Handle task assignment to taskAssigning based on state
|
||||
if (taskState === "waiting") {
|
||||
if (!taskAssigning[assigneeAgentIndex].tasks.find(item => item.id === task_id)) {
|
||||
taskAssigning[assigneeAgentIndex].tasks.push(task ?? { id: task_id, content, status: "waiting" });
|
||||
}
|
||||
setTaskAssigning(taskId, [...taskAssigning]);
|
||||
return; // Return early, do not add to the running queue
|
||||
}
|
||||
|
||||
}
|
||||
// The following logic is for when the task actually starts executing (running)
|
||||
if (taskAssigning && taskAssigning[assigneeAgentIndex]) {
|
||||
else if (taskAssigning && taskAssigning[assigneeAgentIndex]) {
|
||||
// Check if task already exists in the agent's task list
|
||||
const existingTaskIndex = taskAssigning[assigneeAgentIndex].tasks.findIndex(item => item.id === task_id);
|
||||
|
||||
|
|
@ -649,11 +648,14 @@ const chatStore = create<ChatStore>()(
|
|||
// Only update or add to taskRunning, never duplicate
|
||||
if (taskRunningIndex === -1) {
|
||||
// Task not in taskRunning, add it
|
||||
if(task){
|
||||
task.status = taskState === "waiting" ? "waiting" : "running";
|
||||
}
|
||||
taskRunning!.push(
|
||||
task ?? {
|
||||
id: task_id,
|
||||
content,
|
||||
status: "",
|
||||
status: taskState === "waiting" ? "waiting" : "running",
|
||||
agent: JSON.parse(JSON.stringify(taskAgent)),
|
||||
}
|
||||
);
|
||||
|
|
@ -661,7 +663,7 @@ const chatStore = create<ChatStore>()(
|
|||
// Task already in taskRunning, update it
|
||||
taskRunning![taskRunningIndex] = {
|
||||
...taskRunning![taskRunningIndex],
|
||||
status: "",
|
||||
status: taskState === "waiting" ? "waiting" : "running",
|
||||
agent: JSON.parse(JSON.stringify(taskAgent)),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
216
src/store/installationStore.ts
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
import { create } from 'zustand';
|
||||
import { subscribeWithSelector } from 'zustand/middleware';
|
||||
|
||||
// Define all possible installation states
|
||||
export type InstallationState =
|
||||
| 'idle'
|
||||
| 'checking-permissions'
|
||||
| 'showing-carousel'
|
||||
| 'installing'
|
||||
| 'error'
|
||||
| 'completed';
|
||||
|
||||
// Installation log entry
|
||||
export interface InstallationLog {
|
||||
type: 'stdout' | 'stderr';
|
||||
data: string;
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
// Installation store state
|
||||
interface InstallationStoreState {
|
||||
// Core state
|
||||
state: InstallationState;
|
||||
progress: number;
|
||||
logs: InstallationLog[];
|
||||
error?: string;
|
||||
isVisible: boolean;
|
||||
|
||||
// Actions
|
||||
startInstallation: () => void;
|
||||
addLog: (log: InstallationLog) => void;
|
||||
setSuccess: () => void;
|
||||
setError: (error: string) => void;
|
||||
retryInstallation: () => void;
|
||||
completeSetup: () => void;
|
||||
updateProgress: (progress: number) => void;
|
||||
setVisible: (visible: boolean) => void;
|
||||
reset: () => void;
|
||||
|
||||
// Async actions
|
||||
performInstallation: () => Promise<void>;
|
||||
exportLog: () => Promise<void>;
|
||||
}
|
||||
|
||||
// Initial state
|
||||
const initialState = {
|
||||
state: 'idle' as InstallationState,
|
||||
progress: 20,
|
||||
logs: [] as InstallationLog[],
|
||||
error: undefined,
|
||||
isVisible: false,
|
||||
};
|
||||
|
||||
// Create the installation store
|
||||
export const useInstallationStore = create<InstallationStoreState>()(
|
||||
subscribeWithSelector(
|
||||
(set, get) => ({
|
||||
// Initial state
|
||||
...initialState,
|
||||
|
||||
// Basic actions
|
||||
startInstallation: () =>
|
||||
set({
|
||||
state: 'installing',
|
||||
progress: 20,
|
||||
logs: [],
|
||||
error: undefined,
|
||||
isVisible: true,
|
||||
}),
|
||||
|
||||
addLog: (log: InstallationLog) =>
|
||||
set((state) => {
|
||||
const newProgress = Math.min(state.progress + 5, 90);
|
||||
return {
|
||||
logs: [...state.logs, log],
|
||||
progress: newProgress,
|
||||
};
|
||||
}),
|
||||
|
||||
setSuccess: () =>
|
||||
set({
|
||||
state: 'completed',
|
||||
progress: 100,
|
||||
}),
|
||||
|
||||
setError: (error: string) =>
|
||||
set((state) => ({
|
||||
state: 'error',
|
||||
error,
|
||||
logs: [
|
||||
...state.logs,
|
||||
{
|
||||
type: 'stderr',
|
||||
data: error,
|
||||
timestamp: new Date(),
|
||||
},
|
||||
],
|
||||
})),
|
||||
|
||||
retryInstallation: () => {
|
||||
set({
|
||||
...initialState,
|
||||
isVisible: true,
|
||||
state: 'installing',
|
||||
});
|
||||
get().performInstallation();
|
||||
},
|
||||
|
||||
completeSetup: () =>
|
||||
set({
|
||||
state: 'completed',
|
||||
isVisible: false,
|
||||
}),
|
||||
|
||||
updateProgress: (progress: number) =>
|
||||
set({ progress }),
|
||||
|
||||
setVisible: (visible: boolean) =>
|
||||
set({ isVisible: visible }),
|
||||
|
||||
reset: () =>
|
||||
set(initialState),
|
||||
|
||||
// Async actions
|
||||
performInstallation: async () => {
|
||||
const { startInstallation, setSuccess, setError } = get();
|
||||
|
||||
try {
|
||||
startInstallation();
|
||||
const result = await window.electronAPI.checkAndInstallDepsOnUpdate();
|
||||
|
||||
if (result.success) {
|
||||
setSuccess();
|
||||
// Update auth store
|
||||
const { useAuthStore } = await import('./authStore');
|
||||
useAuthStore.getState().setInitState('done');
|
||||
} else {
|
||||
setError('Installation failed');
|
||||
}
|
||||
} catch (error) {
|
||||
setError(error instanceof Error ? error.message : 'Unknown error');
|
||||
}
|
||||
},
|
||||
|
||||
exportLog: async () => {
|
||||
try {
|
||||
const response = await window.electronAPI.exportLog();
|
||||
|
||||
if (!response.success) {
|
||||
alert('Export cancelled: ' + response.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.savedPath) {
|
||||
window.location.href = 'https://github.com/eigent-ai/eigent/issues/new/choose';
|
||||
alert('Log saved: ' + response.savedPath);
|
||||
}
|
||||
} catch (e: any) {
|
||||
alert('Export error: ' + e.message);
|
||||
}
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
// Computed selectors
|
||||
export const useLatestLog = () => useInstallationStore(state =>
|
||||
state.logs[state.logs.length - 1]
|
||||
);
|
||||
|
||||
export const useInstallationActions = () => useInstallationStore(state => ({
|
||||
startInstallation: state.startInstallation,
|
||||
retryInstallation: state.retryInstallation,
|
||||
completeSetup: state.completeSetup,
|
||||
performInstallation: state.performInstallation,
|
||||
exportLog: state.exportLog,
|
||||
}));
|
||||
|
||||
// Combined hook for components that need multiple pieces of state
|
||||
export const useInstallationStatus = () => {
|
||||
const state = useInstallationStore(state => state.state);
|
||||
const isVisible = useInstallationStore(state => state.isVisible);
|
||||
|
||||
return {
|
||||
isInstalling: state === 'installing',
|
||||
installationState: state,
|
||||
shouldShowInstallScreen: isVisible && state !== 'completed',
|
||||
isInstallationComplete: state === 'completed',
|
||||
canRetry: state === 'error',
|
||||
};
|
||||
};
|
||||
|
||||
// Hook for the main installation UI component
|
||||
export const useInstallationUI = () => {
|
||||
const state = useInstallationStore(state => state.state);
|
||||
const progress = useInstallationStore(state => state.progress);
|
||||
const logs = useInstallationStore(state => state.logs);
|
||||
const error = useInstallationStore(state => state.error);
|
||||
const isVisible = useInstallationStore(state => state.isVisible);
|
||||
const performInstallation = useInstallationStore(state => state.performInstallation);
|
||||
const retryInstallation = useInstallationStore(state => state.retryInstallation);
|
||||
const exportLog = useInstallationStore(state => state.exportLog);
|
||||
|
||||
return {
|
||||
installationState: state,
|
||||
progress,
|
||||
latestLog: logs[logs.length - 1],
|
||||
error,
|
||||
isInstalling: state === 'installing',
|
||||
shouldShowInstallScreen: isVisible && state !== 'completed',
|
||||
canRetry: state === 'error',
|
||||
performInstallation,
|
||||
retryInstallation,
|
||||
exportLog,
|
||||
};
|
||||
};
|
||||
11
src/types/electron.d.ts
vendored
|
|
@ -45,9 +45,16 @@ interface ElectronAPI {
|
|||
envRemove: (email: string, key: string) => Promise<any>;
|
||||
getEnvPath: (email: string) => Promise<string>;
|
||||
executeCommand: (command: string,email:string) => Promise<{ success: boolean; stdout?: string; stderr?: string; error?: string }>;
|
||||
installDependencies: () => Promise<{ success: boolean; error?: string }>;
|
||||
frontendReady: () => Promise<{ success: boolean; error?: string }>;
|
||||
checkAndInstallDepsOnUpdate: () => Promise<{ success: boolean; error?: string }>;
|
||||
checkInstallBrowser: () => Promise<{ data:any[] }>;
|
||||
getInstallationStatus: () => Promise<{
|
||||
success: boolean;
|
||||
isInstalling?: boolean;
|
||||
hasLockFile?: boolean;
|
||||
installedExists?: boolean;
|
||||
timestamp?: number;
|
||||
error?: string
|
||||
}>;
|
||||
onInstallDependenciesStart: (callback: () => void) => void;
|
||||
onInstallDependenciesLog: (callback: (data: { type: string; data: string }) => void) => void;
|
||||
onInstallDependenciesComplete: (callback: (data: { success: boolean; code?: number; error?: string }) => void) => void;
|
||||
|
|
|
|||