Compare commits
34 Commits
copilot/fi
...
fix/thinki
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49320936f8 | ||
|
|
e268e69597 | ||
|
|
10e78ac60e | ||
|
|
44b2b859da | ||
|
|
bfef0c5580 | ||
|
|
1e8055031a | ||
|
|
8e33ff8d90 | ||
|
|
a619000340 | ||
|
|
78278ce96d | ||
|
|
76483d828e | ||
|
|
816a92c609 | ||
|
|
83e4d4363f | ||
|
|
1103449a4f | ||
|
|
56c7a7f066 | ||
|
|
caa59c4c50 | ||
|
|
2546dfbe5d | ||
|
|
5fea202a7d | ||
|
|
7dce1d776b | ||
|
|
346af4d338 | ||
|
|
abd5d3b96f | ||
|
|
49bd298d37 | ||
|
|
714a28ac29 | ||
|
|
0cf81c04c8 | ||
|
|
4186e9c990 | ||
|
|
d8f68a6056 | ||
|
|
11bf50e722 | ||
|
|
32a84311aa | ||
|
|
6eaa2b2461 | ||
|
|
9f00f00546 | ||
|
|
bd94d23343 | ||
|
|
5f1c14e2c0 | ||
|
|
cdc12d5092 | ||
|
|
e5967fd874 | ||
|
|
e2f1d80697 |
89
.github/workflows/auto-i18n.yml
vendored
89
.github/workflows/auto-i18n.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Auto I18N
|
||||
name: Auto I18N Weekly
|
||||
|
||||
env:
|
||||
TRANSLATION_API_KEY: ${{ secrets.TRANSLATE_API_KEY }}
|
||||
@@ -7,14 +7,15 @@ env:
|
||||
TRANSLATION_BASE_LOCALE: ${{ vars.AUTO_I18N_BASE_LOCALE || 'en-us'}}
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
schedule:
|
||||
# Runs at 00:00 UTC every Sunday.
|
||||
# This corresponds to 08:00 AM UTC+8 (Beijing time) every Sunday.
|
||||
- cron: "0 0 * * 0"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
auto-i18n:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == 'CherryHQ/cherry-studio'
|
||||
name: Auto I18N
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -24,45 +25,69 @@ jobs:
|
||||
- name: 🐈⬛ Checkout
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 📦 Setting Node.js
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 20
|
||||
package-manager-cache: false
|
||||
node-version: 22
|
||||
|
||||
- name: 📦 Install dependencies in isolated directory
|
||||
- name: 📦 Install corepack
|
||||
run: corepack enable && corepack prepare yarn@4.9.1 --activate
|
||||
|
||||
- name: 📂 Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: 💾 Cache yarn dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
node_modules
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
|
||||
- name: 📦 Install dependencies
|
||||
run: |
|
||||
# 在临时目录安装依赖
|
||||
mkdir -p /tmp/translation-deps
|
||||
cd /tmp/translation-deps
|
||||
echo '{"dependencies": {"@cherrystudio/openai": "^6.5.0", "cli-progress": "^3.12.0", "tsx": "^4.20.3", "@biomejs/biome": "2.2.4"}}' > package.json
|
||||
npm install --no-package-lock
|
||||
|
||||
# 设置 NODE_PATH 让项目能找到这些依赖
|
||||
echo "NODE_PATH=/tmp/translation-deps/node_modules" >> $GITHUB_ENV
|
||||
yarn install
|
||||
|
||||
- name: 🏃♀️ Translate
|
||||
run: npx tsx scripts/sync-i18n.ts && npx tsx scripts/auto-translate-i18n.ts
|
||||
run: yarn sync:i18n && yarn auto:i18n
|
||||
|
||||
- name: 🔍 Format
|
||||
run: cd /tmp/translation-deps && npx biome format --config-path /home/runner/work/cherry-studio/cherry-studio/biome.jsonc --write /home/runner/work/cherry-studio/cherry-studio/src/renderer/src/i18n/
|
||||
run: yarn format
|
||||
|
||||
- name: 🔄 Commit changes
|
||||
- name: 🔍 Check for changes
|
||||
id: git_status
|
||||
run: |
|
||||
git config --local user.email "action@github.com"
|
||||
git config --local user.name "GitHub Action"
|
||||
git add .
|
||||
# Check if there are any uncommitted changes
|
||||
git reset -- package.json yarn.lock # 不提交 package.json 和 yarn.lock 的更改
|
||||
if git diff --cached --quiet; then
|
||||
echo "No changes to commit"
|
||||
else
|
||||
git commit -m "fix(i18n): Auto update translations for PR #${{ github.event.pull_request.number }}"
|
||||
fi
|
||||
git diff --exit-code --quiet || echo "::set-output name=has_changes::true"
|
||||
git status --porcelain
|
||||
|
||||
- name: 🚀 Push changes
|
||||
uses: ad-m/github-push-action@master
|
||||
- name: 📅 Set current date for PR title
|
||||
id: set_date
|
||||
run: echo "CURRENT_DATE=$(date +'%b %d, %Y')" >> $GITHUB_ENV # e.g., "Jun 06, 2024"
|
||||
|
||||
- name: 🚀 Create Pull Request if changes exist
|
||||
if: steps.git_status.outputs.has_changes == 'true'
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
branch: ${{ github.event.pull_request.head.ref }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }} # Use the built-in GITHUB_TOKEN for bot actions
|
||||
commit-message: "feat(bot): Weekly automated script run"
|
||||
title: "🤖 Weekly Automated Update: ${{ env.CURRENT_DATE }}"
|
||||
body: |
|
||||
This PR includes changes generated by the weekly auto i18n.
|
||||
Review the changes before merging.
|
||||
|
||||
---
|
||||
_Generated by the automated weekly workflow_
|
||||
branch: "auto-i18n-weekly-${{ github.run_id }}" # Unique branch name
|
||||
base: "main" # Or 'develop', set your base branch
|
||||
delete-branch: true # Delete the branch after merging or closing the PR
|
||||
|
||||
- name: 📢 Notify if no changes
|
||||
if: steps.git_status.outputs.has_changes != 'true'
|
||||
run: echo "Bot script ran, but no changes were detected. No PR created."
|
||||
|
||||
10
.github/workflows/github-issue-tracker.yml
vendored
10
.github/workflows/github-issue-tracker.yml
vendored
@@ -5,7 +5,7 @@ on:
|
||||
types: [opened]
|
||||
schedule:
|
||||
# Run every day at 8:30 Beijing Time (00:30 UTC)
|
||||
- cron: '30 0 * * *'
|
||||
- cron: "30 0 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -54,9 +54,9 @@ jobs:
|
||||
|
||||
- name: Setup Node.js
|
||||
if: steps.check_time.outputs.should_delay == 'false'
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '20'
|
||||
node-version: 22
|
||||
|
||||
- name: Process issue with Claude
|
||||
if: steps.check_time.outputs.should_delay == 'false'
|
||||
@@ -121,9 +121,9 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '20'
|
||||
node-version: 22
|
||||
|
||||
- name: Process pending issues with Claude
|
||||
uses: anthropics/claude-code-action@main
|
||||
|
||||
4
.github/workflows/issue-management.yml
vendored
4
.github/workflows/issue-management.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
contents: none
|
||||
steps:
|
||||
- name: Close needs-more-info issues
|
||||
uses: actions/stale@v9
|
||||
uses: actions/stale@v10
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
only-labels: 'needs-more-info'
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
days-before-pr-close: -1
|
||||
|
||||
- name: Close inactive issues
|
||||
uses: actions/stale@v9
|
||||
uses: actions/stale@v10
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
days-before-stale: ${{ env.daysBeforeStale }}
|
||||
|
||||
10
.github/workflows/nightly-build.yml
vendored
10
.github/workflows/nightly-build.yml
vendored
@@ -3,7 +3,7 @@ name: Nightly Build
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 17 * * *' # 1:00 BJ Time
|
||||
- cron: "0 17 * * *" # 1:00 BJ Time
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -56,9 +56,9 @@ jobs:
|
||||
ref: main
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 22
|
||||
|
||||
- name: macos-latest dependencies fix
|
||||
if: matrix.os == 'macos-latest'
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
brew install python-setuptools
|
||||
|
||||
- name: Install corepack
|
||||
run: corepack enable && corepack prepare yarn@4.6.0 --activate
|
||||
run: corepack enable && corepack prepare yarn@4.9.1 --activate
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
@@ -208,7 +208,7 @@ jobs:
|
||||
echo "总计: $(find renamed-artifacts -type f | wc -l) 个文件"
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: cherry-studio-nightly-${{ steps.date.outputs.date }}-${{ matrix.os }}
|
||||
path: renamed-artifacts/*
|
||||
|
||||
6
.github/workflows/pr-ci.yml
vendored
6
.github/workflows/pr-ci.yml
vendored
@@ -24,12 +24,12 @@ jobs:
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 22
|
||||
|
||||
- name: Install corepack
|
||||
run: corepack enable && corepack prepare yarn@4.6.0 --activate
|
||||
run: corepack enable && corepack prepare yarn@4.9.1 --activate
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
|
||||
12
.github/workflows/release.yml
vendored
12
.github/workflows/release.yml
vendored
@@ -4,9 +4,9 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Release tag (e.g. v1.0.0)'
|
||||
description: "Release tag (e.g. v1.0.0)"
|
||||
required: true
|
||||
default: 'v1.0.0'
|
||||
default: "v1.0.0"
|
||||
push:
|
||||
tags:
|
||||
- v*.*.*
|
||||
@@ -47,9 +47,9 @@ jobs:
|
||||
npm version "$VERSION" --no-git-tag-version --allow-same-version
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 22
|
||||
|
||||
- name: macos-latest dependencies fix
|
||||
if: matrix.os == 'macos-latest'
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
brew install python-setuptools
|
||||
|
||||
- name: Install corepack
|
||||
run: corepack enable && corepack prepare yarn@4.6.0 --activate
|
||||
run: corepack enable && corepack prepare yarn@4.9.1 --activate
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
@@ -127,5 +127,5 @@ jobs:
|
||||
allowUpdates: true
|
||||
makeLatest: false
|
||||
tag: ${{ steps.get-tag.outputs.tag }}
|
||||
artifacts: 'dist/*.exe,dist/*.zip,dist/*.dmg,dist/*.AppImage,dist/*.snap,dist/*.deb,dist/*.rpm,dist/*.tar.gz,dist/latest*.yml,dist/rc*.yml,dist/beta*.yml,dist/*.blockmap'
|
||||
artifacts: "dist/*.exe,dist/*.zip,dist/*.dmg,dist/*.AppImage,dist/*.snap,dist/*.deb,dist/*.rpm,dist/*.tar.gz,dist/latest*.yml,dist/rc*.yml,dist/beta*.yml,dist/*.blockmap"
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -7,7 +7,6 @@ This file provides guidance to AI coding assistants when working with code in th
|
||||
- **Keep it clear**: Write code that is easy to read, maintain, and explain.
|
||||
- **Match the house style**: Reuse existing patterns, naming, and conventions.
|
||||
- **Search smart**: Prefer `ast-grep` for semantic queries; fall back to `rg`/`grep` when needed.
|
||||
- **Build with HeroUI**: Use HeroUI for every new UI component; never add `antd` or `styled-components`.
|
||||
- **Log centrally**: Route all logging through `loggerService` with the right context—no `console.log`.
|
||||
- **Research via subagent**: Lean on `subagent` for external docs, APIs, news, and references.
|
||||
- **Always propose before executing**: Before making any changes, clearly explain your planned approach and wait for explicit user approval to ensure alignment and prevent unwanted modifications.
|
||||
@@ -41,7 +40,6 @@ This file provides guidance to AI coding assistants when working with code in th
|
||||
- **Services** (`src/main/services/`): MCPService, KnowledgeService, WindowService, etc.
|
||||
- **Build System**: Electron-Vite with experimental rolldown-vite, yarn workspaces.
|
||||
- **State Management**: Redux Toolkit (`src/renderer/src/store/`) for predictable state.
|
||||
- **UI Components**: HeroUI (`@heroui/*`) for all new UI elements.
|
||||
|
||||
### Logging
|
||||
```typescript
|
||||
|
||||
@@ -18,13 +18,13 @@ yarn
|
||||
|
||||
### Setup Node.js
|
||||
|
||||
Download and install [Node.js v20.x.x](https://nodejs.org/en/download)
|
||||
Download and install [Node.js v22.x.x](https://nodejs.org/en/download)
|
||||
|
||||
### Setup Yarn
|
||||
|
||||
```bash
|
||||
corepack enable
|
||||
corepack prepare yarn@4.6.0 --activate
|
||||
corepack prepare yarn@4.9.1 --activate
|
||||
```
|
||||
|
||||
### Install Dependencies
|
||||
|
||||
@@ -11,6 +11,8 @@ The Test Plan is divided into the RC channel and the Beta channel, with the foll
|
||||
|
||||
Users can enable the "Test Plan" and select the version channel in the software's `Settings` > `About`. Please note that the versions in the "Test Plan" cannot guarantee data consistency, so be sure to back up your data before using them.
|
||||
|
||||
After enabling the RC channel or Beta channel, if a stable version is released, users will still be upgraded to the stable version.
|
||||
|
||||
Users are welcome to submit issues or provide feedback through other channels for any bugs encountered during testing. Your feedback is very important to us.
|
||||
|
||||
## Developer Guide
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
|
||||
用户可以在软件的`设置`-`关于`中,开启“测试计划”并选择版本通道。请注意“测试计划”的版本无法保证数据的一致性,请使用前一定要备份数据。
|
||||
|
||||
用户选择RC版通道或Beta版通道后,若发布了正式版,仍旧会升级到正式版。
|
||||
|
||||
用户在测试过程中发现的BUG,欢迎提交issue或通过其他渠道反馈。用户的反馈对我们非常重要。
|
||||
|
||||
## 开发者指南
|
||||
|
||||
@@ -21,6 +21,8 @@ files:
|
||||
- "**/*"
|
||||
- "!**/{.vscode,.yarn,.yarn-lock,.github,.cursorrules,.prettierrc}"
|
||||
- "!electron.vite.config.{js,ts,mjs,cjs}}"
|
||||
- "!.*"
|
||||
- "!components.json"
|
||||
- "!**/{.eslintignore,.eslintrc.js,.eslintrc.json,.eslintcache,root.eslint.config.js,eslint.config.js,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,eslint.config.mjs,dev-app-update.yml,CHANGELOG.md,README.md,biome.jsonc}"
|
||||
- "!**/{.env,.env.*,.npmrc,pnpm-lock.yaml}"
|
||||
- "!**/{tsconfig.json,tsconfig.tsbuildinfo,tsconfig.node.json,tsconfig.web.json}"
|
||||
@@ -133,116 +135,59 @@ artifactBuildCompleted: scripts/artifact-build-completed.js
|
||||
releaseInfo:
|
||||
releaseNotes: |
|
||||
<!--LANG:en-->
|
||||
What's New in v1.7.0-beta.3
|
||||
What's New in v1.7.0-beta.4
|
||||
|
||||
Major Changes:
|
||||
- UI Framework Upgrade: Improved performance and user experience with new design system
|
||||
- App Menu i18n: Menu now supports multiple languages and syncs with app language settings
|
||||
|
||||
New Features:
|
||||
- Enhanced Tool Permission System: Real-time tool approval interface with improved UX
|
||||
- Plugin Management System: Support for Claude Agent plugins (agents, commands, skills)
|
||||
- Skill Tool: Add skill execution capabilities for agents
|
||||
- Mobile App Data Restore: Support restoring data to mobile applications
|
||||
- OpenMinerU Preprocessor: Knowledge base now supports open-source MinerU for document processing
|
||||
- HuggingFace Provider: Added HuggingFace as AI provider
|
||||
- Claude Haiku 4.5: Support for the latest Claude Haiku 4.5 model
|
||||
- Ling Series Models: Added support for Ling-1T and related models
|
||||
- Intel OVMS Painting: New painting provider using Intel OpenVINO Model Server
|
||||
- Automatic Update Checks: Implement periodic update checking with configurable intervals
|
||||
- HuggingChat Mini App: New mini app for HuggingChat integration
|
||||
- AWS Bedrock API Key: Support Bedrock API key authentication with Extended Thinking (reasoning) capability
|
||||
- SophNet Provider: Added support for SophNet LLM provider
|
||||
- Auto Session Rename: Agent sessions automatically rename based on conversation topics
|
||||
- TopP Parameter: Added TopP parameter support for more precise model control
|
||||
- Reasoning Effort Control: Quick access to reasoning effort settings in input bar
|
||||
|
||||
Improvements:
|
||||
- Agent Creation: New agents are now automatically activated upon creation
|
||||
- Lazy Loading: Optimize page load performance with route lazy loading
|
||||
- UI Enhancements: Improved agent item styling and layout consistency
|
||||
- Navigation: Better navbar layout for fullscreen mode on macOS
|
||||
- Settings Tab: Enhanced context slider consistency
|
||||
- Backup Manager: Unified footer layout for local and S3 backup managers
|
||||
- Menu System: Enhanced application menu with improved help section
|
||||
- Proxy Rules: Comprehensive proxy bypass rule matching
|
||||
- German Language: Added German language support
|
||||
- MCP Confirmation: Added confirmation modal when activating protocol-installed MCP servers
|
||||
- Translation: Enhanced translation script with concurrency and validation
|
||||
- Electron & Vite: Updated to Electron 38 and Vite 4.0.1
|
||||
|
||||
Claude Code Tool Improvements:
|
||||
- GlobTool: Now counts lines instead of files in output for better clarity
|
||||
- ReadTool: Automatically removes system reminder tags from output
|
||||
- TodoWriteTool: Improved rendering behavior
|
||||
- Environment Variables: Updated model-related environment variable names
|
||||
- Topics & Sessions: Enhanced UI with better styling and smoother interactions
|
||||
- Quick Panel: Improved option visibility and control
|
||||
- Painting Models: Smarter model initialization with better defaults
|
||||
- System Shutdown: Better handling of shutdown events to prevent data loss
|
||||
- Smaller Package Size: Optimized build configuration for faster downloads
|
||||
|
||||
Bug Fixes:
|
||||
- Fixed session model not being used when sending messages
|
||||
- Fixed tool approval UI and shared workspace plugin inconsistencies
|
||||
- Fixed API server readiness notification to renderer
|
||||
- Fixed grouped items not respecting saved tag order
|
||||
- Fixed assistant/agent activation when creating new ones
|
||||
- Fixed Dashscope Anthropic API host and migrated old configs
|
||||
- Fixed Qwen3 thinking mode control for Ollama
|
||||
- Fixed disappeared MCP button
|
||||
- Fixed create assistant causing blank screen
|
||||
- Fixed up-down button visibility in some cases
|
||||
- Fixed hooks preventing save on composing enter key
|
||||
- Fixed Azure GPT-image-1 and OpenRouter Gemini-image
|
||||
- Fixed Silicon reasoning issues
|
||||
- Fixed topic branch incomplete copy with two-pass ID mapping
|
||||
- Fixed deep research model search context restrictions
|
||||
- Fixed model capability checking logic
|
||||
- Fixed reranker API error response capture
|
||||
- Fixed right-click paste file content into inputbar
|
||||
- Fixed minimax-m2 support in aiCore
|
||||
- Fixed Perplexity provider support and API host formatting
|
||||
- Fixed CherryAI provider support and API host formatting
|
||||
- Fixed i18n translations for painting image size options
|
||||
- Fixed agent session message token usage tracking
|
||||
- Fixed prompt stream handling on completion or error
|
||||
- Fixed message API initialization issues
|
||||
|
||||
<!--LANG:zh-CN-->
|
||||
v1.7.0-beta.3 新特性
|
||||
v1.7.0-beta.4 新特性
|
||||
|
||||
重大变更:
|
||||
- UI 框架升级:采用新设计系统,提升性能和用户体验
|
||||
- 应用菜单国际化:菜单支持多语言,并自动同步应用语言设置
|
||||
|
||||
新功能:
|
||||
- 增强工具权限系统:实时工具审批界面,改进用户体验
|
||||
- 插件管理系统:支持 Claude Agent 插件(agents、commands、skills)
|
||||
- 技能工具:为 Agent 添加技能执行能力
|
||||
- 移动应用数据恢复:支持将数据恢复到移动应用程序
|
||||
- OpenMinerU 预处理器:知识库现支持使用开源 MinerU 处理文档
|
||||
- HuggingFace 提供商:添加 HuggingFace 作为 AI 提供商
|
||||
- Claude Haiku 4.5:支持最新的 Claude Haiku 4.5 模型
|
||||
- Ling 系列模型:添加 Ling-1T 及相关模型支持
|
||||
- Intel OVMS 绘图:使用 Intel OpenVINO 模型服务器的新绘图提供商
|
||||
- 自动更新检查:实现可配置间隔的定期更新检查
|
||||
- HuggingChat 小程序:新增 HuggingChat 集成小程序
|
||||
- AWS Bedrock API 密钥:支持 Bedrock API 密钥身份验证,并支持扩展思考(推理)能力
|
||||
- SophNet 提供商:添加 SophNet LLM 提供商支持
|
||||
- 自动会话重命名:Agent 会话根据对话主题自动重命名
|
||||
- TopP 参数:添加 TopP 参数支持,更精确控制模型输出
|
||||
|
||||
改进:
|
||||
- Agent 创建:新创建的 Agent 现在会自动激活
|
||||
- 懒加载:通过路由懒加载优化页面加载性能
|
||||
- UI 增强:改进 Agent 项目样式和布局一致性
|
||||
- 导航:改进 macOS 全屏模式下的导航栏布局
|
||||
- 设置选项卡:增强上下文滑块一致性
|
||||
- 备份管理器:统一本地和 S3 备份管理器的页脚布局
|
||||
- 菜单系统:增强应用菜单,改进帮助部分
|
||||
- 代理规则:全面的代理绕过规则匹配
|
||||
- 德语支持:添加德语语言支持
|
||||
- MCP 确认:添加激活协议安装的 MCP 服务器时的确认模态框
|
||||
- 翻译:增强翻译脚本的并发和验证功能
|
||||
- Electron & Vite:更新至 Electron 38 和 Vite 4.0.1
|
||||
|
||||
Claude Code 工具改进:
|
||||
- GlobTool:现在计算行数而不是文件数,提供更清晰的输出
|
||||
- ReadTool:自动从输出中移除系统提醒标签
|
||||
- TodoWriteTool:改进渲染行为
|
||||
- 环境变量:更新模型相关的环境变量名称
|
||||
- 主题和会话:增强 UI,改进样式和交互体验
|
||||
- 快速面板:改进选项可见性和控制
|
||||
- 绘图模型:更智能的模型初始化和更好的默认值
|
||||
- 系统关机:更好地处理关机事件,防止数据丢失
|
||||
- 更小的安装包:优化构建配置,下载更快
|
||||
|
||||
问题修复:
|
||||
- 修复发送消息时未使用会话模型
|
||||
- 修复工具审批 UI 和共享工作区插件不一致
|
||||
- 修复 API 服务器就绪通知到渲染器
|
||||
- 修复分组项目不遵守已保存标签顺序
|
||||
- 修复创建新的助手/Agent 时的激活问题
|
||||
- 修复 Dashscope Anthropic API 主机并迁移旧配置
|
||||
- 修复 Ollama 的 Qwen3 思考模式控制
|
||||
- 修复 MCP 按钮消失
|
||||
- 修复创建助手导致空白屏幕
|
||||
- 修复某些情况下上下按钮可见性
|
||||
- 修复钩子在输入法输入时阻止保存
|
||||
- 修复 Azure GPT-image-1 和 OpenRouter Gemini-image
|
||||
- 修复 Silicon 推理问题
|
||||
- 修复主题分支不完整复制,采用两阶段 ID 映射
|
||||
- 修复深度研究模型搜索上下文限制
|
||||
- 修复模型能力检查逻辑
|
||||
- 修复 reranker API 错误响应捕获
|
||||
- 修复右键粘贴文件内容到输入栏
|
||||
- 修复 aiCore 中的 minimax-m2 支持
|
||||
- 修复 Perplexity 提供商支持和 API 主机格式化
|
||||
- 修复 CherryAI 提供商支持和 API 主机格式化
|
||||
- 修复绘图图像大小选项的 i18n 翻译
|
||||
- 修复 Agent 会话消息的 token 使用量跟踪
|
||||
- 修复完成或错误时的提示流处理
|
||||
- 修复消息 API 初始化问题
|
||||
<!--LANG:END-->
|
||||
|
||||
10
package.json
10
package.json
@@ -82,6 +82,7 @@
|
||||
"@libsql/client": "0.14.0",
|
||||
"@libsql/win32-x64-msvc": "^0.4.7",
|
||||
"@napi-rs/system-ocr": "patch:@napi-rs/system-ocr@npm%3A1.0.2#~/.yarn/patches/@napi-rs-system-ocr-npm-1.0.2-59e7a78e8b.patch",
|
||||
"@paymoapp/electron-shutdown-handler": "^1.1.2",
|
||||
"@strongtz/win32-arm64-msvc": "^0.4.7",
|
||||
"express": "^5.1.0",
|
||||
"font-list": "^2.0.0",
|
||||
@@ -113,9 +114,9 @@
|
||||
"@ant-design/v5-patch-for-react-19": "^1.0.3",
|
||||
"@anthropic-ai/sdk": "^0.41.0",
|
||||
"@anthropic-ai/vertex-sdk": "patch:@anthropic-ai/vertex-sdk@npm%3A0.11.4#~/.yarn/patches/@anthropic-ai-vertex-sdk-npm-0.11.4-c19cb41edb.patch",
|
||||
"@aws-sdk/client-bedrock": "^3.840.0",
|
||||
"@aws-sdk/client-bedrock-runtime": "^3.840.0",
|
||||
"@aws-sdk/client-s3": "^3.840.0",
|
||||
"@aws-sdk/client-bedrock": "^3.910.0",
|
||||
"@aws-sdk/client-bedrock-runtime": "^3.910.0",
|
||||
"@aws-sdk/client-s3": "^3.910.0",
|
||||
"@biomejs/biome": "2.2.4",
|
||||
"@cherrystudio/ai-core": "workspace:^1.0.0-alpha.18",
|
||||
"@cherrystudio/embedjs": "^0.1.31",
|
||||
@@ -146,7 +147,6 @@
|
||||
"@eslint/js": "^9.22.0",
|
||||
"@google/genai": "patch:@google/genai@npm%3A1.0.1#~/.yarn/patches/@google-genai-npm-1.0.1-e26f0f9af7.patch",
|
||||
"@hello-pangea/dnd": "^18.0.1",
|
||||
"@heroui/react": "^2.8.3",
|
||||
"@kangfenmao/keyv-storage": "^0.1.0",
|
||||
"@langchain/community": "^1.0.0",
|
||||
"@langchain/core": "patch:@langchain/core@npm%3A1.0.2#~/.yarn/patches/@langchain-core-npm-1.0.2-183ef83fe4.patch",
|
||||
@@ -348,6 +348,7 @@
|
||||
"striptags": "^3.2.0",
|
||||
"styled-components": "^6.1.11",
|
||||
"swr": "^2.3.6",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss": "^4.1.13",
|
||||
"tar": "^7.4.3",
|
||||
"tiny-pinyin": "^1.3.2",
|
||||
@@ -373,6 +374,7 @@
|
||||
"zod": "^4.1.5"
|
||||
},
|
||||
"resolutions": {
|
||||
"@smithy/types": "4.7.1",
|
||||
"@codemirror/language": "6.11.3",
|
||||
"@codemirror/lint": "6.8.5",
|
||||
"@codemirror/view": "6.38.1",
|
||||
|
||||
@@ -470,3 +470,6 @@ export const MACOS_TERMINALS_WITH_COMMANDS: TerminalConfigWithCommand[] = [
|
||||
})
|
||||
}
|
||||
]
|
||||
|
||||
// resources/scripts should be maintained manually
|
||||
export const HOME_CHERRY_DIR = '.cherrystudio'
|
||||
|
||||
@@ -18,8 +18,10 @@ import { sortedObjectByKeys } from './sort'
|
||||
// ========== SCRIPT CONFIGURATION AREA - MODIFY SETTINGS HERE ==========
|
||||
const SCRIPT_CONFIG = {
|
||||
// 🔧 Concurrency Control Configuration
|
||||
MAX_CONCURRENT_TRANSLATIONS: 5, // Max concurrent requests (Make sure the concurrency level does not exceed your provider's limits.)
|
||||
TRANSLATION_DELAY_MS: 100, // Delay between requests to avoid rate limiting (Recommended: 100-500ms, Range: 0-5000ms)
|
||||
MAX_CONCURRENT_TRANSLATIONS: process.env.TRANSLATION_MAX_CONCURRENT_REQUESTS
|
||||
? parseInt(process.env.TRANSLATION_MAX_CONCURRENT_REQUESTS)
|
||||
: 5, // Max concurrent requests (Make sure the concurrency level does not exceed your provider's limits.)
|
||||
TRANSLATION_DELAY_MS: process.env.TRANSLATION_DELAY_MS ? parseInt(process.env.TRANSLATION_DELAY_MS) : 500, // Delay between requests to avoid rate limiting (Recommended: 100-500ms, Range: 0-5000ms)
|
||||
|
||||
// 🔑 API Configuration
|
||||
API_KEY: process.env.TRANSLATION_API_KEY || '', // API key from environment variable
|
||||
|
||||
@@ -171,7 +171,7 @@ const swaggerOptions: swaggerJSDoc.Options = {
|
||||
}
|
||||
]
|
||||
},
|
||||
apis: ['./src/main/apiServer/routes/*.ts', './src/main/apiServer/app.ts']
|
||||
apis: ['./src/main/apiServer/routes/**/*.ts', './src/main/apiServer/app.ts']
|
||||
}
|
||||
|
||||
export function setupOpenAPIDocumentation(app: Express) {
|
||||
|
||||
@@ -21,6 +21,7 @@ import { appMenuService } from './services/AppMenuService'
|
||||
import { configManager } from './services/ConfigManager'
|
||||
import mcpService from './services/MCPService'
|
||||
import { nodeTraceService } from './services/NodeTraceService'
|
||||
import powerMonitorService from './services/PowerMonitorService'
|
||||
import {
|
||||
CHERRY_STUDIO_PROTOCOL,
|
||||
handleProtocolUrl,
|
||||
@@ -30,6 +31,7 @@ import {
|
||||
import selectionService, { initSelectionService } from './services/SelectionService'
|
||||
import { registerShortcuts } from './services/ShortcutService'
|
||||
import { TrayService } from './services/TrayService'
|
||||
import { versionService } from './services/VersionService'
|
||||
import { windowService } from './services/WindowService'
|
||||
import { initWebviewHotkeys } from './services/WebviewService'
|
||||
|
||||
@@ -110,6 +112,10 @@ if (!app.requestSingleInstanceLock()) {
|
||||
// Some APIs can only be used after this event occurs.
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
// Record current version for tracking
|
||||
// A preparation for v2 data refactoring
|
||||
versionService.recordCurrentVersion()
|
||||
|
||||
initWebviewHotkeys()
|
||||
// Set app user model id for windows
|
||||
electronApp.setAppUserModelId(import.meta.env.VITE_MAIN_BUNDLE_ID || 'com.kangfenmao.CherryStudio')
|
||||
@@ -127,6 +133,7 @@ if (!app.requestSingleInstanceLock()) {
|
||||
appMenuService?.setupApplicationMenu()
|
||||
|
||||
nodeTraceService.init()
|
||||
powerMonitorService.init()
|
||||
|
||||
app.on('activate', function () {
|
||||
const mainWindow = windowService.getMainWindow()
|
||||
|
||||
@@ -50,6 +50,7 @@ import * as NutstoreService from './services/NutstoreService'
|
||||
import ObsidianVaultService from './services/ObsidianVaultService'
|
||||
import { ocrService } from './services/ocr/OcrService'
|
||||
import OvmsManager from './services/OvmsManager'
|
||||
import powerMonitorService from './services/PowerMonitorService'
|
||||
import { proxyManager } from './services/ProxyManager'
|
||||
import { pythonService } from './services/PythonService'
|
||||
import { FileServiceManager } from './services/remotefile/FileServiceManager'
|
||||
@@ -115,8 +116,17 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
const appUpdater = new AppUpdater()
|
||||
const notificationService = new NotificationService()
|
||||
|
||||
// Initialize Python service with main window
|
||||
pythonService.setMainWindow(mainWindow)
|
||||
// Register shutdown handlers
|
||||
powerMonitorService.registerShutdownHandler(() => {
|
||||
appUpdater.setAutoUpdate(false)
|
||||
})
|
||||
|
||||
powerMonitorService.registerShutdownHandler(() => {
|
||||
const mw = windowService.getMainWindow()
|
||||
if (mw && !mw.isDestroyed()) {
|
||||
mw.webContents.send(IpcChannel.App_SaveData)
|
||||
}
|
||||
})
|
||||
|
||||
const checkMainWindow = () => {
|
||||
if (!mainWindow || mainWindow.isDestroyed()) {
|
||||
|
||||
@@ -7,16 +7,33 @@ import { app, Menu, shell } from 'electron'
|
||||
|
||||
import { configManager } from './ConfigManager'
|
||||
export class AppMenuService {
|
||||
private languageChangeCallback?: (newLanguage: string) => void
|
||||
|
||||
constructor() {
|
||||
// Subscribe to language change events
|
||||
this.languageChangeCallback = () => {
|
||||
this.setupApplicationMenu()
|
||||
}
|
||||
configManager.subscribe('language', this.languageChangeCallback)
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
// Clean up subscription to prevent memory leaks
|
||||
if (this.languageChangeCallback) {
|
||||
configManager.unsubscribe('language', this.languageChangeCallback)
|
||||
}
|
||||
}
|
||||
|
||||
public setupApplicationMenu(): void {
|
||||
const locale = locales[configManager.getLanguage()]
|
||||
const { common } = locale.translation
|
||||
const { appMenu } = locale.translation
|
||||
|
||||
const template: MenuItemConstructorOptions[] = [
|
||||
{
|
||||
label: app.name,
|
||||
submenu: [
|
||||
{
|
||||
label: common.about + ' ' + app.name,
|
||||
label: appMenu.about + ' ' + app.name,
|
||||
click: () => {
|
||||
// Emit event to navigate to About page
|
||||
const mainWindow = windowService.getMainWindow()
|
||||
@@ -27,50 +44,78 @@ export class AppMenuService {
|
||||
}
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{ role: 'services' },
|
||||
{ role: 'services', label: appMenu.services },
|
||||
{ type: 'separator' },
|
||||
{ role: 'hide' },
|
||||
{ role: 'hideOthers' },
|
||||
{ role: 'unhide' },
|
||||
{ role: 'hide', label: `${appMenu.hide} ${app.name}` },
|
||||
{ role: 'hideOthers', label: appMenu.hideOthers },
|
||||
{ role: 'unhide', label: appMenu.unhide },
|
||||
{ type: 'separator' },
|
||||
{ role: 'quit' }
|
||||
{ role: 'quit', label: `${appMenu.quit} ${app.name}` }
|
||||
]
|
||||
},
|
||||
{
|
||||
role: 'fileMenu'
|
||||
label: appMenu.file,
|
||||
submenu: [{ role: 'close', label: appMenu.close }]
|
||||
},
|
||||
{
|
||||
role: 'editMenu'
|
||||
label: appMenu.edit,
|
||||
submenu: [
|
||||
{ role: 'undo', label: appMenu.undo },
|
||||
{ role: 'redo', label: appMenu.redo },
|
||||
{ type: 'separator' },
|
||||
{ role: 'cut', label: appMenu.cut },
|
||||
{ role: 'copy', label: appMenu.copy },
|
||||
{ role: 'paste', label: appMenu.paste },
|
||||
{ role: 'delete', label: appMenu.delete },
|
||||
{ role: 'selectAll', label: appMenu.selectAll }
|
||||
]
|
||||
},
|
||||
{
|
||||
role: 'viewMenu'
|
||||
label: appMenu.view,
|
||||
submenu: [
|
||||
{ role: 'reload', label: appMenu.reload },
|
||||
{ role: 'forceReload', label: appMenu.forceReload },
|
||||
{ role: 'toggleDevTools', label: appMenu.toggleDevTools },
|
||||
{ type: 'separator' },
|
||||
{ role: 'resetZoom', label: appMenu.resetZoom },
|
||||
{ role: 'zoomIn', label: appMenu.zoomIn },
|
||||
{ role: 'zoomOut', label: appMenu.zoomOut },
|
||||
{ type: 'separator' },
|
||||
{ role: 'togglefullscreen', label: appMenu.toggleFullscreen }
|
||||
]
|
||||
},
|
||||
{
|
||||
role: 'windowMenu'
|
||||
label: appMenu.window,
|
||||
submenu: [
|
||||
{ role: 'minimize', label: appMenu.minimize },
|
||||
{ role: 'zoom', label: appMenu.zoom },
|
||||
{ type: 'separator' },
|
||||
{ role: 'front', label: appMenu.front }
|
||||
]
|
||||
},
|
||||
{
|
||||
role: 'help',
|
||||
label: appMenu.help,
|
||||
submenu: [
|
||||
{
|
||||
label: 'Website',
|
||||
label: appMenu.website,
|
||||
click: () => {
|
||||
shell.openExternal('https://cherry-ai.com')
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Documentation',
|
||||
label: appMenu.documentation,
|
||||
click: () => {
|
||||
shell.openExternal('https://cherry-ai.com/docs')
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Feedback',
|
||||
label: appMenu.feedback,
|
||||
click: () => {
|
||||
shell.openExternal('https://github.com/CherryHQ/cherry-studio/issues/new/choose')
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Releases',
|
||||
label: appMenu.releases,
|
||||
click: () => {
|
||||
shell.openExternal('https://github.com/CherryHQ/cherry-studio/releases')
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { getBinaryName } from '@main/utils/process'
|
||||
import type { TerminalConfig, TerminalConfigWithCommand } from '@shared/config/constant'
|
||||
import {
|
||||
codeTools,
|
||||
HOME_CHERRY_DIR,
|
||||
MACOS_TERMINALS,
|
||||
MACOS_TERMINALS_WITH_COMMANDS,
|
||||
terminalApps,
|
||||
@@ -66,7 +67,7 @@ class CodeToolsService {
|
||||
}
|
||||
|
||||
public async getBunPath() {
|
||||
const dir = path.join(os.homedir(), '.cherrystudio', 'bin')
|
||||
const dir = path.join(os.homedir(), HOME_CHERRY_DIR, 'bin')
|
||||
const bunName = await getBinaryName('bun')
|
||||
const bunPath = path.join(dir, bunName)
|
||||
return bunPath
|
||||
@@ -362,7 +363,7 @@ class CodeToolsService {
|
||||
|
||||
private async isPackageInstalled(cliTool: string): Promise<boolean> {
|
||||
const executableName = await this.getCliExecutableName(cliTool)
|
||||
const binDir = path.join(os.homedir(), '.cherrystudio', 'bin')
|
||||
const binDir = path.join(os.homedir(), HOME_CHERRY_DIR, 'bin')
|
||||
const executablePath = path.join(binDir, executableName + (isWin ? '.exe' : ''))
|
||||
|
||||
// Ensure bin directory exists
|
||||
@@ -389,7 +390,7 @@ class CodeToolsService {
|
||||
logger.info(`${cliTool} is installed, getting current version`)
|
||||
try {
|
||||
const executableName = await this.getCliExecutableName(cliTool)
|
||||
const binDir = path.join(os.homedir(), '.cherrystudio', 'bin')
|
||||
const binDir = path.join(os.homedir(), HOME_CHERRY_DIR, 'bin')
|
||||
const executablePath = path.join(binDir, executableName + (isWin ? '.exe' : ''))
|
||||
|
||||
const { stdout } = await execAsync(`"${executablePath}" --version`, {
|
||||
@@ -500,7 +501,7 @@ class CodeToolsService {
|
||||
try {
|
||||
const packageName = await this.getPackageName(cliTool)
|
||||
const bunPath = await this.getBunPath()
|
||||
const bunInstallPath = path.join(os.homedir(), '.cherrystudio')
|
||||
const bunInstallPath = path.join(os.homedir(), HOME_CHERRY_DIR)
|
||||
const registryUrl = await this.getNpmRegistryUrl()
|
||||
|
||||
const installEnvPrefix = isWin
|
||||
@@ -550,7 +551,7 @@ class CodeToolsService {
|
||||
const packageName = await this.getPackageName(cliTool)
|
||||
const bunPath = await this.getBunPath()
|
||||
const executableName = await this.getCliExecutableName(cliTool)
|
||||
const binDir = path.join(os.homedir(), '.cherrystudio', 'bin')
|
||||
const binDir = path.join(os.homedir(), HOME_CHERRY_DIR, 'bin')
|
||||
const executablePath = path.join(binDir, executableName + (isWin ? '.exe' : ''))
|
||||
|
||||
logger.debug(`Package name: ${packageName}`)
|
||||
@@ -652,7 +653,7 @@ class CodeToolsService {
|
||||
baseCommand = `${baseCommand} ${configParams}`
|
||||
}
|
||||
|
||||
const bunInstallPath = path.join(os.homedir(), '.cherrystudio')
|
||||
const bunInstallPath = path.join(os.homedir(), HOME_CHERRY_DIR)
|
||||
|
||||
if (isInstalled) {
|
||||
// If already installed, run executable directly (with optional update message)
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
ToolListChangedNotificationSchema
|
||||
} from '@modelcontextprotocol/sdk/types.js'
|
||||
import { nanoid } from '@reduxjs/toolkit'
|
||||
import { HOME_CHERRY_DIR } from '@shared/config/constant'
|
||||
import type { MCPProgressEvent } from '@shared/config/types'
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
import { defaultAppHeaders } from '@shared/utils'
|
||||
@@ -715,7 +716,7 @@ class McpService {
|
||||
}
|
||||
|
||||
public async getInstallInfo() {
|
||||
const dir = path.join(os.homedir(), '.cherrystudio', 'bin')
|
||||
const dir = path.join(os.homedir(), HOME_CHERRY_DIR, 'bin')
|
||||
const uvName = await getBinaryName('uv')
|
||||
const bunName = await getBinaryName('bun')
|
||||
const uvPath = path.join(dir, uvName)
|
||||
|
||||
@@ -3,6 +3,7 @@ import { homedir } from 'node:os'
|
||||
import { promisify } from 'node:util'
|
||||
|
||||
import { loggerService } from '@logger'
|
||||
import { HOME_CHERRY_DIR } from '@shared/config/constant'
|
||||
import * as fs from 'fs-extra'
|
||||
import * as path from 'path'
|
||||
|
||||
@@ -145,7 +146,7 @@ class OvmsManager {
|
||||
*/
|
||||
public async runOvms(): Promise<{ success: boolean; message?: string }> {
|
||||
const homeDir = homedir()
|
||||
const ovmsDir = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms')
|
||||
const ovmsDir = path.join(homeDir, HOME_CHERRY_DIR, 'ovms', 'ovms')
|
||||
const configPath = path.join(ovmsDir, 'models', 'config.json')
|
||||
const runBatPath = path.join(ovmsDir, 'run.bat')
|
||||
|
||||
@@ -195,7 +196,7 @@ class OvmsManager {
|
||||
*/
|
||||
public async getOvmsStatus(): Promise<'not-installed' | 'not-running' | 'running'> {
|
||||
const homeDir = homedir()
|
||||
const ovmsPath = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms', 'ovms.exe')
|
||||
const ovmsPath = path.join(homeDir, HOME_CHERRY_DIR, 'ovms', 'ovms', 'ovms.exe')
|
||||
|
||||
try {
|
||||
// Check if OVMS executable exists
|
||||
@@ -273,7 +274,7 @@ class OvmsManager {
|
||||
}
|
||||
|
||||
const homeDir = homedir()
|
||||
const configPath = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms', 'models', 'config.json')
|
||||
const configPath = path.join(homeDir, HOME_CHERRY_DIR, 'ovms', 'ovms', 'models', 'config.json')
|
||||
try {
|
||||
if (!(await fs.pathExists(configPath))) {
|
||||
logger.warn(`Config file does not exist: ${configPath}`)
|
||||
@@ -304,7 +305,7 @@ class OvmsManager {
|
||||
|
||||
private async applyModelPath(modelDirPath: string): Promise<boolean> {
|
||||
const homeDir = homedir()
|
||||
const patchDir = path.join(homeDir, '.cherrystudio', 'ovms', 'patch')
|
||||
const patchDir = path.join(homeDir, HOME_CHERRY_DIR, 'ovms', 'patch')
|
||||
if (!(await fs.pathExists(patchDir))) {
|
||||
return true
|
||||
}
|
||||
@@ -355,7 +356,7 @@ class OvmsManager {
|
||||
logger.info(`Adding model: ${modelName} with ID: ${modelId}, Source: ${modelSource}, Task: ${task}`)
|
||||
|
||||
const homeDir = homedir()
|
||||
const ovdndDir = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms')
|
||||
const ovdndDir = path.join(homeDir, HOME_CHERRY_DIR, 'ovms', 'ovms')
|
||||
const pathModel = path.join(ovdndDir, 'models', modelId)
|
||||
|
||||
try {
|
||||
@@ -468,7 +469,7 @@ class OvmsManager {
|
||||
*/
|
||||
public async checkModelExists(modelId: string): Promise<boolean> {
|
||||
const homeDir = homedir()
|
||||
const ovmsDir = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms')
|
||||
const ovmsDir = path.join(homeDir, HOME_CHERRY_DIR, 'ovms', 'ovms')
|
||||
const configPath = path.join(ovmsDir, 'models', 'config.json')
|
||||
|
||||
try {
|
||||
@@ -495,7 +496,7 @@ class OvmsManager {
|
||||
*/
|
||||
public async updateModelConfig(modelName: string, modelId: string): Promise<boolean> {
|
||||
const homeDir = homedir()
|
||||
const ovmsDir = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms')
|
||||
const ovmsDir = path.join(homeDir, HOME_CHERRY_DIR, 'ovms', 'ovms')
|
||||
const configPath = path.join(ovmsDir, 'models', 'config.json')
|
||||
|
||||
try {
|
||||
@@ -548,7 +549,7 @@ class OvmsManager {
|
||||
*/
|
||||
public async getModels(): Promise<ModelConfig[]> {
|
||||
const homeDir = homedir()
|
||||
const ovmsDir = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms')
|
||||
const ovmsDir = path.join(homeDir, HOME_CHERRY_DIR, 'ovms', 'ovms')
|
||||
const configPath = path.join(ovmsDir, 'models', 'config.json')
|
||||
|
||||
try {
|
||||
|
||||
112
src/main/services/PowerMonitorService.ts
Normal file
112
src/main/services/PowerMonitorService.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { isLinux, isMac, isWin } from '@main/constant'
|
||||
import ElectronShutdownHandler from '@paymoapp/electron-shutdown-handler'
|
||||
import { BrowserWindow } from 'electron'
|
||||
import { powerMonitor } from 'electron'
|
||||
|
||||
const logger = loggerService.withContext('PowerMonitorService')
|
||||
|
||||
type ShutdownHandler = () => void | Promise<void>
|
||||
|
||||
export class PowerMonitorService {
|
||||
private static instance: PowerMonitorService
|
||||
private initialized = false
|
||||
private shutdownHandlers: ShutdownHandler[] = []
|
||||
|
||||
private constructor() {
|
||||
// Private constructor to prevent direct instantiation
|
||||
}
|
||||
|
||||
public static getInstance(): PowerMonitorService {
|
||||
if (!PowerMonitorService.instance) {
|
||||
PowerMonitorService.instance = new PowerMonitorService()
|
||||
}
|
||||
return PowerMonitorService.instance
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a shutdown handler to be called when system shutdown is detected
|
||||
* @param handler - The handler function to be called on shutdown
|
||||
*/
|
||||
public registerShutdownHandler(handler: ShutdownHandler): void {
|
||||
this.shutdownHandlers.push(handler)
|
||||
logger.info('Shutdown handler registered', { totalHandlers: this.shutdownHandlers.length })
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize power monitor to listen for shutdown events
|
||||
*/
|
||||
public init(): void {
|
||||
if (this.initialized) {
|
||||
logger.warn('PowerMonitorService already initialized')
|
||||
return
|
||||
}
|
||||
|
||||
if (isWin) {
|
||||
this.initWindowsShutdownHandler()
|
||||
} else if (isMac || isLinux) {
|
||||
this.initElectronPowerMonitor()
|
||||
}
|
||||
|
||||
this.initialized = true
|
||||
logger.info('PowerMonitorService initialized', { platform: process.platform })
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute all registered shutdown handlers
|
||||
*/
|
||||
private async executeShutdownHandlers(): Promise<void> {
|
||||
logger.info('Executing shutdown handlers', { count: this.shutdownHandlers.length })
|
||||
for (const handler of this.shutdownHandlers) {
|
||||
try {
|
||||
await handler()
|
||||
} catch (error) {
|
||||
logger.error('Error executing shutdown handler', error as Error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize shutdown handler for Windows using @paymoapp/electron-shutdown-handler
|
||||
*/
|
||||
private initWindowsShutdownHandler(): void {
|
||||
try {
|
||||
const zeroMemoryWindow = new BrowserWindow({ show: false })
|
||||
// Set the window handle for the shutdown handler
|
||||
ElectronShutdownHandler.setWindowHandle(zeroMemoryWindow.getNativeWindowHandle())
|
||||
|
||||
// Listen for shutdown event
|
||||
ElectronShutdownHandler.on('shutdown', async () => {
|
||||
logger.info('System shutdown event detected (Windows)')
|
||||
// Execute all registered shutdown handlers
|
||||
await this.executeShutdownHandlers()
|
||||
// Release the shutdown block to allow the system to shut down
|
||||
ElectronShutdownHandler.releaseShutdown()
|
||||
})
|
||||
|
||||
logger.info('Windows shutdown handler registered')
|
||||
} catch (error) {
|
||||
logger.error('Failed to initialize Windows shutdown handler', error as Error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize power monitor for macOS and Linux using Electron's powerMonitor
|
||||
*/
|
||||
private initElectronPowerMonitor(): void {
|
||||
try {
|
||||
powerMonitor.on('shutdown', async () => {
|
||||
logger.info('System shutdown event detected', { platform: process.platform })
|
||||
// Execute all registered shutdown handlers
|
||||
await this.executeShutdownHandlers()
|
||||
})
|
||||
|
||||
logger.info('Electron powerMonitor shutdown listener registered')
|
||||
} catch (error) {
|
||||
logger.error('Failed to initialize Electron powerMonitor', error as Error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default export as singleton instance
|
||||
export default PowerMonitorService.getInstance()
|
||||
@@ -1,8 +1,9 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
|
||||
import type { BrowserWindow } from 'electron'
|
||||
import { ipcMain } from 'electron'
|
||||
|
||||
import { windowService } from './WindowService'
|
||||
|
||||
interface PythonExecutionRequest {
|
||||
id: string
|
||||
script: string
|
||||
@@ -21,7 +22,6 @@ interface PythonExecutionResponse {
|
||||
*/
|
||||
export class PythonService {
|
||||
private static instance: PythonService | null = null
|
||||
private mainWindow: BrowserWindow | null = null
|
||||
private pendingRequests = new Map<string, { resolve: (value: string) => void; reject: (error: Error) => void }>()
|
||||
|
||||
private constructor() {
|
||||
@@ -51,10 +51,6 @@ export class PythonService {
|
||||
})
|
||||
}
|
||||
|
||||
public setMainWindow(mainWindow: BrowserWindow) {
|
||||
this.mainWindow = mainWindow
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute Python code by sending request to renderer PyodideService
|
||||
*/
|
||||
@@ -63,8 +59,8 @@ export class PythonService {
|
||||
context: Record<string, any> = {},
|
||||
timeout: number = 60000
|
||||
): Promise<string> {
|
||||
if (!this.mainWindow) {
|
||||
throw new Error('Main window not set in PythonService')
|
||||
if (!windowService.getMainWindow()) {
|
||||
throw new Error('Main window not found')
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -95,7 +91,7 @@ export class PythonService {
|
||||
|
||||
// Send request to renderer
|
||||
const request: PythonExecutionRequest = { id: requestId, script, context, timeout }
|
||||
this.mainWindow?.webContents.send('python-execution-request', request)
|
||||
windowService.getMainWindow()?.webContents.send('python-execution-request', request)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { Attributes, SpanEntity, TokenUsage, TraceCache } from '@mcp-trace/
|
||||
import { convertSpanToSpanEntity } from '@mcp-trace/trace-core'
|
||||
import { SpanStatusCode } from '@opentelemetry/api'
|
||||
import type { ReadableSpan } from '@opentelemetry/sdk-trace-base'
|
||||
import { HOME_CHERRY_DIR } from '@shared/config/constant'
|
||||
import fs from 'fs/promises'
|
||||
import * as os from 'os'
|
||||
import * as path from 'path'
|
||||
@@ -18,7 +19,7 @@ class SpanCacheService implements TraceCache {
|
||||
pri
|
||||
|
||||
constructor() {
|
||||
this.fileDir = path.join(os.homedir(), '.cherrystudio', 'trace')
|
||||
this.fileDir = path.join(os.homedir(), HOME_CHERRY_DIR, 'trace')
|
||||
}
|
||||
|
||||
createSpan: (span: ReadableSpan) => void = (span: ReadableSpan) => {
|
||||
|
||||
285
src/main/services/VersionService.ts
Normal file
285
src/main/services/VersionService.ts
Normal file
@@ -0,0 +1,285 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { app } from 'electron'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
const logger = loggerService.withContext('VersionService')
|
||||
|
||||
type OS = 'win' | 'mac' | 'linux' | 'unknown'
|
||||
type Environment = 'prod' | 'dev'
|
||||
type Packaged = 'packaged' | 'unpackaged'
|
||||
type Mode = 'install' | 'portable'
|
||||
|
||||
/**
|
||||
* Version record stored in version.log
|
||||
*/
|
||||
interface VersionRecord {
|
||||
version: string
|
||||
os: OS
|
||||
environment: Environment
|
||||
packaged: Packaged
|
||||
mode: Mode
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Service for tracking application version history
|
||||
* Stores version information in userData/version.log for data migration and diagnostics
|
||||
*/
|
||||
class VersionService {
|
||||
private readonly VERSION_LOG_FILE = 'version.log'
|
||||
private versionLogPath: string | null = null
|
||||
|
||||
constructor() {
|
||||
// Lazy initialization of path since app.getPath may not be available during construction
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the full path to version.log file
|
||||
* @returns {string} Full path to version log file
|
||||
*/
|
||||
private getVersionLogPath(): string {
|
||||
if (!this.versionLogPath) {
|
||||
this.versionLogPath = path.join(app.getPath('userData'), this.VERSION_LOG_FILE)
|
||||
}
|
||||
return this.versionLogPath
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets current operating system identifier
|
||||
* @returns {OS} OS identifier
|
||||
*/
|
||||
private getCurrentOS(): OS {
|
||||
switch (process.platform) {
|
||||
case 'win32':
|
||||
return 'win'
|
||||
case 'darwin':
|
||||
return 'mac'
|
||||
case 'linux':
|
||||
return 'linux'
|
||||
default:
|
||||
return 'unknown'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets current environment (production or development)
|
||||
* @returns {Environment} Environment identifier
|
||||
*/
|
||||
private getCurrentEnvironment(): Environment {
|
||||
return import.meta.env.MODE === 'production' ? 'prod' : 'dev'
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets packaging status
|
||||
* @returns {Packaged} Packaging status
|
||||
*/
|
||||
private getPackagedStatus(): Packaged {
|
||||
return app.isPackaged ? 'packaged' : 'unpackaged'
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets installation mode (install or portable)
|
||||
* @returns {Mode} Installation mode
|
||||
*/
|
||||
private getInstallMode(): Mode {
|
||||
return process.env.PORTABLE_EXECUTABLE_DIR !== undefined ? 'portable' : 'install'
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates version log line for current application state
|
||||
* @returns {string} Pipe-separated version record line
|
||||
*/
|
||||
private generateCurrentVersionLine(): string {
|
||||
const version = app.getVersion()
|
||||
const os = this.getCurrentOS()
|
||||
const environment = this.getCurrentEnvironment()
|
||||
const packaged = this.getPackagedStatus()
|
||||
const mode = this.getInstallMode()
|
||||
const timestamp = new Date().toISOString()
|
||||
|
||||
return `${version}|${os}|${environment}|${packaged}|${mode}|${timestamp}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a version log line into a VersionRecord object
|
||||
* @param {string} line - Pipe-separated version record line
|
||||
* @returns {VersionRecord | null} Parsed version record or null if invalid
|
||||
*/
|
||||
private parseVersionLine(line: string): VersionRecord | null {
|
||||
try {
|
||||
const parts = line.trim().split('|')
|
||||
if (parts.length !== 6) {
|
||||
return null
|
||||
}
|
||||
|
||||
const [version, os, environment, packaged, mode, timestamp] = parts
|
||||
|
||||
// Validate data
|
||||
if (
|
||||
!version ||
|
||||
!['win', 'mac', 'linux', 'unknown'].includes(os) ||
|
||||
!['prod', 'dev'].includes(environment) ||
|
||||
!['packaged', 'unpackaged'].includes(packaged) ||
|
||||
!['install', 'portable'].includes(mode) ||
|
||||
!timestamp
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
version,
|
||||
os: os as OS,
|
||||
environment: environment as Environment,
|
||||
packaged: packaged as Packaged,
|
||||
mode: mode as Mode,
|
||||
timestamp
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to parse version line: ${line}`, error as Error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the last 1KB from version.log and returns all lines
|
||||
* Uses reverse reading from file end to avoid reading the entire file
|
||||
* @returns {string[]} Array of version lines from the last 1KB
|
||||
*/
|
||||
private readLastVersionLines(): string[] {
|
||||
const logPath = this.getVersionLogPath()
|
||||
|
||||
try {
|
||||
if (!fs.existsSync(logPath)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const stats = fs.statSync(logPath)
|
||||
const fileSize = stats.size
|
||||
|
||||
if (fileSize === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
// Read from the end of the file, 1KB is enough to find previous version
|
||||
// Typical line: "1.7.0-beta.3|win|prod|packaged|install|2025-01-15T08:30:00.000Z\n" (~70 bytes)
|
||||
// 1KB can store ~14 lines, which is more than enough
|
||||
const bufferSize = Math.min(1024, fileSize)
|
||||
const buffer = Buffer.alloc(bufferSize)
|
||||
|
||||
const fd = fs.openSync(logPath, 'r')
|
||||
try {
|
||||
const startPosition = Math.max(0, fileSize - bufferSize)
|
||||
fs.readSync(fd, buffer, 0, bufferSize, startPosition)
|
||||
|
||||
const content = buffer.toString('utf-8')
|
||||
const lines = content
|
||||
.trim()
|
||||
.split('\n')
|
||||
.filter((line) => line.trim())
|
||||
|
||||
return lines
|
||||
} finally {
|
||||
fs.closeSync(fd)
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to read version log:', error as Error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a version record line to version.log
|
||||
* @param {string} line - Version record line to append
|
||||
*/
|
||||
private appendVersionLine(line: string): void {
|
||||
const logPath = this.getVersionLogPath()
|
||||
|
||||
try {
|
||||
fs.appendFileSync(logPath, line + '\n', 'utf-8')
|
||||
logger.debug(`Version recorded: ${line}`)
|
||||
} catch (error) {
|
||||
logger.error('Failed to append version log:', error as Error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Records the current version on application startup
|
||||
* Only adds a new record if the version has changed since the last run
|
||||
*/
|
||||
recordCurrentVersion(): void {
|
||||
try {
|
||||
const currentLine = this.generateCurrentVersionLine()
|
||||
const lines = this.readLastVersionLines()
|
||||
|
||||
// Add new record if this is the first run or version has changed
|
||||
if (lines.length === 0) {
|
||||
logger.info('First run detected, creating version log')
|
||||
this.appendVersionLine(currentLine)
|
||||
return
|
||||
}
|
||||
|
||||
const lastLine = lines[lines.length - 1]
|
||||
const lastRecord = this.parseVersionLine(lastLine)
|
||||
const currentVersion = app.getVersion()
|
||||
|
||||
// Check if any meaningful field has changed (version, os, environment, packaged, mode)
|
||||
const currentOS = this.getCurrentOS()
|
||||
const currentEnvironment = this.getCurrentEnvironment()
|
||||
const currentPackaged = this.getPackagedStatus()
|
||||
const currentMode = this.getInstallMode()
|
||||
|
||||
const hasMeaningfulChange =
|
||||
!lastRecord ||
|
||||
lastRecord.version !== currentVersion ||
|
||||
lastRecord.os !== currentOS ||
|
||||
lastRecord.environment !== currentEnvironment ||
|
||||
lastRecord.packaged !== currentPackaged ||
|
||||
lastRecord.mode !== currentMode
|
||||
|
||||
if (hasMeaningfulChange) {
|
||||
logger.info(`Version information changed, recording new entry`)
|
||||
this.appendVersionLine(currentLine)
|
||||
} else {
|
||||
logger.debug(`Version information not changed, skip recording`)
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to record current version:', error as Error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the previous version record (last record with different version than current)
|
||||
* Reads from the last 1KB of version.log to find the most recent different version
|
||||
* Useful for detecting version upgrades and running migrations
|
||||
* @returns {VersionRecord | null} Previous version record or null if not available
|
||||
*/
|
||||
getPreviousVersion(): VersionRecord | null {
|
||||
try {
|
||||
const lines = this.readLastVersionLines()
|
||||
if (lines.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const currentVersion = app.getVersion()
|
||||
|
||||
// Read from the end backwards to find the first different version
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
const record = this.parseVersionLine(lines[i])
|
||||
if (record && record.version !== currentVersion) {
|
||||
return record
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
} catch (error) {
|
||||
logger.error('Failed to get previous version:', error as Error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Singleton instance of VersionService
|
||||
*/
|
||||
export const versionService = new VersionService()
|
||||
@@ -1,5 +1,5 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { WebSocketCandidatesResponse, WebSocketStatusResponse } from '@shared/config/types'
|
||||
import type { WebSocketCandidatesResponse, WebSocketStatusResponse } from '@shared/config/types'
|
||||
import * as fs from 'fs'
|
||||
import { networkInterfaces } from 'os'
|
||||
import * as path from 'path'
|
||||
|
||||
@@ -365,6 +365,16 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
type: 'chunk',
|
||||
chunk
|
||||
})
|
||||
|
||||
// Close prompt stream when SDK signals completion or error
|
||||
if (chunk.type === 'finish' || chunk.type === 'error') {
|
||||
logger.info('Closing prompt stream as SDK signaled completion', {
|
||||
chunkType: chunk.type,
|
||||
reason: chunk.type === 'finish' ? 'finished' : 'error_occurred'
|
||||
})
|
||||
closePromptStream()
|
||||
logger.info('Prompt stream closed successfully')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { isWin } from '@main/constant'
|
||||
import { HOME_CHERRY_DIR } from '@shared/config/constant'
|
||||
import type { OcrOvConfig, OcrResult, SupportedOcrFile } from '@types'
|
||||
import { isImageFileMetadata } from '@types'
|
||||
import { exec } from 'child_process'
|
||||
@@ -13,7 +14,7 @@ import { OcrBaseService } from './OcrBaseService'
|
||||
const logger = loggerService.withContext('OvOcrService')
|
||||
const execAsync = promisify(exec)
|
||||
|
||||
const PATH_BAT_FILE = path.join(os.homedir(), '.cherrystudio', 'ovms', 'ovocr', 'run.npu.bat')
|
||||
const PATH_BAT_FILE = path.join(os.homedir(), HOME_CHERRY_DIR, 'ovms', 'ovocr', 'run.npu.bat')
|
||||
|
||||
export class OvOcrService extends OcrBaseService {
|
||||
constructor() {
|
||||
@@ -30,7 +31,7 @@ export class OvOcrService extends OcrBaseService {
|
||||
}
|
||||
|
||||
private getOvOcrPath(): string {
|
||||
return path.join(os.homedir(), '.cherrystudio', 'ovms', 'ovocr')
|
||||
return path.join(os.homedir(), HOME_CHERRY_DIR, 'ovms', 'ovocr')
|
||||
}
|
||||
|
||||
private getImgDir(): string {
|
||||
|
||||
@@ -5,7 +5,7 @@ import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
|
||||
import { loggerService } from '@logger'
|
||||
import { audioExts, documentExts, imageExts, MB, textExts, videoExts } from '@shared/config/constant'
|
||||
import { audioExts, documentExts, HOME_CHERRY_DIR, imageExts, MB, textExts, videoExts } from '@shared/config/constant'
|
||||
import type { FileMetadata, NotesTreeNode } from '@types'
|
||||
import { FileTypes } from '@types'
|
||||
import chardet from 'chardet'
|
||||
@@ -160,7 +160,7 @@ export function getNotesDir() {
|
||||
}
|
||||
|
||||
export function getConfigDir() {
|
||||
return path.join(os.homedir(), '.cherrystudio', 'config')
|
||||
return path.join(os.homedir(), HOME_CHERRY_DIR, 'config')
|
||||
}
|
||||
|
||||
export function getCacheDir() {
|
||||
@@ -172,7 +172,7 @@ export function getAppConfigDir(name: string) {
|
||||
}
|
||||
|
||||
export function getMcpDir() {
|
||||
return path.join(os.homedir(), '.cherrystudio', 'mcp')
|
||||
return path.join(os.homedir(), HOME_CHERRY_DIR, 'mcp')
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,6 +3,7 @@ import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
|
||||
import { isLinux, isPortable, isWin } from '@main/constant'
|
||||
import { HOME_CHERRY_DIR } from '@shared/config/constant'
|
||||
import { app } from 'electron'
|
||||
|
||||
// Please don't import any other modules which is not node/electron built-in modules
|
||||
@@ -17,7 +18,7 @@ function hasWritePermission(path: string) {
|
||||
}
|
||||
|
||||
function getConfigDir() {
|
||||
return path.join(os.homedir(), '.cherrystudio', 'config')
|
||||
return path.join(os.homedir(), HOME_CHERRY_DIR, 'config')
|
||||
}
|
||||
|
||||
export function initAppDataDir() {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { HOME_CHERRY_DIR } from '@shared/config/constant'
|
||||
import { spawn } from 'child_process'
|
||||
import fs from 'fs'
|
||||
import os from 'os'
|
||||
@@ -46,11 +47,11 @@ export async function getBinaryName(name: string): Promise<string> {
|
||||
|
||||
export async function getBinaryPath(name?: string): Promise<string> {
|
||||
if (!name) {
|
||||
return path.join(os.homedir(), '.cherrystudio', 'bin')
|
||||
return path.join(os.homedir(), HOME_CHERRY_DIR, 'bin')
|
||||
}
|
||||
|
||||
const binaryName = await getBinaryName(name)
|
||||
const binariesDir = path.join(os.homedir(), '.cherrystudio', 'bin')
|
||||
const binariesDir = path.join(os.homedir(), HOME_CHERRY_DIR, 'bin')
|
||||
const binariesDirExists = fs.existsSync(binariesDir)
|
||||
return binariesDirExists ? path.join(binariesDir, binaryName) : binaryName
|
||||
}
|
||||
|
||||
@@ -6,11 +6,9 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { Provider } from 'react-redux'
|
||||
import { PersistGate } from 'redux-persist/integration/react'
|
||||
|
||||
import { ToastPortal } from './components/ToastPortal'
|
||||
import TopViewContainer from './components/TopView'
|
||||
import AntdProvider from './context/AntdProvider'
|
||||
import { CodeStyleProvider } from './context/CodeStyleProvider'
|
||||
import { HeroUIProvider } from './context/HeroUIProvider'
|
||||
import { NotificationProvider } from './context/NotificationProvider'
|
||||
import StyleSheetManager from './context/StyleSheetManager'
|
||||
import { ThemeProvider } from './context/ThemeProvider'
|
||||
@@ -34,24 +32,21 @@ function App(): React.ReactElement {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<HeroUIProvider>
|
||||
<StyleSheetManager>
|
||||
<ThemeProvider>
|
||||
<AntdProvider>
|
||||
<NotificationProvider>
|
||||
<CodeStyleProvider>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<TopViewContainer>
|
||||
<Router />
|
||||
</TopViewContainer>
|
||||
</PersistGate>
|
||||
</CodeStyleProvider>
|
||||
</NotificationProvider>
|
||||
</AntdProvider>
|
||||
</ThemeProvider>
|
||||
</StyleSheetManager>
|
||||
<ToastPortal />
|
||||
</HeroUIProvider>
|
||||
<StyleSheetManager>
|
||||
<ThemeProvider>
|
||||
<AntdProvider>
|
||||
<NotificationProvider>
|
||||
<CodeStyleProvider>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<TopViewContainer>
|
||||
<Router />
|
||||
</TopViewContainer>
|
||||
</PersistGate>
|
||||
</CodeStyleProvider>
|
||||
</NotificationProvider>
|
||||
</AntdProvider>
|
||||
</ThemeProvider>
|
||||
</StyleSheetManager>
|
||||
</QueryClientProvider>
|
||||
</Provider>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { BedrockClient, ListFoundationModelsCommand, ListInferenceProfilesCommand } from '@aws-sdk/client-bedrock'
|
||||
import {
|
||||
BedrockRuntimeClient,
|
||||
type BedrockRuntimeClientConfig,
|
||||
ConverseCommand,
|
||||
InvokeModelCommand,
|
||||
InvokeModelWithResponseStreamCommand
|
||||
@@ -11,6 +12,8 @@ import { DEFAULT_MAX_TOKENS } from '@renderer/config/constant'
|
||||
import { findTokenLimit, isReasoningModel } from '@renderer/config/models'
|
||||
import {
|
||||
getAwsBedrockAccessKeyId,
|
||||
getAwsBedrockApiKey,
|
||||
getAwsBedrockAuthType,
|
||||
getAwsBedrockRegion,
|
||||
getAwsBedrockSecretAccessKey
|
||||
} from '@renderer/hooks/useAwsBedrock'
|
||||
@@ -75,32 +78,48 @@ export class AwsBedrockAPIClient extends BaseApiClient<
|
||||
}
|
||||
|
||||
const region = getAwsBedrockRegion()
|
||||
const accessKeyId = getAwsBedrockAccessKeyId()
|
||||
const secretAccessKey = getAwsBedrockSecretAccessKey()
|
||||
const authType = getAwsBedrockAuthType()
|
||||
|
||||
if (!region) {
|
||||
throw new Error('AWS region is required. Please configure AWS-Region in extra headers.')
|
||||
throw new Error('AWS region is required. Please configure AWS region in settings.')
|
||||
}
|
||||
|
||||
if (!accessKeyId || !secretAccessKey) {
|
||||
throw new Error('AWS credentials are required. Please configure AWS-Access-Key-ID and AWS-Secret-Access-Key.')
|
||||
// Build client configuration based on auth type
|
||||
let clientConfig: BedrockRuntimeClientConfig
|
||||
|
||||
if (authType === 'iam') {
|
||||
// IAM credentials authentication
|
||||
const accessKeyId = getAwsBedrockAccessKeyId()
|
||||
const secretAccessKey = getAwsBedrockSecretAccessKey()
|
||||
|
||||
if (!accessKeyId || !secretAccessKey) {
|
||||
throw new Error('AWS credentials are required. Please configure Access Key ID and Secret Access Key.')
|
||||
}
|
||||
|
||||
clientConfig = {
|
||||
region,
|
||||
credentials: {
|
||||
accessKeyId,
|
||||
secretAccessKey
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// API Key authentication
|
||||
const awsBedrockApiKey = getAwsBedrockApiKey()
|
||||
|
||||
if (!awsBedrockApiKey) {
|
||||
throw new Error('AWS Bedrock API Key is required. Please configure API Key in settings.')
|
||||
}
|
||||
|
||||
clientConfig = {
|
||||
region,
|
||||
token: { token: awsBedrockApiKey },
|
||||
authSchemePreference: ['httpBearerAuth']
|
||||
}
|
||||
}
|
||||
|
||||
const client = new BedrockRuntimeClient({
|
||||
region,
|
||||
credentials: {
|
||||
accessKeyId,
|
||||
secretAccessKey
|
||||
}
|
||||
})
|
||||
|
||||
const bedrockClient = new BedrockClient({
|
||||
region,
|
||||
credentials: {
|
||||
accessKeyId,
|
||||
secretAccessKey
|
||||
}
|
||||
})
|
||||
const client = new BedrockRuntimeClient(clientConfig)
|
||||
const bedrockClient = new BedrockClient(clientConfig)
|
||||
|
||||
this.sdkInstance = { client, bedrockClient, region }
|
||||
return this.sdkInstance
|
||||
|
||||
@@ -192,7 +192,7 @@ export class OpenAIAPIClient extends OpenAIBaseClient<
|
||||
extra_body: {
|
||||
google: {
|
||||
thinking_config: {
|
||||
thinkingBudget: 0
|
||||
thinking_budget: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -327,8 +327,8 @@ export class OpenAIAPIClient extends OpenAIBaseClient<
|
||||
extra_body: {
|
||||
google: {
|
||||
thinking_config: {
|
||||
thinkingBudget: -1,
|
||||
includeThoughts: true
|
||||
thinking_budget: -1,
|
||||
include_thoughts: true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -338,8 +338,8 @@ export class OpenAIAPIClient extends OpenAIBaseClient<
|
||||
extra_body: {
|
||||
google: {
|
||||
thinking_config: {
|
||||
thinkingBudget: budgetTokens,
|
||||
includeThoughts: true
|
||||
thinking_budget: budgetTokens,
|
||||
include_thoughts: true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -670,7 +670,7 @@ export class OpenAIAPIClient extends OpenAIBaseClient<
|
||||
} else if (isClaudeReasoningModel(model) && reasoningEffort.thinking?.budget_tokens) {
|
||||
suffix = ` --thinking_budget ${reasoningEffort.thinking.budget_tokens}`
|
||||
} else if (isGeminiReasoningModel(model) && reasoningEffort.extra_body?.google?.thinking_config) {
|
||||
suffix = ` --thinking_budget ${reasoningEffort.extra_body.google.thinking_config.thinkingBudget}`
|
||||
suffix = ` --thinking_budget ${reasoningEffort.extra_body.google.thinking_config.thinking_budget}`
|
||||
}
|
||||
// FIXME: poe 不支持多个text part,上传文本文件的时候用的不是file part而是text part,因此会出问题
|
||||
// 临时解决方案是强制poe用string content,但是其实poe部分支持array
|
||||
|
||||
@@ -85,6 +85,19 @@ export function supportsLargeFileUpload(model: Model): boolean {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查模型是否支持TopP
|
||||
*/
|
||||
export function supportsTopP(model: Model): boolean {
|
||||
const provider = getProviderByModel(model)
|
||||
|
||||
if (provider?.type === 'anthropic' || model?.endpoint_type === 'anthropic') {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取提供商特定的文件大小限制
|
||||
*/
|
||||
|
||||
@@ -34,6 +34,7 @@ import { setupToolsConfig } from '../utils/mcp'
|
||||
import { buildProviderOptions } from '../utils/options'
|
||||
import { getAnthropicThinkingBudget } from '../utils/reasoning'
|
||||
import { buildProviderBuiltinWebSearchConfig } from '../utils/websearch'
|
||||
import { supportsTopP } from './modelCapabilities'
|
||||
import { getTemperature, getTopP } from './modelParameters'
|
||||
|
||||
const logger = loggerService.withContext('parameterBuilder')
|
||||
@@ -176,20 +177,27 @@ export async function buildStreamTextParams(
|
||||
messages: sdkMessages,
|
||||
maxOutputTokens: maxTokens,
|
||||
temperature: getTemperature(assistant, model),
|
||||
topP: getTopP(assistant, model),
|
||||
abortSignal: options.requestOptions?.signal,
|
||||
headers: options.requestOptions?.headers,
|
||||
providerOptions,
|
||||
stopWhen: stepCountIs(20),
|
||||
maxRetries: 0
|
||||
}
|
||||
|
||||
if (supportsTopP(model)) {
|
||||
params.topP = getTopP(assistant, model)
|
||||
}
|
||||
|
||||
if (tools) {
|
||||
params.tools = tools
|
||||
}
|
||||
|
||||
if (assistant.prompt) {
|
||||
params.system = await replacePromptVariables(assistant.prompt, model.name)
|
||||
}
|
||||
|
||||
logger.debug('params', params)
|
||||
|
||||
return {
|
||||
params,
|
||||
modelId: model.id,
|
||||
|
||||
@@ -21,10 +21,45 @@ vi.mock('@renderer/store', () => ({
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@renderer/utils/api', () => ({
|
||||
formatApiHost: vi.fn((host, isSupportedAPIVersion = true) => {
|
||||
if (isSupportedAPIVersion === false) {
|
||||
return host // Return host as-is when isSupportedAPIVersion is false
|
||||
}
|
||||
return `${host}/v1` // Default behavior when isSupportedAPIVersion is true
|
||||
}),
|
||||
routeToEndpoint: vi.fn((host) => ({
|
||||
baseURL: host,
|
||||
endpoint: '/chat/completions'
|
||||
}))
|
||||
}))
|
||||
|
||||
vi.mock('@renderer/config/providers', async (importOriginal) => {
|
||||
const actual = (await importOriginal()) as any
|
||||
return {
|
||||
...actual,
|
||||
isCherryAIProvider: vi.fn(),
|
||||
isPerplexityProvider: vi.fn(),
|
||||
isAnthropicProvider: vi.fn(() => false),
|
||||
isAzureOpenAIProvider: vi.fn(() => false),
|
||||
isGeminiProvider: vi.fn(() => false),
|
||||
isNewApiProvider: vi.fn(() => false)
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@renderer/hooks/useVertexAI', () => ({
|
||||
isVertexProvider: vi.fn(() => false),
|
||||
isVertexAIConfigured: vi.fn(() => false),
|
||||
createVertexProvider: vi.fn()
|
||||
}))
|
||||
|
||||
import { isCherryAIProvider, isPerplexityProvider } from '@renderer/config/providers'
|
||||
import { getProviderByModel } from '@renderer/services/AssistantService'
|
||||
import type { Model, Provider } from '@renderer/types'
|
||||
import { formatApiHost } from '@renderer/utils/api'
|
||||
|
||||
import { COPILOT_DEFAULT_HEADERS, COPILOT_EDITOR_VERSION, isCopilotResponsesModel } from '../constants'
|
||||
import { providerToAiSdkConfig } from '../providerConfig'
|
||||
import { getActualProvider, providerToAiSdkConfig } from '../providerConfig'
|
||||
|
||||
const createWindowKeyv = () => {
|
||||
const store = new Map<string, string>()
|
||||
@@ -46,11 +81,31 @@ const createCopilotProvider = (): Provider => ({
|
||||
isSystem: true
|
||||
})
|
||||
|
||||
const createModel = (id: string, name = id): Model => ({
|
||||
const createModel = (id: string, name = id, provider = 'copilot'): Model => ({
|
||||
id,
|
||||
name,
|
||||
provider: 'copilot',
|
||||
group: 'copilot'
|
||||
provider,
|
||||
group: provider
|
||||
})
|
||||
|
||||
const createCherryAIProvider = (): Provider => ({
|
||||
id: 'cherryai',
|
||||
type: 'openai',
|
||||
name: 'CherryAI',
|
||||
apiKey: 'test-key',
|
||||
apiHost: 'https://api.cherryai.com',
|
||||
models: [],
|
||||
isSystem: false
|
||||
})
|
||||
|
||||
const createPerplexityProvider = (): Provider => ({
|
||||
id: 'perplexity',
|
||||
type: 'openai',
|
||||
name: 'Perplexity',
|
||||
apiKey: 'test-key',
|
||||
apiHost: 'https://api.perplexity.ai',
|
||||
models: [],
|
||||
isSystem: false
|
||||
})
|
||||
|
||||
describe('Copilot responses routing', () => {
|
||||
@@ -87,3 +142,134 @@ describe('Copilot responses routing', () => {
|
||||
expect(config.options.headers?.['Copilot-Integration-Id']).toBe(COPILOT_DEFAULT_HEADERS['Copilot-Integration-Id'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('CherryAI provider configuration', () => {
|
||||
beforeEach(() => {
|
||||
;(globalThis as any).window = {
|
||||
...(globalThis as any).window,
|
||||
keyv: createWindowKeyv()
|
||||
}
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('formats CherryAI provider apiHost with false parameter', () => {
|
||||
const provider = createCherryAIProvider()
|
||||
const model = createModel('gpt-4', 'GPT-4', 'cherryai')
|
||||
|
||||
// Mock the functions to simulate CherryAI provider detection
|
||||
vi.mocked(isCherryAIProvider).mockReturnValue(true)
|
||||
vi.mocked(getProviderByModel).mockReturnValue(provider)
|
||||
|
||||
// Call getActualProvider which should trigger formatProviderApiHost
|
||||
const actualProvider = getActualProvider(model)
|
||||
|
||||
// Verify that formatApiHost was called with false as the second parameter
|
||||
expect(formatApiHost).toHaveBeenCalledWith('https://api.cherryai.com', false)
|
||||
expect(actualProvider.apiHost).toBe('https://api.cherryai.com')
|
||||
})
|
||||
|
||||
it('does not format non-CherryAI provider with false parameter', () => {
|
||||
const provider = {
|
||||
id: 'openai',
|
||||
type: 'openai',
|
||||
name: 'OpenAI',
|
||||
apiKey: 'test-key',
|
||||
apiHost: 'https://api.openai.com',
|
||||
models: [],
|
||||
isSystem: false
|
||||
} as Provider
|
||||
const model = createModel('gpt-4', 'GPT-4', 'openai')
|
||||
|
||||
// Mock the functions to simulate non-CherryAI provider
|
||||
vi.mocked(isCherryAIProvider).mockReturnValue(false)
|
||||
vi.mocked(getProviderByModel).mockReturnValue(provider)
|
||||
|
||||
// Call getActualProvider
|
||||
const actualProvider = getActualProvider(model)
|
||||
|
||||
// Verify that formatApiHost was called with default parameters (true)
|
||||
expect(formatApiHost).toHaveBeenCalledWith('https://api.openai.com')
|
||||
expect(actualProvider.apiHost).toBe('https://api.openai.com/v1')
|
||||
})
|
||||
|
||||
it('handles CherryAI provider with empty apiHost', () => {
|
||||
const provider = createCherryAIProvider()
|
||||
provider.apiHost = ''
|
||||
const model = createModel('gpt-4', 'GPT-4', 'cherryai')
|
||||
|
||||
vi.mocked(isCherryAIProvider).mockReturnValue(true)
|
||||
vi.mocked(getProviderByModel).mockReturnValue(provider)
|
||||
|
||||
const actualProvider = getActualProvider(model)
|
||||
|
||||
expect(formatApiHost).toHaveBeenCalledWith('', false)
|
||||
expect(actualProvider.apiHost).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Perplexity provider configuration', () => {
|
||||
beforeEach(() => {
|
||||
;(globalThis as any).window = {
|
||||
...(globalThis as any).window,
|
||||
keyv: createWindowKeyv()
|
||||
}
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('formats Perplexity provider apiHost with false parameter', () => {
|
||||
const provider = createPerplexityProvider()
|
||||
const model = createModel('sonar', 'Sonar', 'perplexity')
|
||||
|
||||
// Mock the functions to simulate Perplexity provider detection
|
||||
vi.mocked(isCherryAIProvider).mockReturnValue(false)
|
||||
vi.mocked(isPerplexityProvider).mockReturnValue(true)
|
||||
vi.mocked(getProviderByModel).mockReturnValue(provider)
|
||||
|
||||
// Call getActualProvider which should trigger formatProviderApiHost
|
||||
const actualProvider = getActualProvider(model)
|
||||
|
||||
// Verify that formatApiHost was called with false as the second parameter
|
||||
expect(formatApiHost).toHaveBeenCalledWith('https://api.perplexity.ai', false)
|
||||
expect(actualProvider.apiHost).toBe('https://api.perplexity.ai')
|
||||
})
|
||||
|
||||
it('does not format non-Perplexity provider with false parameter', () => {
|
||||
const provider = {
|
||||
id: 'openai',
|
||||
type: 'openai',
|
||||
name: 'OpenAI',
|
||||
apiKey: 'test-key',
|
||||
apiHost: 'https://api.openai.com',
|
||||
models: [],
|
||||
isSystem: false
|
||||
} as Provider
|
||||
const model = createModel('gpt-4', 'GPT-4', 'openai')
|
||||
|
||||
// Mock the functions to simulate non-Perplexity provider
|
||||
vi.mocked(isCherryAIProvider).mockReturnValue(false)
|
||||
vi.mocked(isPerplexityProvider).mockReturnValue(false)
|
||||
vi.mocked(getProviderByModel).mockReturnValue(provider)
|
||||
|
||||
// Call getActualProvider
|
||||
const actualProvider = getActualProvider(model)
|
||||
|
||||
// Verify that formatApiHost was called with default parameters (true)
|
||||
expect(formatApiHost).toHaveBeenCalledWith('https://api.openai.com')
|
||||
expect(actualProvider.apiHost).toBe('https://api.openai.com/v1')
|
||||
})
|
||||
|
||||
it('handles Perplexity provider with empty apiHost', () => {
|
||||
const provider = createPerplexityProvider()
|
||||
provider.apiHost = ''
|
||||
const model = createModel('sonar', 'Sonar', 'perplexity')
|
||||
|
||||
vi.mocked(isCherryAIProvider).mockReturnValue(false)
|
||||
vi.mocked(isPerplexityProvider).mockReturnValue(true)
|
||||
vi.mocked(getProviderByModel).mockReturnValue(provider)
|
||||
|
||||
const actualProvider = getActualProvider(model)
|
||||
|
||||
expect(formatApiHost).toHaveBeenCalledWith('', false)
|
||||
expect(actualProvider.apiHost).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -52,7 +52,7 @@ const AIHUBMIX_RULES: RuleSet = {
|
||||
}
|
||||
}
|
||||
],
|
||||
fallbackRule: (provider: Provider) => provider
|
||||
fallbackRule: (provider: Provider) => extraProviderConfig(provider)
|
||||
}
|
||||
|
||||
export const aihubmixProviderCreator = provider2Provider.bind(null, AIHUBMIX_RULES)
|
||||
|
||||
@@ -9,11 +9,15 @@ import { isOpenAIChatCompletionOnlyModel } from '@renderer/config/models'
|
||||
import {
|
||||
isAnthropicProvider,
|
||||
isAzureOpenAIProvider,
|
||||
isCherryAIProvider,
|
||||
isGeminiProvider,
|
||||
isNewApiProvider
|
||||
isNewApiProvider,
|
||||
isPerplexityProvider
|
||||
} from '@renderer/config/providers'
|
||||
import {
|
||||
getAwsBedrockAccessKeyId,
|
||||
getAwsBedrockApiKey,
|
||||
getAwsBedrockAuthType,
|
||||
getAwsBedrockRegion,
|
||||
getAwsBedrockSecretAccessKey
|
||||
} from '@renderer/hooks/useAwsBedrock'
|
||||
@@ -98,6 +102,10 @@ function formatProviderApiHost(provider: Provider): Provider {
|
||||
formatted.apiHost = formatAzureOpenAIApiHost(formatted.apiHost)
|
||||
} else if (isVertexProvider(formatted)) {
|
||||
formatted.apiHost = formatVertexApiHost(formatted)
|
||||
} else if (isCherryAIProvider(formatted)) {
|
||||
formatted.apiHost = formatApiHost(formatted.apiHost, false)
|
||||
} else if (isPerplexityProvider(formatted)) {
|
||||
formatted.apiHost = formatApiHost(formatted.apiHost, false)
|
||||
} else {
|
||||
formatted.apiHost = formatApiHost(formatted.apiHost)
|
||||
}
|
||||
@@ -192,9 +200,15 @@ export function providerToAiSdkConfig(
|
||||
|
||||
// bedrock
|
||||
if (aiSdkProviderId === 'bedrock') {
|
||||
const authType = getAwsBedrockAuthType()
|
||||
extraOptions.region = getAwsBedrockRegion()
|
||||
extraOptions.accessKeyId = getAwsBedrockAccessKeyId()
|
||||
extraOptions.secretAccessKey = getAwsBedrockSecretAccessKey()
|
||||
|
||||
if (authType === 'apiKey') {
|
||||
extraOptions.apiKey = getAwsBedrockApiKey()
|
||||
} else {
|
||||
extraOptions.accessKeyId = getAwsBedrockAccessKeyId()
|
||||
extraOptions.secretAccessKey = getAwsBedrockSecretAccessKey()
|
||||
}
|
||||
}
|
||||
// google-vertex
|
||||
if (aiSdkProviderId === 'google-vertex' || aiSdkProviderId === 'google-vertex-anthropic') {
|
||||
|
||||
@@ -17,6 +17,7 @@ import { getAiSdkProviderId } from '../provider/factory'
|
||||
import { buildGeminiGenerateImageParams } from './image'
|
||||
import {
|
||||
getAnthropicReasoningParams,
|
||||
getBedrockReasoningParams,
|
||||
getCustomParameters,
|
||||
getGeminiReasoningParams,
|
||||
getOpenAIReasoningParams,
|
||||
@@ -127,6 +128,9 @@ export function buildProviderOptions(
|
||||
case 'google-vertex-anthropic':
|
||||
providerSpecificOptions = buildAnthropicProviderOptions(assistant, model, capabilities)
|
||||
break
|
||||
case 'bedrock':
|
||||
providerSpecificOptions = buildBedrockProviderOptions(assistant, model, capabilities)
|
||||
break
|
||||
default:
|
||||
// 对于其他 provider,使用通用的构建逻辑
|
||||
providerSpecificOptions = {
|
||||
@@ -266,6 +270,32 @@ function buildXAIProviderOptions(
|
||||
return providerOptions
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Bedrock providerOptions
|
||||
*/
|
||||
function buildBedrockProviderOptions(
|
||||
assistant: Assistant,
|
||||
model: Model,
|
||||
capabilities: {
|
||||
enableReasoning: boolean
|
||||
enableWebSearch: boolean
|
||||
enableGenerateImage: boolean
|
||||
}
|
||||
): Record<string, any> {
|
||||
const { enableReasoning } = capabilities
|
||||
let providerOptions: Record<string, any> = {}
|
||||
|
||||
if (enableReasoning) {
|
||||
const reasoningParams = getBedrockReasoningParams(assistant, model)
|
||||
providerOptions = {
|
||||
...providerOptions,
|
||||
...reasoningParams
|
||||
}
|
||||
}
|
||||
|
||||
return providerOptions
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建通用的 providerOptions(用于其他 provider)
|
||||
*/
|
||||
|
||||
@@ -98,7 +98,7 @@ export function getReasoningEffort(assistant: Assistant, model: Model): Reasonin
|
||||
extra_body: {
|
||||
google: {
|
||||
thinking_config: {
|
||||
thinkingBudget: 0
|
||||
thinking_budget: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -259,8 +259,8 @@ export function getReasoningEffort(assistant: Assistant, model: Model): Reasonin
|
||||
extra_body: {
|
||||
google: {
|
||||
thinking_config: {
|
||||
thinkingBudget: -1,
|
||||
includeThoughts: true
|
||||
thinking_budget: -1,
|
||||
include_thoughts: true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -270,8 +270,8 @@ export function getReasoningEffort(assistant: Assistant, model: Model): Reasonin
|
||||
extra_body: {
|
||||
google: {
|
||||
thinking_config: {
|
||||
thinkingBudget: budgetTokens,
|
||||
includeThoughts: true
|
||||
thinking_budget: budgetTokens ?? -1,
|
||||
include_thoughts: true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -431,8 +431,8 @@ export function getGeminiReasoningParams(assistant: Assistant, model: Model): Re
|
||||
if (reasoningEffort === undefined) {
|
||||
return {
|
||||
thinkingConfig: {
|
||||
includeThoughts: false,
|
||||
...(GEMINI_FLASH_MODEL_REGEX.test(model.id) ? { thinkingBudget: 0 } : {})
|
||||
include_thoughts: false,
|
||||
...(GEMINI_FLASH_MODEL_REGEX.test(model.id) ? { thinking_budget: 0 } : {})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -442,7 +442,7 @@ export function getGeminiReasoningParams(assistant: Assistant, model: Model): Re
|
||||
if (effortRatio > 1) {
|
||||
return {
|
||||
thinkingConfig: {
|
||||
includeThoughts: true
|
||||
include_thoughts: true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -452,8 +452,8 @@ export function getGeminiReasoningParams(assistant: Assistant, model: Model): Re
|
||||
|
||||
return {
|
||||
thinkingConfig: {
|
||||
...(budget > 0 ? { thinkingBudget: budget } : {}),
|
||||
includeThoughts: true
|
||||
...(budget > 0 ? { thinking_budget: budget } : {}),
|
||||
include_thoughts: true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -485,6 +485,34 @@ export function getXAIReasoningParams(assistant: Assistant, model: Model): Recor
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Bedrock reasoning parameters
|
||||
*/
|
||||
export function getBedrockReasoningParams(assistant: Assistant, model: Model): Record<string, any> {
|
||||
if (!isReasoningModel(model)) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const reasoningEffort = assistant?.settings?.reasoning_effort
|
||||
|
||||
if (reasoningEffort === undefined) {
|
||||
return {}
|
||||
}
|
||||
|
||||
// Only apply thinking budget for Claude reasoning models
|
||||
if (!isSupportedThinkingTokenClaudeModel(model)) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const budgetTokens = getAnthropicThinkingBudget(assistant, model)
|
||||
return {
|
||||
reasoningConfig: {
|
||||
type: 'enabled',
|
||||
budgetTokens: budgetTokens
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取自定义参数
|
||||
* 从 assistant 设置中提取自定义参数
|
||||
|
||||
BIN
src/renderer/src/assets/images/providers/mcprouter.webp
Normal file
BIN
src/renderer/src/assets/images/providers/mcprouter.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
13
src/renderer/src/assets/images/providers/sophnet.svg
Normal file
13
src/renderer/src/assets/images/providers/sophnet.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="33" height="38" viewBox="0 0 33 38" fill="none">
|
||||
<g clip-path="url(#clip0_4321_9943)">
|
||||
<path d="M1.51221 6.59813C1.51221 4.09263 3.54331 2.06152 6.04881 2.06152H27.9757C30.4812 2.06152 32.5123 4.09263 32.5123 6.59813C32.5123 9.10362 30.4812 11.1347 27.9757 11.1347H6.04881C3.54331 11.1347 1.51221 9.10362 1.51221 6.59813Z" fill="#6200EE"/>
|
||||
<path d="M3.38905 3.56467C5.26076 1.89906 8.12831 2.06615 9.79391 3.93785L22.1493 17.8221C23.8149 19.6938 23.6478 22.5614 21.7761 24.227C19.9044 25.8926 17.0369 25.7255 15.3713 23.8538L3.01586 9.96953C1.35026 8.09782 1.51734 5.23027 3.38905 3.56467Z" fill="#6200EE"/>
|
||||
<path d="M1.51221 20.9643C1.51221 18.4588 3.54331 16.4277 6.04881 16.4277H18.9025C21.408 16.4277 23.4391 18.4588 23.4391 20.9643C23.4391 23.4698 21.408 25.5009 18.9025 25.5009H6.04881C3.54331 25.5009 1.51221 23.4698 1.51221 20.9643Z" fill="#6200EE"/>
|
||||
<path d="M10.5854 32.3052C10.5854 34.8107 8.55431 36.8418 6.04881 36.8418C3.54331 36.8418 1.51221 34.8107 1.51221 32.3052C1.51221 29.7997 3.54331 27.7686 6.04881 27.7686C8.55431 27.7686 10.5854 29.7997 10.5854 32.3052Z" fill="#BF7AFF"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_4321_9943">
|
||||
<rect width="32.5124" height="36.9029" fill="white" transform="translate(0 0.548828)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -41,11 +41,11 @@ body,
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* #root {
|
||||
#root {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex: 1;
|
||||
} */
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
@import 'tailwindcss' source('../../../../renderer');
|
||||
@import 'tw-animate-css';
|
||||
|
||||
/* heroui */
|
||||
@plugin '../../hero.ts';
|
||||
@source '../../../../../node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}';
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
/* 如需自定义:
|
||||
@@ -156,11 +152,6 @@
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
|
||||
/* To disable drag title bar on toast. tailwind css doesn't provide such class name. */
|
||||
.hero-toast {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { Avatar, cn } from '@heroui/react'
|
||||
import { getModelLogoById } from '@renderer/config/models'
|
||||
import type { ApiModel } from '@renderer/types'
|
||||
import React from 'react'
|
||||
|
||||
import Ellipsis from './Ellipsis'
|
||||
|
||||
export interface ModelLabelProps extends Omit<React.ComponentPropsWithRef<'div'>, 'children'> {
|
||||
model?: ApiModel
|
||||
classNames?: {
|
||||
container?: string
|
||||
avatar?: string
|
||||
modelName?: string
|
||||
divider?: string
|
||||
providerName?: string
|
||||
}
|
||||
}
|
||||
|
||||
export const ApiModelLabel: React.FC<ModelLabelProps> = ({ model, className, classNames, ...props }) => {
|
||||
return (
|
||||
<div className={cn('flex items-center gap-1', className, classNames?.container)} {...props}>
|
||||
<Avatar
|
||||
src={model ? (getModelLogoById(model.id) ?? getModelLogoById(model.name)) : undefined}
|
||||
className={cn('h-4 w-4', classNames?.avatar)}
|
||||
/>
|
||||
<Ellipsis className={classNames?.modelName}>{model?.name}</Ellipsis>
|
||||
<span className={classNames?.divider}> | </span>
|
||||
<Ellipsis className={classNames?.providerName}>{model?.provider_name}</Ellipsis>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button, Popover, PopoverContent, PopoverTrigger } from '@heroui/react'
|
||||
import { Button, Popover } from 'antd'
|
||||
import React from 'react'
|
||||
|
||||
import EmojiPicker from '../EmojiPicker'
|
||||
@@ -10,13 +10,10 @@ type Props = {
|
||||
|
||||
export const EmojiAvatarWithPicker: React.FC<Props> = ({ emoji, onPick }) => {
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<Button size="sm" startContent={<span className="text-lg">{emoji}</span>} isIconOnly />
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<EmojiPicker onEmojiClick={onPick}></EmojiPicker>
|
||||
</PopoverContent>
|
||||
<Popover content={<EmojiPicker onEmojiClick={onPick} />} trigger="click">
|
||||
<Button type="text" style={{ width: 32, height: 32, fontSize: 18 }}>
|
||||
{emoji}
|
||||
</Button>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { cn } from '@heroui/react'
|
||||
import { cn } from '@renderer/utils'
|
||||
import type { ButtonProps } from 'antd'
|
||||
import { Button } from 'antd'
|
||||
import React, { memo } from 'react'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Button } from '@heroui/react'
|
||||
import { CheckIcon, XIcon } from 'lucide-react'
|
||||
import { CheckOutlined, CloseOutlined } from '@ant-design/icons'
|
||||
import { Button } from 'antd'
|
||||
import type { FC } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
|
||||
@@ -28,12 +28,22 @@ const ConfirmDialog: FC<Props> = ({ x, y, message, onConfirm, onCancel }) => {
|
||||
<div className="flex min-w-[160px] items-center rounded-lg border border-[var(--color-border)] bg-[var(--color-background)] p-3 shadow-[0_4px_12px_rgba(0,0,0,0.15)]">
|
||||
<div className="mr-2 text-sm leading-[1.4]">{message}</div>
|
||||
<div className="flex justify-center gap-2">
|
||||
<Button onPress={onCancel} radius="full" className="h-6 w-6 min-w-0 p-1" color="danger">
|
||||
<XIcon className="text-danger-foreground" size={16} />
|
||||
</Button>
|
||||
<Button onPress={onConfirm} radius="full" className="h-6 w-6 min-w-0 p-1" color="success">
|
||||
<CheckIcon className="text-success-foreground" size={16} />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onCancel}
|
||||
shape="circle"
|
||||
size="small"
|
||||
danger
|
||||
icon={<CloseOutlined />}
|
||||
style={{ width: 24, height: 24, minWidth: 24 }}
|
||||
/>
|
||||
<Button
|
||||
onClick={onConfirm}
|
||||
shape="circle"
|
||||
size="small"
|
||||
type="primary"
|
||||
icon={<CheckOutlined />}
|
||||
style={{ width: 24, height: 24, minWidth: 24, backgroundColor: '#52c41a' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Button } from '@heroui/button'
|
||||
import { formatErrorMessage } from '@renderer/utils/error'
|
||||
import { Button } from 'antd'
|
||||
import { Alert, Space } from 'antd'
|
||||
import type { ComponentType, ReactNode } from 'react'
|
||||
import type { FallbackProps } from 'react-error-boundary'
|
||||
@@ -24,10 +24,10 @@ const DefaultFallback: ComponentType<FallbackProps> = (props: FallbackProps): Re
|
||||
type="error"
|
||||
action={
|
||||
<Space>
|
||||
<Button size="sm" onPress={debug}>
|
||||
<Button size="small" onClick={debug}>
|
||||
{t('error.boundary.default.devtools')}
|
||||
</Button>
|
||||
<Button size="sm" onPress={reload}>
|
||||
<Button size="small" onClick={reload}>
|
||||
{t('error.boundary.default.reload')}
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { cn } from '@heroui/react'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { cn } from '@renderer/utils'
|
||||
import { ChevronRight } from 'lucide-react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { cn } from '@heroui/react'
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import { cn } from '@renderer/utils'
|
||||
import { Modal } from 'antd'
|
||||
import { Bot, MessageSquare } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
@@ -51,7 +51,7 @@ const PopupContainer: React.FC<Props> = ({ onSelect, resolve }) => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSelect('assistant')}
|
||||
className="group flex flex-col items-center gap-3 rounded-lg bg-[var(--color-background-soft)] p-6 transition-all hover:bg-[var(--color-hover)]"
|
||||
className="group flex cursor-pointer flex-col items-center gap-3 rounded-lg bg-[var(--color-background-soft)] p-6 transition-all hover:bg-[var(--color-hover)]"
|
||||
onMouseEnter={() => setHoveredOption('assistant')}
|
||||
onMouseLeave={() => setHoveredOption(null)}>
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-[var(--color-list-item)] transition-colors">
|
||||
@@ -73,7 +73,7 @@ const PopupContainer: React.FC<Props> = ({ onSelect, resolve }) => {
|
||||
<button
|
||||
onClick={() => handleSelect('agent')}
|
||||
type="button"
|
||||
className="group flex flex-col items-center gap-3 rounded-lg bg-[var(--color-background-soft)] p-6 transition-all hover:bg-[var(--color-hover)]"
|
||||
className="group flex cursor-pointer flex-col items-center gap-3 rounded-lg bg-[var(--color-background-soft)] p-6 transition-all hover:bg-[var(--color-hover)]"
|
||||
onMouseEnter={() => setHoveredOption('agent')}
|
||||
onMouseLeave={() => setHoveredOption(null)}>
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-[var(--color-list-item)] transition-colors">
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { Button } from '@heroui/button'
|
||||
import { Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from '@heroui/modal'
|
||||
import { Progress } from '@heroui/progress'
|
||||
import { Spinner } from '@heroui/spinner'
|
||||
import { loggerService } from '@logger'
|
||||
import { AppLogo } from '@renderer/config/env'
|
||||
import { SettingHelpText, SettingRow } from '@renderer/pages/settings'
|
||||
import { WebSocketCandidatesResponse } from '@shared/config/types'
|
||||
import type { WebSocketCandidatesResponse } from '@shared/config/types'
|
||||
import { Alert, Button, Modal, Progress, Spin } from 'antd'
|
||||
import { QRCodeSVG } from 'qrcode.react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -25,7 +22,7 @@ const LoadingQRCode: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '12px' }}>
|
||||
<Spinner />
|
||||
<Spin />
|
||||
<span style={{ fontSize: '14px', color: 'var(--color-text-2)' }}>
|
||||
{t('settings.data.export_to_phone.lan.generating_qr')}
|
||||
</span>
|
||||
@@ -44,8 +41,8 @@ const ScanQRCode: React.FC<{ qrCodeValue: string }> = ({ qrCodeValue }) => {
|
||||
size={200}
|
||||
imageSettings={{
|
||||
src: AppLogo,
|
||||
width: 60,
|
||||
height: 60,
|
||||
width: 40,
|
||||
height: 40,
|
||||
excavate: true
|
||||
}}
|
||||
/>
|
||||
@@ -72,7 +69,7 @@ const ConnectingAnimation: React.FC = () => {
|
||||
borderRadius: '12px',
|
||||
backgroundColor: 'var(--color-status-warning)'
|
||||
}}>
|
||||
<Spinner size="lg" color="warning" />
|
||||
<Spin size="large" />
|
||||
<span style={{ fontSize: '14px', color: 'var(--color-text)', marginTop: '12px' }}>
|
||||
{t('settings.data.export_to_phone.lan.status.connecting')}
|
||||
</span>
|
||||
@@ -137,7 +134,6 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
const [selectedFolderPath, setSelectedFolderPath] = useState<string | null>(null)
|
||||
const [sendProgress, setSendProgress] = useState(0)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [showCloseConfirm, setShowCloseConfirm] = useState(false)
|
||||
const [autoCloseCountdown, setAutoCloseCountdown] = useState<number | null>(null)
|
||||
|
||||
const { t } = useTranslation()
|
||||
@@ -299,22 +295,20 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
// 尝试关闭弹窗 - 如果正在传输则显示确认
|
||||
const handleCancel = useCallback(() => {
|
||||
if (isSending) {
|
||||
setShowCloseConfirm(true)
|
||||
window.modal.confirm({
|
||||
title: t('settings.data.export_to_phone.lan.confirm_close_title'),
|
||||
content: t('settings.data.export_to_phone.lan.confirm_close_message'),
|
||||
centered: true,
|
||||
okButtonProps: {
|
||||
danger: true
|
||||
},
|
||||
okText: t('settings.data.export_to_phone.lan.force_close'),
|
||||
onOk: () => setIsOpen(false)
|
||||
})
|
||||
} else {
|
||||
setIsOpen(false)
|
||||
}
|
||||
}, [isSending])
|
||||
|
||||
// 确认强制关闭
|
||||
const handleForceClose = useCallback(() => {
|
||||
logger.info('Force closing popup during transfer')
|
||||
setIsOpen(false)
|
||||
}, [])
|
||||
|
||||
// 取消关闭确认
|
||||
const handleCancelClose = useCallback(() => {
|
||||
setShowCloseConfirm(false)
|
||||
}, [])
|
||||
}, [isSending, t])
|
||||
|
||||
// 清理并关闭
|
||||
const handleClose = useCallback(async () => {
|
||||
@@ -376,11 +370,13 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '8px',
|
||||
padding: '8px 12px',
|
||||
borderRadius: '8px',
|
||||
padding: '5px 12px',
|
||||
width: '100%',
|
||||
backgroundColor: connectionStatusStyles.bg,
|
||||
border: `1px solid ${connectionStatusStyles.border}`
|
||||
border: `1px solid ${connectionStatusStyles.border}`,
|
||||
marginBottom: 10
|
||||
}}>
|
||||
<span style={{ fontSize: '14px', fontWeight: '500', color: 'var(--color-text)' }}>{connectionStatusText}</span>
|
||||
</div>
|
||||
@@ -412,7 +408,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
if (!isSending && transferPhase !== 'completed') return null
|
||||
|
||||
return (
|
||||
<div style={{ paddingTop: '8px' }}>
|
||||
<div style={{ paddingTop: '20px' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
@@ -441,11 +437,9 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
</div>
|
||||
|
||||
<Progress
|
||||
value={Math.round(sendProgress)}
|
||||
size="md"
|
||||
color={transferPhase === 'completed' ? 'success' : 'primary'}
|
||||
showValueLabel={false}
|
||||
aria-label="Send progress"
|
||||
percent={Math.round(sendProgress)}
|
||||
status={transferPhase === 'completed' ? 'success' : 'active'}
|
||||
showInfo={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -488,95 +482,50 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
handleCancel()
|
||||
}
|
||||
}}
|
||||
isDismissable={false}
|
||||
isKeyboardDismissDisabled={false}
|
||||
placement="center"
|
||||
onClose={handleClose}>
|
||||
<ModalContent>
|
||||
{() => (
|
||||
<>
|
||||
<ModalHeader>{t('settings.data.export_to_phone.lan.title')}</ModalHeader>
|
||||
<ModalBody>
|
||||
<SettingRow>
|
||||
<StatusIndicator />
|
||||
</SettingRow>
|
||||
open={isOpen}
|
||||
onCancel={handleCancel}
|
||||
afterClose={handleClose}
|
||||
title={t('settings.data.export_to_phone.lan.title')}
|
||||
centered
|
||||
closable={!isSending}
|
||||
maskClosable={false}
|
||||
keyboard={true}
|
||||
footer={null}
|
||||
styles={{ body: { paddingBottom: 10 } }}>
|
||||
<SettingRow>
|
||||
<StatusIndicator />
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow>
|
||||
<div>{t('settings.data.export_to_phone.lan.content')}</div>
|
||||
</SettingRow>
|
||||
<Alert message={t('settings.data.export_to_phone.lan.content')} type="info" style={{ borderRadius: 0 }} />
|
||||
|
||||
<SettingRow style={{ display: 'flex', justifyContent: 'center', minHeight: '180px' }}>
|
||||
<QRCodeDisplay />
|
||||
</SettingRow>
|
||||
<SettingRow style={{ display: 'flex', justifyContent: 'center', minHeight: '180px', marginBlock: 25 }}>
|
||||
<QRCodeDisplay />
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<div style={{ display: 'flex', gap: 10, justifyContent: 'center', width: '100%' }}>
|
||||
<Button color="default" variant="flat" onPress={handleSelectZip} isDisabled={isSending}>
|
||||
{t('settings.data.export_to_phone.lan.selectZip')}
|
||||
</Button>
|
||||
<Button color="primary" onPress={handleSendZip} isDisabled={!canSend} isLoading={isSending}>
|
||||
{transferStatusText || t('settings.data.export_to_phone.lan.sendZip')}
|
||||
</Button>
|
||||
</div>
|
||||
</SettingRow>
|
||||
<SettingRow style={{ display: 'flex', alignItems: 'center', marginBlock: 10 }}>
|
||||
<div style={{ display: 'flex', gap: 10, justifyContent: 'center', width: '100%' }}>
|
||||
<Button onClick={handleSelectZip} disabled={isSending}>
|
||||
{t('settings.data.export_to_phone.lan.selectZip')}
|
||||
</Button>
|
||||
<Button type="primary" onClick={handleSendZip} disabled={!canSend} loading={isSending}>
|
||||
{transferStatusText || t('settings.data.export_to_phone.lan.sendZip')}
|
||||
</Button>
|
||||
</div>
|
||||
</SettingRow>
|
||||
|
||||
<SettingHelpText
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
{selectedFolderPath || t('settings.data.export_to_phone.lan.noZipSelected')}
|
||||
</SettingHelpText>
|
||||
<SettingHelpText
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
{selectedFolderPath || t('settings.data.export_to_phone.lan.noZipSelected')}
|
||||
</SettingHelpText>
|
||||
|
||||
<TransferProgress />
|
||||
<AutoCloseCountdown />
|
||||
<ErrorDisplay />
|
||||
</ModalBody>
|
||||
|
||||
{showCloseConfirm && (
|
||||
<ModalFooter>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
gap: '12px',
|
||||
padding: '8px',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: 'var(--color-status-warning)',
|
||||
border: '1px solid var(--color-status-warning)'
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<span style={{ fontSize: '20px' }}>⚠️</span>
|
||||
<span style={{ fontSize: '14px', color: 'var(--color-text)', fontWeight: '500' }}>
|
||||
{t('settings.data.export_to_phone.lan.confirm_close_title')}
|
||||
</span>
|
||||
</div>
|
||||
<span style={{ fontSize: '13px', color: 'var(--color-text-2)', marginLeft: '28px' }}>
|
||||
{t('settings.data.export_to_phone.lan.confirm_close_message')}
|
||||
</span>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '8px', marginTop: '4px' }}>
|
||||
<Button size="sm" color="default" variant="flat" onPress={handleCancelClose}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button size="sm" color="danger" onPress={handleForceClose}>
|
||||
{t('settings.data.export_to_phone.lan.force_close')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</ModalContent>
|
||||
<TransferProgress />
|
||||
<AutoCloseCountdown />
|
||||
<ErrorDisplay />
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
205
src/renderer/src/components/Popups/UpdateDialogPopup.tsx
Normal file
205
src/renderer/src/components/Popups/UpdateDialogPopup.tsx
Normal file
@@ -0,0 +1,205 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import { handleSaveData } from '@renderer/store'
|
||||
import { Button, Modal } from 'antd'
|
||||
import type { ReleaseNoteInfo, UpdateInfo } from 'builder-util-runtime'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Markdown from 'react-markdown'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const logger = loggerService.withContext('UpdateDialog')
|
||||
|
||||
interface ShowParams {
|
||||
releaseInfo: UpdateInfo | null
|
||||
}
|
||||
|
||||
interface Props extends ShowParams {
|
||||
resolve: (data: any) => void
|
||||
}
|
||||
|
||||
const PopupContainer: React.FC<Props> = ({ releaseInfo, resolve }) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(true)
|
||||
const [isInstalling, setIsInstalling] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (releaseInfo) {
|
||||
logger.info('Update dialog opened', { version: releaseInfo.version })
|
||||
}
|
||||
}, [releaseInfo])
|
||||
|
||||
const handleInstall = async () => {
|
||||
setIsInstalling(true)
|
||||
try {
|
||||
await handleSaveData()
|
||||
await window.api.quitAndInstall()
|
||||
setOpen(false)
|
||||
} catch (error) {
|
||||
logger.error('Failed to save data before update', error as Error)
|
||||
setIsInstalling(false)
|
||||
window.toast.error(t('update.saveDataError'))
|
||||
}
|
||||
}
|
||||
|
||||
const onCancel = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const onClose = () => {
|
||||
resolve({})
|
||||
}
|
||||
|
||||
UpdateDialogPopup.hide = onCancel
|
||||
|
||||
const releaseNotes = releaseInfo?.releaseNotes
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<ModalHeaderWrapper>
|
||||
<h3>{t('update.title')}</h3>
|
||||
<p>{t('update.message').replace('{{version}}', releaseInfo?.version || '')}</p>
|
||||
</ModalHeaderWrapper>
|
||||
}
|
||||
open={open}
|
||||
onCancel={onCancel}
|
||||
afterClose={onClose}
|
||||
transitionName="animation-move-down"
|
||||
centered
|
||||
width={720}
|
||||
footer={[
|
||||
<Button key="later" onClick={onCancel} disabled={isInstalling}>
|
||||
{t('update.later')}
|
||||
</Button>,
|
||||
<Button key="install" type="primary" onClick={handleInstall} loading={isInstalling}>
|
||||
{t('update.install')}
|
||||
</Button>
|
||||
]}>
|
||||
<ModalBodyWrapper>
|
||||
<ReleaseNotesWrapper className="markdown">
|
||||
<Markdown>
|
||||
{typeof releaseNotes === 'string'
|
||||
? releaseNotes
|
||||
: Array.isArray(releaseNotes)
|
||||
? releaseNotes
|
||||
.map((note: ReleaseNoteInfo) => note.note)
|
||||
.filter(Boolean)
|
||||
.join('\n\n')
|
||||
: t('update.noReleaseNotes')}
|
||||
</Markdown>
|
||||
</ReleaseNotesWrapper>
|
||||
</ModalBodyWrapper>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
const TopViewKey = 'UpdateDialogPopup'
|
||||
|
||||
export default class UpdateDialogPopup {
|
||||
static topviewId = 0
|
||||
static hide() {
|
||||
TopView.hide(TopViewKey)
|
||||
}
|
||||
static show(props: ShowParams) {
|
||||
return new Promise<any>((resolve) => {
|
||||
TopView.show(
|
||||
<PopupContainer
|
||||
{...props}
|
||||
resolve={(v) => {
|
||||
resolve(v)
|
||||
TopView.hide(TopViewKey)
|
||||
}}
|
||||
/>,
|
||||
TopViewKey
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const ModalHeaderWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
`
|
||||
|
||||
const ModalBodyWrapper = styled.div`
|
||||
max-height: 450px;
|
||||
overflow-y: auto;
|
||||
padding: 12px 0;
|
||||
`
|
||||
|
||||
const ReleaseNotesWrapper = styled.div`
|
||||
background-color: var(--color-bg-2);
|
||||
border-radius: 8px;
|
||||
|
||||
p {
|
||||
margin: 0 0 12px 0;
|
||||
color: var(--color-text-2);
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin: 16px 0 8px 0;
|
||||
color: var(--color-text-1);
|
||||
font-weight: 600;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
margin: 8px 0;
|
||||
padding-left: 24px;
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
|
||||
li {
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 2px 6px;
|
||||
background-color: var(--color-bg-3);
|
||||
border-radius: 4px;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 12px;
|
||||
background-color: var(--color-bg-3);
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
|
||||
code {
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
`
|
||||
@@ -1,44 +1,32 @@
|
||||
import type { SelectedItemProps } from '@heroui/react'
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
Input,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
Select,
|
||||
SelectItem,
|
||||
Textarea,
|
||||
useDisclosure
|
||||
} from '@heroui/react'
|
||||
import { loggerService } from '@logger'
|
||||
import type { Selection } from '@react-types/shared'
|
||||
import ClaudeIcon from '@renderer/assets/images/models/claude.png'
|
||||
import { ErrorBoundary } from '@renderer/components/ErrorBoundary'
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import { permissionModeCards } from '@renderer/config/agent'
|
||||
import { agentModelFilter, getModelLogoById } from '@renderer/config/models'
|
||||
import { useAgents } from '@renderer/hooks/agents/useAgents'
|
||||
import { useApiModels } from '@renderer/hooks/agents/useModels'
|
||||
import { useUpdateAgent } from '@renderer/hooks/agents/useUpdateAgent'
|
||||
import SelectAgentBaseModelButton from '@renderer/pages/home/components/SelectAgentBaseModelButton'
|
||||
import type {
|
||||
AddAgentForm,
|
||||
AgentEntity,
|
||||
AgentType,
|
||||
ApiModel,
|
||||
BaseAgentForm,
|
||||
PermissionMode,
|
||||
Tool,
|
||||
UpdateAgentForm
|
||||
} from '@renderer/types'
|
||||
import { AgentConfigurationSchema, isAgentType } from '@renderer/types'
|
||||
import { Avatar, Button, Input, Modal, Select } from 'antd'
|
||||
import { AlertTriangleIcon } from 'lucide-react'
|
||||
import type { ChangeEvent, FormEvent } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { ErrorBoundary } from '../../ErrorBoundary'
|
||||
import type { BaseOption, ModelOption } from './shared'
|
||||
import { Option, renderOption } from './shared'
|
||||
import type { BaseOption } from './shared'
|
||||
|
||||
const { TextArea } = Input
|
||||
|
||||
const logger = loggerService.withContext('AddAgentPopup')
|
||||
|
||||
@@ -48,8 +36,6 @@ interface AgentTypeOption extends BaseOption {
|
||||
name: AgentEntity['name']
|
||||
}
|
||||
|
||||
type Option = AgentTypeOption | ModelOption
|
||||
|
||||
type AgentWithTools = AgentEntity & { tools?: Tool[] }
|
||||
|
||||
const buildAgentForm = (existing?: AgentWithTools): BaseAgentForm => ({
|
||||
@@ -64,58 +50,37 @@ const buildAgentForm = (existing?: AgentWithTools): BaseAgentForm => ({
|
||||
configuration: AgentConfigurationSchema.parse(existing?.configuration ?? {})
|
||||
})
|
||||
|
||||
type Props = {
|
||||
interface ShowParams {
|
||||
agent?: AgentWithTools
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
afterSubmit?: (a: AgentEntity) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Modal component for creating or editing an agent.
|
||||
*
|
||||
* Either trigger or isOpen and onClose is given.
|
||||
* @param agent - Optional agent entity for editing mode.
|
||||
* @param isOpen - Optional controlled modal open state. From useDisclosure.
|
||||
* @param onClose - Optional callback when modal closes. From useDisclosure.
|
||||
* @returns Modal component for agent creation/editing
|
||||
*/
|
||||
export const AgentModal: React.FC<Props> = ({ agent, isOpen: _isOpen, onClose: _onClose, afterSubmit }) => {
|
||||
const { isOpen, onClose } = useDisclosure({ isOpen: _isOpen, onClose: _onClose })
|
||||
interface Props extends ShowParams {
|
||||
resolve: (data: any) => void
|
||||
}
|
||||
|
||||
const PopupContainer: React.FC<Props> = ({ agent, afterSubmit, resolve }) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(true)
|
||||
const loadingRef = useRef(false)
|
||||
// const { setTimeoutTimer } = useTimer()
|
||||
const { addAgent } = useAgents()
|
||||
const { updateAgent } = useUpdateAgent()
|
||||
// hard-coded. We only support anthropic for now.
|
||||
const { models } = useApiModels({ providerType: 'anthropic' })
|
||||
const isEditing = (agent?: AgentWithTools) => agent !== undefined
|
||||
|
||||
const [form, setForm] = useState<BaseAgentForm>(() => buildAgentForm(agent))
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
if (open) {
|
||||
setForm(buildAgentForm(agent))
|
||||
}
|
||||
}, [agent, isOpen])
|
||||
}, [agent, open])
|
||||
|
||||
const selectedPermissionMode = form.configuration?.permission_mode ?? 'default'
|
||||
|
||||
const onPermissionModeChange = useCallback((keys: Selection) => {
|
||||
if (keys === 'all') {
|
||||
return
|
||||
}
|
||||
|
||||
const [first] = Array.from(keys)
|
||||
if (!first) {
|
||||
return
|
||||
}
|
||||
|
||||
const onPermissionModeChange = useCallback((value: PermissionMode) => {
|
||||
setForm((prev) => {
|
||||
const parsedConfiguration = AgentConfigurationSchema.parse(prev.configuration ?? {})
|
||||
const nextMode = first as PermissionMode
|
||||
|
||||
if (parsedConfiguration.permission_mode === nextMode) {
|
||||
if (parsedConfiguration.permission_mode === value) {
|
||||
if (!prev.configuration) {
|
||||
return {
|
||||
...prev,
|
||||
@@ -129,7 +94,7 @@ export const AgentModal: React.FC<Props> = ({ agent, isOpen: _isOpen, onClose: _
|
||||
...prev,
|
||||
configuration: {
|
||||
...parsedConfiguration,
|
||||
permission_mode: nextMode
|
||||
permission_mode: value
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -150,55 +115,57 @@ export const AgentModal: React.FC<Props> = ({ agent, isOpen: _isOpen, onClose: _
|
||||
[]
|
||||
)
|
||||
|
||||
const agentOptions: AgentTypeOption[] = useMemo(
|
||||
const agentOptions = useMemo(
|
||||
() =>
|
||||
agentConfig.map(
|
||||
(option) =>
|
||||
({
|
||||
...option,
|
||||
rendered: <Option option={option} />
|
||||
}) as const satisfies SelectedItemProps
|
||||
),
|
||||
agentConfig.map((option) => ({
|
||||
value: option.key,
|
||||
label: (
|
||||
<OptionWrapper>
|
||||
<Avatar src={option.avatar} size={24} />
|
||||
<span>{option.label}</span>
|
||||
</OptionWrapper>
|
||||
)
|
||||
})),
|
||||
[agentConfig]
|
||||
)
|
||||
|
||||
const onAgentTypeChange = useCallback(
|
||||
(e: ChangeEvent<HTMLSelectElement>) => {
|
||||
(value: AgentType) => {
|
||||
const prevConfig = agentConfig.find((config) => config.key === form.type)
|
||||
let newName: string | undefined = form.name
|
||||
if (prevConfig && prevConfig.name === form.name) {
|
||||
const newConfig = agentConfig.find((config) => config.key === e.target.value)
|
||||
const newConfig = agentConfig.find((config) => config.key === value)
|
||||
if (newConfig) {
|
||||
newName = newConfig.name
|
||||
}
|
||||
}
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
type: e.target.value as AgentType,
|
||||
type: value,
|
||||
name: newName
|
||||
}))
|
||||
},
|
||||
[agentConfig, form.name, form.type]
|
||||
)
|
||||
|
||||
const onNameChange = useCallback((name: string) => {
|
||||
const onNameChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
name
|
||||
name: e.target.value
|
||||
}))
|
||||
}, [])
|
||||
|
||||
const onDescChange = useCallback((description: string) => {
|
||||
const onDescChange = useCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
description
|
||||
description: e.target.value
|
||||
}))
|
||||
}, [])
|
||||
|
||||
const onInstChange = useCallback((instructions: string) => {
|
||||
const onInstChange = useCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
instructions
|
||||
instructions: e.target.value
|
||||
}))
|
||||
}, [])
|
||||
|
||||
@@ -231,34 +198,36 @@ export const AgentModal: React.FC<Props> = ({ agent, isOpen: _isOpen, onClose: _
|
||||
}))
|
||||
}, [])
|
||||
|
||||
const modelOptions = useMemo(() => {
|
||||
// mocked data. not final version
|
||||
return (models ?? [])
|
||||
.filter((m) =>
|
||||
agentModelFilter({
|
||||
id: m.id,
|
||||
provider: m.provider || '',
|
||||
name: m.name,
|
||||
group: ''
|
||||
})
|
||||
)
|
||||
.map((model) => ({
|
||||
type: 'model',
|
||||
key: model.id,
|
||||
label: model.name,
|
||||
avatar: getModelLogoById(model.id),
|
||||
providerId: model.provider,
|
||||
providerName: model.provider_name
|
||||
})) satisfies ModelOption[]
|
||||
}, [models])
|
||||
// Create a temporary agentBase object for SelectAgentBaseModelButton
|
||||
const tempAgentBase: AgentEntity = useMemo(
|
||||
() => ({
|
||||
id: agent?.id ?? 'temp-creating',
|
||||
type: form.type,
|
||||
name: form.name,
|
||||
model: form.model,
|
||||
accessible_paths: form.accessible_paths.length > 0 ? form.accessible_paths : ['/'],
|
||||
allowed_tools: form.allowed_tools ?? [],
|
||||
description: form.description,
|
||||
instructions: form.instructions,
|
||||
configuration: form.configuration,
|
||||
created_at: agent?.created_at ?? new Date().toISOString(),
|
||||
updated_at: agent?.updated_at ?? new Date().toISOString()
|
||||
}),
|
||||
[form, agent?.id, agent?.created_at, agent?.updated_at]
|
||||
)
|
||||
|
||||
const onModelChange = useCallback((e: ChangeEvent<HTMLSelectElement>) => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
model: e.target.value
|
||||
}))
|
||||
const handleModelSelect = useCallback(async (model: ApiModel) => {
|
||||
setForm((prev) => ({ ...prev, model: model.id }))
|
||||
}, [])
|
||||
|
||||
const onCancel = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const onClose = () => {
|
||||
resolve({})
|
||||
}
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
@@ -330,9 +299,7 @@ export const AgentModal: React.FC<Props> = ({ agent, isOpen: _isOpen, onClose: _
|
||||
afterSubmit?.(result.data)
|
||||
}
|
||||
loadingRef.current = false
|
||||
|
||||
// setTimeoutTimer('onCreateAgent', () => EventEmitter.emit(EVENT_NAMES.SHOW_ASSISTANTS), 0)
|
||||
onClose()
|
||||
setOpen(false)
|
||||
},
|
||||
[
|
||||
form.type,
|
||||
@@ -344,7 +311,6 @@ export const AgentModal: React.FC<Props> = ({ agent, isOpen: _isOpen, onClose: _
|
||||
form.allowed_tools,
|
||||
form.configuration,
|
||||
agent,
|
||||
onClose,
|
||||
t,
|
||||
updateAgent,
|
||||
afterSubmit,
|
||||
@@ -352,138 +318,312 @@ export const AgentModal: React.FC<Props> = ({ agent, isOpen: _isOpen, onClose: _
|
||||
]
|
||||
)
|
||||
|
||||
AgentModalPopup.hide = onCancel
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
classNames={{
|
||||
base: 'max-h-[90vh]',
|
||||
wrapper: 'overflow-hidden'
|
||||
}}>
|
||||
<ModalContent>
|
||||
{(onClose) => (
|
||||
<>
|
||||
<ModalHeader>{isEditing(agent) ? t('agent.edit.title') : t('agent.add.title')}</ModalHeader>
|
||||
<Form onSubmit={onSubmit} className="min-h-0 w-full shrink overflow-auto">
|
||||
<ModalBody className="min-h-0 w-full flex-1 shrink overflow-auto">
|
||||
<div className="flex gap-2">
|
||||
<Select
|
||||
isRequired
|
||||
isDisabled={isEditing(agent)}
|
||||
selectionMode="single"
|
||||
selectedKeys={[form.type]}
|
||||
disallowEmptySelection
|
||||
onChange={onAgentTypeChange}
|
||||
items={agentOptions}
|
||||
label={t('agent.type.label')}
|
||||
placeholder={t('agent.add.type.placeholder')}
|
||||
renderValue={renderOption}>
|
||||
{(option) => (
|
||||
<SelectItem key={option.key} textValue={option.label}>
|
||||
<Option option={option} />
|
||||
</SelectItem>
|
||||
)}
|
||||
</Select>
|
||||
<Input isRequired value={form.name} onValueChange={onNameChange} label={t('common.name')} />
|
||||
</div>
|
||||
<Select
|
||||
isRequired
|
||||
selectionMode="single"
|
||||
selectedKeys={form.model ? [form.model] : []}
|
||||
disallowEmptySelection
|
||||
onChange={onModelChange}
|
||||
items={modelOptions}
|
||||
label={t('common.model')}
|
||||
placeholder={t('common.placeholders.select.model')}
|
||||
renderValue={renderOption}>
|
||||
{(option) => (
|
||||
<SelectItem key={option.key} textValue={option.label}>
|
||||
<Option option={option} />
|
||||
</SelectItem>
|
||||
)}
|
||||
</Select>
|
||||
<Select
|
||||
isRequired
|
||||
selectionMode="single"
|
||||
selectedKeys={[selectedPermissionMode]}
|
||||
onSelectionChange={onPermissionModeChange}
|
||||
label={t('agent.settings.tooling.permissionMode.title', 'Permission mode')}
|
||||
placeholder={t('agent.settings.tooling.permissionMode.placeholder', 'Select permission mode')}
|
||||
description={t(
|
||||
'agent.settings.tooling.permissionMode.helper',
|
||||
'Choose how the agent handles tool approvals.'
|
||||
)}
|
||||
items={permissionModeCards}>
|
||||
{(item) => (
|
||||
<SelectItem key={item.mode} textValue={t(item.titleKey, item.titleFallback)}>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="font-medium text-sm">{t(item.titleKey, item.titleFallback)}</span>
|
||||
<span className="text-foreground-500 text-xs">
|
||||
{t(item.descriptionKey, item.descriptionFallback)}
|
||||
</span>
|
||||
<span className="text-foreground-400 text-xs">
|
||||
{t(item.behaviorKey, item.behaviorFallback)}
|
||||
</span>
|
||||
{item.caution ? (
|
||||
<span className="flex items-center gap-1 text-danger-500 text-xs">
|
||||
<AlertTriangleIcon size={12} className="text-danger" />
|
||||
{t(
|
||||
'agent.settings.tooling.permissionMode.bypassPermissions.warning',
|
||||
'Use with caution — all tools will run without asking for approval.'
|
||||
)}
|
||||
</span>
|
||||
) : null}
|
||||
title={isEditing(agent) ? t('agent.edit.title') : t('agent.add.title')}
|
||||
open={open}
|
||||
onCancel={onCancel}
|
||||
afterClose={onClose}
|
||||
transitionName="animation-move-down"
|
||||
centered
|
||||
width={500}
|
||||
footer={null}>
|
||||
<StyledForm onSubmit={onSubmit}>
|
||||
<FormContent>
|
||||
<FormRow>
|
||||
<FormItem style={{ flex: 1 }}>
|
||||
<Label>{t('agent.type.label')}</Label>
|
||||
<Select
|
||||
value={form.type}
|
||||
onChange={onAgentTypeChange}
|
||||
options={agentOptions}
|
||||
disabled={isEditing(agent)}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem style={{ flex: 1 }}>
|
||||
<Label>
|
||||
{t('common.name')} <RequiredMark>*</RequiredMark>
|
||||
</Label>
|
||||
<Input value={form.name} onChange={onNameChange} required />
|
||||
</FormItem>
|
||||
</FormRow>
|
||||
|
||||
<FormItem>
|
||||
<Label>
|
||||
{t('common.model')} <RequiredMark>*</RequiredMark>
|
||||
</Label>
|
||||
<SelectAgentBaseModelButton
|
||||
agentBase={tempAgentBase}
|
||||
onSelect={handleModelSelect}
|
||||
fontSize={14}
|
||||
avatarSize={24}
|
||||
iconSize={16}
|
||||
buttonStyle={{
|
||||
padding: '8px 12px',
|
||||
width: '100%',
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: 6,
|
||||
height: 'auto'
|
||||
}}
|
||||
containerClassName="flex items-center justify-between w-full"
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
<FormItem>
|
||||
<Label>
|
||||
{t('agent.settings.tooling.permissionMode.title', 'Permission mode')} <RequiredMark>*</RequiredMark>
|
||||
</Label>
|
||||
<Select
|
||||
value={selectedPermissionMode}
|
||||
onChange={onPermissionModeChange}
|
||||
style={{ width: '100%' }}
|
||||
placeholder={t('agent.settings.tooling.permissionMode.placeholder', 'Select permission mode')}
|
||||
dropdownStyle={{ minWidth: '500px' }}
|
||||
optionLabelProp="label">
|
||||
{permissionModeCards.map((item) => (
|
||||
<Select.Option key={item.mode} value={item.mode} label={t(item.titleKey, item.titleFallback)}>
|
||||
<PermissionOptionWrapper>
|
||||
<div className="title">{t(item.titleKey, item.titleFallback)}</div>
|
||||
<div className="description">{t(item.descriptionKey, item.descriptionFallback)}</div>
|
||||
<div className="behavior">{t(item.behaviorKey, item.behaviorFallback)}</div>
|
||||
{item.caution && (
|
||||
<div className="caution">
|
||||
<AlertTriangleIcon size={12} />
|
||||
{t(
|
||||
'agent.settings.tooling.permissionMode.bypassPermissions.warning',
|
||||
'Use with caution — all tools will run without asking for approval.'
|
||||
)}
|
||||
</div>
|
||||
</SelectItem>
|
||||
)}
|
||||
</Select>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium text-foreground text-sm">
|
||||
{t('agent.session.accessible_paths.label')}
|
||||
</span>
|
||||
<Button size="sm" variant="flat" onPress={addAccessiblePath}>
|
||||
{t('agent.session.accessible_paths.add')}
|
||||
)}
|
||||
</PermissionOptionWrapper>
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<HelpText>
|
||||
{t('agent.settings.tooling.permissionMode.helper', 'Choose how the agent handles tool approvals.')}
|
||||
</HelpText>
|
||||
</FormItem>
|
||||
|
||||
<FormItem>
|
||||
<LabelWithButton>
|
||||
<Label>
|
||||
{t('agent.session.accessible_paths.label')} <RequiredMark>*</RequiredMark>
|
||||
</Label>
|
||||
<Button size="small" onClick={addAccessiblePath}>
|
||||
{t('agent.session.accessible_paths.add')}
|
||||
</Button>
|
||||
</LabelWithButton>
|
||||
{form.accessible_paths.length > 0 ? (
|
||||
<PathList>
|
||||
{form.accessible_paths.map((path) => (
|
||||
<PathItem key={path}>
|
||||
<PathText title={path}>{path}</PathText>
|
||||
<Button size="small" danger onClick={() => removeAccessiblePath(path)}>
|
||||
{t('common.delete')}
|
||||
</Button>
|
||||
</div>
|
||||
{form.accessible_paths.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{form.accessible_paths.map((path) => (
|
||||
<div
|
||||
key={path}
|
||||
className="flex items-center justify-between gap-2 rounded-medium border border-default-200 px-3 py-2">
|
||||
<span className="truncate text-sm" title={path}>
|
||||
{path}
|
||||
</span>
|
||||
<Button size="sm" variant="light" color="danger" onPress={() => removeAccessiblePath(path)}>
|
||||
{t('common.delete')}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-foreground-400 text-sm">{t('agent.session.accessible_paths.empty')}</p>
|
||||
)}
|
||||
</div>
|
||||
<Textarea label={t('common.prompt')} value={form.instructions ?? ''} onValueChange={onInstChange} />
|
||||
<Textarea
|
||||
label={t('common.description')}
|
||||
value={form.description ?? ''}
|
||||
onValueChange={onDescChange}
|
||||
/>
|
||||
</ModalBody>
|
||||
<ModalFooter className="w-full">
|
||||
<Button onPress={onClose}>{t('common.close')}</Button>
|
||||
<Button color="primary" type="submit" isLoading={loadingRef.current}>
|
||||
{isEditing(agent) ? t('common.confirm') : t('common.add')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Form>
|
||||
</>
|
||||
)}
|
||||
</ModalContent>
|
||||
</PathItem>
|
||||
))}
|
||||
</PathList>
|
||||
) : (
|
||||
<EmptyText>{t('agent.session.accessible_paths.empty')}</EmptyText>
|
||||
)}
|
||||
</FormItem>
|
||||
|
||||
<FormItem>
|
||||
<Label>{t('common.prompt')}</Label>
|
||||
<TextArea rows={3} value={form.instructions ?? ''} onChange={onInstChange} />
|
||||
</FormItem>
|
||||
|
||||
<FormItem>
|
||||
<Label>{t('common.description')}</Label>
|
||||
<TextArea rows={2} value={form.description ?? ''} onChange={onDescChange} />
|
||||
</FormItem>
|
||||
</FormContent>
|
||||
|
||||
<FormFooter>
|
||||
<Button onClick={onCancel}>{t('common.close')}</Button>
|
||||
<Button type="primary" htmlType="submit" loading={loadingRef.current}>
|
||||
{isEditing(agent) ? t('common.confirm') : t('common.add')}
|
||||
</Button>
|
||||
</FormFooter>
|
||||
</StyledForm>
|
||||
</Modal>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
const TopViewKey = 'AgentModalPopup'
|
||||
|
||||
export default class AgentModalPopup {
|
||||
static topviewId = 0
|
||||
static hide() {
|
||||
TopView.hide(TopViewKey)
|
||||
}
|
||||
static show(props: ShowParams) {
|
||||
return new Promise<any>((resolve) => {
|
||||
TopView.show(
|
||||
<PopupContainer
|
||||
{...props}
|
||||
resolve={(v) => {
|
||||
resolve(v)
|
||||
TopView.hide(TopViewKey)
|
||||
}}
|
||||
/>,
|
||||
TopViewKey
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Keep the old export for backward compatibility during migration
|
||||
export const AgentModal = AgentModalPopup
|
||||
|
||||
const StyledForm = styled.form`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
`
|
||||
|
||||
const FormContent = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
padding-right: 8px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: var(--color-border);
|
||||
border-radius: 3px;
|
||||
}
|
||||
`
|
||||
|
||||
const FormRow = styled.div`
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
`
|
||||
|
||||
const FormItem = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
`
|
||||
|
||||
const Label = styled.label`
|
||||
font-size: 14px;
|
||||
color: var(--color-text-1);
|
||||
font-weight: 500;
|
||||
`
|
||||
|
||||
const RequiredMark = styled.span`
|
||||
color: #ff4d4f;
|
||||
margin-left: 4px;
|
||||
`
|
||||
|
||||
const HelpText = styled.div`
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
`
|
||||
|
||||
const LabelWithButton = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const PathList = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
`
|
||||
|
||||
const PathItem = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
background-color: var(--color-bg-1);
|
||||
`
|
||||
|
||||
const PathText = styled.span`
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
color: var(--color-text-2);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`
|
||||
|
||||
const EmptyText = styled.p`
|
||||
font-size: 13px;
|
||||
color: var(--color-text-3);
|
||||
margin: 0;
|
||||
`
|
||||
|
||||
const FormFooter = styled.div`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
`
|
||||
|
||||
const OptionWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
`
|
||||
|
||||
const PermissionOptionWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 8px 0;
|
||||
|
||||
.title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-2);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.behavior {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.caution {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: #ff4d4f;
|
||||
margin-top: 4px;
|
||||
padding: 6px 8px;
|
||||
background-color: rgba(255, 77, 79, 0.1);
|
||||
border-radius: 4px;
|
||||
|
||||
svg {
|
||||
flex-shrink: 0;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@@ -1,320 +0,0 @@
|
||||
import {
|
||||
Button,
|
||||
cn,
|
||||
Form,
|
||||
Input,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
Textarea,
|
||||
useDisclosure
|
||||
} from '@heroui/react'
|
||||
import { loggerService } from '@logger'
|
||||
import type { Selection } from '@react-types/shared'
|
||||
import { AllowedToolsSelect } from '@renderer/components/agent'
|
||||
import { useAgent } from '@renderer/hooks/agents/useAgent'
|
||||
import { useSessions } from '@renderer/hooks/agents/useSessions'
|
||||
import { useUpdateSession } from '@renderer/hooks/agents/useUpdateSession'
|
||||
import type {
|
||||
AgentEntity,
|
||||
AgentSessionEntity,
|
||||
BaseSessionForm,
|
||||
CreateSessionForm,
|
||||
Tool,
|
||||
UpdateSessionForm
|
||||
} from '@renderer/types'
|
||||
import type { FormEvent, ReactNode } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { ErrorBoundary } from '../../ErrorBoundary'
|
||||
|
||||
const logger = loggerService.withContext('SessionAgentPopup')
|
||||
|
||||
type AgentWithTools = AgentEntity & { tools?: Tool[] }
|
||||
type SessionWithTools = AgentSessionEntity & { tools?: Tool[] }
|
||||
|
||||
const buildSessionForm = (existing?: SessionWithTools, agent?: AgentWithTools): BaseSessionForm => ({
|
||||
name: existing?.name ?? agent?.name ?? 'Claude Code',
|
||||
description: existing?.description ?? agent?.description,
|
||||
instructions: existing?.instructions ?? agent?.instructions,
|
||||
model: existing?.model ?? agent?.model ?? '',
|
||||
accessible_paths: existing?.accessible_paths
|
||||
? [...existing.accessible_paths]
|
||||
: agent?.accessible_paths
|
||||
? [...agent.accessible_paths]
|
||||
: [],
|
||||
allowed_tools: existing?.allowed_tools
|
||||
? [...existing.allowed_tools]
|
||||
: agent?.allowed_tools
|
||||
? [...agent.allowed_tools]
|
||||
: [],
|
||||
mcps: existing?.mcps ? [...existing.mcps] : agent?.mcps ? [...agent.mcps] : []
|
||||
})
|
||||
|
||||
interface BaseProps {
|
||||
agentId: string
|
||||
session?: SessionWithTools
|
||||
onSessionCreated?: (session: AgentSessionEntity) => void
|
||||
}
|
||||
|
||||
interface TriggerProps extends BaseProps {
|
||||
trigger: { content: ReactNode; className?: string }
|
||||
isOpen?: never
|
||||
onClose?: never
|
||||
}
|
||||
|
||||
interface StateProps extends BaseProps {
|
||||
trigger?: never
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
type Props = TriggerProps | StateProps
|
||||
|
||||
/**
|
||||
* Modal component for creating or editing a Session.
|
||||
* @deprecated may as a reference when migrating to v2
|
||||
*
|
||||
* Either trigger or isOpen and onClose is given.
|
||||
* @param agentId - The ID of agent which the session is related.
|
||||
* @param session - Optional session entity for editing mode.
|
||||
* @param trigger - Optional trigger element that opens the modal. It MUST propagate the click event to trigger the modal.
|
||||
* @param isOpen - Optional controlled modal open state. From useDisclosure.
|
||||
* @param onClose - Optional callback when modal closes. From useDisclosure.
|
||||
* @returns Modal component for agent creation/editing
|
||||
*/
|
||||
export const SessionModal: React.FC<Props> = ({
|
||||
agentId,
|
||||
session,
|
||||
trigger,
|
||||
isOpen: _isOpen,
|
||||
onClose: _onClose,
|
||||
onSessionCreated
|
||||
}) => {
|
||||
const { isOpen, onClose, onOpen } = useDisclosure({ isOpen: _isOpen, onClose: _onClose })
|
||||
const { t } = useTranslation()
|
||||
const loadingRef = useRef(false)
|
||||
// const { setTimeoutTimer } = useTimer()
|
||||
const { createSession } = useSessions(agentId)
|
||||
const { updateSession } = useUpdateSession(agentId)
|
||||
const { agent } = useAgent(agentId)
|
||||
const isEditing = (session?: AgentSessionEntity) => session !== undefined
|
||||
|
||||
const [form, setForm] = useState<BaseSessionForm>(() => buildSessionForm(session, agent ?? undefined))
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setForm(buildSessionForm(session, agent ?? undefined))
|
||||
}
|
||||
}, [session, agent, isOpen])
|
||||
|
||||
const availableTools = useMemo(() => session?.tools ?? agent?.tools ?? [], [agent?.tools, session?.tools])
|
||||
const selectedToolKeys = useMemo(() => new Set(form.allowed_tools ?? []), [form.allowed_tools])
|
||||
|
||||
useEffect(() => {
|
||||
if (!availableTools.length) {
|
||||
return
|
||||
}
|
||||
|
||||
setForm((prev) => {
|
||||
const allowed = prev.allowed_tools ?? []
|
||||
const validTools = allowed.filter((id) => availableTools.some((tool) => tool.id === id))
|
||||
if (validTools.length === allowed.length) {
|
||||
return prev
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
allowed_tools: validTools
|
||||
}
|
||||
})
|
||||
}, [availableTools])
|
||||
|
||||
const onNameChange = useCallback((name: string) => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
name
|
||||
}))
|
||||
}, [])
|
||||
|
||||
const onDescChange = useCallback((description: string) => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
description
|
||||
}))
|
||||
}, [])
|
||||
|
||||
const onInstChange = useCallback((instructions: string) => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
instructions
|
||||
}))
|
||||
}, [])
|
||||
|
||||
const onAllowedToolsChange = useCallback(
|
||||
(keys: Selection) => {
|
||||
setForm((prev) => {
|
||||
const existing = prev.allowed_tools ?? []
|
||||
if (keys === 'all') {
|
||||
return {
|
||||
...prev,
|
||||
allowed_tools: availableTools.map((tool) => tool.id)
|
||||
}
|
||||
}
|
||||
|
||||
const next = Array.from(keys).map(String)
|
||||
const filtered = availableTools.length
|
||||
? next.filter((id) => availableTools.some((tool) => tool.id === id))
|
||||
: next
|
||||
|
||||
if (existing.length === filtered.length && existing.every((id) => filtered.includes(id))) {
|
||||
return prev
|
||||
}
|
||||
|
||||
return {
|
||||
...prev,
|
||||
allowed_tools: filtered
|
||||
}
|
||||
})
|
||||
},
|
||||
[availableTools]
|
||||
)
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
if (loadingRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
loadingRef.current = true
|
||||
|
||||
// Additional validation check besides native HTML validation to ensure security
|
||||
if (!form.model) {
|
||||
window.toast.error(t('error.model.not_exists'))
|
||||
loadingRef.current = false
|
||||
return
|
||||
}
|
||||
|
||||
if (form.accessible_paths.length === 0) {
|
||||
window.toast.error(t('agent.session.accessible_paths.error.at_least_one'))
|
||||
loadingRef.current = false
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (isEditing(session)) {
|
||||
if (!session) {
|
||||
throw new Error('Agent is required for editing mode')
|
||||
}
|
||||
|
||||
const updatePayload = {
|
||||
id: session.id,
|
||||
name: form.name,
|
||||
description: form.description,
|
||||
instructions: form.instructions,
|
||||
model: form.model,
|
||||
accessible_paths: [...form.accessible_paths],
|
||||
allowed_tools: [...(form.allowed_tools ?? [])],
|
||||
mcps: [...(form.mcps ?? [])]
|
||||
} satisfies UpdateSessionForm
|
||||
|
||||
updateSession(updatePayload)
|
||||
logger.debug('Updated agent', updatePayload)
|
||||
} else {
|
||||
const newSession = {
|
||||
name: form.name,
|
||||
description: form.description,
|
||||
instructions: form.instructions,
|
||||
model: form.model,
|
||||
accessible_paths: [...form.accessible_paths],
|
||||
allowed_tools: [...(form.allowed_tools ?? [])],
|
||||
mcps: [...(form.mcps ?? [])]
|
||||
} satisfies CreateSessionForm
|
||||
const createdSession = await createSession(newSession)
|
||||
if (createdSession) {
|
||||
onSessionCreated?.(createdSession)
|
||||
}
|
||||
logger.debug('Added agent', newSession)
|
||||
}
|
||||
|
||||
// setTimeoutTimer('onCreateAgent', () => EventEmitter.emit(EVENT_NAMES.SHOW_ASSISTANTS), 0)
|
||||
onClose()
|
||||
} finally {
|
||||
loadingRef.current = false
|
||||
}
|
||||
},
|
||||
[
|
||||
form.model,
|
||||
form.name,
|
||||
form.description,
|
||||
form.instructions,
|
||||
form.accessible_paths,
|
||||
form.allowed_tools,
|
||||
form.mcps,
|
||||
session,
|
||||
onClose,
|
||||
onSessionCreated,
|
||||
t,
|
||||
updateSession,
|
||||
createSession
|
||||
]
|
||||
)
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
{/* NOTE: Hero UI Modal Pattern: Combine the Button and Modal components into a single
|
||||
encapsulated component. This is because the Modal component needs to bind the onOpen
|
||||
event handler to the Button for proper focus management.
|
||||
|
||||
Or just use external isOpen/onOpen/onClose to control modal state.
|
||||
*/}
|
||||
|
||||
{trigger && (
|
||||
<div
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onOpen()
|
||||
}}
|
||||
className={cn('w-full', trigger.className)}>
|
||||
{trigger.content}
|
||||
</div>
|
||||
)}
|
||||
<Modal isOpen={isOpen} onClose={onClose}>
|
||||
<ModalContent>
|
||||
{(onClose) => (
|
||||
<>
|
||||
<ModalHeader>
|
||||
{isEditing(session) ? t('agent.session.edit.title') : t('agent.session.add.title')}
|
||||
</ModalHeader>
|
||||
<Form onSubmit={onSubmit} className="w-full">
|
||||
<ModalBody className="w-full">
|
||||
<Input isRequired value={form.name} onValueChange={onNameChange} label={t('common.name')} />
|
||||
<Textarea
|
||||
label={t('common.description')}
|
||||
value={form.description ?? ''}
|
||||
onValueChange={onDescChange}
|
||||
/>
|
||||
<AllowedToolsSelect
|
||||
items={availableTools}
|
||||
selectedKeys={selectedToolKeys}
|
||||
onSelectionChange={onAllowedToolsChange}
|
||||
/>
|
||||
<Textarea label={t('common.prompt')} value={form.instructions ?? ''} onValueChange={onInstChange} />
|
||||
</ModalBody>
|
||||
<ModalFooter className="w-full">
|
||||
<Button onPress={onClose}>{t('common.close')}</Button>
|
||||
<Button color="primary" type="submit" isLoading={loadingRef.current}>
|
||||
{isEditing(session) ? t('common.confirm') : t('common.add')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Form>
|
||||
</>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
@@ -1,8 +1,3 @@
|
||||
import type { SelectedItemProps, SelectedItems } from '@heroui/react'
|
||||
import { Avatar } from '@heroui/react'
|
||||
import { getProviderLabel } from '@renderer/i18n/label'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export interface BaseOption {
|
||||
type: 'type' | 'model'
|
||||
key: string
|
||||
@@ -10,43 +5,3 @@ export interface BaseOption {
|
||||
// img src
|
||||
avatar?: string
|
||||
}
|
||||
|
||||
export interface ModelOption extends BaseOption {
|
||||
providerId?: string
|
||||
providerName?: string
|
||||
}
|
||||
|
||||
export function isModelOption(option: BaseOption): option is ModelOption {
|
||||
return option.type === 'model'
|
||||
}
|
||||
|
||||
export const Item = ({ item }: { item: SelectedItemProps<BaseOption> }) => <Option option={item.data} />
|
||||
|
||||
export const renderOption = (items: SelectedItems<BaseOption>) =>
|
||||
items.map((item) => <Item key={item.key} item={item} />)
|
||||
|
||||
export const Option = ({ option }: { option?: BaseOption | null }) => {
|
||||
const { t } = useTranslation()
|
||||
if (!option) {
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<Avatar name="?" className="h-5 w-5" />
|
||||
{t('common.invalid_value')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const providerLabel = (() => {
|
||||
if (!isModelOption(option)) return null
|
||||
if (option.providerName) return option.providerName
|
||||
if (option.providerId) return getProviderLabel(option.providerId)
|
||||
return null
|
||||
})()
|
||||
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<Avatar src={option.avatar} className="h-5 w-5" />
|
||||
<span className="truncate">{option.label}</span>
|
||||
{providerLabel ? <span className="truncate text-foreground-500">| {providerLabel}</span> : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -64,6 +64,7 @@ export type QuickPanelListItem = {
|
||||
isSelected?: boolean
|
||||
isMenu?: boolean
|
||||
disabled?: boolean
|
||||
hidden?: boolean
|
||||
/**
|
||||
* 固定显示项:不参与过滤,始终出现在列表顶部。
|
||||
* 例如“清除”按钮可设置为 alwaysVisible,从而在有匹配项时始终可见;
|
||||
|
||||
@@ -143,7 +143,8 @@ export const QuickPanelView: React.FC<Props> = ({ setInputText }) => {
|
||||
prevSymbolRef.current = ctx.symbol
|
||||
|
||||
// 固定项置顶 + 过滤后的普通项
|
||||
return [...pinnedItems, ...filteredNormalItems]
|
||||
const pinnedFiltered = [...pinnedItems, ...filteredNormalItems]
|
||||
return pinnedFiltered.filter((item) => !item.hidden)
|
||||
}, [ctx.isVisible, ctx.symbol, ctx.list, searchText])
|
||||
|
||||
const canForwardAndBackward = useMemo(() => {
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import { ToastProvider } from '@heroui/toast'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
|
||||
export const ToastPortal = () => {
|
||||
const [mounted, setMounted] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
return () => setMounted(false)
|
||||
}, [])
|
||||
|
||||
if (!mounted) return null
|
||||
|
||||
return createPortal(
|
||||
<ToastProvider
|
||||
placement="top-center"
|
||||
regionProps={{
|
||||
className: 'z-[1001]'
|
||||
}}
|
||||
toastOffset={20}
|
||||
toastProps={{
|
||||
timeout: 3000,
|
||||
classNames: {
|
||||
// This setting causes the 'hero-toast' class to be applied twice to the toast element. This is weird and I don't know why, but it works.
|
||||
base: 'hero-toast'
|
||||
}
|
||||
}}
|
||||
/>,
|
||||
document.body
|
||||
)
|
||||
}
|
||||
@@ -2,12 +2,12 @@
|
||||
import TopViewMinappContainer from '@renderer/components/MinApp/TopViewMinappContainer'
|
||||
import { useAppInit } from '@renderer/hooks/useAppInit'
|
||||
import { useShortcuts } from '@renderer/hooks/useShortcuts'
|
||||
import { Modal } from 'antd'
|
||||
import { message, Modal } from 'antd'
|
||||
import type { PropsWithChildren } from 'react'
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
import { Box } from '../Layout'
|
||||
import { getToastUtilities } from './toast'
|
||||
import { getToastUtilities, initMessageApi } from './toast'
|
||||
|
||||
let onPop = () => {}
|
||||
let onShow = ({ element, id }: { element: React.FC | React.ReactNode; id: string }) => {
|
||||
@@ -36,6 +36,7 @@ const TopViewContainer: React.FC<Props> = ({ children }) => {
|
||||
elementsRef.current = elements
|
||||
|
||||
const [modal, modalContextHolder] = Modal.useModal()
|
||||
const [messageApi, messageContextHolder] = message.useMessage()
|
||||
const { shortcuts } = useShortcuts()
|
||||
const enableQuitFullScreen = shortcuts.find((item) => item.key === 'exit_fullscreen')?.enabled
|
||||
|
||||
@@ -43,8 +44,9 @@ const TopViewContainer: React.FC<Props> = ({ children }) => {
|
||||
|
||||
useEffect(() => {
|
||||
window.modal = modal
|
||||
initMessageApi(messageApi)
|
||||
window.toast = getToastUtilities()
|
||||
}, [modal])
|
||||
}, [messageApi, modal])
|
||||
|
||||
onPop = () => {
|
||||
const views = [...elementsRef.current]
|
||||
@@ -97,6 +99,7 @@ const TopViewContainer: React.FC<Props> = ({ children }) => {
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
{messageContextHolder}
|
||||
{modalContextHolder}
|
||||
<TopViewMinappContainer />
|
||||
{elements.map(({ element: Element, id }) => (
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
import { addToast, closeAll, closeToast, getToastQueue, isToastClosing } from '@heroui/toast'
|
||||
import type { RequireSome } from '@renderer/types'
|
||||
|
||||
type AddToastProps = Parameters<typeof addToast>[0]
|
||||
type ToastPropsColored = Omit<AddToastProps, 'color'>
|
||||
|
||||
const createToast = (color: 'danger' | 'success' | 'warning' | 'default') => {
|
||||
return (arg: ToastPropsColored | string): string | null => {
|
||||
if (typeof arg === 'string') {
|
||||
return addToast({ color, title: arg })
|
||||
} else {
|
||||
return addToast({ color, ...arg })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// syntatic sugar, oh yeah
|
||||
|
||||
/**
|
||||
* Display an error toast notification with red color
|
||||
* @param arg - Toast content (string) or toast options object
|
||||
* @returns Toast ID or null
|
||||
*/
|
||||
export const error = createToast('danger')
|
||||
|
||||
/**
|
||||
* Display a success toast notification with green color
|
||||
* @param arg - Toast content (string) or toast options object
|
||||
* @returns Toast ID or null
|
||||
*/
|
||||
export const success = createToast('success')
|
||||
|
||||
/**
|
||||
* Display a warning toast notification with yellow color
|
||||
* @param arg - Toast content (string) or toast options object
|
||||
* @returns Toast ID or null
|
||||
*/
|
||||
export const warning = createToast('warning')
|
||||
|
||||
/**
|
||||
* Display an info toast notification with default color
|
||||
* @param arg - Toast content (string) or toast options object
|
||||
* @returns Toast ID or null
|
||||
*/
|
||||
export const info = createToast('default')
|
||||
|
||||
/**
|
||||
* Display a loading toast notification that resolves with a promise
|
||||
* @param args - Toast options object containing a promise to resolve
|
||||
* @returns Toast ID or null
|
||||
*/
|
||||
export const loading = (args: RequireSome<AddToastProps, 'promise'>) => {
|
||||
// Disappear immediately by default
|
||||
if (args.timeout === undefined) {
|
||||
args.timeout = 1
|
||||
}
|
||||
return addToast(args)
|
||||
}
|
||||
|
||||
export const getToastUtilities = () =>
|
||||
({
|
||||
getToastQueue,
|
||||
addToast,
|
||||
closeToast,
|
||||
closeAll,
|
||||
isToastClosing,
|
||||
error,
|
||||
success,
|
||||
warning,
|
||||
info,
|
||||
loading
|
||||
}) as const
|
||||
231
src/renderer/src/components/TopView/toast.tsx
Normal file
231
src/renderer/src/components/TopView/toast.tsx
Normal file
@@ -0,0 +1,231 @@
|
||||
import type { RequireSome } from '@renderer/types'
|
||||
import { message as antdMessage } from 'antd'
|
||||
import type { MessageInstance } from 'antd/es/message/interface'
|
||||
import type React from 'react'
|
||||
|
||||
// Global message instance for static usage
|
||||
let messageApi: MessageInstance | null = null
|
||||
|
||||
// Initialize message API - should be called once the App component is mounted
|
||||
export const initMessageApi = (api: MessageInstance) => {
|
||||
messageApi = api
|
||||
}
|
||||
|
||||
// Get message API instance
|
||||
const getMessageApi = (): MessageInstance => {
|
||||
if (!messageApi) {
|
||||
// Fallback to static method if hook API is not available
|
||||
return antdMessage
|
||||
}
|
||||
return messageApi
|
||||
}
|
||||
|
||||
type ToastColor = 'danger' | 'success' | 'warning' | 'default'
|
||||
type MessageType = 'error' | 'success' | 'warning' | 'info'
|
||||
|
||||
interface ToastConfig {
|
||||
title?: React.ReactNode
|
||||
icon?: React.ReactNode
|
||||
description?: React.ReactNode
|
||||
timeout?: number
|
||||
key?: string | number
|
||||
className?: string
|
||||
style?: React.CSSProperties
|
||||
onClick?: () => void
|
||||
onClose?: () => void
|
||||
}
|
||||
|
||||
interface LoadingToastConfig extends ToastConfig {
|
||||
promise: Promise<any>
|
||||
}
|
||||
|
||||
const colorToType = (color: ToastColor): MessageType => {
|
||||
switch (color) {
|
||||
case 'danger':
|
||||
return 'error'
|
||||
case 'success':
|
||||
return 'success'
|
||||
case 'warning':
|
||||
return 'warning'
|
||||
case 'default':
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
// Toast content component
|
||||
const ToastContent: React.FC<{ title?: React.ReactNode; description?: React.ReactNode; icon?: React.ReactNode }> = ({
|
||||
title,
|
||||
description,
|
||||
icon
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
{(icon || title) && (
|
||||
<div className="flex items-center gap-2 font-semibold">
|
||||
{icon}
|
||||
{title}
|
||||
</div>
|
||||
)}
|
||||
{description && <div className="text-sm">{description}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const createToast = (color: ToastColor) => {
|
||||
return (arg: ToastConfig | string): string | null => {
|
||||
const api = getMessageApi()
|
||||
const type = colorToType(color) as 'error' | 'success' | 'warning' | 'info'
|
||||
|
||||
if (typeof arg === 'string') {
|
||||
// antd message methods return a function to close the message
|
||||
api[type](arg)
|
||||
return null
|
||||
}
|
||||
|
||||
const { title, description, icon, timeout, ...restConfig } = arg
|
||||
|
||||
// Convert timeout from milliseconds to seconds (antd uses seconds)
|
||||
const duration = timeout !== undefined ? timeout / 1000 : 3
|
||||
|
||||
return (
|
||||
(api.open({
|
||||
type: type as 'error' | 'success' | 'warning' | 'info',
|
||||
content: <ToastContent title={title} description={description} icon={icon} />,
|
||||
duration,
|
||||
...restConfig
|
||||
}) as any) || null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display an error toast notification with red color
|
||||
* @param arg - Toast content (string) or toast options object
|
||||
* @returns Toast ID or null
|
||||
*/
|
||||
export const error = createToast('danger')
|
||||
|
||||
/**
|
||||
* Display a success toast notification with green color
|
||||
* @param arg - Toast content (string) or toast options object
|
||||
* @returns Toast ID or null
|
||||
*/
|
||||
export const success = createToast('success')
|
||||
|
||||
/**
|
||||
* Display a warning toast notification with yellow color
|
||||
* @param arg - Toast content (string) or toast options object
|
||||
* @returns Toast ID or null
|
||||
*/
|
||||
export const warning = createToast('warning')
|
||||
|
||||
/**
|
||||
* Display an info toast notification with default color
|
||||
* @param arg - Toast content (string) or toast options object
|
||||
* @returns Toast ID or null
|
||||
*/
|
||||
export const info = createToast('default')
|
||||
|
||||
/**
|
||||
* Display a loading toast notification that resolves with a promise
|
||||
* @param args - Toast options object containing a promise to resolve
|
||||
*/
|
||||
export const loading = (args: RequireSome<LoadingToastConfig, 'promise'>): string | null => {
|
||||
const api = getMessageApi()
|
||||
const { title, description, icon, promise, timeout, ...restConfig } = args
|
||||
|
||||
// Generate unique key for this loading message
|
||||
const key = args.key || `loading-${Date.now()}-${Math.random()}`
|
||||
|
||||
// Show loading message
|
||||
api.loading({
|
||||
content: <ToastContent title={title || 'Loading...'} description={description} icon={icon} />,
|
||||
duration: 0, // Don't auto-close
|
||||
key,
|
||||
...restConfig
|
||||
})
|
||||
|
||||
// Handle promise resolution
|
||||
promise
|
||||
.then((result) => {
|
||||
api.success({
|
||||
content: <ToastContent title={title || 'Success'} description={description} />,
|
||||
duration: timeout !== undefined ? timeout / 1000 : 2,
|
||||
key,
|
||||
...restConfig
|
||||
})
|
||||
return result
|
||||
})
|
||||
.catch((err) => {
|
||||
api.error({
|
||||
content: (
|
||||
<ToastContent title={title || 'Error'} description={err?.message || description || 'An error occurred'} />
|
||||
),
|
||||
duration: timeout !== undefined ? timeout / 1000 : 3,
|
||||
key,
|
||||
...restConfig
|
||||
})
|
||||
throw err
|
||||
})
|
||||
|
||||
return key as string
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a toast notification
|
||||
* @param config - Toast configuration object
|
||||
* @returns Toast ID or null
|
||||
*/
|
||||
export const addToast = (config: ToastConfig) => info(config)
|
||||
|
||||
/**
|
||||
* Close a specific toast notification by its key
|
||||
* @param key - Toast key (string)
|
||||
*/
|
||||
export const closeToast = (key: string) => {
|
||||
getMessageApi().destroy(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Close all toast notifications
|
||||
*/
|
||||
export const closeAll = () => {
|
||||
getMessageApi().destroy()
|
||||
}
|
||||
|
||||
/**
|
||||
* Stub functions for compatibility with previous toast API
|
||||
* These are no-ops since antd message doesn't expose a queue
|
||||
*/
|
||||
|
||||
/**
|
||||
* @deprecated This function is a no-op stub for backward compatibility only.
|
||||
* Antd message doesn't expose a queue. Do not rely on this function.
|
||||
* @returns Empty toast queue stub
|
||||
*/
|
||||
export const getToastQueue = (): any => ({ toasts: [] })
|
||||
|
||||
/**
|
||||
* @deprecated This function is a no-op stub for backward compatibility only.
|
||||
* Antd message doesn't track closing state. Do not rely on this function.
|
||||
* @param key - Toast key (unused)
|
||||
* @returns Always returns false
|
||||
*/
|
||||
export const isToastClosing = (key?: string): boolean => {
|
||||
key // unused
|
||||
return false
|
||||
}
|
||||
|
||||
export const getToastUtilities = () =>
|
||||
({
|
||||
getToastQueue,
|
||||
addToast,
|
||||
closeToast,
|
||||
closeAll,
|
||||
isToastClosing,
|
||||
error,
|
||||
success,
|
||||
warning,
|
||||
info,
|
||||
loading
|
||||
}) as const
|
||||
@@ -1,101 +0,0 @@
|
||||
import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader, ScrollShadow } from '@heroui/react'
|
||||
import { loggerService } from '@logger'
|
||||
import { handleSaveData } from '@renderer/store'
|
||||
import type { ReleaseNoteInfo, UpdateInfo } from 'builder-util-runtime'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Markdown from 'react-markdown'
|
||||
|
||||
const logger = loggerService.withContext('UpdateDialog')
|
||||
|
||||
interface UpdateDialogProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
releaseInfo: UpdateInfo | null
|
||||
}
|
||||
|
||||
const UpdateDialog: React.FC<UpdateDialogProps> = ({ isOpen, onClose, releaseInfo }) => {
|
||||
const { t } = useTranslation()
|
||||
const [isInstalling, setIsInstalling] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && releaseInfo) {
|
||||
logger.info('Update dialog opened', { version: releaseInfo.version })
|
||||
}
|
||||
}, [isOpen, releaseInfo])
|
||||
|
||||
const handleInstall = async () => {
|
||||
setIsInstalling(true)
|
||||
try {
|
||||
await handleSaveData()
|
||||
await window.api.quitAndInstall()
|
||||
} catch (error) {
|
||||
logger.error('Failed to save data before update', error as Error)
|
||||
setIsInstalling(false)
|
||||
window.toast.error(t('update.saveDataError'))
|
||||
}
|
||||
}
|
||||
|
||||
const releaseNotes = releaseInfo?.releaseNotes
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
size="2xl"
|
||||
scrollBehavior="inside"
|
||||
classNames={{
|
||||
base: 'max-h-[85vh]',
|
||||
header: 'border-b border-divider',
|
||||
footer: 'border-t border-divider'
|
||||
}}>
|
||||
<ModalContent>
|
||||
{(onModalClose) => (
|
||||
<>
|
||||
<ModalHeader className="flex flex-col gap-1">
|
||||
<h3 className="font-semibold text-lg">{t('update.title')}</h3>
|
||||
<p className="text-default-500 text-small">
|
||||
{t('update.message').replace('{{version}}', releaseInfo?.version || '')}
|
||||
</p>
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<ScrollShadow className="max-h-[450px]" hideScrollBar>
|
||||
<div className="markdown rounded-lg bg-default-50 p-4">
|
||||
<Markdown>
|
||||
{typeof releaseNotes === 'string'
|
||||
? releaseNotes
|
||||
: Array.isArray(releaseNotes)
|
||||
? releaseNotes
|
||||
.map((note: ReleaseNoteInfo) => note.note)
|
||||
.filter(Boolean)
|
||||
.join('\n\n')
|
||||
: t('update.noReleaseNotes')}
|
||||
</Markdown>
|
||||
</div>
|
||||
</ScrollShadow>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button variant="light" onPress={onModalClose} isDisabled={isInstalling}>
|
||||
{t('update.later')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
color="primary"
|
||||
onPress={async () => {
|
||||
await handleInstall()
|
||||
onModalClose()
|
||||
}}
|
||||
isLoading={isInstalling}>
|
||||
{t('update.install')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default UpdateDialog
|
||||
@@ -1,55 +0,0 @@
|
||||
import type { SelectedItems, SelectProps } from '@heroui/react'
|
||||
import { Chip, cn, Select, SelectItem } from '@heroui/react'
|
||||
import type { Tool } from '@renderer/types'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export interface AllowedToolsSelectProps extends Omit<SelectProps, 'children'> {
|
||||
items: Tool[]
|
||||
}
|
||||
|
||||
export const AllowedToolsSelect: React.FC<AllowedToolsSelectProps> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
const { items: availableTools, className, ...rest } = props
|
||||
|
||||
const renderSelectedTools = useCallback((items: SelectedItems<Tool>) => {
|
||||
if (!items.length) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{items.map((item) => (
|
||||
<Chip key={item.key} size="sm" variant="flat" className="max-w-[160px] truncate">
|
||||
{item.data?.name ?? item.textValue ?? item.key}
|
||||
</Chip>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Select
|
||||
aria-label={t('agent.session.allowed_tools.label')}
|
||||
selectionMode="multiple"
|
||||
isMultiline
|
||||
label={t('agent.session.allowed_tools.label')}
|
||||
placeholder={t('agent.session.allowed_tools.placeholder')}
|
||||
description={
|
||||
availableTools.length ? t('agent.session.allowed_tools.helper') : t('agent.session.allowed_tools.empty')
|
||||
}
|
||||
isDisabled={!availableTools.length}
|
||||
items={availableTools}
|
||||
renderValue={renderSelectedTools}
|
||||
className={cn('max-w-xl', className)}
|
||||
{...rest}>
|
||||
{(tool) => (
|
||||
<SelectItem key={tool.id} textValue={tool.name}>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium text-sm">{tool.name}</span>
|
||||
{tool.description ? <span className="text-foreground-500 text-xs">{tool.description}</span> : null}
|
||||
</div>
|
||||
</SelectItem>
|
||||
)}
|
||||
</Select>
|
||||
)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { AllowedToolsSelect } from './AllowedToolsSelect'
|
||||
@@ -27,6 +27,7 @@ export const SYSTEM_MODELS: Record<SystemProviderId | 'defaultModel', Model[]> =
|
||||
],
|
||||
cherryin: [],
|
||||
vertexai: [],
|
||||
sophnet: [],
|
||||
'302ai': [
|
||||
{
|
||||
id: 'deepseek-chat',
|
||||
|
||||
@@ -46,6 +46,7 @@ import Ph8ProviderLogo from '@renderer/assets/images/providers/ph8.png'
|
||||
import PPIOProviderLogo from '@renderer/assets/images/providers/ppio.png'
|
||||
import QiniuProviderLogo from '@renderer/assets/images/providers/qiniu.webp'
|
||||
import SiliconFlowProviderLogo from '@renderer/assets/images/providers/silicon.png'
|
||||
import SophnetProviderLogo from '@renderer/assets/images/providers/sophnet.svg'
|
||||
import StepProviderLogo from '@renderer/assets/images/providers/step.png'
|
||||
import TencentCloudProviderLogo from '@renderer/assets/images/providers/tencent-cloud-ti.png'
|
||||
import TogetherProviderLogo from '@renderer/assets/images/providers/together.png'
|
||||
@@ -246,6 +247,16 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
},
|
||||
sophnet: {
|
||||
id: 'sophnet',
|
||||
name: 'SophNet',
|
||||
type: 'openai',
|
||||
apiKey: '',
|
||||
apiHost: 'https://www.sophnet.com/api/open-apis/v1',
|
||||
models: [],
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
},
|
||||
ppio: {
|
||||
id: 'ppio',
|
||||
name: 'PPIO',
|
||||
@@ -729,7 +740,8 @@ export const PROVIDER_LOGO_MAP: AtLeast<SystemProviderId, string> = {
|
||||
poe: 'poe', // use svg icon component
|
||||
aionly: AiOnlyProviderLogo,
|
||||
longcat: LongCatProviderLogo,
|
||||
huggingface: HuggingfaceProviderLogo
|
||||
huggingface: HuggingfaceProviderLogo,
|
||||
sophnet: SophnetProviderLogo
|
||||
} as const
|
||||
|
||||
export function getProviderLogo(providerId: string) {
|
||||
@@ -808,6 +820,17 @@ export const PROVIDER_URLS: Record<SystemProviderId, ProviderUrls> = {
|
||||
models: 'https://ai.burncloud.com/pricing'
|
||||
}
|
||||
},
|
||||
sophnet: {
|
||||
api: {
|
||||
url: 'https://www.sophnet.com/api/open-apis/v1'
|
||||
},
|
||||
websites: {
|
||||
official: 'https://sophnet.com',
|
||||
apiKey: 'https://sophnet.com/#/project/key',
|
||||
docs: 'https://sophnet.com/docs/component/introduce.html',
|
||||
models: 'https://sophnet.com/#/model/list'
|
||||
}
|
||||
},
|
||||
ppio: {
|
||||
api: {
|
||||
url: 'https://api.ppinfra.com/v3/openai'
|
||||
@@ -1463,6 +1486,14 @@ export const isNewApiProvider = (provider: Provider) => {
|
||||
return ['new-api', 'cherryin'].includes(provider.id) || provider.type === 'new-api'
|
||||
}
|
||||
|
||||
export function isCherryAIProvider(provider: Provider): boolean {
|
||||
return provider.id === 'cherryai'
|
||||
}
|
||||
|
||||
export function isPerplexityProvider(provider: Provider): boolean {
|
||||
return provider.id === 'perplexity'
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为 OpenAI 兼容的提供商
|
||||
* @param {Provider} provider 提供商对象
|
||||
@@ -1488,7 +1519,7 @@ export function isGeminiProvider(provider: Provider): boolean {
|
||||
return provider.type === 'gemini'
|
||||
}
|
||||
|
||||
const NOT_SUPPORT_API_VERSION_PROVIDERS = ['github', 'copilot'] as const satisfies SystemProviderId[]
|
||||
const NOT_SUPPORT_API_VERSION_PROVIDERS = ['github', 'copilot', 'perplexity'] as const satisfies SystemProviderId[]
|
||||
|
||||
export const isSupportAPIVersionProvider = (provider: Provider) => {
|
||||
if (isSystemProvider(provider)) {
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { HeroUIProvider } from '@heroui/react'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
|
||||
const AppHeroUIProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const { language } = useSettings()
|
||||
return (
|
||||
<HeroUIProvider className="flex h-full w-full flex-1" locale={language}>
|
||||
{children}
|
||||
</HeroUIProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export { AppHeroUIProvider as HeroUIProvider }
|
||||
14
src/renderer/src/env.d.ts
vendored
14
src/renderer/src/env.d.ts
vendored
@@ -1,12 +1,22 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
import type { PermissionUpdate } from '@anthropic-ai/claude-agent-sdk'
|
||||
import type { addToast, closeAll, closeToast, getToastQueue, isToastClosing } from '@heroui/toast'
|
||||
import type KeyvStorage from '@kangfenmao/keyv-storage'
|
||||
import type { HookAPI } from 'antd/es/modal/useModal'
|
||||
import type { NavigateFunction } from 'react-router-dom'
|
||||
|
||||
import type { error, info, loading, success, warning } from './components/TopView/toast'
|
||||
import type {
|
||||
addToast,
|
||||
closeAll,
|
||||
closeToast,
|
||||
error,
|
||||
getToastQueue,
|
||||
info,
|
||||
isToastClosing,
|
||||
loading,
|
||||
success,
|
||||
warning
|
||||
} from './components/TopView/toast'
|
||||
|
||||
interface ImportMetaEnv {
|
||||
VITE_RENDERER_INTEGRATED_MODEL: string
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
import { heroui } from '@heroui/react'
|
||||
export default heroui()
|
||||
@@ -41,6 +41,7 @@ export const useAgents = () => {
|
||||
// NOTE: We only use the array for now. useUpdateAgent depends on this behavior.
|
||||
return result.data
|
||||
}, [apiServerConfig.enabled, apiServerRunning, client, t])
|
||||
|
||||
const { data, error, isLoading, mutate } = useSWR(swrKey, fetcher)
|
||||
const { chat } = useRuntime()
|
||||
const { activeAgentId } = chat
|
||||
|
||||
@@ -31,21 +31,24 @@ export const useApiServer = () => {
|
||||
try {
|
||||
const status = await window.api.apiServer.getStatus()
|
||||
setApiServerRunning(status.running)
|
||||
if (status.running && !apiServerConfig.enabled) {
|
||||
setApiServerEnabled(true)
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.error('Failed to check API server status:', error)
|
||||
} finally {
|
||||
setApiServerLoading(false)
|
||||
}
|
||||
}, [])
|
||||
}, [apiServerConfig.enabled, setApiServerEnabled])
|
||||
|
||||
const startApiServer = useCallback(async () => {
|
||||
if (apiServerLoading) return
|
||||
|
||||
setApiServerLoading(true)
|
||||
try {
|
||||
const result = await window.api.apiServer.start()
|
||||
if (result.success) {
|
||||
setApiServerRunning(true)
|
||||
setApiServerEnabled(true)
|
||||
window.toast.success(t('apiServer.messages.startSuccess'))
|
||||
} else {
|
||||
window.toast.error(t('apiServer.messages.startError') + result.error)
|
||||
@@ -55,16 +58,16 @@ export const useApiServer = () => {
|
||||
} finally {
|
||||
setApiServerLoading(false)
|
||||
}
|
||||
}, [apiServerLoading, t])
|
||||
}, [apiServerLoading, setApiServerEnabled, t])
|
||||
|
||||
const stopApiServer = useCallback(async () => {
|
||||
if (apiServerLoading) return
|
||||
|
||||
setApiServerLoading(true)
|
||||
try {
|
||||
const result = await window.api.apiServer.stop()
|
||||
if (result.success) {
|
||||
setApiServerRunning(false)
|
||||
setApiServerEnabled(false)
|
||||
window.toast.success(t('apiServer.messages.stopSuccess'))
|
||||
} else {
|
||||
window.toast.error(t('apiServer.messages.stopError') + result.error)
|
||||
@@ -74,14 +77,14 @@ export const useApiServer = () => {
|
||||
} finally {
|
||||
setApiServerLoading(false)
|
||||
}
|
||||
}, [apiServerLoading, t])
|
||||
}, [apiServerLoading, setApiServerEnabled, t])
|
||||
|
||||
const restartApiServer = useCallback(async () => {
|
||||
if (apiServerLoading) return
|
||||
|
||||
setApiServerLoading(true)
|
||||
try {
|
||||
const result = await window.api.apiServer.restart()
|
||||
setApiServerEnabled(result.success)
|
||||
if (result.success) {
|
||||
await checkApiServerStatus()
|
||||
window.toast.success(t('apiServer.messages.restartSuccess'))
|
||||
@@ -93,7 +96,7 @@ export const useApiServer = () => {
|
||||
} finally {
|
||||
setApiServerLoading(false)
|
||||
}
|
||||
}, [apiServerLoading, checkApiServerStatus, t])
|
||||
}, [apiServerLoading, checkApiServerStatus, setApiServerEnabled, t])
|
||||
|
||||
useEffect(() => {
|
||||
checkApiServerStatus()
|
||||
|
||||
@@ -221,13 +221,12 @@ export function useAppInit() {
|
||||
}
|
||||
}
|
||||
|
||||
window.electron.ipcRenderer.on(IpcChannel.AgentToolPermission_Request, requestListener)
|
||||
window.electron.ipcRenderer.on(IpcChannel.AgentToolPermission_Result, resultListener)
|
||||
const removeListeners = [
|
||||
window.electron.ipcRenderer.on(IpcChannel.AgentToolPermission_Request, requestListener),
|
||||
window.electron.ipcRenderer.on(IpcChannel.AgentToolPermission_Result, resultListener)
|
||||
]
|
||||
|
||||
return () => {
|
||||
window.electron?.ipcRenderer.removeListener(IpcChannel.AgentToolPermission_Request, requestListener)
|
||||
window.electron?.ipcRenderer.removeListener(IpcChannel.AgentToolPermission_Result, resultListener)
|
||||
}
|
||||
return () => removeListeners.forEach((removeListener) => removeListener())
|
||||
}, [dispatch, t])
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import store, { useAppSelector } from '@renderer/store'
|
||||
import { setAwsBedrockAccessKeyId, setAwsBedrockRegion, setAwsBedrockSecretAccessKey } from '@renderer/store/llm'
|
||||
import {
|
||||
setAwsBedrockAccessKeyId,
|
||||
setAwsBedrockApiKey,
|
||||
setAwsBedrockAuthType,
|
||||
setAwsBedrockRegion,
|
||||
setAwsBedrockSecretAccessKey
|
||||
} from '@renderer/store/llm'
|
||||
import type { AwsBedrockAuthType } from '@renderer/types'
|
||||
import { useDispatch } from 'react-redux'
|
||||
|
||||
export function useAwsBedrockSettings() {
|
||||
@@ -8,8 +15,10 @@ export function useAwsBedrockSettings() {
|
||||
|
||||
return {
|
||||
...settings,
|
||||
setAuthType: (authType: AwsBedrockAuthType) => dispatch(setAwsBedrockAuthType(authType)),
|
||||
setAccessKeyId: (accessKeyId: string) => dispatch(setAwsBedrockAccessKeyId(accessKeyId)),
|
||||
setSecretAccessKey: (secretAccessKey: string) => dispatch(setAwsBedrockSecretAccessKey(secretAccessKey)),
|
||||
setApiKey: (apiKey: string) => dispatch(setAwsBedrockApiKey(apiKey)),
|
||||
setRegion: (region: string) => dispatch(setAwsBedrockRegion(region))
|
||||
}
|
||||
}
|
||||
@@ -18,6 +27,10 @@ export function getAwsBedrockSettings() {
|
||||
return store.getState().llm.settings.awsBedrock
|
||||
}
|
||||
|
||||
export function getAwsBedrockAuthType() {
|
||||
return store.getState().llm.settings.awsBedrock.authType
|
||||
}
|
||||
|
||||
export function getAwsBedrockAccessKeyId() {
|
||||
return store.getState().llm.settings.awsBedrock.accessKeyId
|
||||
}
|
||||
@@ -26,6 +39,10 @@ export function getAwsBedrockSecretAccessKey() {
|
||||
return store.getState().llm.settings.awsBedrock.secretAccessKey
|
||||
}
|
||||
|
||||
export function getAwsBedrockApiKey() {
|
||||
return store.getState().llm.settings.awsBedrock.apiKey
|
||||
}
|
||||
|
||||
export function getAwsBedrockRegion() {
|
||||
return store.getState().llm.settings.awsBedrock.region
|
||||
}
|
||||
|
||||
@@ -12,19 +12,7 @@ export default function useUserTheme() {
|
||||
const colorPrimary = Color(theme.colorPrimary)
|
||||
|
||||
document.body.style.setProperty('--color-primary', colorPrimary.toString())
|
||||
// overwrite hero UI primary color.
|
||||
document.body.style.setProperty('--primary', colorPrimary.toString())
|
||||
document.body.style.setProperty('--heroui-primary', colorPrimary.toString())
|
||||
document.body.style.setProperty('--heroui-primary-900', colorPrimary.lighten(0.5).toString())
|
||||
document.body.style.setProperty('--heroui-primary-800', colorPrimary.lighten(0.4).toString())
|
||||
document.body.style.setProperty('--heroui-primary-700', colorPrimary.lighten(0.3).toString())
|
||||
document.body.style.setProperty('--heroui-primary-600', colorPrimary.lighten(0.2).toString())
|
||||
document.body.style.setProperty('--heroui-primary-500', colorPrimary.lighten(0.1).toString())
|
||||
document.body.style.setProperty('--heroui-primary-400', colorPrimary.toString())
|
||||
document.body.style.setProperty('--heroui-primary-300', colorPrimary.darken(0.1).toString())
|
||||
document.body.style.setProperty('--heroui-primary-200', colorPrimary.darken(0.2).toString())
|
||||
document.body.style.setProperty('--heroui-primary-100', colorPrimary.darken(0.3).toString())
|
||||
document.body.style.setProperty('--heroui-primary-50', colorPrimary.darken(0.4).toString())
|
||||
document.body.style.setProperty('--color-primary-soft', colorPrimary.alpha(0.6).toString())
|
||||
document.body.style.setProperty('--color-primary-mute', colorPrimary.alpha(0.3).toString())
|
||||
|
||||
|
||||
@@ -85,7 +85,8 @@ const providerKeyMap = {
|
||||
poe: 'provider.poe',
|
||||
aionly: 'provider.aionly',
|
||||
longcat: 'provider.longcat',
|
||||
huggingface: 'provider.huggingface'
|
||||
huggingface: 'provider.huggingface',
|
||||
sophnet: 'provider.sophnet'
|
||||
} as const
|
||||
|
||||
/**
|
||||
@@ -238,7 +239,7 @@ const paintingsImageSizeOptionsKeyMap = {
|
||||
} as const
|
||||
|
||||
export const getPaintingsImageSizeOptionsLabel = (key: string): string => {
|
||||
return getLabel(paintingsImageSizeOptionsKeyMap, key)
|
||||
return paintingsImageSizeOptionsKeyMap[key] ? getLabel(paintingsImageSizeOptionsKeyMap, key) : key
|
||||
}
|
||||
|
||||
const paintingsQualityOptionsKeyMap = {
|
||||
|
||||
@@ -339,6 +339,41 @@
|
||||
},
|
||||
"title": "API Server"
|
||||
},
|
||||
"appMenu": {
|
||||
"about": "About",
|
||||
"close": "Close Window",
|
||||
"copy": "Copy",
|
||||
"cut": "Cut",
|
||||
"delete": "Delete",
|
||||
"documentation": "Documentation",
|
||||
"edit": "Edit",
|
||||
"feedback": "Feedback",
|
||||
"file": "File",
|
||||
"forceReload": "Force Reload",
|
||||
"front": "Bring All to Front",
|
||||
"help": "Help",
|
||||
"hide": "Hide",
|
||||
"hideOthers": "Hide Others",
|
||||
"minimize": "Minimize",
|
||||
"paste": "Paste",
|
||||
"quit": "Quit",
|
||||
"redo": "Redo",
|
||||
"releases": "Releases",
|
||||
"reload": "Reload",
|
||||
"resetZoom": "Actual Size",
|
||||
"selectAll": "Select All",
|
||||
"services": "Services",
|
||||
"toggleDevTools": "Toggle Developer Tools",
|
||||
"toggleFullscreen": "Toggle Fullscreen",
|
||||
"undo": "Undo",
|
||||
"unhide": "Show All",
|
||||
"view": "View",
|
||||
"website": "Website",
|
||||
"window": "Window",
|
||||
"zoom": "Zoom",
|
||||
"zoomIn": "Zoom In",
|
||||
"zoomOut": "Zoom Out"
|
||||
},
|
||||
"assistants": {
|
||||
"abbr": "Assistants",
|
||||
"clear": {
|
||||
@@ -2482,6 +2517,7 @@
|
||||
"qiniu": "Qiniu AI",
|
||||
"qwenlm": "QwenLM",
|
||||
"silicon": "SiliconFlow",
|
||||
"sophnet": "SophNet",
|
||||
"stepfun": "StepFun",
|
||||
"tencent-cloud-ti": "Tencent Cloud TI",
|
||||
"together": "Together",
|
||||
@@ -3765,6 +3801,7 @@
|
||||
"description": "Do not enable MCP server functionality",
|
||||
"label": "Disable MCP Server"
|
||||
},
|
||||
"discover": "Discover",
|
||||
"duplicateName": "A server with this name already exists",
|
||||
"editJson": "Edit JSON",
|
||||
"editMcpJson": "Edit MCP Configuration",
|
||||
@@ -3775,6 +3812,10 @@
|
||||
"32000": "MCP server failed to start, please check the parameters according to the tutorial",
|
||||
"toolNotFound": "Tool {{name}} not found"
|
||||
},
|
||||
"fetch": {
|
||||
"button": "Fetch Servers",
|
||||
"success": "Successfully fetched MCP servers"
|
||||
},
|
||||
"findMore": "Find More MCP",
|
||||
"headers": "Headers",
|
||||
"headersTooltip": "Custom headers for HTTP requests",
|
||||
@@ -3790,6 +3831,7 @@
|
||||
"logoUrl": "Logo URL",
|
||||
"longRunning": "Long Running Mode",
|
||||
"longRunningTooltip": "When enabled, the server supports long-running tasks. When receiving progress notifications, the timeout will be reset and the maximum execution time will be extended to 10 minutes.",
|
||||
"marketplaces": "Marketplaces",
|
||||
"missingDependencies": "is Missing, please install it to continue.",
|
||||
"more": {
|
||||
"awesome": "Curated MCP Server List",
|
||||
@@ -3838,6 +3880,7 @@
|
||||
"provider": "Provider",
|
||||
"providerPlaceholder": "Provider name",
|
||||
"providerUrl": "Provider URL",
|
||||
"providers": "Providers",
|
||||
"registry": "Package Registry",
|
||||
"registryDefault": "Default",
|
||||
"registryTooltip": "Choose the registry for package installation to resolve network issues with the default registry.",
|
||||
@@ -3860,6 +3903,7 @@
|
||||
"searchNpx": "Search MCP",
|
||||
"serverPlural": "servers",
|
||||
"serverSingular": "server",
|
||||
"servers": "MCP Servers",
|
||||
"sse": "Server-Sent Events (sse)",
|
||||
"startError": "Start failed",
|
||||
"stdio": "Standard Input/Output (stdio)",
|
||||
@@ -4259,6 +4303,12 @@
|
||||
"aws-bedrock": {
|
||||
"access_key_id": "AWS Access Key ID",
|
||||
"access_key_id_help": "Your AWS Access Key ID for accessing AWS Bedrock services",
|
||||
"api_key": "Bedrock API Key",
|
||||
"api_key_help": "Your AWS Bedrock API Key for authentication",
|
||||
"auth_type": "Authentication Type",
|
||||
"auth_type_api_key": "Bedrock API Key",
|
||||
"auth_type_help": "Choose between IAM credentials or Bedrock API Key authentication",
|
||||
"auth_type_iam": "IAM Credentials",
|
||||
"description": "AWS Bedrock is Amazon's fully managed foundation model service that supports various advanced large language models",
|
||||
"region": "AWS Region",
|
||||
"region_help": "Your AWS service region, e.g., us-east-1",
|
||||
|
||||
@@ -339,6 +339,41 @@
|
||||
},
|
||||
"title": "API 服务器"
|
||||
},
|
||||
"appMenu": {
|
||||
"about": "关于",
|
||||
"close": "关闭窗口",
|
||||
"copy": "复制",
|
||||
"cut": "剪切",
|
||||
"delete": "删除",
|
||||
"documentation": "文档",
|
||||
"edit": "编辑",
|
||||
"feedback": "反馈",
|
||||
"file": "文件",
|
||||
"forceReload": "强制重新加载",
|
||||
"front": "全部置于顶层",
|
||||
"help": "帮助",
|
||||
"hide": "隐藏",
|
||||
"hideOthers": "隐藏其他",
|
||||
"minimize": "最小化",
|
||||
"paste": "粘贴",
|
||||
"quit": "退出",
|
||||
"redo": "重做",
|
||||
"releases": "版本发布",
|
||||
"reload": "重新加载",
|
||||
"resetZoom": "实际大小",
|
||||
"selectAll": "全选",
|
||||
"services": "服务",
|
||||
"toggleDevTools": "切换开发者工具",
|
||||
"toggleFullscreen": "切换全屏",
|
||||
"undo": "撤销",
|
||||
"unhide": "全部显示",
|
||||
"view": "视图",
|
||||
"website": "网站",
|
||||
"window": "窗口",
|
||||
"zoom": "缩放",
|
||||
"zoomIn": "放大",
|
||||
"zoomOut": "缩小"
|
||||
},
|
||||
"assistants": {
|
||||
"abbr": "助手",
|
||||
"clear": {
|
||||
@@ -1611,7 +1646,7 @@
|
||||
},
|
||||
"assistant": {
|
||||
"added": {
|
||||
"content": "智能体添加成功"
|
||||
"content": "助手添加成功"
|
||||
}
|
||||
},
|
||||
"attachments": {
|
||||
@@ -2482,6 +2517,7 @@
|
||||
"qiniu": "七牛云 AI 推理",
|
||||
"qwenlm": "QwenLM",
|
||||
"silicon": "硅基流动",
|
||||
"sophnet": "SophNet",
|
||||
"stepfun": "阶跃星辰",
|
||||
"tencent-cloud-ti": "腾讯云 TI",
|
||||
"together": "Together",
|
||||
@@ -3765,6 +3801,7 @@
|
||||
"description": "不启用 MCP 服务功能",
|
||||
"label": "不使用 MCP 服务器"
|
||||
},
|
||||
"discover": "发现",
|
||||
"duplicateName": "已存在同名服务器",
|
||||
"editJson": "编辑 JSON",
|
||||
"editMcpJson": "编辑 MCP 配置",
|
||||
@@ -3775,6 +3812,10 @@
|
||||
"32000": "MCP 服务器启动失败,请根据教程检查参数是否填写完整",
|
||||
"toolNotFound": "未找到工具 {{name}}"
|
||||
},
|
||||
"fetch": {
|
||||
"button": "获取服务器",
|
||||
"success": "服务器获取成功"
|
||||
},
|
||||
"findMore": "更多 MCP",
|
||||
"headers": "请求头",
|
||||
"headersTooltip": "HTTP 请求的自定义请求头",
|
||||
@@ -3790,6 +3831,7 @@
|
||||
"logoUrl": "标志网址",
|
||||
"longRunning": "长时间运行模式",
|
||||
"longRunningTooltip": "启用后,服务器支持长时间任务,接收到进度通知时会重置超时计时器,并延长最大超时时间至10分钟",
|
||||
"marketplaces": "市场",
|
||||
"missingDependencies": "缺失,请安装它以继续",
|
||||
"more": {
|
||||
"awesome": "精选的 MCP 服务器列表",
|
||||
@@ -3838,6 +3880,7 @@
|
||||
"provider": "提供者",
|
||||
"providerPlaceholder": "提供者名称",
|
||||
"providerUrl": "提供者网址",
|
||||
"providers": "提供商",
|
||||
"registry": "包管理源",
|
||||
"registryDefault": "默认",
|
||||
"registryTooltip": "选择用于安装包的源,以解决默认源的网络问题",
|
||||
@@ -3860,6 +3903,7 @@
|
||||
"searchNpx": "搜索 MCP",
|
||||
"serverPlural": "服务器",
|
||||
"serverSingular": "服务器",
|
||||
"servers": "MCP 服务器",
|
||||
"sse": "服务器发送事件 (sse)",
|
||||
"startError": "启动失败",
|
||||
"stdio": "标准输入 / 输出 (stdio)",
|
||||
@@ -4259,6 +4303,12 @@
|
||||
"aws-bedrock": {
|
||||
"access_key_id": "AWS 访问密钥 ID",
|
||||
"access_key_id_help": "您的 AWS 访问密钥 ID,用于访问 AWS Bedrock 服务",
|
||||
"api_key": "Bedrock API 密钥",
|
||||
"api_key_help": "您的 AWS Bedrock API 密钥,用于身份验证",
|
||||
"auth_type": "认证方式",
|
||||
"auth_type_api_key": "Bedrock API 密钥",
|
||||
"auth_type_help": "选择使用 IAM 凭证或 Bedrock API 密钥进行身份验证",
|
||||
"auth_type_iam": "IAM 凭证",
|
||||
"description": "AWS Bedrock 是亚马逊提供的全托管基础模型服务,支持多种先进的大语言模型",
|
||||
"region": "AWS 区域",
|
||||
"region_help": "您的 AWS 服务区域,例如 us-east-1",
|
||||
|
||||
@@ -339,6 +339,41 @@
|
||||
},
|
||||
"title": "API 伺服器"
|
||||
},
|
||||
"appMenu": {
|
||||
"about": "關於",
|
||||
"close": "關閉視窗",
|
||||
"copy": "複製",
|
||||
"cut": "剪下",
|
||||
"delete": "刪除",
|
||||
"documentation": "文件",
|
||||
"edit": "編輯",
|
||||
"feedback": "回饋",
|
||||
"file": "檔案",
|
||||
"forceReload": "強制重新載入",
|
||||
"front": "全部置於頂層",
|
||||
"help": "幫助",
|
||||
"hide": "隱藏",
|
||||
"hideOthers": "隱藏其他",
|
||||
"minimize": "最小化",
|
||||
"paste": "貼上",
|
||||
"quit": "結束",
|
||||
"redo": "重做",
|
||||
"releases": "版本發布",
|
||||
"reload": "重新載入",
|
||||
"resetZoom": "實際大小",
|
||||
"selectAll": "全選",
|
||||
"services": "服務",
|
||||
"toggleDevTools": "切換開發者工具",
|
||||
"toggleFullscreen": "切換全螢幕",
|
||||
"undo": "復原",
|
||||
"unhide": "全部顯示",
|
||||
"view": "檢視",
|
||||
"website": "網站",
|
||||
"window": "視窗",
|
||||
"zoom": "縮放",
|
||||
"zoomIn": "放大",
|
||||
"zoomOut": "縮小"
|
||||
},
|
||||
"assistants": {
|
||||
"abbr": "助手",
|
||||
"clear": {
|
||||
@@ -1611,7 +1646,7 @@
|
||||
},
|
||||
"assistant": {
|
||||
"added": {
|
||||
"content": "智慧代理人新增成功"
|
||||
"content": "助手新增成功"
|
||||
}
|
||||
},
|
||||
"attachments": {
|
||||
@@ -2482,6 +2517,7 @@
|
||||
"qiniu": "七牛雲 AI 推理",
|
||||
"qwenlm": "QwenLM",
|
||||
"silicon": "SiliconFlow",
|
||||
"sophnet": "SophNet",
|
||||
"stepfun": "StepFun",
|
||||
"tencent-cloud-ti": "騰訊雲 TI",
|
||||
"together": "Together",
|
||||
@@ -3765,6 +3801,7 @@
|
||||
"description": "不啟用 MCP 服務功能",
|
||||
"label": "不使用 MCP 伺服器"
|
||||
},
|
||||
"discover": "發現",
|
||||
"duplicateName": "已存在相同名稱的伺服器",
|
||||
"editJson": "編輯 JSON",
|
||||
"editMcpJson": "編輯 MCP 配置",
|
||||
@@ -3775,6 +3812,10 @@
|
||||
"32000": "MCP 伺服器啟動失敗,請根據教程檢查參數是否填寫完整",
|
||||
"toolNotFound": "未找到工具 {{name}}"
|
||||
},
|
||||
"fetch": {
|
||||
"button": "獲取伺服器",
|
||||
"success": "伺服器獲取成功"
|
||||
},
|
||||
"findMore": "更多 MCP",
|
||||
"headers": "請求標頭",
|
||||
"headersTooltip": "HTTP 請求的自定義標頭",
|
||||
@@ -3790,6 +3831,7 @@
|
||||
"logoUrl": "標誌網址",
|
||||
"longRunning": "長時間運行模式",
|
||||
"longRunningTooltip": "啟用後,伺服器支援長時間任務,接收到進度通知時會重置超時計時器,並延長最大超時時間至10分鐘",
|
||||
"marketplaces": "市場",
|
||||
"missingDependencies": "缺失,請安裝它以繼續",
|
||||
"more": {
|
||||
"awesome": "精選的 MCP 伺服器清單",
|
||||
@@ -3838,6 +3880,7 @@
|
||||
"provider": "提供者",
|
||||
"providerPlaceholder": "提供者名稱",
|
||||
"providerUrl": "提供者網址",
|
||||
"providers": "提供商",
|
||||
"registry": "套件管理源",
|
||||
"registryDefault": "預設",
|
||||
"registryTooltip": "選擇用於安裝套件的源,以解決預設源的網路問題",
|
||||
@@ -3860,6 +3903,7 @@
|
||||
"searchNpx": "搜索 MCP",
|
||||
"serverPlural": "伺服器",
|
||||
"serverSingular": "伺服器",
|
||||
"servers": "MCP 伺服器",
|
||||
"sse": "伺服器傳送事件 (sse)",
|
||||
"startError": "啟動失敗",
|
||||
"stdio": "標準輸入 / 輸出 (stdio)",
|
||||
@@ -4259,6 +4303,12 @@
|
||||
"aws-bedrock": {
|
||||
"access_key_id": "AWS 存取密鑰 ID",
|
||||
"access_key_id_help": "您的 AWS 存取密鑰 ID,用於存取 AWS Bedrock 服務",
|
||||
"api_key": "Bedrock API 金鑰",
|
||||
"api_key_help": "您的 AWS Bedrock API 金鑰,用於身份驗證",
|
||||
"auth_type": "認證方式",
|
||||
"auth_type_api_key": "Bedrock API 金鑰",
|
||||
"auth_type_help": "選擇使用 IAM 憑證或 Bedrock API 金鑰進行身份驗證",
|
||||
"auth_type_iam": "IAM 憑證",
|
||||
"description": "AWS Bedrock 是亞馬遜提供的全托管基础模型服務,支持多種先進的大語言模型",
|
||||
"region": "AWS 區域",
|
||||
"region_help": "您的 AWS 服務區域,例如 us-east-1",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"agent": {
|
||||
"add": {
|
||||
"description": "Bewältigen Sie komplexe Aufgaben mit verschiedenen Werkzeugen",
|
||||
"error": {
|
||||
"failed": "Agent hinzufügen fehlgeschlagen",
|
||||
"invalid_agent": "Ungültiger Agent"
|
||||
@@ -338,6 +339,41 @@
|
||||
},
|
||||
"title": "API-Server"
|
||||
},
|
||||
"appMenu": {
|
||||
"about": "Über",
|
||||
"close": "Fenster schließen",
|
||||
"copy": "Kopieren",
|
||||
"cut": "Schneiden",
|
||||
"delete": "Löschen",
|
||||
"documentation": "Dokumentation",
|
||||
"edit": "Bearbeiten",
|
||||
"feedback": "Rückmeldung",
|
||||
"file": "Datei",
|
||||
"forceReload": "Neu laden erzwingen",
|
||||
"front": "Alle in den Vordergrund bringen",
|
||||
"help": "Hilfe",
|
||||
"hide": "Verstecken",
|
||||
"hideOthers": "Andere ausblenden",
|
||||
"minimize": "Minimieren",
|
||||
"paste": "Einfügen",
|
||||
"quit": "Aufhören",
|
||||
"redo": "Wiederholen",
|
||||
"releases": "Veröffentlichungen",
|
||||
"reload": "Neu laden",
|
||||
"resetZoom": "Tatsächliche Größe",
|
||||
"selectAll": "Alle auswählen",
|
||||
"services": "Dienstleistungen",
|
||||
"toggleDevTools": "Entwicklertools ein-/ausblenden",
|
||||
"toggleFullscreen": "Vollbild umschalten",
|
||||
"undo": "Rückgängig machen",
|
||||
"unhide": "Alle anzeigen",
|
||||
"view": "Ansicht",
|
||||
"website": "Website",
|
||||
"window": "Fenster",
|
||||
"zoom": "Zoom",
|
||||
"zoomIn": "Heranzoomen",
|
||||
"zoomOut": "Herauszoomen"
|
||||
},
|
||||
"assistants": {
|
||||
"abbr": "Assistent",
|
||||
"clear": {
|
||||
@@ -547,8 +583,12 @@
|
||||
"chat": {
|
||||
"add": {
|
||||
"assistant": {
|
||||
"description": "Tägliche Gespräche und schnelle Fragen & Antworten",
|
||||
"title": "Assistent hinzufügen"
|
||||
},
|
||||
"option": {
|
||||
"title": "Typ auswählen"
|
||||
},
|
||||
"topic": {
|
||||
"title": "Neues Thema erstellen"
|
||||
}
|
||||
@@ -1606,7 +1646,7 @@
|
||||
},
|
||||
"assistant": {
|
||||
"added": {
|
||||
"content": "Agent erfolgreich hinzugefügt"
|
||||
"content": "Assistent erfolgreich hinzugefügt"
|
||||
}
|
||||
},
|
||||
"attachments": {
|
||||
@@ -2471,12 +2511,13 @@
|
||||
"openrouter": "OpenRouter",
|
||||
"ovms": "Intel OVMS",
|
||||
"perplexity": "Perplexity",
|
||||
"ph8": "PH8 Großmodell-Plattform",
|
||||
"ph8": "PH8",
|
||||
"poe": "Poe",
|
||||
"ppio": "PPIO Cloud",
|
||||
"qiniu": "Qiniu Cloud KI-Inferenz",
|
||||
"qwenlm": "QwenLM",
|
||||
"silicon": "SiliconFlow",
|
||||
"sophnet": "SophNet",
|
||||
"stepfun": "StepFun",
|
||||
"tencent-cloud-ti": "Tencent Cloud TI",
|
||||
"together": "Together",
|
||||
@@ -3760,6 +3801,7 @@
|
||||
"description": "MCP-Service-Funktion nicht aktivieren",
|
||||
"label": "MCP-Server nicht verwenden"
|
||||
},
|
||||
"discover": "Entdecken",
|
||||
"duplicateName": "Server mit gleichem Namen existiert bereits",
|
||||
"editJson": "JSON bearbeiten",
|
||||
"editMcpJson": "MCP-Konfiguration bearbeiten",
|
||||
@@ -3770,6 +3812,10 @@
|
||||
"32000": "MCP-Server starten fehlgeschlagen, bitte überprüfen Sie, ob alle Parameter vollständig ausgefüllt sind",
|
||||
"toolNotFound": "Tool {{name}} nicht gefunden"
|
||||
},
|
||||
"fetch": {
|
||||
"button": "Server abrufen",
|
||||
"success": "MCP-Server erfolgreich abgerufen"
|
||||
},
|
||||
"findMore": "Mehr MCP",
|
||||
"headers": "Request-Header",
|
||||
"headersTooltip": "Benutzerdefinierte Request-Header für HTTP-Anfragen",
|
||||
@@ -3785,6 +3831,7 @@
|
||||
"logoUrl": "Logo-URL",
|
||||
"longRunning": "Lang laufender Modus",
|
||||
"longRunningTooltip": "Nach Aktivierung unterstützt der Server lange Aufgaben. Wenn ein Fortschrittsbenachrichtigung empfangen wird, wird der Timeout-Timer zurückgesetzt und die maximale Timeout-Zeit auf 10 Minuten verlängert",
|
||||
"marketplaces": "Marktplätze",
|
||||
"missingDependencies": "Abhängigkeiten fehlen, bitte installieren Sie sie, um fortzufahren",
|
||||
"more": {
|
||||
"awesome": "Kuratierte MCP-Serverliste",
|
||||
@@ -3833,6 +3880,7 @@
|
||||
"provider": "Anbieter",
|
||||
"providerPlaceholder": "Anbietername",
|
||||
"providerUrl": "Anbieter-Website",
|
||||
"providers": "Anbieter",
|
||||
"registry": "Paketverwaltungsquelle",
|
||||
"registryDefault": "Standard",
|
||||
"registryTooltip": "Quelle für Paketinstallation auswählen um Netzwerkprobleme der Standardquelle zu lösen",
|
||||
@@ -3855,6 +3903,7 @@
|
||||
"searchNpx": "MCP durchsuchen",
|
||||
"serverPlural": "Server",
|
||||
"serverSingular": "Server",
|
||||
"servers": "MCP-Server",
|
||||
"sse": "Server-Sende-Ereignisse (sse)",
|
||||
"startError": "Start fehlgeschlagen",
|
||||
"stdio": "Standard-Eingabe / -Ausgabe (stdio)",
|
||||
@@ -4254,6 +4303,12 @@
|
||||
"aws-bedrock": {
|
||||
"access_key_id": "AWS-Zugriffsschlüssel-ID",
|
||||
"access_key_id_help": "Ihre AWS-Zugriffsschlüssel-ID, um auf AWS Bedrock-Dienste zuzugreifen",
|
||||
"api_key": "Bedrock-API-Schlüssel",
|
||||
"api_key_help": "Ihr AWS Bedrock-API-Schlüssel für die Authentifizierung",
|
||||
"auth_type": "Authentifizierungstyp",
|
||||
"auth_type_api_key": "Bedrock-API-Schlüssel",
|
||||
"auth_type_help": "Wählen Sie zwischen IAM-Anmeldeinformationen oder Bedrock-API-Schlüssel-Authentifizierung",
|
||||
"auth_type_iam": "IAM-Anmeldeinformationen",
|
||||
"description": "AWS Bedrock ist ein vollständig verwalteter Basismodell-Dienst von Amazon, der eine Vielzahl moderner großer Sprachmodelle unterstützt",
|
||||
"region": "AWS-Region",
|
||||
"region_help": "Ihre AWS-Serviceregion, z.B. us-east-1",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"agent": {
|
||||
"add": {
|
||||
"description": "Χειριστείτε πολύπλοκες εργασίες με διάφορα εργαλεία",
|
||||
"error": {
|
||||
"failed": "Αποτυχία προσθήκης πράκτορα",
|
||||
"invalid_agent": "Μη έγκυρος Agent"
|
||||
@@ -338,6 +339,41 @@
|
||||
},
|
||||
"title": "Διακομιστής API"
|
||||
},
|
||||
"appMenu": {
|
||||
"about": "Σχετικά",
|
||||
"close": "Κλείσιμο Παραθύρου",
|
||||
"copy": "Αντιγραφή",
|
||||
"cut": "Κόψε",
|
||||
"delete": "Διαγραφή",
|
||||
"documentation": "Τεκμηρίωση",
|
||||
"edit": "Επεξεργασία",
|
||||
"feedback": "Σχόλια",
|
||||
"file": "Αρχείο",
|
||||
"forceReload": "Εξαναγκασμένη επαναφόρτωση",
|
||||
"front": "Μεταφορά Όλων Μπροστά",
|
||||
"help": "Βοήθεια",
|
||||
"hide": "Κρύψε",
|
||||
"hideOthers": "Απόκρυψη Άλλων",
|
||||
"minimize": "Ελαχιστοποίηση",
|
||||
"paste": "Επικόλληση",
|
||||
"quit": "Παραιτήσου",
|
||||
"redo": "Ξανακάνε",
|
||||
"releases": "Κυκλοφορίες",
|
||||
"reload": "Επαναφόρτωση",
|
||||
"resetZoom": "Πραγματικό Μέγεθος",
|
||||
"selectAll": "Επιλογή Όλων",
|
||||
"services": "Υπηρεσίες",
|
||||
"toggleDevTools": "Εναλλαγή Εργαλείων Προγραμματιστή",
|
||||
"toggleFullscreen": "Εναλλαγή πλήρους οθόνης",
|
||||
"undo": "Αναίρεση",
|
||||
"unhide": "Εμφάνιση Όλων",
|
||||
"view": "Προβολή",
|
||||
"website": "Ιστοσελίδα",
|
||||
"window": "Παράθυρο",
|
||||
"zoom": "Ζουμ",
|
||||
"zoomIn": "Μεγέθυνση",
|
||||
"zoomOut": "Σμίκρυνση"
|
||||
},
|
||||
"assistants": {
|
||||
"abbr": "Βοηθός",
|
||||
"clear": {
|
||||
@@ -547,8 +583,12 @@
|
||||
"chat": {
|
||||
"add": {
|
||||
"assistant": {
|
||||
"description": "Καθημερινές συνομιλίες και γρήγορες ερωταπαντήσεις",
|
||||
"title": "Προσθήκη βοηθού"
|
||||
},
|
||||
"option": {
|
||||
"title": "Επιλέξτε Τύπο"
|
||||
},
|
||||
"topic": {
|
||||
"title": "Δημιουργία νέου θέματος"
|
||||
}
|
||||
@@ -1606,7 +1646,7 @@
|
||||
},
|
||||
"assistant": {
|
||||
"added": {
|
||||
"content": "Ο ενεργοποιημένος αστρόναυτης προστέθηκε επιτυχώς"
|
||||
"content": "Ο βοηθός προστέθηκε επιτυχώς"
|
||||
}
|
||||
},
|
||||
"attachments": {
|
||||
@@ -2471,12 +2511,13 @@
|
||||
"openrouter": "OpenRouter",
|
||||
"ovms": "Intel OVMS",
|
||||
"perplexity": "Perplexity",
|
||||
"ph8": "Πλατφόρμα Ανοιχτής Μεγάλης Μοντέλου PH8",
|
||||
"ph8": "PH8",
|
||||
"poe": "Poe",
|
||||
"ppio": "PPIO Piao Yun",
|
||||
"qiniu": "Qiniu AI",
|
||||
"qwenlm": "QwenLM",
|
||||
"silicon": "Σιδηρική Παρουσία",
|
||||
"sophnet": "SophNet",
|
||||
"stepfun": "Βήμα Ουράς",
|
||||
"tencent-cloud-ti": "Tencent Cloud TI",
|
||||
"together": "Together",
|
||||
@@ -3760,6 +3801,7 @@
|
||||
"description": "Να μην ενεργοποιείται η λειτουργία υπηρεσίας MCP",
|
||||
"label": "Να μην χρησιμοποιείται διακομιστής MCP"
|
||||
},
|
||||
"discover": "Ανακαλύψτε",
|
||||
"duplicateName": "Υπάρχει ήδη ένας διακομιστής με αυτό το όνομα",
|
||||
"editJson": "Επεξεργασία JSON",
|
||||
"editMcpJson": "Επεξεργασία ρύθμισης MCP",
|
||||
@@ -3770,6 +3812,10 @@
|
||||
"32000": "Η εκκίνηση του MCP απέτυχε. Παρακαλώ ελέγξτε αν όλες οι παράμετροι έχουν συμπληρωθεί σύμφωνα με τον οδηγό.",
|
||||
"toolNotFound": "Δεν βρέθηκε το εργαλείο {{name}}"
|
||||
},
|
||||
"fetch": {
|
||||
"button": "Λήψη Διακομιστών",
|
||||
"success": "Επιτυχής ανάκτηση διακομιστών MCP"
|
||||
},
|
||||
"findMore": "Περισσότεροι διακομιστές MCP",
|
||||
"headers": "Κεφαλίδες",
|
||||
"headersTooltip": "Προσαρμοσμένες κεφαλίδες HTTP αιτήσεων",
|
||||
@@ -3785,6 +3831,7 @@
|
||||
"logoUrl": "URL Λογότυπου",
|
||||
"longRunning": "Μακροχρόνια λειτουργία",
|
||||
"longRunningTooltip": "Όταν ενεργοποιηθεί, ο διακομιστής υποστηρίζει μακροχρόνιες εργασίες, επαναφέρει το χρονικό όριο μετά από λήψη ειδοποίησης προόδου και επεκτείνει το μέγιστο χρονικό όριο σε 10 λεπτά.",
|
||||
"marketplaces": "Αγορές",
|
||||
"missingDependencies": "Λείπει, παρακαλώ εγκαταστήστε το για να συνεχίσετε",
|
||||
"more": {
|
||||
"awesome": "Επιλεγμένος κατάλογος διακομιστών MCP",
|
||||
@@ -3833,6 +3880,7 @@
|
||||
"provider": "Πάροχος",
|
||||
"providerPlaceholder": "Όνομα παρόχου",
|
||||
"providerUrl": "URL Παρόχου",
|
||||
"providers": "Πάροχοι",
|
||||
"registry": "Πηγή Διαχείρισης πακέτων",
|
||||
"registryDefault": "Προεπιλεγμένη",
|
||||
"registryTooltip": "Επιλέξτε την πηγή για την εγκατάσταση πακέτων, για να αντιμετωπιστούν προβλήματα δικτύου από την προεπιλεγμένη πηγή.",
|
||||
@@ -3855,6 +3903,7 @@
|
||||
"searchNpx": "Αναζήτηση MCP",
|
||||
"serverPlural": "Διακομιστές",
|
||||
"serverSingular": "Διακομιστής",
|
||||
"servers": "Διακομιστές MCP",
|
||||
"sse": "Συμβάντα Αποστολής από τον Διακομιστή (sse)",
|
||||
"startError": "Εκκίνηση Απέτυχε",
|
||||
"stdio": "Πρότυπη Είσοδος/Έξοδος (stdio)",
|
||||
@@ -4254,6 +4303,12 @@
|
||||
"aws-bedrock": {
|
||||
"access_key_id": "Αναγνωριστικό κλειδιού πρόσβασης AWS",
|
||||
"access_key_id_help": "Το ID του κλειδιού πρόσβασης AWS που χρησιμοποιείται για την πρόσβαση στην υπηρεσία AWS Bedrock",
|
||||
"api_key": "Κλειδί API Bedrock",
|
||||
"api_key_help": "Το κλειδί API του AWS Bedrock για έλεγχο ταυτότητας",
|
||||
"auth_type": "Τύπος Πιστοποίησης",
|
||||
"auth_type_api_key": "Κλειδί API Bedrock",
|
||||
"auth_type_help": "Επιλέξτε μεταξύ πιστοποιητικών IAM ή πιστοποίησης με κλειδί API Bedrock",
|
||||
"auth_type_iam": "Διαπιστευτήρια IAM",
|
||||
"description": "Η AWS Bedrock είναι μια πλήρως διαχειριζόμενη υπηρεσία βασικών μοντέλων που παρέχεται από την Amazon και υποστηρίζει διάφορα προηγμένα μεγάλα γλωσσικά μοντέλα.",
|
||||
"region": "Περιοχές AWS",
|
||||
"region_help": "Η περιοχή υπηρεσίας AWS σας, για παράδειγμα us-east-1",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"agent": {
|
||||
"add": {
|
||||
"description": "Maneja tareas complejas con varias herramientas",
|
||||
"error": {
|
||||
"failed": "Error al añadir agente",
|
||||
"invalid_agent": "Agent inválido"
|
||||
@@ -338,6 +339,41 @@
|
||||
},
|
||||
"title": "Servidor API"
|
||||
},
|
||||
"appMenu": {
|
||||
"about": "Acerca de",
|
||||
"close": "Cerrar ventana",
|
||||
"copy": "Copiar",
|
||||
"cut": "Cortar",
|
||||
"delete": "Eliminar",
|
||||
"documentation": "Documentación",
|
||||
"edit": "Editar",
|
||||
"feedback": "Retroalimentación",
|
||||
"file": "Archivo",
|
||||
"forceReload": "Forzar recarga",
|
||||
"front": "Traer todo al frente",
|
||||
"help": "Ayuda",
|
||||
"hide": "Ocultar",
|
||||
"hideOthers": "Ocultar Otros",
|
||||
"minimize": "Minimizar",
|
||||
"paste": "Pegar",
|
||||
"quit": "Abandonar",
|
||||
"redo": "Rehacer",
|
||||
"releases": "Lanzamientos",
|
||||
"reload": "Recargar",
|
||||
"resetZoom": "Tamaño Real",
|
||||
"selectAll": "Seleccionar todo",
|
||||
"services": "Servicios",
|
||||
"toggleDevTools": "Alternar herramientas de desarrollo",
|
||||
"toggleFullscreen": "Activar pantalla completa",
|
||||
"undo": "Deshacer",
|
||||
"unhide": "Mostrar todo",
|
||||
"view": "Vista",
|
||||
"website": "Sitio web",
|
||||
"window": "Ventana",
|
||||
"zoom": "Zoom",
|
||||
"zoomIn": "Acercar",
|
||||
"zoomOut": "Alejar"
|
||||
},
|
||||
"assistants": {
|
||||
"abbr": "Asistente",
|
||||
"clear": {
|
||||
@@ -547,8 +583,12 @@
|
||||
"chat": {
|
||||
"add": {
|
||||
"assistant": {
|
||||
"description": "Conversaciones diarias y preguntas y respuestas rápidas",
|
||||
"title": "Agregar asistente"
|
||||
},
|
||||
"option": {
|
||||
"title": "Seleccionar Tipo"
|
||||
},
|
||||
"topic": {
|
||||
"title": "Crear nuevo tema"
|
||||
}
|
||||
@@ -2471,12 +2511,13 @@
|
||||
"openrouter": "OpenRouter",
|
||||
"ovms": "Intel OVMS",
|
||||
"perplexity": "Perplejidad",
|
||||
"ph8": "Plataforma Abierta de Grandes Modelos PH8",
|
||||
"ph8": "PH8",
|
||||
"poe": "Poe",
|
||||
"ppio": "PPIO Cloud Piao",
|
||||
"qiniu": "Qiniu AI",
|
||||
"qwenlm": "QwenLM",
|
||||
"silicon": "Silicio Fluido",
|
||||
"sophnet": "SophNet",
|
||||
"stepfun": "Función Salto",
|
||||
"tencent-cloud-ti": "Tencent Nube TI",
|
||||
"together": "Juntos",
|
||||
@@ -3760,6 +3801,7 @@
|
||||
"description": "No habilitar funciones del servicio MCP",
|
||||
"label": "No utilizar servidor MCP"
|
||||
},
|
||||
"discover": "Descubrir",
|
||||
"duplicateName": "Ya existe un servidor con el mismo nombre",
|
||||
"editJson": "Editar JSON",
|
||||
"editMcpJson": "Editar configuración MCP",
|
||||
@@ -3770,6 +3812,10 @@
|
||||
"32000": "El servidor MCP no se pudo iniciar, verifique si los parámetros están completos según la guía",
|
||||
"toolNotFound": "Herramienta no encontrada {{name}}"
|
||||
},
|
||||
"fetch": {
|
||||
"button": "Obtener Servidores",
|
||||
"success": "Servidores MCP obtenidos con éxito"
|
||||
},
|
||||
"findMore": "Más servidores MCP",
|
||||
"headers": "Encabezados",
|
||||
"headersTooltip": "Encabezados personalizados para solicitudes HTTP",
|
||||
@@ -3785,6 +3831,7 @@
|
||||
"logoUrl": "URL del logotipo",
|
||||
"longRunning": "Modo de ejecución prolongada",
|
||||
"longRunningTooltip": "Una vez habilitado, el servidor admite tareas de larga duración, reinicia el temporizador de tiempo de espera al recibir notificaciones de progreso y amplía el tiempo máximo de espera hasta 10 minutos.",
|
||||
"marketplaces": "Mercados",
|
||||
"missingDependencies": "Faltan, instalelas para continuar",
|
||||
"more": {
|
||||
"awesome": "Lista seleccionada de servidores MCP",
|
||||
@@ -3833,6 +3880,7 @@
|
||||
"provider": "Proveedor",
|
||||
"providerPlaceholder": "Nombre del proveedor",
|
||||
"providerUrl": "URL del proveedor",
|
||||
"providers": "Proveedores",
|
||||
"registry": "Repositorio de paquetes",
|
||||
"registryDefault": "Predeterminado",
|
||||
"registryTooltip": "Seleccione un repositorio para instalar paquetes, útil para resolver problemas de red con el repositorio predeterminado.",
|
||||
@@ -3855,6 +3903,7 @@
|
||||
"searchNpx": "Buscar MCP",
|
||||
"serverPlural": "Servidores",
|
||||
"serverSingular": "Servidor",
|
||||
"servers": "Servidores MCP",
|
||||
"sse": "Eventos enviados por el servidor (sse)",
|
||||
"startError": "Inicio fallido",
|
||||
"stdio": "Entrada/Salida estándar (stdio)",
|
||||
@@ -4254,6 +4303,12 @@
|
||||
"aws-bedrock": {
|
||||
"access_key_id": "ID de clave de acceso de AWS",
|
||||
"access_key_id_help": "Su ID de clave de acceso de AWS, utilizado para acceder al servicio AWS Bedrock",
|
||||
"api_key": "Clave de API de Bedrock",
|
||||
"api_key_help": "Tu clave de API de AWS Bedrock para autenticación",
|
||||
"auth_type": "Tipo de autenticación",
|
||||
"auth_type_api_key": "Clave de API de Bedrock",
|
||||
"auth_type_help": "Elige entre credenciales IAM o autenticación con clave API de Bedrock",
|
||||
"auth_type_iam": "Credenciales de IAM",
|
||||
"description": "AWS Bedrock es un servicio de modelos fundamentales completamente gestionado proporcionado por Amazon, que admite diversos modelos avanzados de lenguaje de gran tamaño.",
|
||||
"region": "Región de AWS",
|
||||
"region_help": "Su región de servicio AWS, por ejemplo us-east-1",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"agent": {
|
||||
"add": {
|
||||
"description": "Gérez des tâches complexes avec divers outils",
|
||||
"error": {
|
||||
"failed": "Échec de l'ajout de l'agent",
|
||||
"invalid_agent": "Agent invalide"
|
||||
@@ -338,6 +339,41 @@
|
||||
},
|
||||
"title": "Serveur API"
|
||||
},
|
||||
"appMenu": {
|
||||
"about": "À propos",
|
||||
"close": "Fermer la fenêtre",
|
||||
"copy": "Copier",
|
||||
"cut": "Couper",
|
||||
"delete": "Supprimer",
|
||||
"documentation": "Documentation",
|
||||
"edit": "Modifier",
|
||||
"feedback": "Retour d'information",
|
||||
"file": "Fichier",
|
||||
"forceReload": "Rechargement forcé",
|
||||
"front": "Tout ramener au premier plan",
|
||||
"help": "Aide",
|
||||
"hide": "Cacher",
|
||||
"hideOthers": "Masquer les autres",
|
||||
"minimize": "Minimiser",
|
||||
"paste": "Coller",
|
||||
"quit": "Quitter",
|
||||
"redo": "Refaire",
|
||||
"releases": "Sorties",
|
||||
"reload": "Recharger",
|
||||
"resetZoom": "Taille réelle",
|
||||
"selectAll": "Tout sélectionner",
|
||||
"services": "Services",
|
||||
"toggleDevTools": "Basculer les outils de développement",
|
||||
"toggleFullscreen": "Basculer en plein écran",
|
||||
"undo": "Annuler",
|
||||
"unhide": "Tout afficher",
|
||||
"view": "Vue",
|
||||
"website": "Site web",
|
||||
"window": "Fenêtre",
|
||||
"zoom": "Zoom",
|
||||
"zoomIn": "Zoom Avant",
|
||||
"zoomOut": "Dézoomer"
|
||||
},
|
||||
"assistants": {
|
||||
"abbr": "Aide",
|
||||
"clear": {
|
||||
@@ -547,8 +583,12 @@
|
||||
"chat": {
|
||||
"add": {
|
||||
"assistant": {
|
||||
"description": "Conversations quotidiennes et Q&R rapides",
|
||||
"title": "Ajouter un assistant"
|
||||
},
|
||||
"option": {
|
||||
"title": "Sélectionner le type"
|
||||
},
|
||||
"topic": {
|
||||
"title": "Nouveau sujet"
|
||||
}
|
||||
@@ -2471,12 +2511,13 @@
|
||||
"openrouter": "OpenRouter",
|
||||
"ovms": "Intel OVMS",
|
||||
"perplexity": "Perplexité",
|
||||
"ph8": "Plateforme ouverte de grands modèles PH8",
|
||||
"ph8": "PH8",
|
||||
"poe": "Poe",
|
||||
"ppio": "PPIO Cloud Piou",
|
||||
"qiniu": "Qiniu AI",
|
||||
"qwenlm": "QwenLM",
|
||||
"silicon": "Silicium Fluide",
|
||||
"sophnet": "SophNet",
|
||||
"stepfun": "Échelon Étoile",
|
||||
"tencent-cloud-ti": "Tencent Cloud TI",
|
||||
"together": "Ensemble",
|
||||
@@ -3760,6 +3801,7 @@
|
||||
"description": "Désactiver les fonctionnalités du service MCP",
|
||||
"label": "Ne pas utiliser le serveur MCP"
|
||||
},
|
||||
"discover": "Découvrir",
|
||||
"duplicateName": "Un serveur portant le même nom existe déjà",
|
||||
"editJson": "Modifier le JSON",
|
||||
"editMcpJson": "Редактировать конфигурацию MCP",
|
||||
@@ -3770,6 +3812,10 @@
|
||||
"32000": "Échec du démarrage du serveur MCP, veuillez vérifier si tous les paramètres sont correctement remplis conformément au tutoriel",
|
||||
"toolNotFound": "Outil non trouvé {{name}}"
|
||||
},
|
||||
"fetch": {
|
||||
"button": "Récupérer les serveurs",
|
||||
"success": "Serveurs MCP récupérés avec succès"
|
||||
},
|
||||
"findMore": "Plus de serveurs MCP",
|
||||
"headers": "Заголовки запроса",
|
||||
"headersTooltip": "Пользовательские заголовки HTTP-запроса",
|
||||
@@ -3785,6 +3831,7 @@
|
||||
"logoUrl": "Адрес логотипа",
|
||||
"longRunning": "Mode d'exécution prolongée",
|
||||
"longRunningTooltip": "Une fois activé, le serveur prend en charge les tâches de longue durée, réinitialise le minuteur de temporisation à la réception des notifications de progression, et prolonge le délai d'expiration maximal à 10 minutes.",
|
||||
"marketplaces": "Places de marché",
|
||||
"missingDependencies": "Manquantes, veuillez les installer pour continuer",
|
||||
"more": {
|
||||
"awesome": "Liste sélectionnée de serveurs MCP",
|
||||
@@ -3833,6 +3880,7 @@
|
||||
"provider": "Поставщик",
|
||||
"providerPlaceholder": "Название поставщика",
|
||||
"providerUrl": "Адрес поставщика",
|
||||
"providers": "Fournisseurs",
|
||||
"registry": "Источник управления пакетами",
|
||||
"registryDefault": "По умолчанию",
|
||||
"registryTooltip": "Выберите источник для установки пакетов, чтобы решить проблемы с сетью по умолчанию.",
|
||||
@@ -3855,6 +3903,7 @@
|
||||
"searchNpx": "Поиск MCP",
|
||||
"serverPlural": "Serveurs",
|
||||
"serverSingular": "Serveur",
|
||||
"servers": "Serveurs MCP",
|
||||
"sse": "Серверные отправляемые события (sse)",
|
||||
"startError": "Ошибка запуска",
|
||||
"stdio": "Стандартный ввод/вывод (stdio)",
|
||||
@@ -4254,6 +4303,12 @@
|
||||
"aws-bedrock": {
|
||||
"access_key_id": "Identifiant de clé d'accès AWS",
|
||||
"access_key_id_help": "Votre identifiant de clé d'accès AWS, utilisé pour accéder au service AWS Bedrock",
|
||||
"api_key": "Clé API Bedrock",
|
||||
"api_key_help": "Votre clé API AWS Bedrock pour l'authentification",
|
||||
"auth_type": "Type d'authentification",
|
||||
"auth_type_api_key": "Clé API Bedrock",
|
||||
"auth_type_help": "Choisissez entre l'authentification par identifiants IAM ou par clé API Bedrock",
|
||||
"auth_type_iam": "Identifiants IAM",
|
||||
"description": "AWS Bedrock est un service de modèles de base entièrement géré proposé par Amazon, prenant en charge divers grands modèles linguistiques avancés.",
|
||||
"region": "Région AWS",
|
||||
"region_help": "Votre région de service AWS, par exemple us-east-1",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"agent": {
|
||||
"add": {
|
||||
"description": "さまざまなツールを使って複雑なタスクを処理する",
|
||||
"error": {
|
||||
"failed": "エージェントの追加に失敗しました",
|
||||
"invalid_agent": "無効なエージェント"
|
||||
@@ -338,6 +339,41 @@
|
||||
},
|
||||
"title": "API サーバー"
|
||||
},
|
||||
"appMenu": {
|
||||
"about": "について",
|
||||
"close": "ウィンドウを閉じる",
|
||||
"copy": "コピー",
|
||||
"cut": "切る",
|
||||
"delete": "削除",
|
||||
"documentation": "ドキュメント",
|
||||
"edit": "編集",
|
||||
"feedback": "フィードバック",
|
||||
"file": "ファイル",
|
||||
"forceReload": "強制再読み込み",
|
||||
"front": "すべてを前面に移動",
|
||||
"help": "助け",
|
||||
"hide": "隠す",
|
||||
"hideOthers": "他を隠す",
|
||||
"minimize": "最小化",
|
||||
"paste": "ペースト",
|
||||
"quit": "やめる",
|
||||
"redo": "やり直し",
|
||||
"releases": "リリース",
|
||||
"reload": "リロード",
|
||||
"resetZoom": "実寸",
|
||||
"selectAll": "すべて選択",
|
||||
"services": "サービス",
|
||||
"toggleDevTools": "開発者ツールを切り替え",
|
||||
"toggleFullscreen": "全画面表示を切り替え",
|
||||
"undo": "元に戻す",
|
||||
"unhide": "すべて表示",
|
||||
"view": "表示",
|
||||
"website": "ウェブサイト",
|
||||
"window": "窓",
|
||||
"zoom": "ズーム",
|
||||
"zoomIn": "ズームイン",
|
||||
"zoomOut": "ズームアウト"
|
||||
},
|
||||
"assistants": {
|
||||
"abbr": "アシスタント",
|
||||
"clear": {
|
||||
@@ -547,8 +583,12 @@
|
||||
"chat": {
|
||||
"add": {
|
||||
"assistant": {
|
||||
"description": "日常会話と簡単なQ&A",
|
||||
"title": "アシスタントを追加"
|
||||
},
|
||||
"option": {
|
||||
"title": "種類を選択"
|
||||
},
|
||||
"topic": {
|
||||
"title": "新しいトピック"
|
||||
}
|
||||
@@ -1606,7 +1646,7 @@
|
||||
},
|
||||
"assistant": {
|
||||
"added": {
|
||||
"content": "アシスタントが追加されました"
|
||||
"content": "助手が追加されました"
|
||||
}
|
||||
},
|
||||
"attachments": {
|
||||
@@ -2477,6 +2517,7 @@
|
||||
"qiniu": "七牛云 AI 推理",
|
||||
"qwenlm": "QwenLM",
|
||||
"silicon": "SiliconFlow",
|
||||
"sophnet": "SophNet",
|
||||
"stepfun": "StepFun",
|
||||
"tencent-cloud-ti": "Tencent Cloud TI",
|
||||
"together": "Together",
|
||||
@@ -3760,6 +3801,7 @@
|
||||
"description": "MCP機能を有効にしない",
|
||||
"label": "MCPサーバーを無効にする"
|
||||
},
|
||||
"discover": "発見",
|
||||
"duplicateName": "同じ名前のサーバーが既に存在します",
|
||||
"editJson": "JSONを編集",
|
||||
"editMcpJson": "MCP 設定を編集",
|
||||
@@ -3770,6 +3812,10 @@
|
||||
"32000": "MCP サーバーが起動しませんでした。パラメーターを確認してください",
|
||||
"toolNotFound": "ツール {{name}} が見つかりません"
|
||||
},
|
||||
"fetch": {
|
||||
"button": "サーバーを取得",
|
||||
"success": "MCPサーバーの取得に成功しました"
|
||||
},
|
||||
"findMore": "MCP を見つける",
|
||||
"headers": "ヘッダー",
|
||||
"headersTooltip": "HTTP リクエストのカスタムヘッダー",
|
||||
@@ -3785,6 +3831,7 @@
|
||||
"logoUrl": "ロゴURL",
|
||||
"longRunning": "長時間運行モード",
|
||||
"longRunningTooltip": "このオプションを有効にすると、サーバーは長時間のタスクをサポートします。進行状況通知を受信すると、タイムアウトがリセットされ、最大実行時間が10分に延長されます。",
|
||||
"marketplaces": "マーケットプレイス",
|
||||
"missingDependencies": "が不足しています。続行するにはインストールしてください。",
|
||||
"more": {
|
||||
"awesome": "厳選された MCP サーバーリスト",
|
||||
@@ -3833,6 +3880,7 @@
|
||||
"provider": "プロバイダー",
|
||||
"providerPlaceholder": "プロバイダー名",
|
||||
"providerUrl": "プロバイダーURL",
|
||||
"providers": "プロバイダー",
|
||||
"registry": "パッケージ管理レジストリ",
|
||||
"registryDefault": "デフォルト",
|
||||
"registryTooltip": "デフォルトのレジストリでネットワークの問題が発生した場合、パッケージインストールに使用するレジストリを選択してください。",
|
||||
@@ -3855,6 +3903,7 @@
|
||||
"searchNpx": "MCP を検索",
|
||||
"serverPlural": "サーバー",
|
||||
"serverSingular": "サーバー",
|
||||
"servers": "MCPサーバー",
|
||||
"sse": "サーバー送信イベント (sse)",
|
||||
"startError": "起動に失敗しました",
|
||||
"stdio": "標準入力/出力 (stdio)",
|
||||
@@ -4254,6 +4303,12 @@
|
||||
"aws-bedrock": {
|
||||
"access_key_id": "AWS アクセスキー ID",
|
||||
"access_key_id_help": "あなたの AWS アクセスキー ID は、AWS Bedrock サービスへのアクセスに使用されます",
|
||||
"api_key": "Bedrock APIキー",
|
||||
"api_key_help": "認証用のAWS Bedrock APIキー",
|
||||
"auth_type": "認証タイプ",
|
||||
"auth_type_api_key": "Bedrock APIキー",
|
||||
"auth_type_help": "IAM認証情報とBedrock APIキー認証のどちらかを選択してください",
|
||||
"auth_type_iam": "IAM認証情報",
|
||||
"description": "AWS Bedrock は、Amazon が提供する完全に管理されたベースモデルサービスで、さまざまな最先端の大言語モデルをサポートしています",
|
||||
"region": "AWS リージョン",
|
||||
"region_help": "あなたの AWS サービスリージョン、例:us-east-1",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"agent": {
|
||||
"add": {
|
||||
"description": "Lide com tarefas complexas usando várias ferramentas",
|
||||
"error": {
|
||||
"failed": "Falha ao adicionar agente",
|
||||
"invalid_agent": "Agent inválido"
|
||||
@@ -338,6 +339,41 @@
|
||||
},
|
||||
"title": "Servidor API"
|
||||
},
|
||||
"appMenu": {
|
||||
"about": "Sobre",
|
||||
"close": "Fechar Janela",
|
||||
"copy": "Copiar",
|
||||
"cut": "Corte",
|
||||
"delete": "Excluir",
|
||||
"documentation": "Documentação",
|
||||
"edit": "Editar",
|
||||
"feedback": "Feedback",
|
||||
"file": "Arquivo",
|
||||
"forceReload": "Forçar Recarregamento",
|
||||
"front": "Trazer Tudo para a Frente",
|
||||
"help": "Ajuda",
|
||||
"hide": "Esconder",
|
||||
"hideOthers": "Ocultar Outros",
|
||||
"minimize": "Minimizar",
|
||||
"paste": "Colar",
|
||||
"quit": "Sair",
|
||||
"redo": "Refazer",
|
||||
"releases": "Lançamentos",
|
||||
"reload": "Recarregar",
|
||||
"resetZoom": "Tamanho Real",
|
||||
"selectAll": "Selecionar Todos",
|
||||
"services": "Serviços",
|
||||
"toggleDevTools": "Alternar Ferramentas de Desenvolvedor",
|
||||
"toggleFullscreen": "Alternar Tela Cheia",
|
||||
"undo": "Desfazer",
|
||||
"unhide": "Mostrar Todos",
|
||||
"view": "Visualizar",
|
||||
"website": "Site",
|
||||
"window": "Janela",
|
||||
"zoom": "Zoom",
|
||||
"zoomIn": "Ampliar",
|
||||
"zoomOut": "Reduzir Zoom"
|
||||
},
|
||||
"assistants": {
|
||||
"abbr": "Assistente",
|
||||
"clear": {
|
||||
@@ -547,8 +583,12 @@
|
||||
"chat": {
|
||||
"add": {
|
||||
"assistant": {
|
||||
"description": "Conversas diárias e perguntas e respostas rápidas",
|
||||
"title": "Adicionar assistente"
|
||||
},
|
||||
"option": {
|
||||
"title": "Selecionar Tipo"
|
||||
},
|
||||
"topic": {
|
||||
"title": "Novo Tópico"
|
||||
}
|
||||
@@ -2471,12 +2511,13 @@
|
||||
"openrouter": "OpenRouter",
|
||||
"ovms": "Intel OVMS",
|
||||
"perplexity": "Perplexidade",
|
||||
"ph8": "Plataforma Aberta de Grandes Modelos PH8",
|
||||
"ph8": "PH8",
|
||||
"poe": "Poe",
|
||||
"ppio": "PPIO Nuvem Piao",
|
||||
"qiniu": "Qiniu AI",
|
||||
"qwenlm": "QwenLM",
|
||||
"silicon": "Silício em Fluxo",
|
||||
"sophnet": "SophNet",
|
||||
"stepfun": "Função de Passo Estelar",
|
||||
"tencent-cloud-ti": "Nuvem TI da Tencent",
|
||||
"together": "Juntos",
|
||||
@@ -3760,6 +3801,7 @@
|
||||
"description": "Não ativar a funcionalidade do serviço MCP",
|
||||
"label": "Não usar servidor MCP"
|
||||
},
|
||||
"discover": "Descobrir",
|
||||
"duplicateName": "Já existe um servidor com o mesmo nome",
|
||||
"editJson": "Editar JSON",
|
||||
"editMcpJson": "Editar Configuração MCP",
|
||||
@@ -3770,6 +3812,10 @@
|
||||
"32000": "Falha ao iniciar o servidor MCP, verifique se todos os parâmetros foram preenchidos corretamente conforme o tutorial",
|
||||
"toolNotFound": "Ferramenta não encontrada {{name}}"
|
||||
},
|
||||
"fetch": {
|
||||
"button": "Buscar Servidores",
|
||||
"success": "Servidores MCP obtidos com sucesso"
|
||||
},
|
||||
"findMore": "Mais servidores MCP",
|
||||
"headers": "Cabeçalhos da Requisição",
|
||||
"headersTooltip": "Cabeçalhos HTTP personalizados para as requisições",
|
||||
@@ -3785,6 +3831,7 @@
|
||||
"logoUrl": "URL do Logotipo",
|
||||
"longRunning": "Modo de execução prolongada",
|
||||
"longRunningTooltip": "Quando ativado, o servidor suporta tarefas de longa duração, redefinindo o temporizador de tempo limite ao receber notificações de progresso e estendendo o tempo máximo de tempo limite para 10 minutos.",
|
||||
"marketplaces": "Mercados",
|
||||
"missingDependencies": "Ausente, instale para continuar",
|
||||
"more": {
|
||||
"awesome": "Lista selecionada de servidores MCP",
|
||||
@@ -3833,6 +3880,7 @@
|
||||
"provider": "Fornecedor",
|
||||
"providerPlaceholder": "Nome do Fornecedor",
|
||||
"providerUrl": "URL do Fornecedor",
|
||||
"providers": "Fornecedores",
|
||||
"registry": "Fonte de Gerenciamento de Pacotes",
|
||||
"registryDefault": "Padrão",
|
||||
"registryTooltip": "Selecione uma fonte alternativa para instalar pacotes, caso tenha problemas de rede com a fonte padrão.",
|
||||
@@ -3855,6 +3903,7 @@
|
||||
"searchNpx": "Buscar MCP",
|
||||
"serverPlural": "Servidores",
|
||||
"serverSingular": "Servidor",
|
||||
"servers": "Servidores MCP",
|
||||
"sse": "Eventos do Servidor (sse)",
|
||||
"startError": "Falha ao Iniciar",
|
||||
"stdio": "Entrada/Saída Padrão (stdio)",
|
||||
@@ -4254,6 +4303,12 @@
|
||||
"aws-bedrock": {
|
||||
"access_key_id": "ID da chave de acesso da AWS",
|
||||
"access_key_id_help": "O seu ID da chave de acesso AWS, utilizado para aceder ao serviço AWS Bedrock",
|
||||
"api_key": "Chave de API do Bedrock",
|
||||
"api_key_help": "Sua Chave de API AWS Bedrock para autenticação",
|
||||
"auth_type": "Tipo de Autenticação",
|
||||
"auth_type_api_key": "Chave de API do Bedrock",
|
||||
"auth_type_help": "Escolha entre credenciais IAM ou autenticação por chave de API do Bedrock",
|
||||
"auth_type_iam": "Credenciais IAM",
|
||||
"description": "A AWS Bedrock é um serviço de modelos fundamentais totalmente gerido fornecido pela Amazon, que suporta diversos modelos avançados de linguagem.",
|
||||
"region": "Região da AWS",
|
||||
"region_help": "A sua região de serviço da AWS, por exemplo, us-east-1",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"agent": {
|
||||
"add": {
|
||||
"description": "Справляйтесь со сложными задачами с помощью различных инструментов",
|
||||
"error": {
|
||||
"failed": "Не удалось добавить агента",
|
||||
"invalid_agent": "Недействительный агент"
|
||||
@@ -338,6 +339,41 @@
|
||||
},
|
||||
"title": "API Сервер"
|
||||
},
|
||||
"appMenu": {
|
||||
"about": "О",
|
||||
"close": "Закрыть окно",
|
||||
"copy": "Копировать",
|
||||
"cut": "Резать",
|
||||
"delete": "Удалить",
|
||||
"documentation": "Документация",
|
||||
"edit": "Редактировать",
|
||||
"feedback": "Обратная связь",
|
||||
"file": "Файл",
|
||||
"forceReload": "Принудительная перезагрузка",
|
||||
"front": "Показать все поверх других",
|
||||
"help": "Помощь",
|
||||
"hide": "Скрыть",
|
||||
"hideOthers": "Скрыть остальные",
|
||||
"minimize": "Минимизировать",
|
||||
"paste": "Вставить",
|
||||
"quit": "Выйти",
|
||||
"redo": "Переделать",
|
||||
"releases": "Релизы",
|
||||
"reload": "Перезагрузка",
|
||||
"resetZoom": "Настоящий размер",
|
||||
"selectAll": "Выбрать все",
|
||||
"services": "Услуги",
|
||||
"toggleDevTools": "Переключить инструменты разработчика",
|
||||
"toggleFullscreen": "Переключить полноэкранный режим",
|
||||
"undo": "Отменить",
|
||||
"unhide": "Показать все",
|
||||
"view": "Вид",
|
||||
"website": "Веб-сайт",
|
||||
"window": "Окно",
|
||||
"zoom": "Zoom",
|
||||
"zoomIn": "Увеличить",
|
||||
"zoomOut": "Отдалить"
|
||||
},
|
||||
"assistants": {
|
||||
"abbr": "Ассистент",
|
||||
"clear": {
|
||||
@@ -547,8 +583,12 @@
|
||||
"chat": {
|
||||
"add": {
|
||||
"assistant": {
|
||||
"description": "Ежедневные разговоры и быстрые вопросы и ответы",
|
||||
"title": "Добавить ассистента"
|
||||
},
|
||||
"option": {
|
||||
"title": "Выберите тип"
|
||||
},
|
||||
"topic": {
|
||||
"title": "Новый топик"
|
||||
}
|
||||
@@ -2477,6 +2517,7 @@
|
||||
"qiniu": "Qiniu AI",
|
||||
"qwenlm": "QwenLM",
|
||||
"silicon": "SiliconFlow",
|
||||
"sophnet": "SophNet",
|
||||
"stepfun": "StepFun",
|
||||
"tencent-cloud-ti": "Tencent Cloud TI",
|
||||
"together": "Together",
|
||||
@@ -3760,6 +3801,7 @@
|
||||
"description": "Не включать функциональность сервера MCP",
|
||||
"label": "Отключить сервер MCP"
|
||||
},
|
||||
"discover": "Откройте",
|
||||
"duplicateName": "Сервер с таким именем уже существует",
|
||||
"editJson": "Редактировать JSON",
|
||||
"editMcpJson": "Редактировать MCP",
|
||||
@@ -3770,6 +3812,10 @@
|
||||
"32000": "MCP сервер не запущен, пожалуйста, проверьте параметры",
|
||||
"toolNotFound": "Инструмент {{name}} не найден"
|
||||
},
|
||||
"fetch": {
|
||||
"button": "Получить серверы",
|
||||
"success": "Успешно получены MCP-серверы"
|
||||
},
|
||||
"findMore": "Найти больше MCP",
|
||||
"headers": "Заголовки",
|
||||
"headersTooltip": "Пользовательские заголовки для HTTP-запросов",
|
||||
@@ -3785,6 +3831,7 @@
|
||||
"logoUrl": "URL логотипа",
|
||||
"longRunning": "Длительный режим работы",
|
||||
"longRunningTooltip": "Включив эту опцию, сервер будет поддерживать длительные задачи. При получении уведомлений о ходе выполнения будет сброшен тайм-аут и максимальное время выполнения будет увеличено до 10 минут.",
|
||||
"marketplaces": "Торговые площадки",
|
||||
"missingDependencies": "отсутствует, пожалуйста, установите для продолжения.",
|
||||
"more": {
|
||||
"awesome": "Кураторский список серверов MCP",
|
||||
@@ -3833,6 +3880,7 @@
|
||||
"provider": "Провайдер",
|
||||
"providerPlaceholder": "Имя провайдера",
|
||||
"providerUrl": "URL провайдера",
|
||||
"providers": "Поставщики",
|
||||
"registry": "Реестр пакетов",
|
||||
"registryDefault": "По умолчанию",
|
||||
"registryTooltip": "Выберите реестр для установки пакетов, если возникают проблемы с сетью при использовании реестра по умолчанию.",
|
||||
@@ -3855,6 +3903,7 @@
|
||||
"searchNpx": "Найти MCP",
|
||||
"serverPlural": "серверы",
|
||||
"serverSingular": "сервер",
|
||||
"servers": "Серверы MCP",
|
||||
"sse": "События, отправляемые сервером (sse)",
|
||||
"startError": "Запуск не удалось",
|
||||
"stdio": "Стандартный ввод/вывод (stdio)",
|
||||
@@ -4254,6 +4303,12 @@
|
||||
"aws-bedrock": {
|
||||
"access_key_id": "AWS Ключ доступа ID",
|
||||
"access_key_id_help": "Ваш AWS Ключ доступа ID для доступа к AWS Bedrock",
|
||||
"api_key": "Ключ API Bedrock",
|
||||
"api_key_help": "Ваш ключ API AWS Bedrock для аутентификации",
|
||||
"auth_type": "Тип аутентификации",
|
||||
"auth_type_api_key": "Ключ API Bedrock",
|
||||
"auth_type_help": "Выберите между аутентификацией с помощью учетных данных IAM или ключа API Bedrock",
|
||||
"auth_type_iam": "Учетные данные IAM",
|
||||
"description": "AWS Bedrock — это полное управляемое сервисное предложение для моделей, поддерживающее различные современные модели языка",
|
||||
"region": "AWS регион",
|
||||
"region_help": "Ваш регион AWS, например us-east-1",
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Alert } from '@heroui/react'
|
||||
import { loggerService } from '@logger'
|
||||
import type { ContentSearchRef } from '@renderer/components/ContentSearch'
|
||||
import { ContentSearch } from '@renderer/components/ContentSearch'
|
||||
@@ -17,7 +16,7 @@ import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import type { Assistant, Topic } from '@renderer/types'
|
||||
import { classNames } from '@renderer/utils'
|
||||
import { Flex } from 'antd'
|
||||
import { Alert, Flex } from 'antd'
|
||||
import { debounce } from 'lodash'
|
||||
import { AnimatePresence, motion } from 'motion/react'
|
||||
import type { FC } from 'react'
|
||||
@@ -170,11 +169,7 @@ const Chat: FC<Props> = (props) => {
|
||||
return () => <div> Active Session ID is invalid.</div>
|
||||
}
|
||||
if (!apiServer.enabled) {
|
||||
return () => (
|
||||
<div>
|
||||
<Alert color="warning" title={t('agent.warning.enable_server')} />
|
||||
</div>
|
||||
)
|
||||
return () => <Alert type="warning" message={t('agent.warning.enable_server')} style={{ margin: '5px 16px' }} />
|
||||
}
|
||||
return () => <AgentSessionMessages agentId={activeAgentId} sessionId={activeSessionId} />
|
||||
}, [activeAgentId, activeSessionId, apiServer.enabled, t])
|
||||
@@ -191,22 +186,14 @@ const Chat: FC<Props> = (props) => {
|
||||
|
||||
// TODO: more info
|
||||
const AgentInvalid = useCallback(() => {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<div>
|
||||
<Alert color="warning" title="Select an agent" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
return <Alert type="warning" message="Select an agent" style={{ margin: '5px 16px' }} />
|
||||
}, [])
|
||||
|
||||
// TODO: more info
|
||||
const SessionInvalid = useCallback(() => {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<div>
|
||||
<Alert color="warning" title="Create a session" />
|
||||
</div>
|
||||
<Alert type="warning" message="Create a session" style={{ margin: '5px 16px' }} />
|
||||
</div>
|
||||
)
|
||||
}, [])
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Tooltip } from '@heroui/react'
|
||||
import { loggerService } from '@logger'
|
||||
import { ActionIconButton } from '@renderer/components/Buttons'
|
||||
import { QuickPanelView } from '@renderer/components/QuickPanel'
|
||||
@@ -11,6 +10,7 @@ import { useShortcutDisplay } from '@renderer/hooks/useShortcuts'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import PasteService from '@renderer/services/PasteService'
|
||||
import { pauseTrace } from '@renderer/services/SpanManagerService'
|
||||
import { estimateUserPromptUsage } from '@renderer/services/TokenService'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { newMessagesActions, selectMessagesForTopic } from '@renderer/store/newMessage'
|
||||
import { sendMessage as dispatchSendMessage } from '@renderer/store/thunk/messageThunk'
|
||||
@@ -22,6 +22,7 @@ import { abortCompletion } from '@renderer/utils/abortController'
|
||||
import { buildAgentSessionTopicId } from '@renderer/utils/agentSession'
|
||||
import { getSendMessageShortcutLabel, isSendMessageKeyPressed } from '@renderer/utils/input'
|
||||
import { createMainTextBlock, createMessage } from '@renderer/utils/messageUtils/create'
|
||||
import { Tooltip } from 'antd'
|
||||
import type { TextAreaRef } from 'antd/es/input/TextArea'
|
||||
import TextArea from 'antd/es/input/TextArea'
|
||||
import { isEmpty } from 'lodash'
|
||||
@@ -199,11 +200,15 @@ const AgentSessionInputbar: FC<Props> = ({ agentId, sessionId }) => {
|
||||
}
|
||||
: undefined
|
||||
|
||||
// Calculate token usage for the user message
|
||||
const usage = await estimateUserPromptUsage({ content: text })
|
||||
|
||||
const userMessage: Message = createMessage('user', sessionTopicId, agentId, {
|
||||
id: userMessageId,
|
||||
blocks: userMessageBlocks.map((block) => block?.id),
|
||||
model,
|
||||
modelId: model?.id
|
||||
modelId: model?.id,
|
||||
usage
|
||||
})
|
||||
|
||||
const assistantStub: Assistant = {
|
||||
@@ -309,7 +314,7 @@ const AgentSessionInputbar: FC<Props> = ({ agentId, sessionId }) => {
|
||||
/>
|
||||
<Toolbar>
|
||||
<ToolbarGroup>
|
||||
<Tooltip placement="top" content={t('chat.input.new_topic', { Command: newTopicShortcut })} delay={0}>
|
||||
<Tooltip placement="top" title={t('chat.input.new_topic', { Command: newTopicShortcut })}>
|
||||
<ActionIconButton
|
||||
onClick={handleCreateSession}
|
||||
disabled={createSessionDisabled}
|
||||
@@ -321,7 +326,7 @@ const AgentSessionInputbar: FC<Props> = ({ agentId, sessionId }) => {
|
||||
<ToolbarGroup>
|
||||
<SendMessageButton sendMessage={sendMessage} disabled={sendDisabled} />
|
||||
{canAbort && (
|
||||
<Tooltip placement="top" content={t('chat.input.pause')}>
|
||||
<Tooltip placement="top" title={t('chat.input.pause')}>
|
||||
<ActionIconButton onClick={abortAgentSession} style={{ marginRight: -2 }}>
|
||||
<CirclePause size={20} color="var(--color-error)" />
|
||||
</ActionIconButton>
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { DropResult } from '@hello-pangea/dnd'
|
||||
import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd'
|
||||
import { loggerService } from '@logger'
|
||||
import { ActionIconButton } from '@renderer/components/Buttons'
|
||||
import { MdiLightbulbOn } from '@renderer/components/Icons'
|
||||
import type { QuickPanelListItem } from '@renderer/components/QuickPanel'
|
||||
import {
|
||||
isAnthropicModel,
|
||||
@@ -230,6 +231,15 @@ const InputbarTools = ({
|
||||
quickPhrasesButtonRef.current?.openQuickPanel()
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('assistants.settings.reasoning_effort.label'),
|
||||
description: '',
|
||||
icon: <MdiLightbulbOn />,
|
||||
isMenu: true,
|
||||
action: () => {
|
||||
thinkingButtonRef.current?.openQuickPanel()
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('assistants.presets.edit.model.select.title'),
|
||||
description: '',
|
||||
@@ -245,6 +255,7 @@ const InputbarTools = ({
|
||||
icon: <FileSearch />,
|
||||
isMenu: true,
|
||||
disabled: files.length > 0,
|
||||
hidden: !showKnowledgeBaseButton,
|
||||
action: () => {
|
||||
knowledgeBaseButtonRef.current?.openQuickPanel()
|
||||
}
|
||||
@@ -312,7 +323,7 @@ const InputbarTools = ({
|
||||
translate()
|
||||
}
|
||||
}
|
||||
]
|
||||
] satisfies QuickPanelListItem[]
|
||||
}
|
||||
|
||||
const handleDragEnd = (result: DropResult) => {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Button } from '@heroui/button'
|
||||
import CodeViewer from '@renderer/components/CodeViewer'
|
||||
import { useCodeStyle } from '@renderer/context/CodeStyleProvider'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
@@ -33,6 +32,7 @@ import {
|
||||
} from '@renderer/types/error'
|
||||
import type { ErrorMessageBlock, Message } from '@renderer/types/newMessage'
|
||||
import { formatAiSdkError, formatError, safeToString } from '@renderer/utils/error'
|
||||
import { Button } from 'antd'
|
||||
import { Alert as AntdAlert, Modal } from 'antd'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
@@ -144,9 +144,11 @@ const MessageErrorInfo: React.FC<{ block: ErrorMessageBlock; message: Message }>
|
||||
onClick={showErrorDetail}
|
||||
style={{ cursor: 'pointer' }}
|
||||
action={
|
||||
<Button size="sm" className="p-0" variant="light" onPress={showErrorDetail}>
|
||||
{t('common.detail')}
|
||||
</Button>
|
||||
<>
|
||||
<Button size="middle" color="default" variant="text" onClick={showErrorDetail}>
|
||||
{t('common.detail')}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<ErrorDetailModal open={showDetailModal} onClose={() => setShowDetailModal(false)} error={block.error} />
|
||||
@@ -198,10 +200,10 @@ const ErrorDetailModal: React.FC<ErrorDetailModalProps> = ({ open, onClose, erro
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
footer={[
|
||||
<Button key="copy" size="sm" variant="light" onPress={copyErrorDetails}>
|
||||
<Button key="copy" variant="text" color="default" onClick={copyErrorDetails}>
|
||||
{t('common.copy')}
|
||||
</Button>,
|
||||
<Button key="close" size="sm" variant="light" onPress={onClose}>
|
||||
<Button key="close" variant="text" color={'default'} onClick={onClose}>
|
||||
{t('common.close')}
|
||||
</Button>
|
||||
]}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Spinner } from '@heroui/react'
|
||||
import { MessageBlockStatus, MessageBlockType, type PlaceholderMessageBlock } from '@renderer/types/newMessage'
|
||||
import React from 'react'
|
||||
import { BeatLoader } from 'react-spinners'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface PlaceholderBlockProps {
|
||||
@@ -10,7 +10,7 @@ const PlaceholderBlock: React.FC<PlaceholderBlockProps> = ({ block }) => {
|
||||
if (block.status === MessageBlockStatus.PROCESSING && block.type === MessageBlockType.UNKNOWN) {
|
||||
return (
|
||||
<MessageContentLoading>
|
||||
<Spinner color="current" variant="dots" />
|
||||
<BeatLoader color="var(--color-text-1)" size={8} speedMultiplier={0.8} />
|
||||
</MessageContentLoading>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { cn } from '@heroui/react'
|
||||
import { loggerService } from '@logger'
|
||||
import HorizontalScrollContainer from '@renderer/components/HorizontalScrollContainer'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
@@ -15,7 +14,7 @@ import { getModelUniqId } from '@renderer/services/ModelService'
|
||||
import { estimateMessageUsage } from '@renderer/services/TokenService'
|
||||
import type { Assistant, Topic } from '@renderer/types'
|
||||
import type { Message, MessageBlock } from '@renderer/types/newMessage'
|
||||
import { classNames } from '@renderer/utils'
|
||||
import { classNames, cn } from '@renderer/utils'
|
||||
import { isMessageProcessing } from '@renderer/utils/messageUtils/is'
|
||||
import { Divider } from 'antd'
|
||||
import type { Dispatch, FC, SetStateAction } from 'react'
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AccordionItem, Chip, Code } from '@heroui/react'
|
||||
import type { CollapseProps } from 'antd'
|
||||
import { Tag } from 'antd'
|
||||
import { CheckCircle, Terminal, XCircle } from 'lucide-react'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
@@ -15,7 +16,13 @@ interface ParsedBashOutput {
|
||||
tool_use_error?: string
|
||||
}
|
||||
|
||||
export function BashOutputTool({ input, output }: { input: BashOutputToolInput; output?: BashOutputToolOutput }) {
|
||||
export function BashOutputTool({
|
||||
input,
|
||||
output
|
||||
}: {
|
||||
input: BashOutputToolInput
|
||||
output?: BashOutputToolOutput
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
// 解析 XML 输出
|
||||
const parsedOutput = useMemo(() => {
|
||||
if (!output) return null
|
||||
@@ -84,93 +91,88 @@ export function BashOutputTool({ input, output }: { input: BashOutputToolInput;
|
||||
} as const
|
||||
}, [parsedOutput])
|
||||
|
||||
return (
|
||||
<AccordionItem
|
||||
key={AgentToolsType.BashOutput}
|
||||
aria-label="BashOutput Tool"
|
||||
title={
|
||||
const children = parsedOutput ? (
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Status Info */}
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{parsedOutput.exit_code !== undefined && (
|
||||
<Tag color={parsedOutput.exit_code === 0 ? 'success' : 'danger'}>Exit Code: {parsedOutput.exit_code}</Tag>
|
||||
)}
|
||||
{parsedOutput.timestamp && (
|
||||
<Tag className="py-0 font-mono text-xs">{new Date(parsedOutput.timestamp).toLocaleString()}</Tag>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Standard Output */}
|
||||
{parsedOutput.stdout && (
|
||||
<div>
|
||||
<div className="mb-2 font-medium text-default-600 text-xs">stdout:</div>
|
||||
<pre className="whitespace-pre-wrap font-mono text-default-700 text-xs dark:text-default-300">
|
||||
{parsedOutput.stdout}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Standard Error */}
|
||||
{parsedOutput.stderr && (
|
||||
<div className="border border-danger-200">
|
||||
<div className="mb-2 font-medium text-danger-600 text-xs">stderr:</div>
|
||||
<pre className="whitespace-pre-wrap font-mono text-danger-600 text-xs dark:text-danger-400">
|
||||
{parsedOutput.stderr}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tool Use Error */}
|
||||
{parsedOutput.tool_use_error && (
|
||||
<div className="border border-danger-200">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<XCircle className="h-4 w-4 text-danger" />
|
||||
<span className="font-medium text-danger-600 text-xs">Error:</span>
|
||||
</div>
|
||||
<pre className="whitespace-pre-wrap font-mono text-danger-600 text-xs dark:text-danger-400">
|
||||
{parsedOutput.tool_use_error}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
// 原始输出(如果解析失败或非 XML 格式)
|
||||
output && (
|
||||
<div>
|
||||
<pre className="whitespace-pre-wrap font-mono text-default-700 text-xs dark:text-default-300">{output}</pre>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
return {
|
||||
key: AgentToolsType.BashOutput,
|
||||
label: (
|
||||
<>
|
||||
<ToolTitle
|
||||
icon={<Terminal className="h-4 w-4" />}
|
||||
label="Bash Output"
|
||||
params={
|
||||
<div className="flex items-center gap-2">
|
||||
<Code size="sm" className="py-0 text-xs">
|
||||
{input.bash_id}
|
||||
</Code>
|
||||
<Tag className="py-0 font-mono text-xs">{input.bash_id}</Tag>
|
||||
{statusConfig && (
|
||||
<Chip
|
||||
size="sm"
|
||||
<Tag
|
||||
color={statusConfig.color}
|
||||
variant="flat"
|
||||
startContent={statusConfig.icon}
|
||||
className="h-5">
|
||||
icon={statusConfig.icon}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: '2px'
|
||||
}}>
|
||||
{statusConfig.text}
|
||||
</Chip>
|
||||
</Tag>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
}
|
||||
classNames={{
|
||||
content: 'space-y-3 px-1'
|
||||
}}>
|
||||
{parsedOutput ? (
|
||||
<>
|
||||
{/* Status Info */}
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{parsedOutput.exit_code !== undefined && (
|
||||
<Chip size="sm" color={parsedOutput.exit_code === 0 ? 'success' : 'danger'} variant="flat">
|
||||
Exit Code: {parsedOutput.exit_code}
|
||||
</Chip>
|
||||
)}
|
||||
{parsedOutput.timestamp && (
|
||||
<Code size="sm" className="py-0 text-xs">
|
||||
{new Date(parsedOutput.timestamp).toLocaleString()}
|
||||
</Code>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
|
||||
{/* Standard Output */}
|
||||
{parsedOutput.stdout && (
|
||||
<div>
|
||||
<div className="mb-2 font-medium text-default-600 text-xs">stdout:</div>
|
||||
<pre className="whitespace-pre-wrap font-mono text-default-700 text-xs dark:text-default-300">
|
||||
{parsedOutput.stdout}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Standard Error */}
|
||||
{parsedOutput.stderr && (
|
||||
<div className="border border-danger-200">
|
||||
<div className="mb-2 font-medium text-danger-600 text-xs">stderr:</div>
|
||||
<pre className="whitespace-pre-wrap font-mono text-danger-600 text-xs dark:text-danger-400">
|
||||
{parsedOutput.stderr}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tool Use Error */}
|
||||
{parsedOutput.tool_use_error && (
|
||||
<div className="border border-danger-200">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<XCircle className="h-4 w-4 text-danger" />
|
||||
<span className="font-medium text-danger-600 text-xs">Error:</span>
|
||||
</div>
|
||||
<pre className="whitespace-pre-wrap font-mono text-danger-600 text-xs dark:text-danger-400">
|
||||
{parsedOutput.tool_use_error}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
// 原始输出(如果解析失败或非 XML 格式)
|
||||
output && (
|
||||
<div>
|
||||
<pre className="whitespace-pre-wrap font-mono text-default-700 text-xs dark:text-default-300">{output}</pre>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</AccordionItem>
|
||||
)
|
||||
children: children
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,35 @@
|
||||
import { AccordionItem, Code } from '@heroui/react'
|
||||
import type { CollapseProps } from 'antd'
|
||||
import { Tag } from 'antd'
|
||||
import { Terminal } from 'lucide-react'
|
||||
|
||||
import { ToolTitle } from './GenericTools'
|
||||
import type { BashToolInput as BashToolInputType, BashToolOutput as BashToolOutputType } from './types'
|
||||
|
||||
export function BashTool({ input, output }: { input: BashToolInputType; output?: BashToolOutputType }) {
|
||||
export function BashTool({
|
||||
input,
|
||||
output
|
||||
}: {
|
||||
input: BashToolInputType
|
||||
output?: BashToolOutputType
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
// 如果有输出,计算输出行数
|
||||
const outputLines = output ? output.split('\n').length : 0
|
||||
|
||||
return (
|
||||
<AccordionItem
|
||||
key="tool"
|
||||
aria-label="Bash Tool"
|
||||
title={
|
||||
return {
|
||||
key: 'tool',
|
||||
label: (
|
||||
<>
|
||||
<ToolTitle
|
||||
icon={<Terminal className="h-4 w-4" />}
|
||||
label="Bash"
|
||||
params={input.description}
|
||||
stats={output ? `${outputLines} ${outputLines === 1 ? 'line' : 'lines'}` : undefined}
|
||||
/>
|
||||
}
|
||||
subtitle={
|
||||
<Code size="sm" className="line-clamp-1 w-max max-w-full text-ellipsis py-0 text-xs">
|
||||
{input.command}
|
||||
</Code>
|
||||
}>
|
||||
<div className="whitespace-pre-line">{output}</div>
|
||||
</AccordionItem>
|
||||
)
|
||||
<div className="mt-1">
|
||||
<Tag className="whitespace-pre-wrap break-all font-mono">{input.command}</Tag>
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
children: <div className="whitespace-pre-line">{output}</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AccordionItem } from '@heroui/react'
|
||||
import type { CollapseProps } from 'antd'
|
||||
import { FileEdit } from 'lucide-react'
|
||||
|
||||
import { ToolTitle } from './GenericTools'
|
||||
@@ -28,19 +28,26 @@ export const renderCodeBlock = (content: string, variant: 'old' | 'new') => {
|
||||
)
|
||||
}
|
||||
|
||||
export function EditTool({ input, output }: { input: EditToolInput; output?: EditToolOutput }) {
|
||||
return (
|
||||
<AccordionItem
|
||||
key={AgentToolsType.Edit}
|
||||
aria-label="Edit Tool"
|
||||
title={<ToolTitle icon={<FileEdit className="h-4 w-4" />} label="Edit" params={input.file_path} />}>
|
||||
{/* Diff View */}
|
||||
{/* Old Content */}
|
||||
{renderCodeBlock(input.old_string, 'old')}
|
||||
{/* New Content */}
|
||||
{renderCodeBlock(input.new_string, 'new')}
|
||||
{/* Output */}
|
||||
{output}
|
||||
</AccordionItem>
|
||||
)
|
||||
export function EditTool({
|
||||
input,
|
||||
output
|
||||
}: {
|
||||
input: EditToolInput
|
||||
output?: EditToolOutput
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
return {
|
||||
key: AgentToolsType.Edit,
|
||||
label: <ToolTitle icon={<FileEdit className="h-4 w-4" />} label="Edit" params={input.file_path} />,
|
||||
children: (
|
||||
<>
|
||||
{/* Diff View */}
|
||||
{/* Old Content */}
|
||||
{renderCodeBlock(input.old_string, 'old')}
|
||||
{/* New Content */}
|
||||
{renderCodeBlock(input.new_string, 'new')}
|
||||
{/* Output */}
|
||||
{output}
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AccordionItem } from '@heroui/react'
|
||||
import type { CollapseProps } from 'antd'
|
||||
import { DoorOpen } from 'lucide-react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
|
||||
@@ -6,19 +6,22 @@ import { ToolTitle } from './GenericTools'
|
||||
import type { ExitPlanModeToolInput, ExitPlanModeToolOutput } from './types'
|
||||
import { AgentToolsType } from './types'
|
||||
|
||||
export function ExitPlanModeTool({ input, output }: { input: ExitPlanModeToolInput; output?: ExitPlanModeToolOutput }) {
|
||||
return (
|
||||
<AccordionItem
|
||||
key={AgentToolsType.ExitPlanMode}
|
||||
aria-label="ExitPlanMode Tool"
|
||||
title={
|
||||
<ToolTitle
|
||||
icon={<DoorOpen className="h-4 w-4" />}
|
||||
label="ExitPlanMode"
|
||||
stats={`${input.plan.split('\n\n').length} plans`}
|
||||
/>
|
||||
}>
|
||||
{<ReactMarkdown>{input.plan + '\n\n' + (output ?? '')}</ReactMarkdown>}
|
||||
</AccordionItem>
|
||||
)
|
||||
export function ExitPlanModeTool({
|
||||
input,
|
||||
output
|
||||
}: {
|
||||
input: ExitPlanModeToolInput
|
||||
output?: ExitPlanModeToolOutput
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
return {
|
||||
key: AgentToolsType.ExitPlanMode,
|
||||
label: (
|
||||
<ToolTitle
|
||||
icon={<DoorOpen className="h-4 w-4" />}
|
||||
label="ExitPlanMode"
|
||||
stats={`${input.plan.split('\n\n').length} plans`}
|
||||
/>
|
||||
),
|
||||
children: <ReactMarkdown>{input.plan + '\n\n' + (output ?? '')}</ReactMarkdown>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,29 @@
|
||||
import { AccordionItem } from '@heroui/react'
|
||||
import type { CollapseProps } from 'antd'
|
||||
import { FolderSearch } from 'lucide-react'
|
||||
|
||||
import { ToolTitle } from './GenericTools'
|
||||
import type { GlobToolInput as GlobToolInputType, GlobToolOutput as GlobToolOutputType } from './types'
|
||||
|
||||
export function GlobTool({ input, output }: { input: GlobToolInputType; output?: GlobToolOutputType }) {
|
||||
export function GlobTool({
|
||||
input,
|
||||
output
|
||||
}: {
|
||||
input: GlobToolInputType
|
||||
output?: GlobToolOutputType
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
// 如果有输出,计算文件数量
|
||||
const lineCount = output ? output.split('\n').filter((line) => line.trim()).length : 0
|
||||
|
||||
return (
|
||||
<AccordionItem
|
||||
key="tool"
|
||||
aria-label="Glob Tool"
|
||||
title={
|
||||
<ToolTitle
|
||||
icon={<FolderSearch className="h-4 w-4" />}
|
||||
label="Glob"
|
||||
params={input.pattern}
|
||||
stats={output ? `${lineCount} of output` : undefined}
|
||||
/>
|
||||
}>
|
||||
<div>{output}</div>
|
||||
</AccordionItem>
|
||||
)
|
||||
return {
|
||||
key: 'tool',
|
||||
label: (
|
||||
<ToolTitle
|
||||
icon={<FolderSearch className="h-4 w-4" />}
|
||||
label="Glob"
|
||||
params={input.pattern}
|
||||
stats={output ? `${lineCount} ${lineCount === 1 ? 'file' : 'files'}` : undefined}
|
||||
/>
|
||||
),
|
||||
children: <div>{output}</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,34 @@
|
||||
import { AccordionItem } from '@heroui/react'
|
||||
import type { CollapseProps } from 'antd'
|
||||
import { FileSearch } from 'lucide-react'
|
||||
|
||||
import { ToolTitle } from './GenericTools'
|
||||
import type { GrepToolInput, GrepToolOutput } from './types'
|
||||
|
||||
export function GrepTool({ input, output }: { input: GrepToolInput; output?: GrepToolOutput }) {
|
||||
export function GrepTool({
|
||||
input,
|
||||
output
|
||||
}: {
|
||||
input: GrepToolInput
|
||||
output?: GrepToolOutput
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
// 如果有输出,计算结果行数
|
||||
const resultLines = output ? output.split('\n').filter((line) => line.trim()).length : 0
|
||||
|
||||
return (
|
||||
<AccordionItem
|
||||
key="tool"
|
||||
aria-label="Grep Tool"
|
||||
title={
|
||||
<ToolTitle
|
||||
icon={<FileSearch className="h-4 w-4" />}
|
||||
label="Grep"
|
||||
params={
|
||||
<>
|
||||
{input.pattern}
|
||||
{input.output_mode && <span className="ml-1">({input.output_mode})</span>}
|
||||
</>
|
||||
}
|
||||
stats={output ? `${resultLines} ${resultLines === 1 ? 'line' : 'lines'}` : undefined}
|
||||
/>
|
||||
}>
|
||||
<div>{output}</div>
|
||||
</AccordionItem>
|
||||
)
|
||||
return {
|
||||
key: 'tool',
|
||||
label: (
|
||||
<ToolTitle
|
||||
icon={<FileSearch className="h-4 w-4" />}
|
||||
label="Grep"
|
||||
params={
|
||||
<>
|
||||
{input.pattern}
|
||||
{input.output_mode && <span className="ml-1">({input.output_mode})</span>}
|
||||
</>
|
||||
}
|
||||
stats={output ? `${resultLines} ${resultLines === 1 ? 'line' : 'lines'}` : undefined}
|
||||
/>
|
||||
),
|
||||
children: <div>{output}</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AccordionItem } from '@heroui/react'
|
||||
import type { CollapseProps } from 'antd'
|
||||
import { FileText } from 'lucide-react'
|
||||
|
||||
import { renderCodeBlock } from './EditTool'
|
||||
@@ -6,18 +6,24 @@ import { ToolTitle } from './GenericTools'
|
||||
import type { MultiEditToolInput, MultiEditToolOutput } from './types'
|
||||
import { AgentToolsType } from './types'
|
||||
|
||||
export function MultiEditTool({ input }: { input: MultiEditToolInput; output?: MultiEditToolOutput }) {
|
||||
return (
|
||||
<AccordionItem
|
||||
key={AgentToolsType.MultiEdit}
|
||||
aria-label="MultiEdit Tool"
|
||||
title={<ToolTitle icon={<FileText className="h-4 w-4" />} label="MultiEdit" params={input.file_path} />}>
|
||||
{input.edits.map((edit, index) => (
|
||||
<div key={index}>
|
||||
{renderCodeBlock(edit.old_string, 'old')}
|
||||
{renderCodeBlock(edit.new_string, 'new')}
|
||||
</div>
|
||||
))}
|
||||
</AccordionItem>
|
||||
)
|
||||
export function MultiEditTool({
|
||||
input
|
||||
}: {
|
||||
input: MultiEditToolInput
|
||||
output?: MultiEditToolOutput
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
return {
|
||||
key: AgentToolsType.MultiEdit,
|
||||
label: <ToolTitle icon={<FileText className="h-4 w-4" />} label="MultiEdit" params={input.file_path} />,
|
||||
children: (
|
||||
<div>
|
||||
{input.edits.map((edit, index) => (
|
||||
<div key={index}>
|
||||
{renderCodeBlock(edit.old_string, 'old')}
|
||||
{renderCodeBlock(edit.new_string, 'new')}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user