diff --git a/.env.template b/.env.template
index ac257fc..d631d7b 100644
--- a/.env.template
+++ b/.env.template
@@ -2,21 +2,24 @@
# ===================
# DeepSeek API Configuration
+# Get your API key from: https://platform.deepseek.com/api_keys
DEEPSEEK_API_KEY=
DEEPSEEK_BASE_URL=https://api.deepseek.com/v1
-# OpenAI API Configuration
-OPENAI_API_KEY=
-OPENAI_BASE_URL=https://api.openai.com/v1
+# Alibaba Cloud Qwen API Configuration
+# Get your API key from: https://bailian.console.aliyun.com/
+QWEN_API_KEY=
-# Anthropic API Configuration
-ANTHROPIC_API_KEY=your_anthropic_api_key_here
-ANTHROPIC_BASE_URL=https://api.anthropic.com/v1
+# Google Gemini API Configuration
+# Get your API key from: https://aistudio.google.com/app/apikey
+GOOGLE_API_KEY=
-# Alibaba Cloud Bailian API Keys (for Douyin/Xiaohongshu services)
-BAILIAN_API_KEY=
+# Anthropic Claude API Configuration
+# Get your API key from: https://console.anthropic.com/settings/keys
+ANTHROPIC_API_KEY=
-# OpenRouter API Configuration
+# OpenRouter API Configuration (supports multiple providers)
+# Get your API key from: https://openrouter.ai/keys
OPENROUTER_API_KEY=
# Text-to-Speech Configuration
diff --git a/README.md b/README.md
index 73bda7f..f3ce5a0 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,8 @@
-# DeepFundAI Browser
+# Manus Electron
-An AI-powered intelligent browser built with Next.js and Electron. Features multi-modal AI task execution, scheduled tasks, social media integration, and advanced file management capabilities.
+[English](./README.md) | [简体中文](./README.zh-CN.md)
+
+An AI-powered intelligent browser built with Next.js and Electron. Features multi-modal AI task execution, scheduled tasks, social media integration, and advanced file management capabilities with support for multiple AI providers.
Built with [Next.js](https://nextjs.org) and [Electron](https://electronjs.org).
@@ -28,7 +30,7 @@ Before running the application, you need to configure API keys:
cp .env.template .env.local
# Edit .env.local and fill in your API keys
-# Required: DEEPSEEK_API_KEY, BAILIAN_API_KEY
+# Supported: DEEPSEEK_API_KEY, QWEN_API_KEY, GOOGLE_API_KEY, ANTHROPIC_API_KEY, OPENROUTER_API_KEY
```
For detailed configuration instructions, see [CONFIGURATION.md](./docs/CONFIGURATION.md).
@@ -67,12 +69,13 @@ The built application will include your API configuration, so end users don't ne
## Features
-- AI-powered intelligent browser
-- Multi-modal AI task execution
-- Scheduled tasks system
-- Social media integration (Douyin, Xiaohongshu)
-- Speech recognition and text-to-speech
-- File management capabilities
+- **Multiple AI Providers**: Support for DeepSeek, Qwen, Google Gemini, Anthropic Claude, and OpenRouter
+- **UI Configuration**: Configure AI models and API keys directly in the app, no file editing required
+- **AI-Powered Browser**: Intelligent browser with automated task execution
+- **Multi-Modal AI**: Vision and text processing capabilities
+- **Scheduled Tasks**: Create and manage automated recurring tasks
+- **Speech & TTS**: Voice recognition and text-to-speech integration
+- **File Management**: Advanced file operations and management
## Screenshots
@@ -96,12 +99,13 @@ View past tasks with search and playback capabilities.

-## API Services Used
+## Supported AI Providers
-- **DeepSeek**: Main AI language model
-- **Alibaba Cloud Bailian**: Vision model and social media services
-- **OpenRouter**: Alternative AI models
-- **Microsoft Azure**: Text-to-speech services
+- **DeepSeek**: deepseek-chat, deepseek-reasoner
+- **Qwen (Alibaba Cloud)**: qwen-max, qwen-plus, qwen-vl-max
+- **Google Gemini**: gemini-1.5-flash, gemini-2.0-flash, gemini-1.5-pro, and more
+- **Anthropic Claude**: claude-3.7-sonnet, claude-3.5-sonnet, claude-3-opus, and more
+- **OpenRouter**: Multiple providers (Claude, GPT, Gemini, Mistral, Cohere, etc.)
## Documentation
diff --git a/README.zh-CN.md b/README.zh-CN.md
new file mode 100644
index 0000000..3fd177b
--- /dev/null
+++ b/README.zh-CN.md
@@ -0,0 +1,120 @@
+# Manus Electron
+
+[English](./README.md) | [简体中文](./README.zh-CN.md)
+
+一个基于 Next.js 和 Electron 构建的 AI 智能浏览器。支持多模态 AI 任务执行、定时任务、社交媒体集成以及高级文件管理功能,并支持多个 AI 提供商。
+
+基于 [Next.js](https://nextjs.org) 和 [Electron](https://electronjs.org) 构建。
+
+## 技术栈
+
+- **前端**: Next.js 15 + React 19
+- **桌面应用**: Electron 33
+- **UI**: Ant Design + Tailwind CSS
+- **状态管理**: Zustand
+- **存储**: IndexedDB (via electron-store)
+- **AI Agent**: @jarvis-agent (基于 [Eko](https://github.com/FellouAI/eko))
+- **构建工具**: Vite + TypeScript
+
+## 开发环境配置
+Node 版本: 20.19.3
+
+## 快速开始
+
+### 1. 配置 API 密钥
+
+运行应用前,需要配置 API 密钥:
+
+```bash
+# 复制配置模板
+cp .env.template .env.local
+
+# 编辑 .env.local 并填入你的 API 密钥
+# 支持: DEEPSEEK_API_KEY, QWEN_API_KEY, GOOGLE_API_KEY, ANTHROPIC_API_KEY, OPENROUTER_API_KEY
+```
+
+详细配置说明请参见 [CONFIGURATION.zh-CN.md](./docs/CONFIGURATION.zh-CN.md)。
+
+### 2. 开发环境设置
+
+首先,运行开发服务器:
+
+```bash
+# 安装依赖
+pnpm install
+
+# 构建桌面应用客户端
+pnpm run build:deps
+
+# 启动 Web 开发服务器
+pnpm run next
+
+# 启动桌面应用
+pnpm run electron
+```
+
+### 3. 构建桌面应用
+
+构建用于分发的桌面应用:
+
+```bash
+# 配置生产环境 API 密钥
+# 编辑 .env.production 文件并填入实际的 API 密钥
+
+# 构建应用
+pnpm run build
+```
+
+构建的应用将包含你的 API 配置,终端用户无需额外配置。
+
+## 功能特性
+
+- **多 AI 提供商支持**: 支持 DeepSeek、Qwen、Google Gemini、Anthropic Claude 和 OpenRouter
+- **UI 配置**: 直接在应用中配置 AI 模型和 API 密钥,无需编辑文件
+- **AI 智能浏览器**: 具有自动化任务执行的智能浏览器
+- **多模态 AI**: 视觉和文本处理能力
+- **定时任务**: 创建和管理自动化定期任务
+- **语音识别与 TTS**: 语音识别和文字转语音集成
+- **文件管理**: 高级文件操作和管理
+
+## 截图
+
+### 首页
+输入任务,让 AI 自动执行。
+
+
+
+### 主界面
+左侧:AI 思考和执行步骤。右侧:实时浏览器操作预览。
+
+
+
+### 定时任务
+创建具有自定义间隔和执行步骤的定时任务。
+
+
+
+### 历史记录
+查看过去的任务,支持搜索和回放功能。
+
+
+
+## 支持的 AI 提供商
+
+- **DeepSeek**: deepseek-chat, deepseek-reasoner
+- **Qwen (阿里云)**: qwen-max, qwen-plus, qwen-vl-max
+- **Google Gemini**: gemini-1.5-flash, gemini-2.0-flash, gemini-1.5-pro 等
+- **Anthropic Claude**: claude-3.7-sonnet, claude-3.5-sonnet, claude-3-opus 等
+- **OpenRouter**: 多个提供商(Claude、GPT、Gemini、Mistral、Cohere 等)
+
+## 文档
+
+- [配置指南](./docs/CONFIGURATION.zh-CN.md) - 详细的 API 密钥设置说明
+
+## 致谢
+
+特别感谢 [Eko](https://github.com/FellouAI/eko) - 一个生产就绪的 Agent 框架,为本项目提供了 AI 能力支持。
+
+## 贡献
+
+请确保所有 API 密钥仅在开发环境文件中配置。永远不要将实际的 API 密钥提交到仓库中。
diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md
index cca4e9a..8ef4092 100644
--- a/docs/CONFIGURATION.md
+++ b/docs/CONFIGURATION.md
@@ -1,17 +1,81 @@
# Configuration Guide
-This guide explains how to configure API keys and environment variables for the DeepFundAI application.
+This guide explains how to configure AI models and API keys for the Manus Electron application.
## Configuration Strategy
-The application uses a multi-level configuration strategy:
-- **Development**: Uses `.env.local` file
-- **Production**: Uses bundled `.env.production` file (packaged with the app)
-- **Priority**: Bundled config > System environment variables
+The application supports multiple configuration methods with the following priority:
-This allows developers to configure API keys once during build, and end users don't need any additional configuration.
+**Priority Order**: User UI Configuration > Environment Variables > Default Values
-## Environment Variables Setup
+### Configuration Methods
+
+1. **UI Configuration (Recommended for End Users)**
+ - Configure directly in the application settings
+ - No need to edit files or restart the app
+ - Changes take effect immediately
+
+2. **Environment Variables (For Development)**
+ - Uses `.env.local` file in development
+ - Uses bundled `.env.production` file in production builds
+ - Suitable for developers and automated deployments
+
+3. **Default Values**
+ - Built-in fallback values
+ - Used when no other configuration is provided
+
+## Supported AI Providers
+
+The application supports the following AI providers:
+
+| Provider | Models | Get API Key |
+|----------|--------|-------------|
+| **DeepSeek** | deepseek-chat, deepseek-reasoner | [platform.deepseek.com](https://platform.deepseek.com/api_keys) |
+| **Qwen (Alibaba)** | qwen-max, qwen-plus, qwen-vl-max | [bailian.console.aliyun.com](https://bailian.console.aliyun.com/) |
+| **Google Gemini** | gemini-1.5-flash, gemini-2.0-flash, gemini-1.5-pro, etc. | [aistudio.google.com](https://aistudio.google.com/app/apikey) |
+| **Anthropic Claude** | claude-3.7-sonnet, claude-3.5-sonnet, claude-3-opus, etc. | [console.anthropic.com](https://console.anthropic.com/settings/keys) |
+| **OpenRouter** | Multiple providers (Claude, GPT, Gemini, etc.) | [openrouter.ai](https://openrouter.ai/keys) |
+
+## UI Configuration (Recommended)
+
+### Configure AI Provider in the Application
+
+1. **Launch the Application**
+ - Open the Manus Electron application
+
+2. **Access Model Settings**
+ - On the home page, you'll see the model configuration panel
+ - The panel is located above the input area
+
+3. **Select Provider**
+ - Click the provider dropdown
+ - Choose from: Deepseek, Qwen, Google Gemini, Anthropic, or OpenRouter
+
+4. **Select Model**
+ - After selecting a provider, choose your preferred model
+ - Different providers offer different models with varying capabilities
+
+5. **Configure API Key**
+ - Click "Edit API Key"
+ - Enter your API key for the selected provider
+ - Click the checkmark to save
+ - API key status indicator shows:
+ - 🟢 **Set by user**: You configured it in the UI
+ - 🟢 **Set via environment variable**: Configured in .env file
+ - 🟡 **Not configured**: No API key found
+
+6. **Get API Key**
+ - Click "Get API Key" link to open the provider's API key page
+ - Sign up or log in to get your API key
+ - Copy and paste it into the application
+
+### Configuration Takes Effect Immediately
+
+- No need to restart the application
+- Changes apply to the next message you send
+- All running tasks are terminated when configuration changes
+
+## Environment Variables Setup (For Developers)
### 1. Copy Configuration Template
@@ -30,21 +94,24 @@ Edit `.env.local` and fill in your API keys:
# ===================
# DeepSeek API Configuration
+# Get your API key from: https://platform.deepseek.com/api_keys
DEEPSEEK_API_KEY=your_actual_deepseek_api_key_here
DEEPSEEK_BASE_URL=https://api.deepseek.com/v1
-# OpenAI API Configuration
-OPENAI_API_KEY=your_actual_openai_api_key_here
-OPENAI_BASE_URL=https://api.openai.com/v1
+# Alibaba Cloud Qwen API Configuration
+# Get your API key from: https://bailian.console.aliyun.com/
+QWEN_API_KEY=your_actual_qwen_api_key_here
-# Anthropic API Configuration
-ANTHROPIC_API_KEY=your_actual_anthropic_api_key_here
-ANTHROPIC_BASE_URL=https://api.anthropic.com/v1
+# Google Gemini API Configuration
+# Get your API key from: https://aistudio.google.com/app/apikey
+GOOGLE_API_KEY=your_actual_google_api_key_here
-# Alibaba Cloud Bailian API Keys (for Douyin/Xiaohongshu services)
-BAILIAN_API_KEY=your_actual_bailian_douyin_api_key_here
+# Anthropic Claude API Configuration
+# Get your API key from: https://console.anthropic.com/settings/keys
+ANTHROPIC_API_KEY=your_actual_anthropic_api_key_here
-# OpenRouter API Configuration
+# OpenRouter API Configuration (supports multiple providers)
+# Get your API key from: https://openrouter.ai/keys
OPENROUTER_API_KEY=your_actual_openrouter_api_key_here
# Text-to-Speech Configuration
@@ -69,53 +136,77 @@ NEXT_PUBLIC_APP_ENV=development
ELECTRON_IS_DEV=true
```
-## API Key Sources
-
-### DeepSeek
-- Visit: https://platform.deepseek.com/
-- Create account and generate API key
-
-### OpenAI
-- Visit: https://platform.openai.com/
-- Create account and generate API key
+## Model Capabilities & Token Limits
-### Anthropic
-- Visit: https://console.anthropic.com/
-- Create account and generate API key
+Different models have different maximum token limits:
-### Alibaba Cloud Bailian
-- Visit: https://bailian.console.aliyun.com/
-- Create service and generate API keys
-- Separate keys recommended for Douyin and Xiaohongshu services
+| Model | Provider | Max Tokens | Best For |
+|-------|----------|------------|----------|
+| deepseek-reasoner | DeepSeek | 65,536 | Complex reasoning tasks |
+| claude-3-7-sonnet | Anthropic | 128,000 | Long-context tasks |
+| gemini-2.0-flash-thinking | Google | 65,536 | Reasoning with multimodal |
+| deepseek-chat | DeepSeek | 8,192 | General tasks |
+| qwen-max | Qwen | 8,192 | Chinese language tasks |
+| claude-3.5-sonnet | Anthropic | 8,000 | Balanced performance |
-### OpenRouter
-- Visit: https://openrouter.ai/
-- Create account and generate API key
-
-### Text-to-Speech
-- Visit: https://azure.microsoft.com/en-us/services/cognitive-services/text-to-speech/
-- Create Azure Cognitive Services account
-- Get region and API key
+The application automatically configures the correct token limit based on your selected model.
## Security Notes
- **Never commit actual API keys** to version control
- Use `.env.local` for local development (already in `.gitignore`)
-- For production, use your hosting platform's environment variable configuration
+- User-configured API keys are stored securely in electron-store (encrypted)
- All hardcoded API keys have been removed from source code
- Configuration template provides placeholder values for security
+## Configuration Priority Examples
+
+### Example 1: User Configuration Overrides Environment Variable
+
+```
+User UI: DEEPSEEK_API_KEY = "sk-user-key"
+.env.local: DEEPSEEK_API_KEY = "sk-env-key"
+Result: Uses "sk-user-key"
+```
+
+### Example 2: Environment Variable as Fallback
+
+```
+User UI: DEEPSEEK_API_KEY = (not set)
+.env.local: DEEPSEEK_API_KEY = "sk-env-key"
+Result: Uses "sk-env-key"
+```
+
+### Example 3: Default Values
+
+```
+User UI: DEEPSEEK_API_KEY = (not set)
+.env.local: DEEPSEEK_API_KEY = (not set)
+Result: No API key, will show error when trying to use
+```
+
## Development Workflow
+### For End Users
+1. Launch the application
+2. Click provider dropdown on home page
+3. Select your preferred AI provider
+4. Enter API key in the UI
+5. Start chatting!
+
+### For Developers
1. Copy `.env.template` to `.env.local`
2. Fill in your actual API keys in `.env.local`
3. Restart the development server if it's running
4. The application will automatically use the environment variables
+5. Can override specific keys in the UI if needed
## Production Deployment
### For Desktop Application Build
+**Option 1: Bundle API Keys (Not Recommended for Distribution)**
+
Before building the desktop application, configure the `.env.production` file:
```bash
@@ -129,24 +220,48 @@ Then build the application:
npm run build
```
-The `.env.production` file will be bundled with the application, so end users don't need to configure anything.
+The `.env.production` file will be bundled with the application.
+
+**Option 2: User Configuration (Recommended)**
+
+Build the application without API keys:
-### For Web Deployment
+```bash
+npm run build
+```
-Set the environment variables in your hosting platform:
-- Vercel: Environment Variables in project settings
-- Netlify: Environment Variables in site settings
-- Other platforms: Refer to their documentation for environment variable setup
+End users will configure their own API keys in the UI after installation.
## Troubleshooting
-### Desktop Application
+### UI Configuration Issues
+
+**Problem**: API key status shows "Not configured"
+- **Solution**: Click "Edit API Key" and enter your API key
+- Verify you clicked the checkmark to save
+
+**Problem**: Changes not taking effect
+- **Solution**: Configuration reloads automatically
+- Check console for error messages
+- Try selecting a different model and switching back
+
+**Problem**: Can't find the configuration panel
+- **Solution**: The model configuration panel is on the home page, above the input area
+- Make sure you're on the home page, not in a chat session
+
+### API Key Errors
+
+**Problem**: "API key is invalid" error
+- **Solution**:
+ - Verify you copied the complete API key
+ - Check that the API key is active in the provider's dashboard
+ - Ensure you have sufficient credits/quota
-If you encounter API key errors in the desktop application:
-1. Check that `.env.production` is properly configured before building
-2. Verify the application was built after configuring `.env.production`
-3. Check application logs for configuration loading messages
-4. Ensure required API keys are present and valid
+**Problem**: "Cannot connect to API" error
+- **Solution**:
+ - Check your internet connection
+ - Verify the API provider's service is operational
+ - Try a different provider to isolate the issue
### Development Environment
@@ -154,10 +269,11 @@ If you encounter API key errors in development:
1. Check that all required API keys are set in `.env.local`
2. Verify API keys are valid and have sufficient quota
3. Restart the development server after changing environment variables
-4. Check browser console for specific error messages
+4. Check browser console and terminal for specific error messages
### Common Issues
-- **No API keys found**: Ensure `.env.production` is configured before building
-- **Configuration not loading**: Check that the file exists in the build output
-- **API authentication errors**: Verify API keys are correct and have proper permissions
\ No newline at end of file
+- **Configuration not saving**: Check electron-store permissions
+- **API authentication errors**: Verify API keys are correct and have proper permissions
+- **Model not available**: Some providers may have regional restrictions
+- **Rate limiting**: You may have exceeded the API provider's rate limits
\ No newline at end of file
diff --git a/docs/CONFIGURATION.zh-CN.md b/docs/CONFIGURATION.zh-CN.md
new file mode 100644
index 0000000..3b26e22
--- /dev/null
+++ b/docs/CONFIGURATION.zh-CN.md
@@ -0,0 +1,279 @@
+# 配置指南
+
+本指南介绍如何为 Manus Electron 应用配置 AI 模型和 API 密钥。
+
+## 配置策略
+
+应用支持多种配置方式,优先级如下:
+
+**优先级顺序**:用户 UI 配置 > 环境变量 > 默认值
+
+### 配置方式
+
+1. **UI 配置(推荐给终端用户)**
+ - 直接在应用设置中配置
+ - 无需编辑文件或重启应用
+ - 配置立即生效
+
+2. **环境变量(适合开发者)**
+ - 开发环境使用 `.env.local` 文件
+ - 生产构建使用打包的 `.env.production` 文件
+ - 适合开发者和自动化部署
+
+3. **默认值**
+ - 内置的后备值
+ - 在没有其他配置时使用
+
+## 支持的 AI 提供商
+
+应用支持以下 AI 提供商:
+
+| 提供商 | 模型 | 获取 API 密钥 |
+|--------|------|--------------|
+| **DeepSeek** | deepseek-chat, deepseek-reasoner | [platform.deepseek.com](https://platform.deepseek.com/api_keys) |
+| **Qwen (阿里云)** | qwen-max, qwen-plus, qwen-vl-max | [bailian.console.aliyun.com](https://bailian.console.aliyun.com/) |
+| **Google Gemini** | gemini-1.5-flash, gemini-2.0-flash, gemini-1.5-pro 等 | [aistudio.google.com](https://aistudio.google.com/app/apikey) |
+| **Anthropic Claude** | claude-3.7-sonnet, claude-3.5-sonnet, claude-3-opus 等 | [console.anthropic.com](https://console.anthropic.com/settings/keys) |
+| **OpenRouter** | 多个提供商(Claude, GPT, Gemini 等) | [openrouter.ai](https://openrouter.ai/keys) |
+
+## UI 配置(推荐)
+
+### 在应用中配置 AI 提供商
+
+1. **启动应用**
+ - 打开 Manus Electron 应用
+
+2. **访问模型设置**
+ - 在首页,你会看到模型配置面板
+ - 面板位于输入框上方
+
+3. **选择提供商**
+ - 点击提供商下拉菜单
+ - 从以下选项中选择:Deepseek、Qwen、Google Gemini、Anthropic 或 OpenRouter
+
+4. **选择模型**
+ - 选择提供商后,选择你偏好的模型
+ - 不同提供商提供不同能力的模型
+
+5. **配置 API 密钥**
+ - 点击"编辑 API 密钥"
+ - 输入所选提供商的 API 密钥
+ - 点击对勾保存
+ - API 密钥状态指示器显示:
+ - 🟢 **用户设置**:你在 UI 中配置的
+ - 🟢 **环境变量设置**:在 .env 文件中配置的
+ - 🟡 **未配置**:未找到 API 密钥
+
+6. **获取 API 密钥**
+ - 点击"获取 API 密钥"链接打开提供商的 API 密钥页面
+ - 注册或登录以获取你的 API 密钥
+ - 复制并粘贴到应用中
+
+### 配置立即生效
+
+- 无需重启应用
+- 更改将应用于你发送的下一条消息
+- 配置更改时所有运行中的任务将被终止
+
+## 环境变量配置(适合开发者)
+
+### 1. 复制配置模板
+
+复制模板文件以创建本地环境配置:
+
+```bash
+cp .env.template .env.local
+```
+
+### 2. 配置 API 密钥
+
+编辑 `.env.local` 并填入你的 API 密钥:
+
+```bash
+# AI 服务 API 密钥
+# ===================
+
+# DeepSeek API 配置
+# 从这里获取 API 密钥:https://platform.deepseek.com/api_keys
+DEEPSEEK_API_KEY=你的_deepseek_api_密钥
+DEEPSEEK_BASE_URL=https://api.deepseek.com/v1
+
+# 阿里云通义千问 API 配置
+# 从这里获取 API 密钥:https://bailian.console.aliyun.com/
+QWEN_API_KEY=你的_qwen_api_密钥
+
+# Google Gemini API 配置
+# 从这里获取 API 密钥:https://aistudio.google.com/app/apikey
+GOOGLE_API_KEY=你的_google_api_密钥
+
+# Anthropic Claude API 配置
+# 从这里获取 API 密钥:https://console.anthropic.com/settings/keys
+ANTHROPIC_API_KEY=你的_anthropic_api_密钥
+
+# OpenRouter API 配置(支持多个提供商)
+# 从这里获取 API 密钥:https://openrouter.ai/keys
+OPENROUTER_API_KEY=你的_openrouter_api_密钥
+
+# 语音转文字配置
+TTS_REGION=eastasia
+TTS_KEY=你的_tts_密钥
+
+# 应用设置
+# ===================
+
+# 截图设置
+EKO_SCREENSHOT_SCALE=0.5
+# 或者使用最大宽度进行比例缩放
+# EKO_SCREENSHOT_MAX_WIDTH=1280
+
+# 开发设置
+# ===================
+
+# Next.js 开发设置
+NEXT_PUBLIC_APP_ENV=development
+
+# Electron 设置
+ELECTRON_IS_DEV=true
+```
+
+## 模型能力与 Token 限制
+
+不同模型有不同的最大 token 限制:
+
+| 模型 | 提供商 | 最大 Tokens | 最适合 |
+|------|--------|-------------|--------|
+| deepseek-reasoner | DeepSeek | 65,536 | 复杂推理任务 |
+| claude-3-7-sonnet | Anthropic | 128,000 | 长文本任务 |
+| gemini-2.0-flash-thinking | Google | 65,536 | 多模态推理 |
+| deepseek-chat | DeepSeek | 8,192 | 通用任务 |
+| qwen-max | Qwen | 8,192 | 中文任务 |
+| claude-3.5-sonnet | Anthropic | 8,000 | 平衡性能 |
+
+应用会根据你选择的模型自动配置正确的 token 限制。
+
+## 安全注意事项
+
+- **永远不要将实际的 API 密钥提交到版本控制**
+- 本地开发使用 `.env.local`(已在 `.gitignore` 中)
+- 用户配置的 API 密钥安全存储在 electron-store 中(已加密)
+- 所有硬编码的 API 密钥已从源代码中删除
+- 配置模板提供占位符值以确保安全
+
+## 配置优先级示例
+
+### 示例 1:用户配置覆盖环境变量
+
+```
+用户 UI:DEEPSEEK_API_KEY = "sk-user-key"
+.env.local:DEEPSEEK_API_KEY = "sk-env-key"
+结果:使用 "sk-user-key"
+```
+
+### 示例 2:环境变量作为后备
+
+```
+用户 UI:DEEPSEEK_API_KEY =(未设置)
+.env.local:DEEPSEEK_API_KEY = "sk-env-key"
+结果:使用 "sk-env-key"
+```
+
+### 示例 3:默认值
+
+```
+用户 UI:DEEPSEEK_API_KEY =(未设置)
+.env.local:DEEPSEEK_API_KEY =(未设置)
+结果:没有 API 密钥,尝试使用时会显示错误
+```
+
+## 开发工作流程
+
+### 终端用户
+1. 启动应用
+2. 在首页点击提供商下拉菜单
+3. 选择你偏好的 AI 提供商
+4. 在 UI 中输入 API 密钥
+5. 开始聊天!
+
+### 开发者
+1. 复制 `.env.template` 到 `.env.local`
+2. 在 `.env.local` 中填入你的实际 API 密钥
+3. 如果开发服务器正在运行,重启它
+4. 应用将自动使用环境变量
+5. 如需要,可在 UI 中覆盖特定密钥
+
+## 生产部署
+
+### 桌面应用构建
+
+**选项 1:打包 API 密钥(不推荐用于分发)**
+
+在构建桌面应用前,配置 `.env.production` 文件:
+
+```bash
+# 编辑生产配置文件
+# 将所有占位符 API 密钥替换为实际值
+```
+
+然后构建应用:
+
+```bash
+npm run build
+```
+
+`.env.production` 文件将被打包到应用中。
+
+**选项 2:用户配置(推荐)**
+
+不带 API 密钥构建应用:
+
+```bash
+npm run build
+```
+
+终端用户在安装后会在 UI 中配置自己的 API 密钥。
+
+## 故障排除
+
+### UI 配置问题
+
+**问题**:API 密钥状态显示"未配置"
+- **解决方案**:点击"编辑 API 密钥"并输入你的 API 密钥
+- 确认你点击了对勾保存
+
+**问题**:更改未生效
+- **解决方案**:配置会自动重新加载
+- 检查控制台是否有错误消息
+- 尝试选择不同的模型然后切换回来
+
+**问题**:找不到配置面板
+- **解决方案**:模型配置面板在首页,输入框上方
+- 确保你在首页,而不是在聊天会话中
+
+### API 密钥错误
+
+**问题**:"API 密钥无效"错误
+- **解决方案**:
+ - 确认你复制了完整的 API 密钥
+ - 检查 API 密钥在提供商的控制台中是否激活
+ - 确保你有足够的额度/配额
+
+**问题**:"无法连接到 API"错误
+- **解决方案**:
+ - 检查你的网络连接
+ - 确认 API 提供商的服务正常运行
+ - 尝试不同的提供商以隔离问题
+
+### 开发环境
+
+如果在开发中遇到 API 密钥错误:
+1. 检查所有必需的 API 密钥是否在 `.env.local` 中设置
+2. 确认 API 密钥有效且有足够的配额
+3. 更改环境变量后重启开发服务器
+4. 检查浏览器控制台和终端的具体错误消息
+
+### 常见问题
+
+- **配置无法保存**:检查 electron-store 权限
+- **API 认证错误**:确认 API 密钥正确且有适当权限
+- **模型不可用**:某些提供商可能有地区限制
+- **速率限制**:你可能超过了 API 提供商的速率限制
diff --git a/electron/main/index.ts b/electron/main/index.ts
index 21dea52..c495567 100644
--- a/electron/main/index.ts
+++ b/electron/main/index.ts
@@ -3,7 +3,6 @@ import {
app,
BrowserWindow,
dialog,
- ipcMain,
WebContentsView,
protocol,
} from "electron";
@@ -27,9 +26,9 @@ import { EkoService } from "./services/eko-service";
import { ServerManager } from "./services/server-manager";
import { MainWindowManager } from "./windows/main-window";
import { taskScheduler } from "./services/task-scheduler";
-import { taskWindowManager } from "./services/task-window-manager";
import { windowContextManager, type WindowContext } from "./services/window-context-manager";
import { cwd } from "node:process";
+import { registerAllIpcHandlers } from "./ipc";
Object.assign(console, log.functions);
@@ -328,219 +327,8 @@ app.on("window-all-closed", () => {
// Scheduled tasks will continue executing in background
});
-ipcMain.handle('get-main-view-screenshot', async (event) => {
- // Get corresponding detailView based on caller window
- const context = windowContextManager.getContext(event.sender.id);
- if (!context || !context.detailView) {
- throw new Error('DetailView not found for this window');
- }
-
- const image = await context.detailView.webContents.capturePage()
- return {
- imageBase64: image.toDataURL(),
- imageType: "image/jpeg",
- }
-});
+// Register all IPC handlers
+registerAllIpcHandlers();
reloadOnChange();
// setupAutoUpdater();
-
-// EkoService IPC handlers - supports window isolation
-ipcMain.handle('eko:run', async (event, message: string) => {
- const context = windowContextManager.getContext(event.sender.id);
- if (!context || !context.ekoService) {
- throw new Error('EkoService not found for this window');
- }
- return await context.ekoService.run(message);
-});
-
-ipcMain.handle('eko:modify', async (event, taskId: string, message: string) => {
- try {
- console.log('IPC eko:modify received:', taskId, message);
- const context = windowContextManager.getContext(event.sender.id);
- if (!context || !context.ekoService) {
- throw new Error('EkoService not found for this window');
- }
- return await context.ekoService.modify(taskId, message);
- } catch (error: any) {
- console.error('IPC eko:modify error:', error);
- throw error;
- }
-});
-
-ipcMain.handle('eko:execute', async (event, taskId: string) => {
- try {
- console.log('IPC eko:execute received:', taskId);
- const context = windowContextManager.getContext(event.sender.id);
- if (!context || !context.ekoService) {
- throw new Error('EkoService not found for this window');
- }
- return await context.ekoService.execute(taskId);
- } catch (error: any) {
- console.error('IPC eko:execute error:', error);
- throw error;
- }
-});
-
-ipcMain.handle('eko:getTaskStatus', async (event, taskId: string) => {
- try {
- console.log('IPC eko:getTaskStatus received:', taskId);
- const context = windowContextManager.getContext(event.sender.id);
- if (!context || !context.ekoService) {
- throw new Error('EkoService not found for this window');
- }
- return await context.ekoService.getTaskStatus(taskId);
- } catch (error: any) {
- console.error('IPC eko:getTaskStatus error:', error);
- throw error;
- }
-});
-
-ipcMain.handle('eko:cancel-task', async (event, taskId: string) => {
- try {
- console.log('IPC eko:cancel-task received:', taskId);
- const context = windowContextManager.getContext(event.sender.id);
- if (!context || !context.ekoService) {
- throw new Error('EkoService not found for this window');
- }
- const result = await context.ekoService.cancleTask(taskId);
- return { success: true, result };
- } catch (error: any) {
- console.error('IPC eko:cancel-task error:', error);
- throw error;
- }
-});
-
-// IPC handler for controlling detail view visibility - supports window isolation
-ipcMain.handle('set-detail-view-visible', async (event, visible: boolean) => {
- try {
- console.log('IPC set-detail-view-visible received:', visible);
- const context = windowContextManager.getContext(event.sender.id);
- if (!context || !context.detailView) {
- throw new Error('DetailView not found for this window');
- }
-
- context.detailView.setVisible(visible);
-
- return { success: true, visible };
- } catch (error: any) {
- console.error('IPC set-detail-view-visible error:', error);
- throw error;
- }
-});
-
-// URL-related IPC handlers - supports window isolation
-ipcMain.handle('get-current-url', async (event) => {
- try {
- console.log('IPC get-current-url received');
- const context = windowContextManager.getContext(event.sender.id);
- if (!context || !context.detailView) {
- return '';
- }
- return context.detailView.webContents.getURL();
- } catch (error: any) {
- console.error('IPC get-current-url error:', error);
- return '';
- }
-});
-
-// History view management IPC handlers - supports window isolation
-ipcMain.handle('show-history-view', async (event, screenshot: string) => {
- try {
- console.log('IPC show-history-view received');
- const context = windowContextManager.getContext(event.sender.id);
- if (!context) {
- throw new Error('Window context not found');
- }
-
- // Create history view
- if (context.historyView) {
- context.window.contentView.removeChildView(context.historyView);
- }
-
- context.historyView = new WebContentsView();
-
- // Load screenshot content
- const htmlContent = `
-
-
-
-
-
-
-
-
- `;
-
- await context.historyView.webContents.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(htmlContent)}`);
-
- // Set history view position (overlay detail panel position)
- context.window.contentView.addChildView(context.historyView);
- context.historyView.setBounds({
- x: 818,
- y: 264,
- width: 748,
- height: 560,
- });
-
- return { success: true };
- } catch (error: any) {
- console.error('IPC show-history-view error:', error);
- throw error;
- }
-});
-
-ipcMain.handle('hide-history-view', async (event) => {
- try {
- console.log('IPC hide-history-view received');
- const context = windowContextManager.getContext(event.sender.id);
- if (context && context.historyView) {
- context.window.contentView.removeChildView(context.historyView);
- context.historyView = null;
- }
- return { success: true };
- } catch (error: any) {
- console.error('IPC hide-history-view error:', error);
- throw error;
- }
-});
-
-// IPC handler for opening task window history panel
-ipcMain.handle('open-task-history', async (_event, taskId: string) => {
- try {
- console.log('[IPC] open-task-history received:', taskId);
-
- // Check if task window already exists
- let taskWindow = taskWindowManager.getTaskWindow(taskId);
-
- if (taskWindow) {
- // Window exists, activate it
- console.log('[IPC] Task window exists, activating window');
- taskWindow.window.show();
- taskWindow.window.focus();
- } else {
- // Window doesn't exist, create new window
- console.log('[IPC] Task window does not exist, creating new window');
-
- // Generate new executionId (for creating window, won't execute task immediately)
- const executionId = `view_history_${Date.now()}`;
-
- // Create task window
- taskWindow = await taskWindowManager.createTaskWindow(taskId, executionId);
- }
-
- // Wait for window content to load, then send open history panel event
- setTimeout(() => {
- taskWindow!.window.webContents.send('open-history-panel', { taskId });
- console.log('[IPC] Sent open-history-panel event to task window');
- }, 1000); // Delay 1 second to ensure page is loaded
-
- return { success: true };
- } catch (error: any) {
- console.error('[IPC] open-task-history error:', error);
- throw error;
- }
-});
diff --git a/electron/main/ipc/config-handlers.ts b/electron/main/ipc/config-handlers.ts
new file mode 100644
index 0000000..3fc678d
--- /dev/null
+++ b/electron/main/ipc/config-handlers.ts
@@ -0,0 +1,96 @@
+import { ipcMain } from "electron";
+import { ConfigManager, type UserModelConfigs, type ProviderType } from "../utils/config-manager";
+import { windowContextManager } from "../services/window-context-manager";
+
+/**
+ * Register all configuration-related IPC handlers
+ */
+export function registerConfigHandlers() {
+ // Get user model configurations
+ ipcMain.handle('config:get-user-configs', async () => {
+ try {
+ const configManager = ConfigManager.getInstance();
+ return configManager.getUserModelConfigs();
+ } catch (error: any) {
+ console.error('IPC config:get-user-configs error:', error);
+ throw error;
+ }
+ });
+
+ // Save user model configurations
+ ipcMain.handle('config:save-user-configs', async (_event, configs: UserModelConfigs) => {
+ try {
+ const configManager = ConfigManager.getInstance();
+ configManager.saveUserModelConfigs(configs);
+
+ // Reload EkoService configuration for all windows
+ const contexts = windowContextManager.getAllContexts();
+ contexts.forEach(context => {
+ if (context.ekoService) {
+ context.ekoService.reloadConfig();
+ }
+ });
+
+ return { success: true };
+ } catch (error: any) {
+ console.error('IPC config:save-user-configs error:', error);
+ throw error;
+ }
+ });
+
+ // Get model configuration for specific provider
+ ipcMain.handle('config:get-model-config', async (_event, provider: ProviderType) => {
+ try {
+ const configManager = ConfigManager.getInstance();
+ return configManager.getModelConfig(provider);
+ } catch (error: any) {
+ console.error('IPC config:get-model-config error:', error);
+ throw error;
+ }
+ });
+
+ // Get API key source (user/env/none)
+ ipcMain.handle('config:get-api-key-source', async (_event, provider: ProviderType) => {
+ try {
+ const configManager = ConfigManager.getInstance();
+ return configManager.getApiKeySource(provider);
+ } catch (error: any) {
+ console.error('IPC config:get-api-key-source error:', error);
+ throw error;
+ }
+ });
+
+ // Get selected provider
+ ipcMain.handle('config:get-selected-provider', async () => {
+ try {
+ const configManager = ConfigManager.getInstance();
+ return configManager.getSelectedProvider();
+ } catch (error: any) {
+ console.error('IPC config:get-selected-provider error:', error);
+ throw error;
+ }
+ });
+
+ // Set selected provider
+ ipcMain.handle('config:set-selected-provider', async (_event, provider: ProviderType) => {
+ try {
+ const configManager = ConfigManager.getInstance();
+ configManager.setSelectedProvider(provider);
+
+ // Reload EkoService configuration for all windows
+ const contexts = windowContextManager.getAllContexts();
+ contexts.forEach(context => {
+ if (context.ekoService) {
+ context.ekoService.reloadConfig();
+ }
+ });
+
+ return { success: true };
+ } catch (error: any) {
+ console.error('IPC config:set-selected-provider error:', error);
+ throw error;
+ }
+ });
+
+ console.log('[IPC] Configuration handlers registered');
+}
diff --git a/electron/main/ipc/eko-handlers.ts b/electron/main/ipc/eko-handlers.ts
new file mode 100644
index 0000000..45c7c00
--- /dev/null
+++ b/electron/main/ipc/eko-handlers.ts
@@ -0,0 +1,80 @@
+import { ipcMain } from "electron";
+import { windowContextManager } from "../services/window-context-manager";
+
+/**
+ * Register all Eko service related IPC handlers
+ * All handlers support window isolation through windowContextManager
+ */
+export function registerEkoHandlers() {
+ // Run new task
+ ipcMain.handle('eko:run', async (event, message: string) => {
+ const context = windowContextManager.getContext(event.sender.id);
+ if (!context || !context.ekoService) {
+ throw new Error('EkoService not found for this window');
+ }
+ return await context.ekoService.run(message);
+ });
+
+ // Modify existing task
+ ipcMain.handle('eko:modify', async (event, taskId: string, message: string) => {
+ try {
+ console.log('IPC eko:modify received:', taskId, message);
+ const context = windowContextManager.getContext(event.sender.id);
+ if (!context || !context.ekoService) {
+ throw new Error('EkoService not found for this window');
+ }
+ return await context.ekoService.modify(taskId, message);
+ } catch (error: any) {
+ console.error('IPC eko:modify error:', error);
+ throw error;
+ }
+ });
+
+ // Execute task
+ ipcMain.handle('eko:execute', async (event, taskId: string) => {
+ try {
+ console.log('IPC eko:execute received:', taskId);
+ const context = windowContextManager.getContext(event.sender.id);
+ if (!context || !context.ekoService) {
+ throw new Error('EkoService not found for this window');
+ }
+ return await context.ekoService.execute(taskId);
+ } catch (error: any) {
+ console.error('IPC eko:execute error:', error);
+ throw error;
+ }
+ });
+
+ // Get task status
+ ipcMain.handle('eko:getTaskStatus', async (event, taskId: string) => {
+ try {
+ console.log('IPC eko:getTaskStatus received:', taskId);
+ const context = windowContextManager.getContext(event.sender.id);
+ if (!context || !context.ekoService) {
+ throw new Error('EkoService not found for this window');
+ }
+ return await context.ekoService.getTaskStatus(taskId);
+ } catch (error: any) {
+ console.error('IPC eko:getTaskStatus error:', error);
+ throw error;
+ }
+ });
+
+ // Cancel task
+ ipcMain.handle('eko:cancel-task', async (event, taskId: string) => {
+ try {
+ console.log('IPC eko:cancel-task received:', taskId);
+ const context = windowContextManager.getContext(event.sender.id);
+ if (!context || !context.ekoService) {
+ throw new Error('EkoService not found for this window');
+ }
+ const result = await context.ekoService.cancleTask(taskId);
+ return { success: true, result };
+ } catch (error: any) {
+ console.error('IPC eko:cancel-task error:', error);
+ throw error;
+ }
+ });
+
+ console.log('[IPC] Eko service handlers registered');
+}
diff --git a/electron/main/ipc/history-handlers.ts b/electron/main/ipc/history-handlers.ts
new file mode 100644
index 0000000..6eb68bb
--- /dev/null
+++ b/electron/main/ipc/history-handlers.ts
@@ -0,0 +1,114 @@
+import { ipcMain, WebContentsView } from "electron";
+import { windowContextManager } from "../services/window-context-manager";
+import { taskWindowManager } from "../services/task-window-manager";
+
+/**
+ * Register all history related IPC handlers
+ * Handles history view display and task history window management
+ * All handlers support window isolation through windowContextManager
+ */
+export function registerHistoryHandlers() {
+ // Show history view with screenshot
+ ipcMain.handle('show-history-view', async (event, screenshot: string) => {
+ try {
+ console.log('IPC show-history-view received');
+ const context = windowContextManager.getContext(event.sender.id);
+ if (!context) {
+ throw new Error('Window context not found');
+ }
+
+ // Create history view
+ if (context.historyView) {
+ context.window.contentView.removeChildView(context.historyView);
+ }
+
+ context.historyView = new WebContentsView();
+
+ // Load screenshot content
+ const htmlContent = `
+
+
+
+
+
+
+
+
+ `;
+
+ await context.historyView.webContents.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(htmlContent)}`);
+
+ // Set history view position (overlay detail panel position)
+ context.window.contentView.addChildView(context.historyView);
+ context.historyView.setBounds({
+ x: 818,
+ y: 264,
+ width: 748,
+ height: 560,
+ });
+
+ return { success: true };
+ } catch (error: any) {
+ console.error('IPC show-history-view error:', error);
+ throw error;
+ }
+ });
+
+ // Hide history view
+ ipcMain.handle('hide-history-view', async (event) => {
+ try {
+ console.log('IPC hide-history-view received');
+ const context = windowContextManager.getContext(event.sender.id);
+ if (context && context.historyView) {
+ context.window.contentView.removeChildView(context.historyView);
+ context.historyView = null;
+ }
+ return { success: true };
+ } catch (error: any) {
+ console.error('IPC hide-history-view error:', error);
+ throw error;
+ }
+ });
+
+ // Open task history window
+ ipcMain.handle('open-task-history', async (_event, taskId: string) => {
+ try {
+ console.log('[IPC] open-task-history received:', taskId);
+
+ // Check if task window already exists
+ let taskWindow = taskWindowManager.getTaskWindow(taskId);
+
+ if (taskWindow) {
+ // Window exists, activate it
+ console.log('[IPC] Task window exists, activating window');
+ taskWindow.window.show();
+ taskWindow.window.focus();
+ } else {
+ // Window doesn't exist, create new window
+ console.log('[IPC] Task window does not exist, creating new window');
+
+ // Generate new executionId (for creating window, won't execute task immediately)
+ const executionId = `view_history_${Date.now()}`;
+
+ // Create task window
+ taskWindow = await taskWindowManager.createTaskWindow(taskId, executionId);
+ }
+
+ // Wait for window content to load, then send open history panel event
+ setTimeout(() => {
+ taskWindow!.window.webContents.send('open-history-panel', { taskId });
+ console.log('[IPC] Sent open-history-panel event to task window');
+ }, 1000); // Delay 1 second to ensure page is loaded
+
+ return { success: true };
+ } catch (error: any) {
+ console.error('[IPC] open-task-history error:', error);
+ throw error;
+ }
+ });
+
+ console.log('[IPC] History handlers registered');
+}
diff --git a/electron/main/ipc/index.ts b/electron/main/ipc/index.ts
new file mode 100644
index 0000000..11aad7e
--- /dev/null
+++ b/electron/main/ipc/index.ts
@@ -0,0 +1,25 @@
+import { registerEkoHandlers } from "./eko-handlers";
+import { registerViewHandlers } from "./view-handlers";
+import { registerHistoryHandlers } from "./history-handlers";
+import { registerConfigHandlers } from "./config-handlers";
+
+/**
+ * Register all IPC handlers
+ * Centralized registration point for all IPC communication handlers
+ */
+export function registerAllIpcHandlers() {
+ registerEkoHandlers();
+ registerViewHandlers();
+ registerHistoryHandlers();
+ registerConfigHandlers();
+
+ console.log('[IPC] All IPC handlers registered successfully');
+}
+
+// Export individual registration functions for selective use if needed
+export {
+ registerEkoHandlers,
+ registerViewHandlers,
+ registerHistoryHandlers,
+ registerConfigHandlers
+};
diff --git a/electron/main/ipc/view-handlers.ts b/electron/main/ipc/view-handlers.ts
new file mode 100644
index 0000000..d4372d4
--- /dev/null
+++ b/electron/main/ipc/view-handlers.ts
@@ -0,0 +1,58 @@
+import { ipcMain } from "electron";
+import { windowContextManager } from "../services/window-context-manager";
+
+/**
+ * Register all view control related IPC handlers
+ * Handles screenshot, visibility control, and URL operations
+ * All handlers support window isolation through windowContextManager
+ */
+export function registerViewHandlers() {
+ // Get main view screenshot
+ ipcMain.handle('get-main-view-screenshot', async (event) => {
+ const context = windowContextManager.getContext(event.sender.id);
+ if (!context || !context.detailView) {
+ throw new Error('DetailView not found for this window');
+ }
+
+ const image = await context.detailView.webContents.capturePage();
+ return {
+ imageBase64: image.toDataURL(),
+ imageType: "image/jpeg",
+ };
+ });
+
+ // Set detail view visibility
+ ipcMain.handle('set-detail-view-visible', async (event, visible: boolean) => {
+ try {
+ console.log('IPC set-detail-view-visible received:', visible);
+ const context = windowContextManager.getContext(event.sender.id);
+ if (!context || !context.detailView) {
+ throw new Error('DetailView not found for this window');
+ }
+
+ context.detailView.setVisible(visible);
+
+ return { success: true, visible };
+ } catch (error: any) {
+ console.error('IPC set-detail-view-visible error:', error);
+ throw error;
+ }
+ });
+
+ // Get current URL from detail view
+ ipcMain.handle('get-current-url', async (event) => {
+ try {
+ console.log('IPC get-current-url received');
+ const context = windowContextManager.getContext(event.sender.id);
+ if (!context || !context.detailView) {
+ return '';
+ }
+ return context.detailView.webContents.getURL();
+ } catch (error: any) {
+ console.error('IPC get-current-url error:', error);
+ return '';
+ }
+ });
+
+ console.log('[IPC] View control handlers registered');
+}
diff --git a/electron/main/services/eko-service.ts b/electron/main/services/eko-service.ts
index e5ffe70..ccdb5b0 100644
--- a/electron/main/services/eko-service.ts
+++ b/electron/main/services/eko-service.ts
@@ -3,11 +3,14 @@ import { BrowserAgent, FileAgent } from "@jarvis-agent/electron";
import type { EkoResult } from "@jarvis-agent/core/types";
import { BrowserWindow, WebContentsView, app } from "electron";
import path from "node:path";
+import { ConfigManager } from "../utils/config-manager";
export class EkoService {
private eko: Eko | null = null;
private mainWindow: BrowserWindow;
private detailView: WebContentsView;
+ private mcpClient!: SimpleSseMcpClient;
+ private agents!: any[];
constructor(mainWindow: BrowserWindow, detailView: WebContentsView) {
this.mainWindow = mainWindow;
@@ -15,79 +18,11 @@ export class EkoService {
this.initializeEko();
}
- private initializeEko() {
- console.log(process.env)
- // LLMs configuration - read from environment variables
- const llms: LLMs = {
- default: {
- provider: "deepseek",
- model: "deepseek-chat",
- apiKey: process.env.DEEPSEEK_API_KEY || "",
- config: {
- baseURL: process.env.DEEPSEEK_BASE_URL || "https://api.deepseek.com/v1",
- maxTokens: 8192,
- mode: 'regular',
- },
-
- fetch: (url, options) => {
- // Intercept request and add thinking parameter
- const body = JSON.parse((options?.body as string) || '{}');
- body.thinking = { type: "disabled" };
-
- Log.info('Deepseek request options:\n', body);
-
- return fetch(url, {
- ...options,
- body: JSON.stringify(body)
- });
- }
- },
- "qwen-vl": {
- provider: "openai",
- model: "qwen-vl-max-2025-08-13",
- apiKey: process.env.BAILIAN_API_KEY || "", // Use Bailian API key from environment
- config: {
- baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
- maxTokens: 16000,
- timeout: 60000,
- temperature: 0.7
- },
- fetch: (url, options) => {
- Log.info('Vision model request parameters', options)
- return fetch(url, options);
- }
- },
- 'open-router': {
- provider: "openrouter",
- model: "openai/gpt-5-mini",
- apiKey: process.env.OPENROUTER_API_KEY || "",
- config: {
- // baseURL: "https://openai-proxy.awsv.cn/v1",
- },
- },
- };
-
- // Get correct application path
- const appPath = app.isPackaged
- ? path.join(app.getPath('userData'), 'static') // Packaged path
- : path.join(process.cwd(), 'public', 'static'); // Development environment path
-
- Log.info(`FileAgent working path: ${appPath}`);
-
- // MCP client configuration - configure based on your MCP server address
- const sseUrl = "http://localhost:5173/api/mcp/sse";
- let mcpClient = new SimpleSseMcpClient(sseUrl);
-
- const echartMcpUrl = "http://localhost:3033/sse";
- const echartMcpClient = new SimpleSseMcpClient(echartMcpUrl);
-
-
-
- // Create agents - can now use FileAgent since we're in Node.js environment
- const agents = [new BrowserAgent(this.detailView, mcpClient), new FileAgent(this.detailView, appPath)];
-
- // Stream callback handler
- const callback = {
+ /**
+ * Create stream callback handler
+ */
+ private createCallback() {
+ return {
onMessage: (message: StreamCallbackMessage): Promise => {
Log.info('EkoService stream callback:', message);
@@ -143,10 +78,74 @@ export class EkoService {
console.log('EkoService human callback:', message);
}
};
+ }
+
+ private initializeEko() {
+ // Get LLMs configuration from ConfigManager
+ // Priority: user config > env > default
+ const configManager = ConfigManager.getInstance();
+ const llms: LLMs = configManager.getLLMsConfig();
+
+ // Get correct application path
+ const appPath = app.isPackaged
+ ? path.join(app.getPath('userData'), 'static') // Packaged path
+ : path.join(process.cwd(), 'public', 'static'); // Development environment path
+
+ Log.info(`FileAgent working path: ${appPath}`);
- // Initialize Eko instance
- this.eko = new Eko({ llms, agents, callback });
- console.log('EkoService initialized with FileAgent support');
+ // MCP client configuration - configure based on your MCP server address
+ const sseUrl = "http://localhost:5173/api/mcp/sse";
+ this.mcpClient = new SimpleSseMcpClient(sseUrl);
+
+ // Create agents - can now use FileAgent since we're in Node.js environment
+ this.agents = [new BrowserAgent(this.detailView, this.mcpClient), new FileAgent(this.detailView, appPath)];
+
+ // Create callback and initialize Eko instance
+ const callback = this.createCallback();
+ this.eko = new Eko({ llms, agents: this.agents, callback });
+ Log.info('EkoService initialized with LLMs:', llms.default?.model);
+ }
+
+ /**
+ * Reload LLM configuration and reinitialize Eko instance
+ * Called when user changes model configuration in UI
+ */
+ public reloadConfig(): void {
+ Log.info('Reloading EkoService configuration...');
+
+ // Abort all running tasks before reloading
+ if (this.eko) {
+ const allTaskIds = this.eko.getAllTaskId();
+ allTaskIds.forEach(taskId => {
+ try {
+ this.eko!.abortTask(taskId, 'config-reload');
+ } catch (error) {
+ Log.error(`Failed to abort task ${taskId}:`, error);
+ }
+ });
+ }
+
+ // Get new LLMs configuration
+ const configManager = ConfigManager.getInstance();
+ const llms: LLMs = configManager.getLLMsConfig();
+
+ Log.info('New LLMs config:', llms.default?.model);
+
+ // Create new Eko instance with updated config and fresh callback
+ const callback = this.createCallback();
+ this.eko = new Eko({ llms, agents: this.agents, callback });
+
+ Log.info('EkoService configuration reloaded successfully');
+
+ // Notify frontend about config reload
+ if (!this.mainWindow || this.mainWindow.isDestroyed()) {
+ return;
+ }
+
+ this.mainWindow.webContents.send('eko-config-reloaded', {
+ model: llms.default?.model,
+ provider: llms.default?.provider
+ });
}
/**
@@ -154,32 +153,62 @@ export class EkoService {
*/
async run(message: string): Promise {
if (!this.eko) {
- throw new Error('Eko service not initialized');
+ const errorMsg = 'Eko service not initialized';
+ Log.error(errorMsg);
+ this.sendErrorToFrontend(errorMsg);
+ return null;
}
-
+
console.log('EkoService running task:', message);
let result = null;
try {
result = await this.eko.run(message);
- } catch (error) {
+ } catch (error: any) {
Log.error('EkoService run error:', error);
+
+ // Extract error message
+ const errorMessage = error?.message || error?.toString() || 'Unknown error occurred';
+ this.sendErrorToFrontend(errorMessage, error);
}
return result;
}
+ /**
+ * Send error message to frontend
+ */
+ private sendErrorToFrontend(errorMessage: string, error?: any, taskId?: string): void {
+ if (!this.mainWindow || this.mainWindow.isDestroyed()) {
+ Log.warn('Main window destroyed, cannot send error message');
+ return;
+ }
+
+ this.mainWindow.webContents.send('eko-stream-message', {
+ type: 'error',
+ error: errorMessage,
+ detail: error?.stack || error?.toString() || errorMessage,
+ taskId: taskId // Include taskId if available
+ });
+ }
+
/**
* Modify existing task
*/
async modify(taskId: string, message: string): Promise {
if (!this.eko) {
- throw new Error('Eko service not initialized');
+ const errorMsg = 'Eko service not initialized';
+ Log.error(errorMsg);
+ this.sendErrorToFrontend(errorMsg, undefined, taskId);
+ return null;
}
+
let result = null;
try {
await this.eko.modify(taskId, message);
result = await this.eko.execute(taskId);
- } catch (error) {
+ } catch (error: any) {
Log.error('EkoService modify error:', error);
+ const errorMessage = error?.message || error?.toString() || 'Failed to modify task';
+ this.sendErrorToFrontend(errorMessage, error, taskId);
}
return result;
}
@@ -187,13 +216,23 @@ export class EkoService {
/**
* Execute task
*/
- async execute(taskId: string): Promise {
+ async execute(taskId: string): Promise {
if (!this.eko) {
- throw new Error('Eko service not initialized');
+ const errorMsg = 'Eko service not initialized';
+ Log.error(errorMsg);
+ this.sendErrorToFrontend(errorMsg, undefined, taskId);
+ return null;
}
console.log('EkoService executing task:', taskId);
- return await this.eko.execute(taskId);
+ try {
+ return await this.eko.execute(taskId);
+ } catch (error: any) {
+ Log.error('EkoService execute error:', error);
+ const errorMessage = error?.message || error?.toString() || 'Failed to execute task';
+ this.sendErrorToFrontend(errorMessage, error, taskId);
+ return null;
+ }
}
/**
diff --git a/electron/main/utils/config-manager.ts b/electron/main/utils/config-manager.ts
index 271feee..97f7133 100644
--- a/electron/main/utils/config-manager.ts
+++ b/electron/main/utils/config-manager.ts
@@ -2,6 +2,50 @@ import { config } from "dotenv";
import path from "node:path";
import { app } from "electron";
import fs from "fs";
+import { store } from "./store";
+
+/**
+ * Supported providers
+ */
+export type ProviderType = 'deepseek' | 'qwen' | 'google' | 'anthropic' | 'openrouter';
+
+/**
+ * Model configuration interface
+ */
+export interface ModelConfig {
+ provider: string;
+ model: string;
+ apiKey?: string;
+ baseURL?: string;
+}
+
+/**
+ * User model configurations stored in electron-store
+ */
+export interface UserModelConfigs {
+ deepseek?: {
+ apiKey?: string;
+ baseURL?: string;
+ model?: string;
+ };
+ qwen?: {
+ apiKey?: string;
+ model?: string;
+ };
+ google?: {
+ apiKey?: string;
+ model?: string;
+ };
+ anthropic?: {
+ apiKey?: string;
+ model?: string;
+ };
+ openrouter?: {
+ apiKey?: string;
+ model?: string;
+ };
+ selectedProvider?: ProviderType;
+}
/**
* Configuration Manager for handling environment variables in both development and production
@@ -95,4 +139,261 @@ export class ConfigManager {
console.warn('[ConfigManager] Missing required API keys:', validation.missingKeys);
}
}
+
+ /**
+ * Get user model configurations from electron-store
+ */
+ public getUserModelConfigs(): UserModelConfigs {
+ return store.get('modelConfigs', {}) as UserModelConfigs;
+ }
+
+ /**
+ * Save user model configurations to electron-store
+ */
+ public saveUserModelConfigs(configs: UserModelConfigs): void {
+ store.set('modelConfigs', configs);
+ console.log('[ConfigManager] User model configurations saved');
+ }
+
+ /**
+ * Get final model configuration with priority: user config > env > default
+ */
+ public getModelConfig(provider: ProviderType): ModelConfig | null {
+ const userConfigs = this.getUserModelConfigs();
+
+ switch (provider) {
+ case 'deepseek':
+ return {
+ provider: 'deepseek',
+ model: userConfigs.deepseek?.model || 'deepseek-chat',
+ apiKey: userConfigs.deepseek?.apiKey || process.env.DEEPSEEK_API_KEY || '',
+ baseURL: userConfigs.deepseek?.baseURL || process.env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com/v1'
+ };
+
+ case 'qwen':
+ return {
+ provider: 'openai',
+ model: userConfigs.qwen?.model || 'qwen-max',
+ apiKey: userConfigs.qwen?.apiKey || process.env.QWEN_API_KEY || '',
+ baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1'
+ };
+
+ case 'google':
+ return {
+ provider: 'google',
+ model: userConfigs.google?.model || 'gemini-1.5-flash-latest',
+ apiKey: userConfigs.google?.apiKey || process.env.GOOGLE_API_KEY || ''
+ };
+
+ case 'anthropic':
+ return {
+ provider: 'anthropic',
+ model: userConfigs.anthropic?.model || 'claude-3-5-sonnet-latest',
+ apiKey: userConfigs.anthropic?.apiKey || process.env.ANTHROPIC_API_KEY || ''
+ };
+
+ case 'openrouter':
+ return {
+ provider: 'openrouter',
+ model: userConfigs.openrouter?.model || 'anthropic/claude-3.5-sonnet',
+ apiKey: userConfigs.openrouter?.apiKey || process.env.OPENROUTER_API_KEY || ''
+ };
+
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * Get API key source info (for UI display)
+ */
+ public getApiKeySource(provider: ProviderType): 'user' | 'env' | 'none' {
+ const userConfigs = this.getUserModelConfigs();
+
+ // Check user config first (highest priority)
+ if (userConfigs[provider]?.apiKey) {
+ return 'user';
+ }
+
+ // Then check environment variables
+ const envKeys: Record = {
+ deepseek: 'DEEPSEEK_API_KEY',
+ qwen: 'QWEN_API_KEY',
+ google: 'GOOGLE_API_KEY',
+ anthropic: 'ANTHROPIC_API_KEY',
+ openrouter: 'OPENROUTER_API_KEY'
+ };
+
+ const envKey = envKeys[provider];
+ if (process.env[envKey]) {
+ return 'env';
+ }
+
+ return 'none';
+ }
+
+ /**
+ * Get selected provider (with fallback)
+ */
+ public getSelectedProvider(): ProviderType {
+ const userConfigs = this.getUserModelConfigs();
+ return userConfigs.selectedProvider || 'deepseek';
+ }
+
+ /**
+ * Set selected provider
+ */
+ public setSelectedProvider(provider: ProviderType): void {
+ const userConfigs = this.getUserModelConfigs();
+ userConfigs.selectedProvider = provider;
+ this.saveUserModelConfigs(userConfigs);
+ }
+
+ /**
+ * Get maxTokens for specific model
+ */
+ private getMaxTokensForModel(provider: ProviderType, model: string): number {
+ // Define maxTokens for different models
+ const tokenLimits: Record = {
+ // Deepseek
+ 'deepseek-chat': 8192,
+ 'deepseek-reasoner': 65536,
+
+ // Google
+ 'gemini-2.0-flash-thinking-exp-01-21': 65536,
+ 'gemini-1.5-flash-latest': 8192,
+ 'gemini-2.0-flash-exp': 8192,
+ 'gemini-1.5-flash-002': 8192,
+ 'gemini-1.5-flash-8b': 8192,
+ 'gemini-1.5-pro-latest': 8192,
+ 'gemini-1.5-pro-002': 8192,
+ 'gemini-exp-1206': 8192,
+
+ // Anthropic
+ 'claude-3-7-sonnet-20250219': 128000,
+ 'claude-3-5-sonnet-latest': 8000,
+ 'claude-3-5-sonnet-20240620': 8000,
+ 'claude-3-5-haiku-latest': 8000,
+ 'claude-3-opus-latest': 8000,
+ 'claude-3-sonnet-20240229': 8000,
+ 'claude-3-haiku-20240307': 8000,
+
+ // Qwen
+ 'qwen-max': 8192,
+ 'qwen-plus': 8192,
+ 'qwen-vl-max': 8192,
+ };
+
+ // Return specific token limit or default based on provider
+ return tokenLimits[model] || (provider === 'openrouter' ? 8000 : 8192);
+ }
+
+ /**
+ * Get LLMs configuration for Eko framework
+ * Returns configured LLMs object based on selected provider
+ */
+ public getLLMsConfig(): any {
+ const selectedProvider = this.getSelectedProvider();
+ const providerConfig = this.getModelConfig(selectedProvider);
+
+ if (!providerConfig) {
+ console.error(`[ConfigManager] No config found for provider: ${selectedProvider}`);
+ return { default: null };
+ }
+
+ const logInfo = (msg: string, ...args: any[]) => console.log(`[ConfigManager] ${msg}`, ...args);
+ const maxTokens = this.getMaxTokensForModel(selectedProvider, providerConfig.model);
+
+ // Build default LLM based on selected provider
+ let defaultLLM: any;
+
+ switch (selectedProvider) {
+ case 'deepseek':
+ defaultLLM = {
+ provider: providerConfig.provider,
+ model: providerConfig.model,
+ apiKey: providerConfig.apiKey || "",
+ config: {
+ baseURL: providerConfig.baseURL || "https://api.deepseek.com/v1",
+ maxTokens,
+ mode: 'regular',
+ },
+ fetch: (url: string, options?: any) => {
+ // Intercept request and add thinking parameter for deepseek
+ const body = JSON.parse((options?.body as string) || '{}');
+ body.thinking = { type: "disabled" };
+ logInfo('Deepseek request:', providerConfig.model);
+ return fetch(url, {
+ ...options,
+ body: JSON.stringify(body)
+ });
+ }
+ };
+ break;
+
+ case 'qwen':
+ defaultLLM = {
+ provider: providerConfig.provider,
+ model: providerConfig.model,
+ apiKey: providerConfig.apiKey || "",
+ config: {
+ baseURL: providerConfig.baseURL || "https://dashscope.aliyuncs.com/compatible-mode/v1",
+ maxTokens,
+ timeout: 60000,
+ temperature: 0.7
+ },
+ fetch: (url: string, options?: any) => {
+ logInfo('Qwen request:', providerConfig.model);
+ return fetch(url, options);
+ }
+ };
+ break;
+
+ case 'google':
+ defaultLLM = {
+ provider: providerConfig.provider,
+ model: providerConfig.model,
+ apiKey: providerConfig.apiKey || "",
+ config: {
+ maxTokens,
+ temperature: 0.7
+ }
+ };
+ break;
+
+ case 'anthropic':
+ defaultLLM = {
+ provider: providerConfig.provider,
+ model: providerConfig.model,
+ apiKey: providerConfig.apiKey || "",
+ config: {
+ maxTokens,
+ temperature: 0.7
+ }
+ };
+ break;
+
+ case 'openrouter':
+ defaultLLM = {
+ provider: providerConfig.provider,
+ model: providerConfig.model,
+ apiKey: providerConfig.apiKey || "",
+ config: {
+ maxTokens
+ }
+ };
+ break;
+
+ default:
+ console.error(`[ConfigManager] Unsupported provider: ${selectedProvider}`);
+ return { default: null };
+ }
+
+ logInfo(`Using provider: ${selectedProvider}, model: ${providerConfig.model}, maxTokens: ${maxTokens}`);
+
+ // Return LLMs configuration
+ return {
+ default: defaultLLM,
+ };
+ }
}
\ No newline at end of file
diff --git a/electron/preload/index.d.ts b/electron/preload/index.d.ts
deleted file mode 100644
index 98fe44a..0000000
--- a/electron/preload/index.d.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-import { ElectronAPI } from '@electron-toolkit/preload'
-
-declare global {
- interface Window {
- electron: ElectronAPI
- api: {
- getAppVersion: () => Promise
- getPlatform: () => Promise
- onNewTab: (callback: () => void) => void
- onCloseTab: (callback: () => void) => void
- onNavigateBack: (callback: () => void) => void
- onNavigateForward: (callback: () => void) => void
- onReloadPage: (callback: () => void) => void
- onShowAbout: (callback: () => void) => void
- removeAllListeners: (channel: string) => void
- navigateTo: (url: string) => Promise
- sendToMainViewExecuteCode: (func: string, args: any[]) => Promise
- getMainViewSize: () => Promise<{ width: number; height: number }>
- getMainViewUrlAndTitle: () => Promise<{ url: string; title: string }>
- getMainViewScreenshot: () => Promise<{ imageBase64: string; imageType: "image/jpeg" | "image/png" }>
- getHiddenWindowSourceId: () => Promise
-
- // Voice and TTS related APIs
- sendVoiceTextToChat: (text: string) => Promise
- onVoiceTextReceived: (callback: (text: string) => void) => void
- sendTTSSubtitle: (text: string, isStart: boolean) => Promise
- onTTSSubtitleReceived: (callback: (text: string, isStart: boolean) => void) => void
-
- // EkoService related APIs
- ekoRun: (message: string) => Promise
- ekoModify: (taskId: string, message: string) => Promise<{ success: boolean }>
- ekoExecute: (taskId: string) => Promise
- ekoGetTaskStatus: (taskId: string) => Promise
- ekoCancelTask: (taskId: string) => Promise<{ success: boolean; result: any }>
- onEkoStreamMessage: (callback: (message: any) => void) => void
-
- // File update related APIs (for view preload)
- onFileUpdated: (callback: (status: string, content: string) => void) => void
-
- // Generic invoke method
- invoke: (channel: string, ...args: any[]) => Promise
-
- // Scheduled task execution completion listener
- onTaskExecutionComplete: (callback: (event: any) => void) => void
-
- // Open history panel listener
- onOpenHistoryPanel: (callback: (event: any) => void) => void
-
- // Task aborted by system listener
- onTaskAbortedBySystem: (callback: (event: any) => void) => void
- }
- process?: {
- type: string
- platform: string
- versions: any
- }
- }
-}
\ No newline at end of file
diff --git a/electron/preload/index.ts b/electron/preload/index.ts
index 6468f00..2b08bf3 100644
--- a/electron/preload/index.ts
+++ b/electron/preload/index.ts
@@ -40,6 +40,14 @@ const api = {
ekoCancelTask: (taskId: string) => ipcRenderer.invoke('eko:cancel-task', taskId),
onEkoStreamMessage: (callback: (message: any) => void) => ipcRenderer.on('eko-stream-message', (_, message) => callback(message)),
+ // Model configuration APIs
+ getUserModelConfigs: () => ipcRenderer.invoke('config:get-user-configs'),
+ saveUserModelConfigs: (configs: any) => ipcRenderer.invoke('config:save-user-configs', configs),
+ getModelConfig: (provider: 'deepseek' | 'qwen' | 'google' | 'anthropic' | 'openrouter') => ipcRenderer.invoke('config:get-model-config', provider),
+ getApiKeySource: (provider: 'deepseek' | 'qwen' | 'google' | 'anthropic' | 'openrouter') => ipcRenderer.invoke('config:get-api-key-source', provider),
+ getSelectedProvider: () => ipcRenderer.invoke('config:get-selected-provider'),
+ setSelectedProvider: (provider: 'deepseek' | 'qwen' | 'google' | 'anthropic' | 'openrouter') => ipcRenderer.invoke('config:set-selected-provider', provider),
+
// Detail view control APIs
setDetailViewVisible: (visible: boolean) => ipcRenderer.invoke('set-detail-view-visible', visible),
// URL retrieval and monitoring APIs
diff --git a/src/components/ModelConfigBar.tsx b/src/components/ModelConfigBar.tsx
new file mode 100644
index 0000000..9a08b74
--- /dev/null
+++ b/src/components/ModelConfigBar.tsx
@@ -0,0 +1,275 @@
+import React, { useState, useEffect } from 'react';
+import { Select, Button, Input, App } from 'antd';
+import { EditOutlined, CheckOutlined, CloseOutlined, LinkOutlined } from '@ant-design/icons';
+import type { UserModelConfigs } from '@/type';
+
+const { Option } = Select;
+
+// Provider options
+const PROVIDERS = [
+ { value: 'deepseek', label: 'Deepseek', getKeyUrl: 'https://platform.deepseek.com/api_keys' },
+ { value: 'qwen', label: 'Qwen (Alibaba)', getKeyUrl: 'https://bailian.console.aliyun.com/' },
+ { value: 'google', label: 'Google Gemini', getKeyUrl: 'https://aistudio.google.com/app/apikey' },
+ { value: 'anthropic', label: 'Anthropic', getKeyUrl: 'https://console.anthropic.com/settings/keys' },
+ { value: 'openrouter', label: 'OpenRouter', getKeyUrl: 'https://openrouter.ai/keys' },
+];
+
+// Model options for each provider
+const MODELS: Record = {
+ deepseek: [
+ 'deepseek-chat',
+ 'deepseek-reasoner',
+ ],
+ google: [
+ 'gemini-1.5-flash-latest',
+ 'gemini-2.0-flash-thinking-exp-01-21',
+ 'gemini-2.0-flash-exp',
+ 'gemini-1.5-flash-002',
+ 'gemini-1.5-flash-8b',
+ 'gemini-1.5-pro-latest',
+ 'gemini-1.5-pro-002',
+ 'gemini-exp-1206',
+ ],
+ openrouter: [
+ 'anthropic/claude-3.5-sonnet',
+ 'anthropic/claude-3-haiku',
+ 'deepseek/deepseek-coder',
+ 'google/gemini-flash-1.5',
+ 'google/gemini-pro-1.5',
+ 'x-ai/grok-beta',
+ 'mistralai/mistral-nemo',
+ 'qwen/qwen-110b-chat',
+ 'cohere/command',
+ ],
+ anthropic: [
+ 'claude-3-7-sonnet-20250219',
+ 'claude-3-5-sonnet-latest',
+ 'claude-3-5-sonnet-20240620',
+ 'claude-3-5-haiku-latest',
+ 'claude-3-opus-latest',
+ 'claude-3-sonnet-20240229',
+ 'claude-3-haiku-20240307',
+ ],
+ qwen: [
+ 'qwen-max',
+ 'qwen-plus',
+ 'qwen-vl-max',
+ ],
+};
+
+type ProviderType = 'deepseek' | 'qwen' | 'google' | 'anthropic' | 'openrouter';
+
+export const ModelConfigBar: React.FC = () => {
+
+ const message = App.useApp().message;
+
+ const [selectedProvider, setSelectedProvider] = useState('deepseek');
+ const [selectedModel, setSelectedModel] = useState('deepseek-chat');
+ const [apiKeySource, setApiKeySource] = useState<'user' | 'env' | 'none'>('none');
+ const [configs, setConfigs] = useState({});
+ const [isEditingApiKey, setIsEditingApiKey] = useState(false);
+ const [tempApiKey, setTempApiKey] = useState('');
+
+ // Load initial configurations
+ useEffect(() => {
+ loadConfigs();
+ }, []);
+
+ // Update model when provider changes
+ useEffect(() => {
+ const models = MODELS[selectedProvider];
+ if (models && models.length > 0) {
+ const currentModel = configs[selectedProvider]?.model || models[0];
+ setSelectedModel(currentModel);
+ }
+ }, [selectedProvider, configs]);
+
+ const loadConfigs = async () => {
+ try {
+ const userConfigs = await window.api.getUserModelConfigs();
+ const provider = await window.api.getSelectedProvider();
+ const source = await window.api.getApiKeySource(provider);
+
+ setConfigs(userConfigs);
+ setSelectedProvider(provider);
+ setApiKeySource(source);
+ } catch (error) {
+ console.error('Failed to load model configs:', error);
+ }
+ };
+
+ const handleProviderChange = async (value: ProviderType) => {
+ try {
+ setSelectedProvider(value);
+ await window.api.setSelectedProvider(value);
+ const source = await window.api.getApiKeySource(value);
+ setApiKeySource(source);
+ } catch (error) {
+ console.error('Failed to change provider:', error);
+ message.error('Failed to change provider');
+ }
+ };
+
+ const handleModelChange = async (value: string) => {
+ try {
+ setSelectedModel(value);
+ const updatedConfigs = {
+ ...configs,
+ [selectedProvider]: {
+ ...configs[selectedProvider],
+ model: value,
+ },
+ };
+ await window.api.saveUserModelConfigs(updatedConfigs);
+ setConfigs(updatedConfigs);
+ message.success('Model updated');
+ } catch (error) {
+ console.error('Failed to update model:', error);
+ message.error('Failed to update model');
+ }
+ };
+
+ const handleEditApiKey = () => {
+ setIsEditingApiKey(true);
+ setTempApiKey(configs[selectedProvider]?.apiKey || '');
+ };
+
+ const handleCancelEdit = () => {
+ setIsEditingApiKey(false);
+ setTempApiKey('');
+ };
+
+ const handleSaveApiKey = async () => {
+ // Validate API Key is not empty
+ if (!tempApiKey || tempApiKey.trim() === '') {
+ message.warning('API Key cannot be empty');
+ return;
+ }
+
+ try {
+ const updatedConfigs = {
+ ...configs,
+ [selectedProvider]: {
+ ...configs[selectedProvider],
+ apiKey: tempApiKey.trim(),
+ },
+ };
+ await window.api.saveUserModelConfigs(updatedConfigs);
+ setConfigs(updatedConfigs);
+ setIsEditingApiKey(false);
+ setApiKeySource('user');
+ message.success('API Key saved');
+ } catch (error) {
+ console.error('Failed to save API key:', error);
+ message.error('Failed to save API Key');
+ }
+ };
+
+ const currentProvider = PROVIDERS.find(p => p.value === selectedProvider);
+
+ return (
+
+ {/* Provider and Model Selection */}
+
+
+
+
+
+
+ {/* API Key Section */}
+
+
+
+ {selectedProvider.charAt(0).toUpperCase() + selectedProvider.slice(1)} API Key:
+
+
+ {apiKeySource === 'env' && !isEditingApiKey && (
+
+
+ Set via environment variable
+
+ )}
+
+ {apiKeySource === 'user' && !isEditingApiKey && (
+
+
+ Set by user
+
+ )}
+
+ {apiKeySource === 'none' && !isEditingApiKey && (
+ Not configured
+ )}
+
+ {isEditingApiKey && (
+ setTempApiKey(e.target.value)}
+ placeholder="Enter your API Key"
+ className="flex-1 max-w-sm"
+ size="small"
+ onPressEnter={handleSaveApiKey}
+ />
+ )}
+
+
+
+ {!isEditingApiKey ? (
+ <>
+
}
+ onClick={handleEditApiKey}
+ size="small"
+ type="text"
+ className="text-gray-300 hover:text-white"
+ >
+ Edit API Key
+
+
+
+ Get API Key
+
+ >
+ ) : (
+ <>
+
+
+ >
+ )}
+
+
+
+ );
+};
diff --git a/src/components/chat/MessageComponents.tsx b/src/components/chat/MessageComponents.tsx
index 2a05411..12d55ad 100644
--- a/src/components/chat/MessageComponents.tsx
+++ b/src/components/chat/MessageComponents.tsx
@@ -245,6 +245,10 @@ const AgentGroupDisplay = ({
{agentMessage.status === 'completed' ? (
+ ) : agentMessage.status === 'error' ? (
+
+ ✕
+
) : (
)}
@@ -262,7 +266,7 @@ const AgentGroupDisplay = ({
{/* Agent execution steps */}
{!isCollapsed && (
- {agentMessage.messages.map((message, index) => {
+ {agentMessage.messages.map((message) => {
return (
diff --git a/src/hooks/useTaskManager.ts b/src/hooks/useTaskManager.ts
index 3d75c0b..5bb6cec 100644
--- a/src/hooks/useTaskManager.ts
+++ b/src/hooks/useTaskManager.ts
@@ -15,6 +15,7 @@ interface UseTaskManagerReturn {
createTask: (taskId: string, initialData: Partial
) => void;
updateMessages: (taskId: string, messages: DisplayMessage[]) => void;
addToolHistory: (taskId: string, toolData: any) => void;
+ replaceTaskId: (oldTaskId: string, newTaskId: string) => void;
// History mode
enterHistoryMode: (task: Task) => void;
@@ -120,6 +121,43 @@ export const useTaskManager = (): UseTaskManagerReturn => {
});
}, [saveTask]);
+ // Replace task ID (for temporary task -> real task transition)
+ const replaceTaskId = useCallback((oldTaskId: string, newTaskId: string) => {
+ if (isHistoryMode) return;
+
+ setTasks(prevTasks => {
+ const existingTaskIndex = prevTasks.findIndex(task => task.id === oldTaskId);
+ if (existingTaskIndex >= 0) {
+ const updatedTasks = [...prevTasks];
+ // Create new task object with new ID, keep all other data
+ const newTask = {
+ ...updatedTasks[existingTaskIndex],
+ id: newTaskId,
+ updatedAt: new Date()
+ };
+
+ // Replace old task with new task
+ updatedTasks[existingTaskIndex] = newTask;
+
+ // Save new task to IndexedDB
+ saveTask(newTask);
+
+ // Delete old temporary task from IndexedDB
+ taskStorage.deleteTask(oldTaskId).catch(error => {
+ console.error('Failed to delete temporary task:', error);
+ });
+
+ return updatedTasks;
+ }
+ return prevTasks;
+ });
+
+ // Update currentTaskId if it matches the old ID
+ if (currentTaskId === oldTaskId) {
+ setCurrentTaskId(newTaskId);
+ }
+ }, [isHistoryMode, saveTask, currentTaskId]);
+
// Enter history mode
const enterHistoryMode = useCallback((task: Task) => {
setIsHistoryMode(true);
@@ -153,6 +191,7 @@ export const useTaskManager = (): UseTaskManagerReturn => {
createTask,
updateMessages,
addToolHistory,
+ replaceTaskId,
enterHistoryMode,
exitHistoryMode,
diff --git a/src/pages/home.tsx b/src/pages/home.tsx
index 9299b59..281088e 100644
--- a/src/pages/home.tsx
+++ b/src/pages/home.tsx
@@ -5,6 +5,7 @@ import { Input, Button } from 'antd'
import {ImageType, ReportType, SlideType, WebType, SpreadsheetType, VisualizeType, MoreType} from '@/icons/source-type-icons'
import { ScheduledTaskModal, ScheduledTaskListPanel } from '@/components/scheduled-task'
import { useScheduledTaskStore } from '@/stores/scheduled-task-store'
+import { ModelConfigBar } from '@/components/ModelConfigBar'
export default function Home() {
const [query, setQuery] = useState('')
@@ -79,16 +80,28 @@ export default function Home() {
I am Jarvis, a robot powered by llm. What can I do for you?
- {/* Query input box */}
-
-
setQuery(e.target.value)}
- onKeyDown={handleKeyDown}
- className='!h-full !bg-tool-call !text-text-01-dark !placeholder-text-12-dark !py-3 !px-4'
- placeholder='Please enter your task'
- autoSize={false}
- />
+ {/* Unified Input Area: Model Config + Query Input */}
+
+
+ {/* Model Configuration Bar */}
+
+
+ {/* Query input box */}
+
+ setQuery(e.target.value)}
+ onKeyDown={handleKeyDown}
+ className='!h-full !bg-transparent !text-text-01-dark !placeholder-text-12-dark !py-3 !px-4 !border !border-solid'
+ placeholder='Please enter your task'
+ autoSize={false}
+ style={{
+ borderColor: 'rgba(255, 255, 255, 0.2)',
+ borderWidth: '1px',
+ }}
+ />
+
+
diff --git a/src/pages/main.tsx b/src/pages/main.tsx
index 4e0f54d..fc5704c 100644
--- a/src/pages/main.tsx
+++ b/src/pages/main.tsx
@@ -34,6 +34,7 @@ export default function main() {
createTask,
updateMessages,
addToolHistory,
+ replaceTaskId,
enterHistoryMode,
} = useTaskManager();
@@ -349,47 +350,60 @@ export default function main() {
const updatedMessages = messageProcessorRef.current.processStreamMessage(message);
console.log('Updated message list:', updatedMessages);
- // Set task ID (if not already set)
- if (message.taskId && !currentTaskId) {
+ // Handle task ID replacement: temporary task -> real task
+ const isCurrentTaskTemporary = taskIdRef.current?.startsWith('temp-');
+ const hasRealTaskId = message.taskId && !message.taskId.startsWith('temp-');
+
+ if (isCurrentTaskTemporary && hasRealTaskId) {
+ const tempTaskId = taskIdRef.current;
+ const realTaskId = message.taskId;
+
+ console.log(`Replacing temporary task ${tempTaskId} with real task ${realTaskId}`);
+
+ // Replace task ID
+ replaceTaskId(tempTaskId, realTaskId);
+
+ // Update taskIdRef
+ taskIdRef.current = realTaskId;
+
+ // Update task with new workflow info if available
+ if (message.type === 'workflow' && message.workflow?.name) {
+ updateTask(realTaskId, {
+ name: message.workflow.name,
+ workflow: message.workflow,
+ messages: updatedMessages
+ });
+ } else {
+ updateTask(realTaskId, { messages: updatedMessages });
+ }
+
+ return; // Exit early, task ID has been replaced
+ }
+
+ // Set task ID (if not already set and not temporary)
+ if (message.taskId && !currentTaskId && !message.taskId.startsWith('temp-')) {
setCurrentTaskId(message.taskId);
}
// Update or create task
const taskIdToUpdate = message.taskId || taskIdRef.current;
if (taskIdToUpdate) {
- const existingTask = tasks.find(task => task.id === taskIdToUpdate);
-
- if (existingTask) {
- // Update existing task
- const updates: Partial
= {
- messages: updatedMessages
- };
+ const updates: Partial = {
+ messages: updatedMessages
+ };
- if (message.type === 'workflow' && message.workflow?.name) {
- updates.name = message.workflow.name;
- updates.workflow = message.workflow;
- }
+ if (message.type === 'workflow' && message.workflow?.name) {
+ updates.name = message.workflow.name;
+ updates.workflow = message.workflow;
+ }
- updateTask(taskIdToUpdate, updates);
- } else {
- // Create new task
- const initialData: Partial = {
- name: (message.type === 'workflow' && message.workflow?.name)
- ? message.workflow.name
- : `Task ${taskIdToUpdate.slice(0, 8)}`,
- workflow: (message.type === 'workflow' && message.workflow)
- ? message.workflow
- : undefined,
- messages: updatedMessages,
- status: 'running', // Set to running state on initialization
- // Set taskType based on whether it's scheduled task mode
- taskType: isTaskDetailMode ? 'scheduled' : 'normal',
- scheduledTaskId: isTaskDetailMode ? scheduledTaskIdFromUrl : undefined,
- startTime: new Date(), // Record start time
- };
-
- createTask(taskIdToUpdate, initialData);
+ // For error messages, also update task status
+ if (message.type === 'error') {
+ updates.status = 'error';
}
+
+ // Always update task (will only work if task exists)
+ updateTask(taskIdToUpdate, updates);
}
// Detect tool call messages, automatically show detail panel
@@ -604,8 +618,23 @@ export default function main() {
// Add user message to message processor
const updatedMessages = messageProcessorRef.current.addUserMessage(message.trim());
- // Immediately update current task's messages (if task exists)
- if (taskIdRef.current) {
+ // If no current task, create temporary task immediately to display user message
+ if (!taskIdRef.current) {
+ const tempTaskId = `temp-${newExecutionId}`;
+ taskIdRef.current = tempTaskId;
+ setCurrentTaskId(tempTaskId);
+
+ // Create temporary task with user message
+ createTask(tempTaskId, {
+ name: 'Processing...',
+ messages: updatedMessages,
+ status: 'running',
+ taskType: isTaskDetailMode ? 'scheduled' : 'normal',
+ scheduledTaskId: isTaskDetailMode ? scheduledTaskIdFromUrl : undefined,
+ startTime: new Date(),
+ });
+ } else {
+ // Update existing task's messages
updateMessages(taskIdRef.current, updatedMessages);
// Set existing task to running state
updateTask(taskIdRef.current, { status: 'running' });
@@ -623,19 +652,24 @@ export default function main() {
}
try {
- if (!taskIdRef.current) {
- // Use IPC to call main thread's EkoService
+ // Check if current task is temporary
+ const isTemporaryTask = taskIdRef.current.startsWith('temp-');
+
+ if (isTemporaryTask) {
+ // Use IPC to call main thread's EkoService to run new task
const req = window.api.ekoRun(message.trim());
setEkoRequest(req);
result = await req;
- result && setCurrentTaskId(result.taskId);
+ // Note: real taskId will be set via stream callback's replaceTaskId
} else {
+ // Modify existing task
const req = window.api.ekoModify(taskIdRef.current, message.trim());
setEkoRequest(req);
result = await req;
}
+
// Update task status based on result (directly using eko-core status)
- if (result) {
+ if (result && taskIdRef.current) {
updateTask(taskIdRef.current, {
status: result.stopReason
});
diff --git a/src/type.d.ts b/src/type.d.ts
index b6f6e7c..a6f25f8 100644
--- a/src/type.d.ts
+++ b/src/type.d.ts
@@ -1,3 +1,32 @@
+// Supported providers
+export type ProviderType = 'deepseek' | 'qwen' | 'google' | 'anthropic' | 'openrouter';
+
+// Model configuration types
+export interface UserModelConfigs {
+ deepseek?: {
+ apiKey?: string
+ baseURL?: string
+ model?: string
+ }
+ qwen?: {
+ apiKey?: string
+ model?: string
+ }
+ google?: {
+ apiKey?: string
+ model?: string
+ }
+ anthropic?: {
+ apiKey?: string
+ model?: string
+ }
+ openrouter?: {
+ apiKey?: string
+ model?: string
+ }
+ selectedProvider?: ProviderType
+}
+
declare global {
interface Window {
api: {
@@ -24,6 +53,14 @@ declare global {
onEkoStreamMessage: (callback: (message: any) => void) => void
ekoGetTaskStatus: (taskId: string) => Promise
ekoCancelTask: (taskId: string) => Promise
+
+ // Model configuration APIs
+ getUserModelConfigs: () => Promise
+ saveUserModelConfigs: (configs: UserModelConfigs) => Promise<{ success: boolean }>
+ getModelConfig: (provider: ProviderType) => Promise
+ getApiKeySource: (provider: ProviderType) => Promise<'user' | 'env' | 'none'>
+ getSelectedProvider: () => Promise
+ setSelectedProvider: (provider: ProviderType) => Promise<{ success: boolean }>
}
// PDF.js type declarations
pdfjsLib?: {
diff --git a/src/utils/messageTransform.ts b/src/utils/messageTransform.ts
index bf09d41..b5a1676 100644
--- a/src/utils/messageTransform.ts
+++ b/src/utils/messageTransform.ts
@@ -17,7 +17,7 @@ export class MessageProcessor {
// Process streaming messages and convert to structured display messages
public processStreamMessage(message: StreamCallbackMessage): DisplayMessage[] {
console.log('MessageProcessor processing message:', message.type, message);
-
+
switch (message.type) {
case 'workflow':
this.handleWorkflowMessage(message);
@@ -41,6 +41,9 @@ export class MessageProcessor {
case 'agent_result':
this.handleAgentResultMessage(message);
break;
+ case 'error':
+ this.handleErrorMessage(message);
+ break;
}
console.log('MessageProcessor current message count:', this.messages.length);
@@ -249,6 +252,30 @@ export class MessageProcessor {
return [...this.messages];
}
+ // Handle error message
+ private handleErrorMessage(message: any) {
+ console.error('Error message received:', message);
+
+ // Create error message as AgentGroupMessage with error status
+ const errorMsg: AgentGroupMessage = {
+ id: uuidv4(),
+ type: 'agent_group',
+ taskId: message.taskId || 'unknown',
+ agentName: 'System',
+ messages: [
+ {
+ type: 'text',
+ id: uuidv4(),
+ content: `❌ Error: ${message.error || 'Unknown error occurred'}\n\n${message.detail || ''}`
+ }
+ ],
+ status: 'error',
+ timestamp: new Date()
+ };
+
+ this.messages.push(errorMsg);
+ }
+
// Get current message list
public getMessages(): DisplayMessage[] {
return [...this.messages];