Compare commits
83 Commits
v1.7.0-sor
...
feat/sora2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
606a80d3ee | ||
|
|
8470e252d6 | ||
|
|
131444ac52 | ||
|
|
ab3083f943 | ||
|
|
1e1d5c4a14 | ||
|
|
c8ab0b9428 | ||
|
|
33ce41704d | ||
|
|
4eb3aa31ee | ||
|
|
d1a9dfa3e6 | ||
|
|
0e5ebcfd00 | ||
|
|
c4e0a6acfe | ||
|
|
2243bb2862 | ||
|
|
1f7d2fa93f | ||
|
|
fb680ce764 | ||
|
|
dc5bc64040 | ||
|
|
1c2ce7e0aa | ||
|
|
a290ee7f39 | ||
|
|
79c697c34d | ||
|
|
76271cbf77 | ||
|
|
9e0ee24fd7 | ||
|
|
5eb2772d53 | ||
|
|
f943f05cb1 | ||
|
|
96ce645064 | ||
|
|
1a972ac0e0 | ||
|
|
2e173631a0 | ||
|
|
c457d4a868 | ||
|
|
b74655651d | ||
|
|
f27a481c3c | ||
|
|
4028b26c1d | ||
|
|
011b6f2df1 | ||
|
|
7b3b73d390 | ||
|
|
004d6d8201 | ||
|
|
7cf57adceb | ||
|
|
866e8e8734 | ||
|
|
80e1784777 | ||
|
|
b5f2c63396 | ||
|
|
4e76806cc3 | ||
|
|
09ed82eb49 | ||
|
|
0d760ffa2e | ||
|
|
b068fc25da | ||
|
|
a0627f76d5 | ||
|
|
85daceb417 | ||
|
|
2fab33de41 | ||
|
|
e88b4c091d | ||
|
|
6c097e6733 | ||
|
|
c9c859731f | ||
|
|
c85fad90b5 | ||
|
|
88f7e6a854 | ||
|
|
de37e2355d | ||
|
|
261b79198a | ||
|
|
81f186abd6 | ||
|
|
f44a4f7f96 | ||
|
|
15b7eb78c1 | ||
|
|
f27b04c5b0 | ||
|
|
efd5e9dcf2 | ||
|
|
a02b8f4609 | ||
|
|
3b69b2bc49 | ||
|
|
c8dfae1d70 | ||
|
|
2ab3ddd804 | ||
|
|
7a62418f41 | ||
|
|
58c5df9284 | ||
|
|
c20394f460 | ||
|
|
8518734e48 | ||
|
|
29a01ef49a | ||
|
|
5e4b516402 | ||
|
|
1c89262929 | ||
|
|
b68a0ffaba | ||
|
|
41041fa296 | ||
|
|
66b88aec74 | ||
|
|
f54e583f34 | ||
|
|
1e1bfafb88 | ||
|
|
63459e3ec4 | ||
|
|
de10a7fd6c | ||
|
|
dced99ce57 | ||
|
|
0cafdeb540 | ||
|
|
258666e382 | ||
|
|
8a45fe70d0 | ||
|
|
d8363b5591 | ||
|
|
397a24b833 | ||
|
|
7b90dfb46c | ||
|
|
26a9dba01a | ||
|
|
a176814ad1 | ||
|
|
ea51439aac |
15
.github/workflows/claude-translator.yml
vendored
15
.github/workflows/claude-translator.yml
vendored
@@ -16,10 +16,13 @@ on:
|
||||
jobs:
|
||||
translate:
|
||||
if: |
|
||||
(github.event_name == 'issues') ||
|
||||
(github.event_name == 'issue_comment' && github.event.sender.type != 'Bot') ||
|
||||
(github.event_name == 'pull_request_review' && github.event.sender.type != 'Bot') ||
|
||||
(github.event_name == 'pull_request_review_comment' && github.event.sender.type != 'Bot')
|
||||
(github.event_name == 'issues')
|
||||
|| (github.event_name == 'issue_comment' && github.event.sender.type != 'Bot')
|
||||
|| (
|
||||
(github.event_name == 'pull_request_review' || github.event_name == 'pull_request_review_comment')
|
||||
&& github.event.sender.type != 'Bot'
|
||||
&& github.event.pull_request.head.repo.fork == false
|
||||
)
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -42,7 +45,7 @@ jobs:
|
||||
# See: https://github.com/anthropics/claude-code-action/blob/main/docs/security.md
|
||||
github_token: ${{ secrets.TOKEN_GITHUB_WRITE }}
|
||||
allowed_non_write_users: "*"
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
anthropic_api_key: ${{ secrets.CLAUDE_TRANSLATOR_APIKEY }}
|
||||
claude_args: "--allowed-tools Bash(gh issue:*),Bash(gh api:repos/*/issues:*),Bash(gh api:repos/*/pulls/*/reviews/*),Bash(gh api:repos/*/pulls/comments/*)"
|
||||
prompt: |
|
||||
你是一个多语言翻译助手。你需要响应 GitHub Webhooks 中的以下四种事件:
|
||||
@@ -105,3 +108,5 @@ jobs:
|
||||
|
||||
使用以下命令获取完整信息:
|
||||
gh issue view ${{ github.event.issue.number }} --json title,body,comments
|
||||
env:
|
||||
ANTHROPIC_BASE_URL: ${{ secrets.CLAUDE_TRANSLATOR_BASEURL }}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
diff --git a/dist/index.mjs b/dist/index.mjs
|
||||
index b957cb824faa79cf01ba3a504f221870bd8e306a..4d71d30f655775d61537d9d8b73f6e17d41fa67e 100644
|
||||
index 69ab1599c76801dc1167551b6fa283dded123466..f0af43bba7ad1196fe05338817e65b4ebda40955 100644
|
||||
--- a/dist/index.mjs
|
||||
+++ b/dist/index.mjs
|
||||
@@ -452,7 +452,7 @@ function convertToGoogleGenerativeAIMessages(prompt, options) {
|
||||
@@ -477,7 +477,7 @@ function convertToGoogleGenerativeAIMessages(prompt, options) {
|
||||
|
||||
// src/get-model-path.ts
|
||||
function getModelPath(modelId) {
|
||||
44
.yarn/patches/@opeoginni-github-copilot-openai-compatible-npm-0.1.18-3f65760532.patch
vendored
Normal file
44
.yarn/patches/@opeoginni-github-copilot-openai-compatible-npm-0.1.18-3f65760532.patch
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
diff --git a/dist/index.js b/dist/index.js
|
||||
index 53f411e55a4c9a06fd29bb4ab8161c4ad15980cd..71b91f196c8b886ed90dd237dec5625d79d5677e 100644
|
||||
--- a/dist/index.js
|
||||
+++ b/dist/index.js
|
||||
@@ -12676,10 +12676,13 @@ var OpenAIResponsesLanguageModel = class {
|
||||
}
|
||||
});
|
||||
} else if (value.item.type === "message") {
|
||||
- controller.enqueue({
|
||||
- type: "text-end",
|
||||
- id: value.item.id
|
||||
- });
|
||||
+ // Fix for gpt-5-codex: use currentTextId to ensure text-end matches text-start
|
||||
+ if (currentTextId) {
|
||||
+ controller.enqueue({
|
||||
+ type: "text-end",
|
||||
+ id: currentTextId
|
||||
+ });
|
||||
+ }
|
||||
currentTextId = null;
|
||||
} else if (isResponseOutputItemDoneReasoningChunk(value)) {
|
||||
const activeReasoningPart = activeReasoning[value.item.id];
|
||||
diff --git a/dist/index.mjs b/dist/index.mjs
|
||||
index 7719264da3c49a66c2626082f6ccaae6e3ef5e89..090fd8cf142674192a826148428ed6a0c4a54e35 100644
|
||||
--- a/dist/index.mjs
|
||||
+++ b/dist/index.mjs
|
||||
@@ -12670,10 +12670,13 @@ var OpenAIResponsesLanguageModel = class {
|
||||
}
|
||||
});
|
||||
} else if (value.item.type === "message") {
|
||||
- controller.enqueue({
|
||||
- type: "text-end",
|
||||
- id: value.item.id
|
||||
- });
|
||||
+ // Fix for gpt-5-codex: use currentTextId to ensure text-end matches text-start
|
||||
+ if (currentTextId) {
|
||||
+ controller.enqueue({
|
||||
+ type: "text-end",
|
||||
+ id: currentTextId
|
||||
+ });
|
||||
+ }
|
||||
currentTextId = null;
|
||||
} else if (isResponseOutputItemDoneReasoningChunk(value)) {
|
||||
const activeReasoningPart = activeReasoning[value.item.id];
|
||||
@@ -125,21 +125,113 @@ afterSign: scripts/notarize.js
|
||||
artifactBuildCompleted: scripts/artifact-build-completed.js
|
||||
releaseInfo:
|
||||
releaseNotes: |
|
||||
What's New in v1.6.3
|
||||
<!--LANG:en-->
|
||||
What's New in v1.7.0-beta.1
|
||||
|
||||
Features:
|
||||
- Notes: Add spell-check control, automatic table line wrapping, export functionality, and LLM-based renaming
|
||||
- UI: Expand topic rename clickable area, add middle-click tab closing, remove redundant scrollbars, fix message menubar overflow
|
||||
- Editor: Add read-only extension support, make TextFilePreview read-only but copyable
|
||||
- Models: Update support for DeepSeek v3.2, Claude 4.5, GLM 4.6, Gemini regex, and vision models
|
||||
- Code Tools: Add GitHub Copilot CLI integration
|
||||
Major Features:
|
||||
- Agent System: Introducing intelligent Agent capabilities alongside Assistants. Agents can autonomously solve complex problems using Claude Code SDK with tool calling, file operations, and multi-turn reasoning
|
||||
- Agent Management: Create, configure, and manage agents with custom settings including model selection, tool permissions, accessible paths, and MCP server integrations
|
||||
- Agent Sessions: Dedicated session management for agent interactions with persistent message history and context tracking
|
||||
- Unified UI: Streamlined interface combining Assistants and Agents tabs with improved navigation and settings management
|
||||
|
||||
Agent Features:
|
||||
- Tool Support: Web search, file operations, bash commands, and custom MCP tools
|
||||
- Advanced Configuration: Max turns, temperature, token limits
|
||||
- Permission Control: Configurable tool approval modes (manual, automatic, none)
|
||||
- Session Persistence: Automatic message saving with optimized streaming and database integration
|
||||
- Model Selection: API-based model filtering with provider-specific support
|
||||
|
||||
UI/UX Improvements:
|
||||
- Unified assistant/agent tabs with smooth animations
|
||||
- In-place session name editing
|
||||
- Virtual list rendering for improved performance
|
||||
- Session count indicators for active agents
|
||||
- Enhanced settings popup with tabbed interface
|
||||
- Webview keyboard shortcut interception for search functionality
|
||||
|
||||
API & Infrastructure:
|
||||
- RESTful API for agent and session management
|
||||
- Drizzle ORM integration for agent database
|
||||
- OAuth support for Claude Code authentication
|
||||
- Express validator for request validation
|
||||
- Comprehensive error handling with Zod schemas
|
||||
|
||||
Model Updates:
|
||||
- Gemini 2.5 Image Flash support
|
||||
- Grok 4 Fast with reasoning capabilities
|
||||
- Qwen3-omni and Qwen3-vl thinking models
|
||||
- DeepSeek, Claude 4.5, GLM 4.6 support
|
||||
- GitHub Copilot CLI integration with gpt-5-codex
|
||||
|
||||
Bug Fixes:
|
||||
- Fix Swagger UI accessibility issues
|
||||
- Fix AI SDK error display with syntax highlighting
|
||||
- Fix webview search shortcut handling
|
||||
- Fix agent model visibility for CherryIn provider
|
||||
- Fix session message ordering and persistence
|
||||
- Fix anthropic model visibility in agent configuration
|
||||
- Fix knowledge base deletion and web search RAG errors
|
||||
- Fix migration for missing providers
|
||||
- Fix forked topic retaining old name after rename
|
||||
- Restore first token latency reporting in metrics
|
||||
- Fix UI scrollbar and overflow issues
|
||||
|
||||
Technical Updates:
|
||||
- Upgrade to Electron 37.6.0
|
||||
- Update dependencies across packages
|
||||
- React 19.2.0 upgrade
|
||||
- Enhanced Claude Code service with streaming support
|
||||
- Improved message transformation and streaming lifecycle
|
||||
- Database migration system with automatic schema sync
|
||||
- Optimized bundle size and dependency management
|
||||
|
||||
<!--LANG:zh-CN-->
|
||||
v1.7.0-beta.1 新特性
|
||||
|
||||
核心功能:
|
||||
- Agent 系统:引入智能 Agent 能力,与助手(Assistant)并存。Agent 基于 Claude Code SDK 构建,具备工具调用、文件操作和多轮推理能力,可自主解决复杂问题
|
||||
- Agent 管理:创建、配置和管理 Agent,支持自定义模型选择、工具权限、可访问路径和 MCP 服务器集成
|
||||
- Agent 会话:专属会话管理系统,支持持久化消息历史和上下文追踪
|
||||
- 统一界面:精简的助手和 Agent 标签页界面,改进导航和设置管理体验
|
||||
|
||||
Agent 功能特性:
|
||||
- 工具支持:网页搜索、文件操作、Bash 命令执行和自定义 MCP 工具
|
||||
- 高级配置:最大轮次、温度、Token 限制
|
||||
- 权限控制:可配置的工具批准模式(手动、自动、无需批准)
|
||||
- 会话持久化:自动消息保存,优化的流式传输和数据库集成
|
||||
- 模型选择:基于 API 的模型过滤,支持特定提供商
|
||||
|
||||
界面与交互优化:
|
||||
- 统一的助手/Agent 标签页,带有流畅动画效果
|
||||
- 会话名称原地编辑功能
|
||||
- 虚拟列表渲染,提升性能表现
|
||||
- 活跃 Agent 的会话计数指示器
|
||||
- 增强的设置弹窗,采用标签页界面
|
||||
- Webview 键盘快捷键拦截,支持搜索功能
|
||||
|
||||
API 与基础设施:
|
||||
- RESTful API 用于 Agent 和会话管理
|
||||
- 集成 Drizzle ORM 管理 Agent 数据库
|
||||
- Claude Code OAuth 认证支持
|
||||
- Express validator 请求验证
|
||||
- 基于 Zod 模式的完善错误处理
|
||||
|
||||
模型更新:
|
||||
- 支持 Gemini 2.5 Image Flash
|
||||
- Grok 4 Fast 推理能力
|
||||
- Qwen3-omni 和 Qwen3-vl 思考模型
|
||||
- DeepSeek、Claude 4.5、GLM 4.6 支持
|
||||
- GitHub Copilot CLI 集成 gpt-5-codex
|
||||
|
||||
问题修复:
|
||||
- 修复 Swagger UI 无法打开
|
||||
- 修复 AI SDK 错误显示,添加语法高亮
|
||||
- 修复 Webview 搜索快捷键处理
|
||||
- 修复 CherryIn 提供商的 Agent 模型可见性
|
||||
- 修复会话消息排序和持久化
|
||||
- 修复 Anthropic 模型在 Agent 配置中的可见性
|
||||
- 修复知识库删除和网页搜索 RAG 错误
|
||||
- 修复缺失提供商的迁移问题
|
||||
|
||||
技术更新:
|
||||
- 升级至 React 19.2.0
|
||||
- 增强 Claude Code 服务流式传输支持
|
||||
- 改进消息转换和流式生命周期
|
||||
- 数据库迁移系统,支持自动模式同步
|
||||
- 优化打包大小和依赖管理
|
||||
<!--LANG:END-->
|
||||
|
||||
@@ -2,6 +2,7 @@ import tseslint from '@electron-toolkit/eslint-config-ts'
|
||||
import eslint from '@eslint/js'
|
||||
import eslintReact from '@eslint-react/eslint-plugin'
|
||||
import { defineConfig } from 'eslint/config'
|
||||
import importZod from 'eslint-plugin-import-zod'
|
||||
import oxlint from 'eslint-plugin-oxlint'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import simpleImportSort from 'eslint-plugin-simple-import-sort'
|
||||
@@ -15,7 +16,8 @@ export default defineConfig([
|
||||
{
|
||||
plugins: {
|
||||
'simple-import-sort': simpleImportSort,
|
||||
'unused-imports': unusedImports
|
||||
'unused-imports': unusedImports,
|
||||
'import-zod': importZod
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
@@ -25,6 +27,7 @@ export default defineConfig([
|
||||
'simple-import-sort/exports': 'error',
|
||||
'unused-imports/no-unused-imports': 'error',
|
||||
'@eslint-react/no-prop-types': 'error',
|
||||
'import-zod/prefer-zod-namespace': 'error'
|
||||
}
|
||||
},
|
||||
// Configuration for ensuring compatibility with the original ESLint(8.x) rules
|
||||
|
||||
19
package.json
19
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "CherryStudio",
|
||||
"version": "1.7.0-sora.1",
|
||||
"version": "1.7.0-sora.3",
|
||||
"private": true,
|
||||
"description": "A powerful AI assistant for producer.",
|
||||
"main": "./out/main/index.js",
|
||||
@@ -83,6 +83,7 @@
|
||||
"@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",
|
||||
"@strongtz/win32-arm64-msvc": "^0.4.7",
|
||||
"express": "^5.1.0",
|
||||
"font-list": "^2.0.0",
|
||||
"graceful-fs": "^4.2.11",
|
||||
"jsdom": "26.1.0",
|
||||
@@ -92,6 +93,7 @@
|
||||
"selection-hook": "^1.0.12",
|
||||
"sharp": "^0.34.3",
|
||||
"swagger-jsdoc": "^6.2.8",
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"tesseract.js": "patch:tesseract.js@npm%3A6.0.1#~/.yarn/patches/tesseract.js-npm-6.0.1-2562a7e46d.patch",
|
||||
"turndown": "7.2.0"
|
||||
},
|
||||
@@ -153,7 +155,7 @@
|
||||
"@opentelemetry/sdk-trace-base": "^2.0.0",
|
||||
"@opentelemetry/sdk-trace-node": "^2.0.0",
|
||||
"@opentelemetry/sdk-trace-web": "^2.0.0",
|
||||
"@opeoginni/github-copilot-openai-compatible": "0.1.18",
|
||||
"@opeoginni/github-copilot-openai-compatible": "patch:@opeoginni/github-copilot-openai-compatible@npm%3A0.1.18#~/.yarn/patches/@opeoginni-github-copilot-openai-compatible-npm-0.1.18-3f65760532.patch",
|
||||
"@playwright/test": "^1.52.0",
|
||||
"@radix-ui/react-context-menu": "^2.2.16",
|
||||
"@reduxjs/toolkit": "^2.2.5",
|
||||
@@ -258,11 +260,11 @@
|
||||
"emoji-picker-element": "^1.22.1",
|
||||
"epub": "patch:epub@npm%3A1.3.0#~/.yarn/patches/epub-npm-1.3.0-8325494ffe.patch",
|
||||
"eslint": "^9.22.0",
|
||||
"eslint-plugin-import-zod": "^1.2.0",
|
||||
"eslint-plugin-oxlint": "^1.15.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"express": "^5.1.0",
|
||||
"express-validator": "^7.2.1",
|
||||
"fast-diff": "^1.3.0",
|
||||
"fast-xml-parser": "^5.2.0",
|
||||
@@ -295,15 +297,15 @@
|
||||
"motion": "^12.10.5",
|
||||
"notion-helper": "^1.3.22",
|
||||
"npx-scope-finder": "^1.2.0",
|
||||
"oxlint": "^1.15.0",
|
||||
"oxlint": "^1.22.0",
|
||||
"oxlint-tsgolint": "^0.2.0",
|
||||
"p-queue": "^8.1.0",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"pdf-parse": "^1.1.1",
|
||||
"playwright": "^1.52.0",
|
||||
"proxy-agent": "^6.5.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-error-boundary": "^6.0.0",
|
||||
"react-hotkeys-hook": "^4.6.1",
|
||||
"react-i18next": "^14.1.2",
|
||||
@@ -335,7 +337,6 @@
|
||||
"string-width": "^7.2.0",
|
||||
"striptags": "^3.2.0",
|
||||
"styled-components": "^6.1.11",
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"swr": "^2.3.6",
|
||||
"tailwindcss": "^4.1.13",
|
||||
"tar": "^7.4.3",
|
||||
@@ -371,6 +372,7 @@
|
||||
"app-builder-lib@npm:26.0.13": "patch:app-builder-lib@npm%3A26.0.13#~/.yarn/patches/app-builder-lib-npm-26.0.13-a064c9e1d0.patch",
|
||||
"app-builder-lib@npm:26.0.15": "patch:app-builder-lib@npm%3A26.0.15#~/.yarn/patches/app-builder-lib-npm-26.0.15-360e5b0476.patch",
|
||||
"atomically@npm:^1.7.0": "patch:atomically@npm%3A1.7.0#~/.yarn/patches/atomically-npm-1.7.0-e742e5293b.patch",
|
||||
"esbuild": "^0.25.0",
|
||||
"file-stream-rotator@npm:^0.6.1": "patch:file-stream-rotator@npm%3A0.6.1#~/.yarn/patches/file-stream-rotator-npm-0.6.1-eab45fb13d.patch",
|
||||
"libsql@npm:^0.4.4": "patch:libsql@npm%3A0.4.7#~/.yarn/patches/libsql-npm-0.4.7-444e260fb1.patch",
|
||||
"node-abi": "4.12.0",
|
||||
@@ -378,10 +380,11 @@
|
||||
"openai@npm:^4.87.3": "npm:@cherrystudio/openai@6.3.0-fork.1",
|
||||
"pdf-parse@npm:1.1.1": "patch:pdf-parse@npm%3A1.1.1#~/.yarn/patches/pdf-parse-npm-1.1.1-04a6109b2a.patch",
|
||||
"pkce-challenge@npm:^4.1.0": "patch:pkce-challenge@npm%3A4.1.0#~/.yarn/patches/pkce-challenge-npm-4.1.0-fbc51695a3.patch",
|
||||
"tar-fs": "^2.1.4",
|
||||
"undici": "6.21.2",
|
||||
"vite": "npm:rolldown-vite@latest",
|
||||
"tesseract.js@npm:*": "patch:tesseract.js@npm%3A6.0.1#~/.yarn/patches/tesseract.js-npm-6.0.1-2562a7e46d.patch",
|
||||
"@ai-sdk/google@npm:2.0.17": "patch:@ai-sdk/google@npm%3A2.0.17#~/.yarn/patches/@ai-sdk-google-npm-2.0.17-fd88491de4.patch"
|
||||
"@ai-sdk/google@npm:2.0.20": "patch:@ai-sdk/google@npm%3A2.0.20#~/.yarn/patches/@ai-sdk-google-npm-2.0.20-b9102f9d54.patch"
|
||||
},
|
||||
"packageManager": "yarn@4.9.1",
|
||||
"lint-staged": {
|
||||
|
||||
@@ -13,7 +13,7 @@ import { LanguageModelV2 } from '@ai-sdk/provider'
|
||||
import { createXai } from '@ai-sdk/xai'
|
||||
import { createOpenRouter } from '@openrouter/ai-sdk-provider'
|
||||
import { customProvider, Provider } from 'ai'
|
||||
import { z } from 'zod'
|
||||
import * as z from 'zod'
|
||||
|
||||
/**
|
||||
* 基础 Provider IDs
|
||||
|
||||
@@ -53,6 +53,7 @@ export enum IpcChannel {
|
||||
|
||||
Webview_SetOpenLinkExternal = 'webview:set-open-link-external',
|
||||
Webview_SetSpellCheckEnabled = 'webview:set-spell-check-enabled',
|
||||
Webview_SearchHotkey = 'webview:search-hotkey',
|
||||
|
||||
// Open
|
||||
Open_Path = 'open:path',
|
||||
@@ -316,6 +317,7 @@ export enum IpcChannel {
|
||||
ApiServer_Stop = 'api-server:stop',
|
||||
ApiServer_Restart = 'api-server:restart',
|
||||
ApiServer_GetStatus = 'api-server:get-status',
|
||||
// NOTE: This api is not be used.
|
||||
ApiServer_GetConfig = 'api-server:get-config',
|
||||
|
||||
// Anthropic OAuth
|
||||
@@ -335,6 +337,7 @@ export enum IpcChannel {
|
||||
|
||||
// OCR
|
||||
OCR_ocr = 'ocr:ocr',
|
||||
OCR_ListProviders = 'ocr:list-providers',
|
||||
|
||||
// OVMS
|
||||
Ovms_AddModel = 'ovms:add-model',
|
||||
|
||||
@@ -22,3 +22,12 @@ export type MCPProgressEvent = {
|
||||
callId: string
|
||||
progress: number // 0-1 range
|
||||
}
|
||||
|
||||
export type WebviewKeyEvent = {
|
||||
webviewId: number
|
||||
key: string
|
||||
control: boolean
|
||||
meta: boolean
|
||||
shift: boolean
|
||||
alt: boolean
|
||||
}
|
||||
|
||||
@@ -5,105 +5,171 @@ const { execSync } = require('child_process')
|
||||
const { downloadWithPowerShell } = require('./download')
|
||||
|
||||
// Base URL for downloading OVMS binaries
|
||||
const OVMS_PKG_NAME = 'ovms250911.zip'
|
||||
const OVMS_RELEASE_BASE_URL = [`https://gitcode.com/gcw_ggDjjkY3/kjfile/releases/download/download/${OVMS_PKG_NAME}`]
|
||||
const OVMS_RELEASE_BASE_URL =
|
||||
'https://storage.openvinotoolkit.org/repositories/openvino_model_server/packages/2025.3.0/ovms_windows_python_on.zip'
|
||||
const OVMS_EX_URL = 'https://gitcode.com/gcw_ggDjjkY3/kjfile/releases/download/download/ovms_25.3_ex.zip'
|
||||
|
||||
/**
|
||||
* Downloads and extracts the OVMS binary for the specified platform
|
||||
* error code:
|
||||
* 101: Unsupported CPU (not Intel Ultra)
|
||||
* 102: Unsupported platform (not Windows)
|
||||
* 103: Download failed
|
||||
* 104: Installation failed
|
||||
* 105: Failed to create ovdnd.exe
|
||||
* 106: Failed to create run.bat
|
||||
* 110: Cleanup of old installation failed
|
||||
*/
|
||||
async function downloadOvmsBinary() {
|
||||
// Create output directory structure - OVMS goes into its own subdirectory
|
||||
|
||||
/**
|
||||
* Clean old OVMS installation if it exists
|
||||
*/
|
||||
function cleanOldOvmsInstallation() {
|
||||
console.log('Cleaning the existing OVMS installation...')
|
||||
const csDir = path.join(os.homedir(), '.cherrystudio')
|
||||
|
||||
// Ensure directories exist
|
||||
fs.mkdirSync(csDir, { recursive: true })
|
||||
|
||||
const csOvmsDir = path.join(csDir, 'ovms')
|
||||
// Delete existing OVMS directory if it exists
|
||||
if (fs.existsSync(csOvmsDir)) {
|
||||
fs.rmSync(csOvmsDir, { recursive: true })
|
||||
}
|
||||
|
||||
const tempdir = os.tmpdir()
|
||||
const tempFilename = path.join(tempdir, 'ovms.zip')
|
||||
|
||||
// Try each URL until one succeeds
|
||||
let downloadSuccess = false
|
||||
let lastError = null
|
||||
|
||||
for (let i = 0; i < OVMS_RELEASE_BASE_URL.length; i++) {
|
||||
const downloadUrl = OVMS_RELEASE_BASE_URL[i]
|
||||
console.log(`Attempting download from URL ${i + 1}/${OVMS_RELEASE_BASE_URL.length}: ${downloadUrl}`)
|
||||
|
||||
try {
|
||||
console.log(`Downloading OVMS from ${downloadUrl} to ${tempFilename}...`)
|
||||
|
||||
// Try PowerShell download first, fallback to Node.js download if it fails
|
||||
await downloadWithPowerShell(downloadUrl, tempFilename)
|
||||
|
||||
// If we get here, download was successful
|
||||
downloadSuccess = true
|
||||
console.log(`Successfully downloaded from: ${downloadUrl}`)
|
||||
break
|
||||
fs.rmSync(csOvmsDir, { recursive: true })
|
||||
} catch (error) {
|
||||
console.warn(`Download failed from ${downloadUrl}: ${error.message}`)
|
||||
lastError = error
|
||||
|
||||
// Clean up failed download file if it exists
|
||||
if (fs.existsSync(tempFilename)) {
|
||||
try {
|
||||
fs.unlinkSync(tempFilename)
|
||||
} catch (cleanupError) {
|
||||
console.warn(`Failed to clean up temporary file: ${cleanupError.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Continue to next URL if this one failed
|
||||
if (i < OVMS_RELEASE_BASE_URL.length - 1) {
|
||||
console.log(`Trying next URL...`)
|
||||
}
|
||||
console.warn(`Failed to clean up old OVMS installation: ${error.message}`)
|
||||
return 110
|
||||
}
|
||||
}
|
||||
|
||||
// Check if any download succeeded
|
||||
if (!downloadSuccess) {
|
||||
console.error(`All download URLs failed. Last error: ${lastError?.message || 'Unknown error'}`)
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Install OVMS Base package
|
||||
*/
|
||||
async function installOvmsBase() {
|
||||
// Download the base package
|
||||
const tempdir = os.tmpdir()
|
||||
const tempFilename = path.join(tempdir, 'ovms.zip')
|
||||
|
||||
try {
|
||||
console.log(`Downloading OVMS Base Package from ${OVMS_RELEASE_BASE_URL} to ${tempFilename}...`)
|
||||
|
||||
// Try PowerShell download first, fallback to Node.js download if it fails
|
||||
await downloadWithPowerShell(OVMS_RELEASE_BASE_URL, tempFilename)
|
||||
console.log(`Successfully downloaded from: ${OVMS_RELEASE_BASE_URL}`)
|
||||
} catch (error) {
|
||||
console.error(`Download OVMS Base failed: ${error.message}`)
|
||||
fs.unlinkSync(tempFilename)
|
||||
return 103
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`Extracting to ${csDir}...`)
|
||||
// unzip the base package to the target directory
|
||||
const csDir = path.join(os.homedir(), '.cherrystudio')
|
||||
const csOvmsDir = path.join(csDir, 'ovms')
|
||||
fs.mkdirSync(csOvmsDir, { recursive: true })
|
||||
|
||||
try {
|
||||
// Use tar.exe to extract the ZIP file
|
||||
console.log(`Extracting OVMS to ${csDir}...`)
|
||||
execSync(`tar -xf ${tempFilename} -C ${csDir}`, { stdio: 'inherit' })
|
||||
console.log(`OVMS extracted to ${csDir}`)
|
||||
console.log(`Extracting OVMS Base to ${csOvmsDir}...`)
|
||||
execSync(`tar -xf ${tempFilename} -C ${csOvmsDir}`, { stdio: 'inherit' })
|
||||
console.log(`OVMS extracted to ${csOvmsDir}`)
|
||||
|
||||
// Clean up temporary file
|
||||
fs.unlinkSync(tempFilename)
|
||||
console.log(`Installation directory: ${csDir}`)
|
||||
} catch (error) {
|
||||
console.error(`Error installing OVMS: ${error.message}`)
|
||||
if (fs.existsSync(tempFilename)) {
|
||||
fs.unlinkSync(tempFilename)
|
||||
}
|
||||
|
||||
// Check if ovmsDir is empty and remove it if so
|
||||
try {
|
||||
const ovmsDir = path.join(csDir, 'ovms')
|
||||
const files = fs.readdirSync(ovmsDir)
|
||||
if (files.length === 0) {
|
||||
fs.rmSync(ovmsDir, { recursive: true })
|
||||
console.log(`Removed empty directory: ${ovmsDir}`)
|
||||
}
|
||||
} catch (cleanupError) {
|
||||
console.warn(`Warning: Failed to clean up directory: ${cleanupError.message}`)
|
||||
return 105
|
||||
}
|
||||
|
||||
fs.unlinkSync(tempFilename)
|
||||
return 104
|
||||
}
|
||||
|
||||
const csOvmsBinDir = path.join(csOvmsDir, 'ovms')
|
||||
// copy ovms.exe to ovdnd.exe
|
||||
try {
|
||||
fs.copyFileSync(path.join(csOvmsBinDir, 'ovms.exe'), path.join(csOvmsBinDir, 'ovdnd.exe'))
|
||||
console.log('Copied ovms.exe to ovdnd.exe')
|
||||
} catch (error) {
|
||||
console.error(`Error copying ovms.exe to ovdnd.exe: ${error.message}`)
|
||||
return 105
|
||||
}
|
||||
|
||||
// copy {csOvmsBinDir}/setupvars.bat to {csOvmsBinDir}/run.bat, and append the following lines to run.bat:
|
||||
// del %USERPROFILE%\.cherrystudio\ovms_log.log
|
||||
// ovms.exe --config_path models/config.json --rest_port 8000 --log_level DEBUG --log_path %USERPROFILE%\.cherrystudio\ovms_log.log
|
||||
const runBatPath = path.join(csOvmsBinDir, 'run.bat')
|
||||
try {
|
||||
fs.copyFileSync(path.join(csOvmsBinDir, 'setupvars.bat'), runBatPath)
|
||||
fs.appendFileSync(runBatPath, '\r\n')
|
||||
fs.appendFileSync(runBatPath, 'del %USERPROFILE%\\.cherrystudio\\ovms_log.log\r\n')
|
||||
fs.appendFileSync(
|
||||
runBatPath,
|
||||
'ovms.exe --config_path models/config.json --rest_port 8000 --log_level DEBUG --log_path %USERPROFILE%\\.cherrystudio\\ovms_log.log\r\n'
|
||||
)
|
||||
console.log(`Created run.bat at: ${runBatPath}`)
|
||||
} catch (error) {
|
||||
console.error(`Error creating run.bat: ${error.message}`)
|
||||
return 106
|
||||
}
|
||||
|
||||
// create {csOvmsBinDir}/models/config.json with content '{"model_config_list": []}'
|
||||
const configJsonPath = path.join(csOvmsBinDir, 'models', 'config.json')
|
||||
fs.mkdirSync(path.dirname(configJsonPath), { recursive: true })
|
||||
fs.writeFileSync(configJsonPath, '{"mediapipe_config_list":[],"model_config_list":[]}')
|
||||
console.log(`Created config file: ${configJsonPath}`)
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Install OVMS Extra package
|
||||
*/
|
||||
async function installOvmsExtra() {
|
||||
// Download the extra package
|
||||
const tempdir = os.tmpdir()
|
||||
const tempFilename = path.join(tempdir, 'ovms_ex.zip')
|
||||
|
||||
try {
|
||||
console.log(`Downloading OVMS Extra Package from ${OVMS_EX_URL} to ${tempFilename}...`)
|
||||
|
||||
// Try PowerShell download first, fallback to Node.js download if it fails
|
||||
await downloadWithPowerShell(OVMS_EX_URL, tempFilename)
|
||||
console.log(`Successfully downloaded from: ${OVMS_EX_URL}`)
|
||||
} catch (error) {
|
||||
console.error(`Download OVMS Extra failed: ${error.message}`)
|
||||
fs.unlinkSync(tempFilename)
|
||||
return 103
|
||||
}
|
||||
|
||||
// unzip the extra package to the target directory
|
||||
const csDir = path.join(os.homedir(), '.cherrystudio')
|
||||
const csOvmsDir = path.join(csDir, 'ovms')
|
||||
|
||||
try {
|
||||
// Use tar.exe to extract the ZIP file
|
||||
console.log(`Extracting OVMS Extra to ${csOvmsDir}...`)
|
||||
execSync(`tar -xf ${tempFilename} -C ${csOvmsDir}`, { stdio: 'inherit' })
|
||||
console.log(`OVMS extracted to ${csOvmsDir}`)
|
||||
|
||||
// Clean up temporary file
|
||||
fs.unlinkSync(tempFilename)
|
||||
console.log(`Installation directory: ${csDir}`)
|
||||
} catch (error) {
|
||||
console.error(`Error installing OVMS Extra: ${error.message}`)
|
||||
fs.unlinkSync(tempFilename)
|
||||
return 104
|
||||
}
|
||||
|
||||
// apply ovms patch, copy all files in {csOvmsDir}/patch/ovms to {csOvmsDir}/ovms with overwrite mode
|
||||
const patchDir = path.join(csOvmsDir, 'patch', 'ovms')
|
||||
const csOvmsBinDir = path.join(csOvmsDir, 'ovms')
|
||||
try {
|
||||
const files = fs.readdirSync(patchDir)
|
||||
files.forEach((file) => {
|
||||
const srcPath = path.join(patchDir, file)
|
||||
const destPath = path.join(csOvmsBinDir, file)
|
||||
fs.copyFileSync(srcPath, destPath)
|
||||
console.log(`Applied patch file: ${file}`)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(`Error applying OVMS patch: ${error.message}`)
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -158,7 +224,27 @@ async function installOvms() {
|
||||
return 102
|
||||
}
|
||||
|
||||
return await downloadOvmsBinary()
|
||||
// Clean old installation if it exists
|
||||
const cleanupCode = cleanOldOvmsInstallation()
|
||||
if (cleanupCode !== 0) {
|
||||
console.error(`OVMS cleanup failed with code: ${cleanupCode}`)
|
||||
return cleanupCode
|
||||
}
|
||||
|
||||
const installBaseCode = await installOvmsBase()
|
||||
if (installBaseCode !== 0) {
|
||||
console.error(`OVMS Base installation failed with code: ${installBaseCode}`)
|
||||
cleanOldOvmsInstallation()
|
||||
return installBaseCode
|
||||
}
|
||||
|
||||
const installExtraCode = await installOvmsExtra()
|
||||
if (installExtraCode !== 0) {
|
||||
console.error(`OVMS Extra installation failed with code: ${installExtraCode}`)
|
||||
return installExtraCode
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// Run the installation
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { isEmpty } from 'lodash'
|
||||
|
||||
import { ApiModel, ApiModelsFilter, ApiModelsResponse } from '../../../renderer/src/types/apiModels'
|
||||
import { loggerService } from '../../services/LoggerService'
|
||||
import { getAvailableProviders, listAllAvailableModels, transformModelToOpenAI } from '../utils'
|
||||
import {
|
||||
getAvailableProviders,
|
||||
getProviderAnthropicModelChecker,
|
||||
listAllAvailableModels,
|
||||
transformModelToOpenAI
|
||||
} from '../utils'
|
||||
|
||||
const logger = loggerService.withContext('ModelsService')
|
||||
|
||||
@@ -16,9 +23,7 @@ export class ModelsService {
|
||||
let providers = await getAvailableProviders()
|
||||
|
||||
if (filter.providerType === 'anthropic') {
|
||||
providers = providers.filter(
|
||||
(p) => p.type === 'anthropic' || (p.anthropicApiHost !== undefined && p.anthropicApiHost.trim() !== '')
|
||||
)
|
||||
providers = providers.filter((p) => p.type === 'anthropic' || !isEmpty(p.anthropicApiHost?.trim()))
|
||||
}
|
||||
|
||||
const models = await listAllAvailableModels(providers)
|
||||
@@ -27,18 +32,18 @@ export class ModelsService {
|
||||
|
||||
for (const model of models) {
|
||||
const provider = providers.find((p) => p.id === model.provider)
|
||||
logger.debug(`Processing model ${model.id} from provider ${model.provider}`, {
|
||||
isAnthropicModel: provider?.isAnthropicModel
|
||||
})
|
||||
if (
|
||||
!provider ||
|
||||
(filter.providerType === 'anthropic' && provider.isAnthropicModel && !provider.isAnthropicModel(model))
|
||||
) {
|
||||
logger.debug(`Processing model ${model.id}`)
|
||||
if (!provider) {
|
||||
logger.debug(`Skipping model ${model.id} . Reason: Provider not found.`)
|
||||
continue
|
||||
}
|
||||
// Special case: For "aihubmix", it should be covered by above condition, but just in case
|
||||
if (provider.id === 'aihubmix' && filter.providerType === 'anthropic' && !model.id.includes('claude')) {
|
||||
continue
|
||||
|
||||
if (filter.providerType === 'anthropic') {
|
||||
const checker = getProviderAnthropicModelChecker(provider.id)
|
||||
if (!checker(model)) {
|
||||
logger.debug(`Skipping model ${model.id} from ${model.provider}. Reason: Not an Anthropic model.`)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
const openAIModel = transformModelToOpenAI(model, provider)
|
||||
|
||||
@@ -279,3 +279,16 @@ export function validateProvider(provider: Provider): boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export const getProviderAnthropicModelChecker = (providerId: string): ((m: Model) => boolean) => {
|
||||
switch (providerId) {
|
||||
case 'cherryin':
|
||||
case 'new-api':
|
||||
return (m: Model) => m.endpoint_type === 'anthropic'
|
||||
case 'aihubmix':
|
||||
return (m: Model) => m.id.includes('claude')
|
||||
default:
|
||||
// allow all models when checker not configured
|
||||
return () => true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import selectionService, { initSelectionService } from './services/SelectionServ
|
||||
import { registerShortcuts } from './services/ShortcutService'
|
||||
import { TrayService } from './services/TrayService'
|
||||
import { windowService } from './services/WindowService'
|
||||
import { initWebviewHotkeys } from './services/WebviewService'
|
||||
|
||||
const logger = loggerService.withContext('MainEntry')
|
||||
|
||||
@@ -108,6 +109,7 @@ if (!app.requestSingleInstanceLock()) {
|
||||
// Some APIs can only be used after this event occurs.
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
initWebviewHotkeys()
|
||||
// Set app user model id for windows
|
||||
electronApp.setAppUserModelId(import.meta.env.VITE_MAIN_BUNDLE_ID || 'com.kangfenmao.CherryStudio')
|
||||
|
||||
@@ -157,11 +159,26 @@ if (!app.requestSingleInstanceLock()) {
|
||||
logger.error('Failed to initialize Agent service:', error)
|
||||
}
|
||||
|
||||
// Start API server if enabled
|
||||
// Start API server if enabled or if agents exist
|
||||
try {
|
||||
const config = await apiServerService.getCurrentConfig()
|
||||
logger.info('API server config:', config)
|
||||
if (config.enabled) {
|
||||
|
||||
// Check if there are any agents
|
||||
let shouldStart = config.enabled
|
||||
if (!shouldStart) {
|
||||
try {
|
||||
const { total } = await agentService.listAgents({ limit: 1 })
|
||||
if (total > 0) {
|
||||
shouldStart = true
|
||||
logger.info(`Detected ${total} agent(s), auto-starting API server`)
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.warn('Failed to check agent count:', error)
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldStart) {
|
||||
await apiServerService.start()
|
||||
}
|
||||
} catch (error: any) {
|
||||
|
||||
@@ -786,7 +786,6 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
ipcMain.handle(IpcChannel.Webview_SetOpenLinkExternal, (_, webviewId: number, isExternal: boolean) =>
|
||||
setOpenLinkExternal(webviewId, isExternal)
|
||||
)
|
||||
|
||||
ipcMain.handle(IpcChannel.Webview_SetSpellCheckEnabled, (_, webviewId: number, isEnable: boolean) => {
|
||||
const webview = webContents.fromId(webviewId)
|
||||
if (!webview) return
|
||||
@@ -876,6 +875,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
ipcMain.handle(IpcChannel.OCR_ocr, (_, file: SupportedOcrFile, provider: OcrProvider) =>
|
||||
ocrService.ocr(file, provider)
|
||||
)
|
||||
ipcMain.handle(IpcChannel.OCR_ListProviders, () => ocrService.listProviderIds())
|
||||
|
||||
// OVMS
|
||||
ipcMain.handle(IpcChannel.Ovms_AddModel, (_, modelName: string, modelId: string, modelSource: string, task: string) =>
|
||||
|
||||
473
src/main/mcpServers/didi-mcp.ts
Normal file
473
src/main/mcpServers/didi-mcp.ts
Normal file
@@ -0,0 +1,473 @@
|
||||
/**
|
||||
* DiDi MCP Server Implementation
|
||||
*
|
||||
* Based on official DiDi MCP API capabilities.
|
||||
* API Documentation: https://mcp.didichuxing.com/api?tap=api
|
||||
*
|
||||
* Provides ride-hailing services including map search, price estimation,
|
||||
* order management, and driver tracking.
|
||||
*
|
||||
* Note: Only available in Mainland China.
|
||||
*/
|
||||
|
||||
import { loggerService } from '@logger'
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
||||
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
|
||||
|
||||
const logger = loggerService.withContext('DiDiMCPServer')
|
||||
|
||||
export class DiDiMcpServer {
|
||||
private _server: Server
|
||||
private readonly baseUrl = 'http://mcp.didichuxing.com/mcp-servers'
|
||||
private apiKey: string
|
||||
|
||||
constructor(apiKey?: string) {
|
||||
this._server = new Server(
|
||||
{
|
||||
name: 'didi-mcp-server',
|
||||
version: '0.1.0'
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Get API key from parameter or environment variables
|
||||
this.apiKey = apiKey || process.env.DIDI_API_KEY || ''
|
||||
if (!this.apiKey) {
|
||||
logger.warn('DIDI_API_KEY environment variable is not set')
|
||||
}
|
||||
|
||||
this.setupRequestHandlers()
|
||||
}
|
||||
|
||||
get server(): Server {
|
||||
return this._server
|
||||
}
|
||||
|
||||
private setupRequestHandlers() {
|
||||
// List available tools
|
||||
this._server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
return {
|
||||
tools: [
|
||||
{
|
||||
name: 'maps_textsearch',
|
||||
description: 'Search for POI locations based on keywords and city',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
city: {
|
||||
type: 'string',
|
||||
description: 'Query city'
|
||||
},
|
||||
keywords: {
|
||||
type: 'string',
|
||||
description: 'Search keywords'
|
||||
},
|
||||
location: {
|
||||
type: 'string',
|
||||
description: 'Location coordinates, format: longitude,latitude'
|
||||
}
|
||||
},
|
||||
required: ['keywords', 'city']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'taxi_cancel_order',
|
||||
description: 'Cancel a taxi order',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
order_id: {
|
||||
type: 'string',
|
||||
description: 'Order ID from order creation or query results'
|
||||
},
|
||||
reason: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Cancellation reason (optional). Examples: no longer needed, waiting too long, urgent matter'
|
||||
}
|
||||
},
|
||||
required: ['order_id']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'taxi_create_order',
|
||||
description: 'Create taxi order directly via API without opening any app interface',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
caller_car_phone: {
|
||||
type: 'string',
|
||||
description: 'Caller phone number (optional)'
|
||||
},
|
||||
estimate_trace_id: {
|
||||
type: 'string',
|
||||
description: 'Estimation trace ID from estimation results'
|
||||
},
|
||||
product_category: {
|
||||
type: 'string',
|
||||
description: 'Vehicle category ID from estimation results, comma-separated for multiple types'
|
||||
}
|
||||
},
|
||||
required: ['product_category', 'estimate_trace_id']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'taxi_estimate',
|
||||
description: 'Get available ride-hailing vehicle types and fare estimates',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
from_lat: {
|
||||
type: 'string',
|
||||
description: 'Departure latitude, must be from map tools'
|
||||
},
|
||||
from_lng: {
|
||||
type: 'string',
|
||||
description: 'Departure longitude, must be from map tools'
|
||||
},
|
||||
from_name: {
|
||||
type: 'string',
|
||||
description: 'Departure location name'
|
||||
},
|
||||
to_lat: {
|
||||
type: 'string',
|
||||
description: 'Destination latitude, must be from map tools'
|
||||
},
|
||||
to_lng: {
|
||||
type: 'string',
|
||||
description: 'Destination longitude, must be from map tools'
|
||||
},
|
||||
to_name: {
|
||||
type: 'string',
|
||||
description: 'Destination name'
|
||||
}
|
||||
},
|
||||
required: ['from_lng', 'from_lat', 'from_name', 'to_lng', 'to_lat', 'to_name']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'taxi_generate_ride_app_link',
|
||||
description: 'Generate deep links to open ride-hailing apps based on origin, destination and vehicle type',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
from_lat: {
|
||||
type: 'string',
|
||||
description: 'Departure latitude, must be from map tools'
|
||||
},
|
||||
from_lng: {
|
||||
type: 'string',
|
||||
description: 'Departure longitude, must be from map tools'
|
||||
},
|
||||
product_category: {
|
||||
type: 'string',
|
||||
description: 'Vehicle category IDs from estimation results, comma-separated for multiple types'
|
||||
},
|
||||
to_lat: {
|
||||
type: 'string',
|
||||
description: 'Destination latitude, must be from map tools'
|
||||
},
|
||||
to_lng: {
|
||||
type: 'string',
|
||||
description: 'Destination longitude, must be from map tools'
|
||||
}
|
||||
},
|
||||
required: ['from_lng', 'from_lat', 'to_lng', 'to_lat']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'taxi_get_driver_location',
|
||||
description: 'Get real-time driver location for a taxi order',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
order_id: {
|
||||
type: 'string',
|
||||
description: 'Taxi order ID'
|
||||
}
|
||||
},
|
||||
required: ['order_id']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'taxi_query_order',
|
||||
description: 'Query taxi order status and information such as driver contact, license plate, ETA',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
order_id: {
|
||||
type: 'string',
|
||||
description: 'Order ID from order creation results, if available; otherwise queries incomplete orders'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
// Handle tool calls
|
||||
this._server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const { name, arguments: args } = request.params
|
||||
|
||||
try {
|
||||
switch (name) {
|
||||
case 'maps_textsearch':
|
||||
return await this.handleMapsTextSearch(args)
|
||||
case 'taxi_cancel_order':
|
||||
return await this.handleTaxiCancelOrder(args)
|
||||
case 'taxi_create_order':
|
||||
return await this.handleTaxiCreateOrder(args)
|
||||
case 'taxi_estimate':
|
||||
return await this.handleTaxiEstimate(args)
|
||||
case 'taxi_generate_ride_app_link':
|
||||
return await this.handleTaxiGenerateRideAppLink(args)
|
||||
case 'taxi_get_driver_location':
|
||||
return await this.handleTaxiGetDriverLocation(args)
|
||||
case 'taxi_query_order':
|
||||
return await this.handleTaxiQueryOrder(args)
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${name}`)
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Error calling tool ${name}:`, error as Error)
|
||||
throw error
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private async handleMapsTextSearch(args: any) {
|
||||
const { city, keywords, location } = args
|
||||
|
||||
const params = {
|
||||
name: 'maps_textsearch',
|
||||
arguments: {
|
||||
keywords,
|
||||
city,
|
||||
...(location && { location })
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.makeRequest('tools/call', params)
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(response, null, 2)
|
||||
}
|
||||
]
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Maps text search error:', error as Error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private async handleTaxiCancelOrder(args: any) {
|
||||
const { order_id, reason } = args
|
||||
|
||||
const params = {
|
||||
name: 'taxi_cancel_order',
|
||||
arguments: {
|
||||
order_id,
|
||||
...(reason && { reason })
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.makeRequest('tools/call', params)
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(response, null, 2)
|
||||
}
|
||||
]
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Taxi cancel order error:', error as Error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private async handleTaxiCreateOrder(args: any) {
|
||||
const { caller_car_phone, estimate_trace_id, product_category } = args
|
||||
|
||||
const params = {
|
||||
name: 'taxi_create_order',
|
||||
arguments: {
|
||||
product_category,
|
||||
estimate_trace_id,
|
||||
...(caller_car_phone && { caller_car_phone })
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.makeRequest('tools/call', params)
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(response, null, 2)
|
||||
}
|
||||
]
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Taxi create order error:', error as Error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private async handleTaxiEstimate(args: any) {
|
||||
const { from_lng, from_lat, from_name, to_lng, to_lat, to_name } = args
|
||||
|
||||
const params = {
|
||||
name: 'taxi_estimate',
|
||||
arguments: {
|
||||
from_lng,
|
||||
from_lat,
|
||||
from_name,
|
||||
to_lng,
|
||||
to_lat,
|
||||
to_name
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.makeRequest('tools/call', params)
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(response, null, 2)
|
||||
}
|
||||
]
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Taxi estimate error:', error as Error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private async handleTaxiGenerateRideAppLink(args: any) {
|
||||
const { from_lng, from_lat, to_lng, to_lat, product_category } = args
|
||||
|
||||
const params = {
|
||||
name: 'taxi_generate_ride_app_link',
|
||||
arguments: {
|
||||
from_lng,
|
||||
from_lat,
|
||||
to_lng,
|
||||
to_lat,
|
||||
...(product_category && { product_category })
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.makeRequest('tools/call', params)
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(response, null, 2)
|
||||
}
|
||||
]
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Taxi generate ride app link error:', error as Error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private async handleTaxiGetDriverLocation(args: any) {
|
||||
const { order_id } = args
|
||||
|
||||
const params = {
|
||||
name: 'taxi_get_driver_location',
|
||||
arguments: {
|
||||
order_id
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.makeRequest('tools/call', params)
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(response, null, 2)
|
||||
}
|
||||
]
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Taxi get driver location error:', error as Error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private async handleTaxiQueryOrder(args: any) {
|
||||
const { order_id } = args
|
||||
|
||||
const params = {
|
||||
name: 'taxi_query_order',
|
||||
arguments: {
|
||||
...(order_id && { order_id })
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.makeRequest('tools/call', params)
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(response, null, 2)
|
||||
}
|
||||
]
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Taxi query order error:', error as Error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private async makeRequest(method: string, params: any): Promise<any> {
|
||||
const requestData = {
|
||||
jsonrpc: '2.0',
|
||||
method: method,
|
||||
id: Date.now(),
|
||||
...(Object.keys(params).length > 0 && { params })
|
||||
}
|
||||
|
||||
// API key is passed as URL parameter
|
||||
const url = `${this.baseUrl}?key=${this.apiKey}`
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(requestData)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
throw new Error(`HTTP ${response.status}: ${errorText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (data.error) {
|
||||
throw new Error(`API Error: ${JSON.stringify(data.error)}`)
|
||||
}
|
||||
|
||||
return data.result
|
||||
}
|
||||
}
|
||||
|
||||
export default DiDiMcpServer
|
||||
@@ -3,7 +3,7 @@ import { loggerService } from '@logger'
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
||||
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
|
||||
import { net } from 'electron'
|
||||
import { z } from 'zod'
|
||||
import * as z from 'zod'
|
||||
|
||||
const logger = loggerService.withContext('DifyKnowledgeServer')
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
||||
import { BuiltinMCPServerName, BuiltinMCPServerNames } from '@types'
|
||||
|
||||
import BraveSearchServer from './brave-search'
|
||||
import DiDiMcpServer from './didi-mcp'
|
||||
import DifyKnowledgeServer from './dify-knowledge'
|
||||
import FetchServer from './fetch'
|
||||
import FileSystemServer from './filesystem'
|
||||
@@ -42,6 +43,10 @@ export function createInMemoryMCPServer(
|
||||
case BuiltinMCPServerNames.python: {
|
||||
return new PythonServer().server
|
||||
}
|
||||
case BuiltinMCPServerNames.didiMCP: {
|
||||
const apiKey = envs.DIDI_API_KEY
|
||||
return new DiDiMcpServer(apiKey).server
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown in-memory MCP server: ${name}`)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprot
|
||||
import { net } from 'electron'
|
||||
import { JSDOM } from 'jsdom'
|
||||
import TurndownService from 'turndown'
|
||||
import { z } from 'zod'
|
||||
import * as z from 'zod'
|
||||
|
||||
export const RequestPayloadSchema = z.object({
|
||||
url: z.url(),
|
||||
|
||||
@@ -8,7 +8,7 @@ import fs from 'fs/promises'
|
||||
import { minimatch } from 'minimatch'
|
||||
import os from 'os'
|
||||
import path from 'path'
|
||||
import { z } from 'zod'
|
||||
import * as z from 'zod'
|
||||
|
||||
const logger = loggerService.withContext('MCP:FileSystemServer')
|
||||
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
import { ApiServerConfig } from '@types'
|
||||
import {
|
||||
ApiServerConfig,
|
||||
GetApiServerStatusResult,
|
||||
RestartApiServerStatusResult,
|
||||
StartApiServerStatusResult,
|
||||
StopApiServerStatusResult
|
||||
} from '@types'
|
||||
import { ipcMain } from 'electron'
|
||||
|
||||
import { apiServer } from '../apiServer'
|
||||
@@ -52,7 +58,7 @@ export class ApiServerService {
|
||||
|
||||
registerIpcHandlers(): void {
|
||||
// API Server
|
||||
ipcMain.handle(IpcChannel.ApiServer_Start, async () => {
|
||||
ipcMain.handle(IpcChannel.ApiServer_Start, async (): Promise<StartApiServerStatusResult> => {
|
||||
try {
|
||||
await this.start()
|
||||
return { success: true }
|
||||
@@ -61,7 +67,7 @@ export class ApiServerService {
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.ApiServer_Stop, async () => {
|
||||
ipcMain.handle(IpcChannel.ApiServer_Stop, async (): Promise<StopApiServerStatusResult> => {
|
||||
try {
|
||||
await this.stop()
|
||||
return { success: true }
|
||||
@@ -70,7 +76,7 @@ export class ApiServerService {
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.ApiServer_Restart, async () => {
|
||||
ipcMain.handle(IpcChannel.ApiServer_Restart, async (): Promise<RestartApiServerStatusResult> => {
|
||||
try {
|
||||
await this.restart()
|
||||
return { success: true }
|
||||
@@ -79,7 +85,7 @@ export class ApiServerService {
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.ApiServer_GetStatus, async () => {
|
||||
ipcMain.handle(IpcChannel.ApiServer_GetStatus, async (): Promise<GetApiServerStatusResult> => {
|
||||
try {
|
||||
const config = await this.getCurrentConfig()
|
||||
return {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { session, shell, webContents } from 'electron'
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
import { app, session, shell, webContents } from 'electron'
|
||||
|
||||
/**
|
||||
* init the useragent of the webview session
|
||||
@@ -36,3 +37,66 @@ export function setOpenLinkExternal(webviewId: number, isExternal: boolean) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const attachKeyboardHandler = (contents: Electron.WebContents) => {
|
||||
if (contents.getType?.() !== 'webview') {
|
||||
return
|
||||
}
|
||||
|
||||
const handleBeforeInput = (event: Electron.Event, input: Electron.Input) => {
|
||||
if (!input) {
|
||||
return
|
||||
}
|
||||
|
||||
const key = input.key?.toLowerCase()
|
||||
if (!key) {
|
||||
return
|
||||
}
|
||||
|
||||
const isFindShortcut = (input.control || input.meta) && key === 'f'
|
||||
const isEscape = key === 'escape'
|
||||
const isEnter = key === 'enter'
|
||||
|
||||
if (!isFindShortcut && !isEscape && !isEnter) {
|
||||
return
|
||||
}
|
||||
|
||||
const host = contents.hostWebContents
|
||||
if (!host || host.isDestroyed()) {
|
||||
return
|
||||
}
|
||||
|
||||
// Always prevent Cmd/Ctrl+F to override the guest page's native find dialog
|
||||
if (isFindShortcut) {
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
// Send the hotkey event to the renderer
|
||||
// The renderer will decide whether to preventDefault for Escape and Enter
|
||||
// based on whether the search bar is visible
|
||||
host.send(IpcChannel.Webview_SearchHotkey, {
|
||||
webviewId: contents.id,
|
||||
key,
|
||||
control: Boolean(input.control),
|
||||
meta: Boolean(input.meta),
|
||||
shift: Boolean(input.shift),
|
||||
alt: Boolean(input.alt)
|
||||
})
|
||||
}
|
||||
|
||||
contents.on('before-input-event', handleBeforeInput)
|
||||
contents.once('destroyed', () => {
|
||||
contents.removeListener('before-input-event', handleBeforeInput)
|
||||
})
|
||||
}
|
||||
|
||||
export function initWebviewHotkeys() {
|
||||
webContents.getAllWebContents().forEach((contents) => {
|
||||
if (contents.isDestroyed()) return
|
||||
attachKeyboardHandler(contents)
|
||||
})
|
||||
|
||||
app.on('web-contents-created', (_, contents) => {
|
||||
attachKeyboardHandler(contents)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
OAuthTokens
|
||||
} from '@modelcontextprotocol/sdk/shared/auth.js'
|
||||
import EventEmitter from 'events'
|
||||
import { z } from 'zod'
|
||||
import * as z from 'zod'
|
||||
|
||||
export interface OAuthStorageData {
|
||||
clientInfo?: OAuthClientInformation
|
||||
|
||||
@@ -2,6 +2,7 @@ import { loggerService } from '@logger'
|
||||
import { isLinux } from '@main/constant'
|
||||
import { BuiltinOcrProviderIds, OcrHandler, OcrProvider, OcrResult, SupportedOcrFile } from '@types'
|
||||
|
||||
import { ovOcrService } from './builtin/OvOcrService'
|
||||
import { ppocrService } from './builtin/PpocrService'
|
||||
import { systemOcrService } from './builtin/SystemOcrService'
|
||||
import { tesseractService } from './builtin/TesseractService'
|
||||
@@ -22,6 +23,10 @@ export class OcrService {
|
||||
this.registry.delete(providerId)
|
||||
}
|
||||
|
||||
public listProviderIds(): string[] {
|
||||
return Array.from(this.registry.keys())
|
||||
}
|
||||
|
||||
public async ocr(file: SupportedOcrFile, provider: OcrProvider): Promise<OcrResult> {
|
||||
const handler = this.registry.get(provider.id)
|
||||
if (!handler) {
|
||||
@@ -39,3 +44,5 @@ ocrService.register(BuiltinOcrProviderIds.tesseract, tesseractService.ocr.bind(t
|
||||
!isLinux && ocrService.register(BuiltinOcrProviderIds.system, systemOcrService.ocr.bind(systemOcrService))
|
||||
|
||||
ocrService.register(BuiltinOcrProviderIds.paddleocr, ppocrService.ocr.bind(ppocrService))
|
||||
|
||||
ovOcrService.isAvailable() && ocrService.register(BuiltinOcrProviderIds.ovocr, ovOcrService.ocr.bind(ovOcrService))
|
||||
|
||||
128
src/main/services/ocr/builtin/OvOcrService.ts
Normal file
128
src/main/services/ocr/builtin/OvOcrService.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { isWin } from '@main/constant'
|
||||
import { isImageFileMetadata, OcrOvConfig, OcrResult, SupportedOcrFile } from '@types'
|
||||
import { exec } from 'child_process'
|
||||
import * as fs from 'fs'
|
||||
import * as os from 'os'
|
||||
import * as path from 'path'
|
||||
import { promisify } from 'util'
|
||||
|
||||
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')
|
||||
|
||||
export class OvOcrService extends OcrBaseService {
|
||||
constructor() {
|
||||
super()
|
||||
}
|
||||
|
||||
public isAvailable(): boolean {
|
||||
return (
|
||||
isWin &&
|
||||
os.cpus()[0].model.toLowerCase().includes('intel') &&
|
||||
os.cpus()[0].model.toLowerCase().includes('ultra') &&
|
||||
fs.existsSync(PATH_BAT_FILE)
|
||||
)
|
||||
}
|
||||
|
||||
private getOvOcrPath(): string {
|
||||
return path.join(os.homedir(), '.cherrystudio', 'ovms', 'ovocr')
|
||||
}
|
||||
|
||||
private getImgDir(): string {
|
||||
return path.join(this.getOvOcrPath(), 'img')
|
||||
}
|
||||
|
||||
private getOutputDir(): string {
|
||||
return path.join(this.getOvOcrPath(), 'output')
|
||||
}
|
||||
|
||||
private async clearDirectory(dirPath: string): Promise<void> {
|
||||
if (fs.existsSync(dirPath)) {
|
||||
const files = await fs.promises.readdir(dirPath)
|
||||
for (const file of files) {
|
||||
const filePath = path.join(dirPath, file)
|
||||
const stats = await fs.promises.stat(filePath)
|
||||
if (stats.isDirectory()) {
|
||||
await this.clearDirectory(filePath)
|
||||
await fs.promises.rmdir(filePath)
|
||||
} else {
|
||||
await fs.promises.unlink(filePath)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If the directory does not exist, create it
|
||||
await fs.promises.mkdir(dirPath, { recursive: true })
|
||||
}
|
||||
}
|
||||
|
||||
private async copyFileToImgDir(sourceFilePath: string, targetFileName: string): Promise<void> {
|
||||
const imgDir = this.getImgDir()
|
||||
const targetFilePath = path.join(imgDir, targetFileName)
|
||||
await fs.promises.copyFile(sourceFilePath, targetFilePath)
|
||||
}
|
||||
|
||||
private async runOcrBatch(): Promise<void> {
|
||||
const ovOcrPath = this.getOvOcrPath()
|
||||
|
||||
try {
|
||||
// Execute run.bat in the ov-ocr directory
|
||||
await execAsync(`"${PATH_BAT_FILE}"`, {
|
||||
cwd: ovOcrPath,
|
||||
timeout: 60000 // 60 second timeout
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error(`Error running ovocr batch: ${error}`)
|
||||
throw new Error(`Failed to run OCR batch: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
private async ocrImage(filePath: string, options?: OcrOvConfig): Promise<OcrResult> {
|
||||
logger.info(`OV OCR called on ${filePath} with options ${JSON.stringify(options)}`)
|
||||
|
||||
try {
|
||||
// 1. Clear img directory and output directory
|
||||
await this.clearDirectory(this.getImgDir())
|
||||
await this.clearDirectory(this.getOutputDir())
|
||||
|
||||
// 2. Copy file to img directory
|
||||
const fileName = path.basename(filePath)
|
||||
await this.copyFileToImgDir(filePath, fileName)
|
||||
logger.info(`File copied to img directory: ${fileName}`)
|
||||
|
||||
// 3. Run run.bat
|
||||
logger.info('Running OV OCR batch process...')
|
||||
await this.runOcrBatch()
|
||||
|
||||
// 4. Check that output/[basename].txt file exists
|
||||
const baseNameWithoutExt = path.basename(fileName, path.extname(fileName))
|
||||
const outputFilePath = path.join(this.getOutputDir(), `${baseNameWithoutExt}.txt`)
|
||||
if (!fs.existsSync(outputFilePath)) {
|
||||
throw new Error(`OV OCR output file not found at: ${outputFilePath}`)
|
||||
}
|
||||
|
||||
// 5. Read output/[basename].txt file content
|
||||
const ocrText = await fs.promises.readFile(outputFilePath, 'utf-8')
|
||||
logger.info(`OV OCR text extracted: ${ocrText.substring(0, 100)}...`)
|
||||
|
||||
// 6. Return result
|
||||
return { text: ocrText }
|
||||
} catch (error) {
|
||||
logger.error(`Error during OV OCR process: ${error}`)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
public ocr = async (file: SupportedOcrFile, options?: OcrOvConfig): Promise<OcrResult> => {
|
||||
if (isImageFileMetadata(file)) {
|
||||
return this.ocrImage(file.path, options)
|
||||
} else {
|
||||
throw new Error('Unsupported file type, currently only image files are supported')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const ovOcrService = new OvOcrService()
|
||||
@@ -1,7 +1,7 @@
|
||||
import { loadOcrImage } from '@main/utils/ocr'
|
||||
import { ImageFileMetadata, isImageFileMetadata, OcrPpocrConfig, OcrResult, SupportedOcrFile } from '@types'
|
||||
import { net } from 'electron'
|
||||
import { z } from 'zod'
|
||||
import * as z from 'zod'
|
||||
|
||||
import { OcrBaseService } from './OcrBaseService'
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { SpanEntity, TokenUsage } from '@mcp-trace/trace-core'
|
||||
import { SpanContext } from '@opentelemetry/api'
|
||||
import { TerminalConfig, UpgradeChannel } from '@shared/config/constant'
|
||||
import type { LogLevel, LogSourceWithContext } from '@shared/config/logger'
|
||||
import type { FileChangeEvent } from '@shared/config/types'
|
||||
import type { FileChangeEvent, WebviewKeyEvent } from '@shared/config/types'
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
import type { Notification } from '@types'
|
||||
import {
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
FileListResponse,
|
||||
FileMetadata,
|
||||
FileUploadResponse,
|
||||
GetApiServerStatusResult,
|
||||
KnowledgeBaseParams,
|
||||
KnowledgeItem,
|
||||
KnowledgeSearchResult,
|
||||
@@ -22,8 +23,11 @@ import {
|
||||
OcrProvider,
|
||||
OcrResult,
|
||||
Provider,
|
||||
RestartApiServerStatusResult,
|
||||
S3Config,
|
||||
Shortcut,
|
||||
StartApiServerStatusResult,
|
||||
StopApiServerStatusResult,
|
||||
SupportedOcrFile,
|
||||
ThemeMode,
|
||||
WebDavConfig
|
||||
@@ -390,7 +394,16 @@ const api = {
|
||||
setOpenLinkExternal: (webviewId: number, isExternal: boolean) =>
|
||||
ipcRenderer.invoke(IpcChannel.Webview_SetOpenLinkExternal, webviewId, isExternal),
|
||||
setSpellCheckEnabled: (webviewId: number, isEnable: boolean) =>
|
||||
ipcRenderer.invoke(IpcChannel.Webview_SetSpellCheckEnabled, webviewId, isEnable)
|
||||
ipcRenderer.invoke(IpcChannel.Webview_SetSpellCheckEnabled, webviewId, isEnable),
|
||||
onFindShortcut: (callback: (payload: WebviewKeyEvent) => void) => {
|
||||
const listener = (_event: Electron.IpcRendererEvent, payload: WebviewKeyEvent) => {
|
||||
callback(payload)
|
||||
}
|
||||
ipcRenderer.on(IpcChannel.Webview_SearchHotkey, listener)
|
||||
return () => {
|
||||
ipcRenderer.off(IpcChannel.Webview_SearchHotkey, listener)
|
||||
}
|
||||
}
|
||||
},
|
||||
storeSync: {
|
||||
subscribe: () => ipcRenderer.invoke(IpcChannel.StoreSync_Subscribe),
|
||||
@@ -467,7 +480,8 @@ const api = {
|
||||
},
|
||||
ocr: {
|
||||
ocr: (file: SupportedOcrFile, provider: OcrProvider): Promise<OcrResult> =>
|
||||
ipcRenderer.invoke(IpcChannel.OCR_ocr, file, provider)
|
||||
ipcRenderer.invoke(IpcChannel.OCR_ocr, file, provider),
|
||||
listProviders: (): Promise<string[]> => ipcRenderer.invoke(IpcChannel.OCR_ListProviders)
|
||||
},
|
||||
cherryai: {
|
||||
generateSignature: (params: { method: string; path: string; query: string; body: Record<string, any> }) =>
|
||||
@@ -487,6 +501,12 @@ const api = {
|
||||
ipcRenderer.removeListener(channel, listener)
|
||||
}
|
||||
}
|
||||
},
|
||||
apiServer: {
|
||||
getStatus: (): Promise<GetApiServerStatusResult> => ipcRenderer.invoke(IpcChannel.ApiServer_GetStatus),
|
||||
start: (): Promise<StartApiServerStatusResult> => ipcRenderer.invoke(IpcChannel.ApiServer_Start),
|
||||
restart: (): Promise<RestartApiServerStatusResult> => ipcRenderer.invoke(IpcChannel.ApiServer_Restart),
|
||||
stop: (): Promise<StopApiServerStatusResult> => ipcRenderer.invoke(IpcChannel.ApiServer_Stop)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { AISDKWebSearchResult, MCPTool, WebSearchResults, WebSearchSource } from '@renderer/types'
|
||||
import { Chunk, ChunkType } from '@renderer/types/chunk'
|
||||
import { ProviderSpecificError } from '@renderer/types/provider-specific-error'
|
||||
import { formatErrorMessage } from '@renderer/utils/error'
|
||||
import { convertLinks, flushLinkConverterBuffer } from '@renderer/utils/linkConverter'
|
||||
import type { ClaudeCodeRawValue } from '@shared/agents/claudecode/types'
|
||||
import type { TextStreamPart, ToolSet } from 'ai'
|
||||
@@ -355,7 +357,11 @@ export class AiSdkToChunkAdapter {
|
||||
case 'error':
|
||||
this.onChunk({
|
||||
type: ChunkType.ERROR,
|
||||
error: chunk.error as Record<string, any>
|
||||
error: new ProviderSpecificError({
|
||||
message: formatErrorMessage(chunk.error),
|
||||
provider: 'unknown',
|
||||
cause: chunk.error
|
||||
})
|
||||
})
|
||||
break
|
||||
|
||||
|
||||
@@ -12,7 +12,15 @@ import { loggerService } from '@logger'
|
||||
import { getEnableDeveloperMode } from '@renderer/hooks/useSettings'
|
||||
import { addSpan, endSpan } from '@renderer/services/SpanManagerService'
|
||||
import { StartSpanParams } from '@renderer/trace/types/ModelSpanEntity'
|
||||
import type { Assistant, GenerateImageParams, Model, Provider } from '@renderer/types'
|
||||
import type {
|
||||
Assistant,
|
||||
DeleteVideoParams,
|
||||
DeleteVideoResult,
|
||||
GenerateImageParams,
|
||||
Model,
|
||||
Provider,
|
||||
RetrieveVideoContentParams
|
||||
} from '@renderer/types'
|
||||
import type { AiSdkModel, StreamTextParams } from '@renderer/types/aiCoreTypes'
|
||||
import {
|
||||
CreateVideoParams,
|
||||
@@ -90,10 +98,8 @@ export default class ModernAiProvider {
|
||||
throw new Error('Model is required for completions. Please use constructor with model parameter.')
|
||||
}
|
||||
|
||||
// 确保配置存在
|
||||
if (!this.config) {
|
||||
this.config = providerToAiSdkConfig(this.actualProvider, this.model)
|
||||
}
|
||||
// 每次请求时重新生成配置以确保API key轮换生效
|
||||
this.config = providerToAiSdkConfig(this.actualProvider, this.model)
|
||||
|
||||
// 准备特殊配置
|
||||
await prepareSpecialProviderConfig(this.actualProvider, this.config)
|
||||
@@ -524,10 +530,17 @@ export default class ModernAiProvider {
|
||||
/**
|
||||
* We manually implement this method before aisdk supports it well
|
||||
*/
|
||||
public async retrieveVideoContent(params: RetrieveVideoParams): Promise<RetrieveVideoContentResult> {
|
||||
public async retrieveVideoContent(params: RetrieveVideoContentParams): Promise<RetrieveVideoContentResult> {
|
||||
return this.legacyProvider.retrieveVideoContent(params)
|
||||
}
|
||||
|
||||
/**
|
||||
* We manually implement this method before aisdk supports it well
|
||||
*/
|
||||
public async deleteVideo(params: DeleteVideoParams): Promise<DeleteVideoResult> {
|
||||
return this.legacyProvider.deleteVideo(params)
|
||||
}
|
||||
|
||||
public getBaseURL(): string {
|
||||
return this.legacyProvider.getBaseURL()
|
||||
}
|
||||
|
||||
@@ -70,13 +70,19 @@ export abstract class BaseApiClient<
|
||||
{
|
||||
public provider: Provider
|
||||
protected host: string
|
||||
protected apiKey: string
|
||||
protected sdkInstance?: TSdkInstance
|
||||
|
||||
constructor(provider: Provider) {
|
||||
this.provider = provider
|
||||
this.host = this.getBaseURL()
|
||||
this.apiKey = this.getApiKey()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current API key with rotation support
|
||||
* This getter ensures API keys rotate on each access when multiple keys are configured
|
||||
*/
|
||||
protected get apiKey(): string {
|
||||
return this.getApiKey()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import OpenAI, { AzureOpenAI } from '@cherrystudio/openai'
|
||||
import { loggerService } from '@logger'
|
||||
import { COPILOT_DEFAULT_HEADERS } from '@renderer/aiCore/provider/constants'
|
||||
import {
|
||||
isClaudeReasoningModel,
|
||||
isOpenAIReasoningModel,
|
||||
@@ -166,7 +167,8 @@ export abstract class OpenAIBaseClient<
|
||||
baseURL: this.getBaseURL(),
|
||||
defaultHeaders: {
|
||||
...this.defaultHeaders(),
|
||||
...this.provider.extra_headers
|
||||
...this.provider.extra_headers,
|
||||
...(this.provider.id === 'copilot' ? COPILOT_DEFAULT_HEADERS : {})
|
||||
}
|
||||
}) as TSdkInstance
|
||||
}
|
||||
|
||||
@@ -36,7 +36,12 @@ import {
|
||||
OpenAIResponseSdkTool,
|
||||
OpenAIResponseSdkToolCall
|
||||
} from '@renderer/types/sdk'
|
||||
import { CreateVideoParams, RetrieveVideoContentParams, RetrieveVideoParams } from '@renderer/types/video'
|
||||
import {
|
||||
CreateVideoParams,
|
||||
DeleteVideoParams,
|
||||
RetrieveVideoContentParams,
|
||||
RetrieveVideoParams
|
||||
} from '@renderer/types/video'
|
||||
import { addImageFileToContents } from '@renderer/utils/formats'
|
||||
import {
|
||||
isSupportedToolUse,
|
||||
@@ -168,6 +173,11 @@ export class OpenAIResponseAPIClient extends OpenAIBaseClient<
|
||||
return sdk.videos.downloadContent(params.videoId, params.query, params.options)
|
||||
}
|
||||
|
||||
public async deleteVideo(params: DeleteVideoParams): Promise<OpenAI.Videos.VideoDeleteResponse> {
|
||||
const sdk = await this.getSdkInstance()
|
||||
return sdk.videos.delete(params.videoId, params.options)
|
||||
}
|
||||
|
||||
private async handlePdfFile(file: FileMetadata): Promise<OpenAI.Responses.ResponseInputFile | undefined> {
|
||||
if (file.size > 32 * MB) return undefined
|
||||
try {
|
||||
|
||||
@@ -5,7 +5,14 @@ import { isDedicatedImageGenerationModel, isFunctionCallingModel } from '@render
|
||||
import { getProviderByModel } from '@renderer/services/AssistantService'
|
||||
import { withSpanResult } from '@renderer/services/SpanManagerService'
|
||||
import { StartSpanParams } from '@renderer/trace/types/ModelSpanEntity'
|
||||
import type { GenerateImageParams, Model, Provider } from '@renderer/types'
|
||||
import type {
|
||||
DeleteVideoParams,
|
||||
DeleteVideoResult,
|
||||
GenerateImageParams,
|
||||
Model,
|
||||
Provider,
|
||||
RetrieveVideoContentParams
|
||||
} from '@renderer/types'
|
||||
import type { RequestOptions, SdkModel } from '@renderer/types/sdk'
|
||||
import {
|
||||
CreateVideoParams,
|
||||
@@ -210,7 +217,7 @@ export default class AiProvider {
|
||||
}
|
||||
}
|
||||
|
||||
public async retrieveVideoContent(params: RetrieveVideoParams): Promise<RetrieveVideoContentResult> {
|
||||
public async retrieveVideoContent(params: RetrieveVideoContentParams): Promise<RetrieveVideoContentResult> {
|
||||
if (this.apiClient instanceof OpenAIResponseAPIClient && params.type === 'openai') {
|
||||
const response = await this.apiClient.retrieveVideoContent(params)
|
||||
return {
|
||||
@@ -222,6 +229,18 @@ export default class AiProvider {
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteVideo(params: DeleteVideoParams): Promise<DeleteVideoResult> {
|
||||
if (this.apiClient instanceof OpenAIResponseAPIClient && params.type === 'openai') {
|
||||
const result = await this.apiClient.deleteVideo(params)
|
||||
return {
|
||||
type: 'openai',
|
||||
result
|
||||
}
|
||||
} else {
|
||||
throw new Error('Video deletion is not supported by this provider')
|
||||
}
|
||||
}
|
||||
|
||||
public getBaseURL(): string {
|
||||
return this.apiClient.getBaseURL()
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import type { MCPTool, Message, Model, Provider } from '@renderer/types'
|
||||
import type { Chunk } from '@renderer/types/chunk'
|
||||
import { extractReasoningMiddleware, LanguageModelMiddleware, simulateStreamingMiddleware } from 'ai'
|
||||
|
||||
import { noThinkMiddleware } from './noThinkMiddleware'
|
||||
|
||||
const logger = loggerService.withContext('AiSdkMiddlewareBuilder')
|
||||
|
||||
/**
|
||||
@@ -186,6 +188,14 @@ function addProviderSpecificMiddlewares(builder: AiSdkMiddlewareBuilder, config:
|
||||
// 其他provider的通用处理
|
||||
break
|
||||
}
|
||||
|
||||
// OVMS+MCP's specific middleware
|
||||
if (config.provider.id === 'ovms' && config.mcpTools && config.mcpTools.length > 0) {
|
||||
builder.add({
|
||||
name: 'no-think',
|
||||
middleware: noThinkMiddleware()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
52
src/renderer/src/aiCore/middleware/noThinkMiddleware.ts
Normal file
52
src/renderer/src/aiCore/middleware/noThinkMiddleware.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { LanguageModelMiddleware } from 'ai'
|
||||
|
||||
const logger = loggerService.withContext('noThinkMiddleware')
|
||||
|
||||
/**
|
||||
* No Think Middleware
|
||||
* Automatically appends ' /no_think' string to the end of user messages for the provider
|
||||
* This prevents the model from generating unnecessary thinking process and returns results directly
|
||||
* @returns LanguageModelMiddleware
|
||||
*/
|
||||
export function noThinkMiddleware(): LanguageModelMiddleware {
|
||||
return {
|
||||
middlewareVersion: 'v2',
|
||||
|
||||
transformParams: async ({ params }) => {
|
||||
const transformedParams = { ...params }
|
||||
// Process messages in prompt
|
||||
if (transformedParams.prompt && Array.isArray(transformedParams.prompt)) {
|
||||
transformedParams.prompt = transformedParams.prompt.map((message) => {
|
||||
// Only process user messages
|
||||
if (message.role === 'user') {
|
||||
// Process content array
|
||||
if (Array.isArray(message.content)) {
|
||||
const lastContent = message.content[message.content.length - 1]
|
||||
// If the last content is text type, append ' /no_think'
|
||||
if (lastContent && lastContent.type === 'text' && typeof lastContent.text === 'string') {
|
||||
// Avoid duplicate additions
|
||||
if (!lastContent.text.endsWith('/no_think')) {
|
||||
logger.debug('Adding /no_think to user message')
|
||||
return {
|
||||
...message,
|
||||
content: [
|
||||
...message.content.slice(0, -1),
|
||||
{
|
||||
...lastContent,
|
||||
text: lastContent.text + ' /no_think'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return message
|
||||
})
|
||||
}
|
||||
|
||||
return transformedParams
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,13 +63,14 @@ function handleSpecialProviders(model: Model, provider: Provider): Provider {
|
||||
// return createVertexProvider(provider)
|
||||
// }
|
||||
|
||||
if (isNewApiProvider(provider)) {
|
||||
return newApiResolverCreator(model, provider)
|
||||
}
|
||||
|
||||
if (isSystemProvider(provider)) {
|
||||
if (provider.id === 'aihubmix') {
|
||||
return aihubmixProviderCreator(model, provider)
|
||||
}
|
||||
if (isNewApiProvider(provider)) {
|
||||
return newApiResolverCreator(model, provider)
|
||||
}
|
||||
if (provider.id === 'vertexai') {
|
||||
return vertexAnthropicProviderCreator(model, provider)
|
||||
}
|
||||
|
||||
@@ -55,6 +55,14 @@ export const NEW_PROVIDER_CONFIGS: ProviderConfig[] = [
|
||||
creatorFunctionName: 'createPerplexity',
|
||||
supportsImageGeneration: false,
|
||||
aliases: ['perplexity']
|
||||
},
|
||||
{
|
||||
id: 'mistral',
|
||||
name: 'Mistral',
|
||||
import: () => import('@ai-sdk/mistral'),
|
||||
creatorFunctionName: 'createMistral',
|
||||
supportsImageGeneration: false,
|
||||
aliases: ['mistral']
|
||||
}
|
||||
] as const
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { Assistant, KnowledgeReference } from '@renderer/types'
|
||||
import { ExtractResults, KnowledgeExtractResults } from '@renderer/utils/extract'
|
||||
import { type InferToolInput, type InferToolOutput, tool } from 'ai'
|
||||
import { isEmpty } from 'lodash'
|
||||
import { z } from 'zod'
|
||||
import * as z from 'zod'
|
||||
|
||||
/**
|
||||
* 知识库搜索工具
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import store from '@renderer/store'
|
||||
import { selectCurrentUserId, selectGlobalMemoryEnabled, selectMemoryConfig } from '@renderer/store/memory'
|
||||
import { type InferToolInput, type InferToolOutput, tool } from 'ai'
|
||||
import { z } from 'zod'
|
||||
import * as z from 'zod'
|
||||
|
||||
import { MemoryProcessor } from '../../services/MemoryProcessor'
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import WebSearchService from '@renderer/services/WebSearchService'
|
||||
import { WebSearchProvider, WebSearchProviderResponse } from '@renderer/types'
|
||||
import { ExtractResults } from '@renderer/utils/extract'
|
||||
import { type InferToolInput, type InferToolOutput, tool } from 'ai'
|
||||
import { z } from 'zod'
|
||||
import * as z from 'zod'
|
||||
|
||||
/**
|
||||
* 使用预提取关键词的网络搜索工具
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
getThinkModelType,
|
||||
isDeepSeekHybridInferenceModel,
|
||||
isDoubaoThinkingAutoModel,
|
||||
isGrok4FastReasoningModel,
|
||||
isGrokReasoningModel,
|
||||
isOpenAIReasoningModel,
|
||||
isQwenAlwaysThinkModel,
|
||||
@@ -52,7 +53,12 @@ export function getReasoningEffort(assistant: Assistant, model: Model): Reasonin
|
||||
return {}
|
||||
}
|
||||
// Don't disable reasoning for models that require it
|
||||
if (isGrokReasoningModel(model) || isOpenAIReasoningModel(model) || model.id.includes('seed-oss')) {
|
||||
if (
|
||||
isGrokReasoningModel(model) ||
|
||||
isOpenAIReasoningModel(model) ||
|
||||
isQwenAlwaysThinkModel(model) ||
|
||||
model.id.includes('seed-oss')
|
||||
) {
|
||||
return {}
|
||||
}
|
||||
return { reasoning: { enabled: false, exclude: true } }
|
||||
@@ -100,6 +106,7 @@ export function getReasoningEffort(assistant: Assistant, model: Model): Reasonin
|
||||
// reasoningEffort有效的情况
|
||||
// DeepSeek hybrid inference models, v3.1 and maybe more in the future
|
||||
// 不同的 provider 有不同的思考控制方式,在这里统一解决
|
||||
|
||||
if (isDeepSeekHybridInferenceModel(model)) {
|
||||
if (isSystemProvider(provider)) {
|
||||
switch (provider.id) {
|
||||
@@ -142,6 +149,16 @@ export function getReasoningEffort(assistant: Assistant, model: Model): Reasonin
|
||||
|
||||
// OpenRouter models
|
||||
if (model.provider === SystemProviderIds.openrouter) {
|
||||
// Grok 4 Fast doesn't support effort levels, always use enabled: true
|
||||
if (isGrok4FastReasoningModel(model)) {
|
||||
return {
|
||||
reasoning: {
|
||||
enabled: true // Ignore effort level, just enable reasoning
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Other OpenRouter models that support effort levels
|
||||
if (isSupportedReasoningEffortModel(model) || isSupportedThinkingTokenModel(model)) {
|
||||
return {
|
||||
reasoning: {
|
||||
@@ -412,6 +429,13 @@ export function getGeminiReasoningParams(assistant: Assistant, model: Model): Re
|
||||
return {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get XAI-specific reasoning parameters
|
||||
* This function should only be called for XAI provider models
|
||||
* @param assistant - The assistant configuration
|
||||
* @param model - The model being used
|
||||
* @returns XAI-specific reasoning parameters
|
||||
*/
|
||||
export function getXAIReasoningParams(assistant: Assistant, model: Model): Record<string, any> {
|
||||
if (!isSupportedReasoningEffortGrokModel(model)) {
|
||||
return {}
|
||||
@@ -419,6 +443,11 @@ export function getXAIReasoningParams(assistant: Assistant, model: Model): Recor
|
||||
|
||||
const { reasoning_effort: reasoningEffort } = getAssistantSettings(assistant)
|
||||
|
||||
if (!reasoningEffort) {
|
||||
return {}
|
||||
}
|
||||
|
||||
// For XAI provider Grok models, use reasoningEffort parameter directly
|
||||
return {
|
||||
reasoningEffort
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
CreateAgentRequest,
|
||||
CreateAgentResponse,
|
||||
CreateAgentResponseSchema,
|
||||
CreateAgentSessionResponse,
|
||||
CreateAgentSessionResponseSchema,
|
||||
CreateSessionForm,
|
||||
CreateSessionRequest,
|
||||
GetAgentResponse,
|
||||
@@ -171,12 +173,12 @@ export class AgentApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
public async createSession(agentId: string, session: CreateSessionForm): Promise<GetAgentSessionResponse> {
|
||||
public async createSession(agentId: string, session: CreateSessionForm): Promise<CreateAgentSessionResponse> {
|
||||
const url = this.getSessionPaths(agentId).base
|
||||
try {
|
||||
const payload = session satisfies CreateSessionRequest
|
||||
const response = await this.axios.post(url, payload)
|
||||
const data = GetAgentSessionResponseSchema.parse(response.data)
|
||||
const data = CreateAgentSessionResponseSchema.parse(response.data)
|
||||
return data
|
||||
} catch (error) {
|
||||
throw processError(error, 'Failed to add session.')
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 1.5 KiB |
@@ -326,7 +326,7 @@ const CodeBlockWrapper = styled.div<{ $isInSpecialView: boolean }>`
|
||||
* 一是 CodeViewer 在气泡样式下的用户消息中无法撑开气泡,
|
||||
* 二是 代码块内容过少时 toolbar 会和 title 重叠。
|
||||
*/
|
||||
min-width: 45ch;
|
||||
min-width: 35ch;
|
||||
|
||||
.code-toolbar {
|
||||
background-color: ${(props) => (props.$isInSpecialView ? 'transparent' : 'var(--color-background-mute)')};
|
||||
|
||||
@@ -2,7 +2,7 @@ import { linter } from '@codemirror/lint' // statically imported by @uiw/codemir
|
||||
import { EditorView } from '@codemirror/view'
|
||||
import { loggerService } from '@logger'
|
||||
import { Extension, keymap } from '@uiw/react-codemirror'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import { getNormalizedExtension } from './utils'
|
||||
|
||||
@@ -203,3 +203,80 @@ export function useHeightListener({ onHeightChange }: UseHeightListenerProps) {
|
||||
})
|
||||
}, [onHeightChange])
|
||||
}
|
||||
|
||||
interface UseScrollToLineOptions {
|
||||
highlight?: boolean
|
||||
}
|
||||
|
||||
export function useScrollToLine(editorViewRef: React.MutableRefObject<EditorView | null>) {
|
||||
const findLineElement = useCallback((view: EditorView, position: number): HTMLElement | null => {
|
||||
const domAtPos = view.domAtPos(position)
|
||||
let node: Node | null = domAtPos.node
|
||||
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
node = node.parentElement
|
||||
}
|
||||
|
||||
while (node) {
|
||||
if (node instanceof HTMLElement && node.classList.contains('cm-line')) {
|
||||
return node
|
||||
}
|
||||
node = node.parentElement
|
||||
}
|
||||
|
||||
return null
|
||||
}, [])
|
||||
|
||||
const highlightLine = useCallback((view: EditorView, element: HTMLElement) => {
|
||||
const previousHighlight = view.dom.querySelector('.animation-locate-highlight') as HTMLElement | null
|
||||
if (previousHighlight) {
|
||||
previousHighlight.classList.remove('animation-locate-highlight')
|
||||
}
|
||||
|
||||
element.classList.add('animation-locate-highlight')
|
||||
|
||||
const handleAnimationEnd = () => {
|
||||
element.classList.remove('animation-locate-highlight')
|
||||
element.removeEventListener('animationend', handleAnimationEnd)
|
||||
}
|
||||
|
||||
element.addEventListener('animationend', handleAnimationEnd)
|
||||
}, [])
|
||||
|
||||
return useCallback(
|
||||
(lineNumber: number, options?: UseScrollToLineOptions) => {
|
||||
const view = editorViewRef.current
|
||||
if (!view) return
|
||||
|
||||
const targetLine = view.state.doc.line(Math.min(lineNumber, view.state.doc.lines))
|
||||
|
||||
const lineElement = findLineElement(view, targetLine.from)
|
||||
if (lineElement) {
|
||||
lineElement.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' })
|
||||
|
||||
if (options?.highlight) {
|
||||
requestAnimationFrame(() => highlightLine(view, lineElement))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
view.dispatch({
|
||||
effects: EditorView.scrollIntoView(targetLine.from, {
|
||||
y: 'start'
|
||||
})
|
||||
})
|
||||
|
||||
if (!options?.highlight) {
|
||||
return
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
const fallbackElement = findLineElement(view, targetLine.from)
|
||||
if (fallbackElement) {
|
||||
highlightLine(view, fallbackElement)
|
||||
}
|
||||
}, 200)
|
||||
},
|
||||
[editorViewRef, findLineElement, highlightLine]
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,13 +5,14 @@ import diff from 'fast-diff'
|
||||
import { useCallback, useEffect, useImperativeHandle, useMemo, useRef } from 'react'
|
||||
import { memo } from 'react'
|
||||
|
||||
import { useBlurHandler, useHeightListener, useLanguageExtensions, useSaveKeymap } from './hooks'
|
||||
import { useBlurHandler, useHeightListener, useLanguageExtensions, useSaveKeymap, useScrollToLine } from './hooks'
|
||||
|
||||
// 标记非用户编辑的变更
|
||||
const External = Annotation.define<boolean>()
|
||||
|
||||
export interface CodeEditorHandles {
|
||||
save?: () => void
|
||||
scrollToLine?: (lineNumber: number, options?: { highlight?: boolean }) => void
|
||||
}
|
||||
|
||||
export interface CodeEditorProps {
|
||||
@@ -181,8 +182,11 @@ const CodeEditor = ({
|
||||
].flat()
|
||||
}, [extensions, langExtensions, wrapped, saveKeymapExtension, blurExtension, heightListenerExtension])
|
||||
|
||||
const scrollToLine = useScrollToLine(editorViewRef)
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
save: handleSave
|
||||
save: handleSave,
|
||||
scrollToLine
|
||||
}))
|
||||
|
||||
return (
|
||||
|
||||
48
src/renderer/src/components/HighlightText.tsx
Normal file
48
src/renderer/src/components/HighlightText.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { FC, memo, useMemo } from 'react'
|
||||
|
||||
interface HighlightTextProps {
|
||||
text: string
|
||||
keyword: string
|
||||
caseSensitive?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Text highlighting component that marks keyword matches
|
||||
*/
|
||||
const HighlightText: FC<HighlightTextProps> = ({ text, keyword, caseSensitive = false, className }) => {
|
||||
const highlightedText = useMemo(() => {
|
||||
if (!keyword || !text) {
|
||||
return <span>{text}</span>
|
||||
}
|
||||
|
||||
// Escape regex special characters
|
||||
const escapedKeyword = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
const flags = caseSensitive ? 'g' : 'gi'
|
||||
const regex = new RegExp(`(${escapedKeyword})`, flags)
|
||||
|
||||
// Split text by keyword matches
|
||||
const parts = text.split(regex)
|
||||
|
||||
return (
|
||||
<>
|
||||
{parts.map((part, index) => {
|
||||
// Check if part matches keyword
|
||||
const isMatch = regex.test(part)
|
||||
regex.lastIndex = 0 // Reset regex state
|
||||
|
||||
if (isMatch) {
|
||||
return <mark key={index}>{part}</mark>
|
||||
}
|
||||
return <span key={index}>{part}</span>
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}, [text, keyword, caseSensitive])
|
||||
|
||||
const combinedClassName = className ? `ant-typography ${className}` : 'ant-typography'
|
||||
|
||||
return <span className={combinedClassName}>{highlightedText}</span>
|
||||
}
|
||||
|
||||
export default memo(HighlightText)
|
||||
@@ -25,7 +25,7 @@ const WebviewContainer = memo(
|
||||
onNavigateCallback: (appid: string, url: string) => void
|
||||
}) => {
|
||||
const webviewRef = useRef<WebviewTag | null>(null)
|
||||
const { enableSpellCheck } = useSettings()
|
||||
const { enableSpellCheck, minappsOpenLinkExternal } = useSettings()
|
||||
|
||||
const setRef = (appid: string) => {
|
||||
onSetRefCallback(appid, null)
|
||||
@@ -76,6 +76,8 @@ const WebviewContainer = memo(
|
||||
const webviewId = webviewRef.current?.getWebContentsId()
|
||||
if (webviewId) {
|
||||
window.api?.webview?.setSpellCheckEnabled?.(webviewId, enableSpellCheck)
|
||||
// Set link opening behavior for this webview
|
||||
window.api?.webview?.setOpenLinkExternal?.(webviewId, minappsOpenLinkExternal)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,6 +106,22 @@ const WebviewContainer = memo(
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [appid, url])
|
||||
|
||||
// Update webview settings when they change
|
||||
useEffect(() => {
|
||||
if (!webviewRef.current) return
|
||||
|
||||
try {
|
||||
const webviewId = webviewRef.current.getWebContentsId()
|
||||
if (webviewId) {
|
||||
window.api?.webview?.setSpellCheckEnabled?.(webviewId, enableSpellCheck)
|
||||
window.api?.webview?.setOpenLinkExternal?.(webviewId, minappsOpenLinkExternal)
|
||||
}
|
||||
} catch (error) {
|
||||
// WebView may not be ready yet, settings will be applied in dom-ready event
|
||||
logger.debug(`WebView ${appid} not ready for settings update`)
|
||||
}
|
||||
}, [appid, minappsOpenLinkExternal, enableSpellCheck])
|
||||
|
||||
const WebviewStyle: React.CSSProperties = {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
import { loggerService } from '@logger'
|
||||
import type { Selection } from '@react-types/shared'
|
||||
import ClaudeIcon from '@renderer/assets/images/models/claude.png'
|
||||
import { getModelLogo } from '@renderer/config/models'
|
||||
import { agentModelFilter, getModelLogo } from '@renderer/config/models'
|
||||
import { permissionModeCards } from '@renderer/constants/permissionModes'
|
||||
import { useAgents } from '@renderer/hooks/agents/useAgents'
|
||||
import { useApiModels } from '@renderer/hooks/agents/useModels'
|
||||
@@ -245,14 +245,23 @@ export const AgentModal: React.FC<Props> = ({ agent, trigger, isOpen: _isOpen, o
|
||||
|
||||
const modelOptions = useMemo(() => {
|
||||
// mocked data. not final version
|
||||
return (models ?? []).map((model) => ({
|
||||
type: 'model',
|
||||
key: model.id,
|
||||
label: model.name,
|
||||
avatar: getModelLogo(model.id),
|
||||
providerId: model.provider,
|
||||
providerName: model.provider_name
|
||||
})) satisfies ModelOption[]
|
||||
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: getModelLogo(model.id),
|
||||
providerId: model.provider,
|
||||
providerName: model.provider_name
|
||||
})) satisfies ModelOption[]
|
||||
}, [models])
|
||||
|
||||
const onModelChange = useCallback((e: ChangeEvent<HTMLSelectElement>) => {
|
||||
|
||||
@@ -98,7 +98,7 @@ export const SessionModal: React.FC<Props> = ({
|
||||
const loadingRef = useRef(false)
|
||||
// const { setTimeoutTimer } = useTimer()
|
||||
const { createSession } = useSessions(agentId)
|
||||
const updateSession = useUpdateSession(agentId)
|
||||
const { updateSession } = useUpdateSession(agentId)
|
||||
const { agent } = useAgent(agentId)
|
||||
const isEditing = (session?: AgentSessionEntity) => session !== undefined
|
||||
|
||||
|
||||
2
src/renderer/src/components/RichEditor/constants.ts
Normal file
2
src/renderer/src/components/RichEditor/constants.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
// Attribute used to store the original source line number in markdown editors
|
||||
export const MARKDOWN_SOURCE_LINE_ATTR = 'data-source-line'
|
||||
@@ -1,5 +1,6 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { ContentSearch, type ContentSearchRef } from '@renderer/components/ContentSearch'
|
||||
import { MARKDOWN_SOURCE_LINE_ATTR } from '@renderer/components/RichEditor/constants'
|
||||
import DragHandle from '@tiptap/extension-drag-handle-react'
|
||||
import { EditorContent } from '@tiptap/react'
|
||||
import { Tooltip } from 'antd'
|
||||
@@ -29,6 +30,156 @@ import type { FormattingCommand, RichEditorProps, RichEditorRef } from './types'
|
||||
import { useRichEditor } from './useRichEditor'
|
||||
const logger = loggerService.withContext('RichEditor')
|
||||
|
||||
/**
|
||||
* Find element by line number with fallback strategies:
|
||||
* 1. Exact line + content match
|
||||
* 2. Exact line match
|
||||
* 3. Closest line <= target
|
||||
*/
|
||||
function findElementByLine(editorDom: HTMLElement, lineNumber: number, lineContent?: string): HTMLElement | null {
|
||||
const allElements = Array.from(editorDom.querySelectorAll(`[${MARKDOWN_SOURCE_LINE_ATTR}]`)) as HTMLElement[]
|
||||
if (allElements.length === 0) {
|
||||
logger.warn('No elements with data-source-line attribute found')
|
||||
return null
|
||||
}
|
||||
const exactMatches = editorDom.querySelectorAll(
|
||||
`[${MARKDOWN_SOURCE_LINE_ATTR}="${lineNumber}"]`
|
||||
) as NodeListOf<HTMLElement>
|
||||
|
||||
// Strategy 1: Exact line + content match
|
||||
if (exactMatches.length > 1 && lineContent) {
|
||||
for (const match of Array.from(exactMatches)) {
|
||||
if (match.textContent?.includes(lineContent)) {
|
||||
return match
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 2: Exact line match
|
||||
if (exactMatches.length > 0) {
|
||||
return exactMatches[0]
|
||||
}
|
||||
|
||||
// Strategy 3: Closest line <= target
|
||||
let closestElement: HTMLElement | null = null
|
||||
let closestLine = 0
|
||||
|
||||
for (const el of allElements) {
|
||||
const sourceLine = parseInt(el.getAttribute(MARKDOWN_SOURCE_LINE_ATTR) || '0', 10)
|
||||
if (sourceLine <= lineNumber && sourceLine > closestLine) {
|
||||
closestLine = sourceLine
|
||||
closestElement = el
|
||||
}
|
||||
}
|
||||
|
||||
return closestElement
|
||||
}
|
||||
|
||||
/**
|
||||
* Create fixed-position highlight overlay at element location
|
||||
* with boundary detection to prevent overflow and toolbar overlap
|
||||
*/
|
||||
function createHighlightOverlay(element: HTMLElement, container: HTMLElement): void {
|
||||
try {
|
||||
// Remove previous overlay
|
||||
const previousOverlay = document.body.querySelector('.highlight-overlay')
|
||||
if (previousOverlay) {
|
||||
previousOverlay.remove()
|
||||
}
|
||||
|
||||
const editorWrapper = container.closest('.rich-editor-wrapper')
|
||||
|
||||
// Create overlay at element position
|
||||
const rect = element.getBoundingClientRect()
|
||||
const overlay = document.createElement('div')
|
||||
overlay.className = 'highlight-overlay animation-locate-highlight'
|
||||
overlay.style.position = 'fixed'
|
||||
overlay.style.left = `${rect.left}px`
|
||||
overlay.style.top = `${rect.top}px`
|
||||
overlay.style.width = `${rect.width}px`
|
||||
overlay.style.height = `${rect.height}px`
|
||||
overlay.style.pointerEvents = 'none'
|
||||
overlay.style.zIndex = '9999'
|
||||
overlay.style.borderRadius = '4px'
|
||||
|
||||
document.body.appendChild(overlay)
|
||||
|
||||
// Update overlay position and visibility on scroll
|
||||
const updatePosition = () => {
|
||||
const newRect = element.getBoundingClientRect()
|
||||
const newContainerRect = container.getBoundingClientRect()
|
||||
|
||||
// Update position
|
||||
overlay.style.left = `${newRect.left}px`
|
||||
overlay.style.top = `${newRect.top}px`
|
||||
overlay.style.width = `${newRect.width}px`
|
||||
overlay.style.height = `${newRect.height}px`
|
||||
|
||||
// Get current toolbar bottom (it might change)
|
||||
const currentToolbar = editorWrapper?.querySelector('[class*="ToolbarWrapper"]')
|
||||
const currentToolbarRect = currentToolbar?.getBoundingClientRect()
|
||||
const currentToolbarBottom = currentToolbarRect ? currentToolbarRect.bottom : newContainerRect.top
|
||||
|
||||
// Check if overlay is within visible bounds
|
||||
const overlayTop = newRect.top
|
||||
const overlayBottom = newRect.bottom
|
||||
const visibleTop = currentToolbarBottom // Don't overlap toolbar
|
||||
const visibleBottom = newContainerRect.bottom
|
||||
|
||||
// Hide overlay if any part is outside the visible container area
|
||||
if (overlayTop < visibleTop || overlayBottom > visibleBottom) {
|
||||
overlay.style.opacity = '0'
|
||||
overlay.style.visibility = 'hidden'
|
||||
} else {
|
||||
overlay.style.opacity = '1'
|
||||
overlay.style.visibility = 'visible'
|
||||
}
|
||||
}
|
||||
|
||||
container.addEventListener('scroll', updatePosition)
|
||||
|
||||
// Auto-remove after animation
|
||||
const handleAnimationEnd = () => {
|
||||
overlay.remove()
|
||||
container.removeEventListener('scroll', updatePosition)
|
||||
overlay.removeEventListener('animationend', handleAnimationEnd)
|
||||
}
|
||||
overlay.addEventListener('animationend', handleAnimationEnd)
|
||||
} catch (error) {
|
||||
logger.error('Failed to create highlight overlay:', error as Error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to element and show highlight after scroll completes
|
||||
*/
|
||||
function scrollAndHighlight(element: HTMLElement, container: HTMLElement): void {
|
||||
element.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'nearest' })
|
||||
|
||||
let scrollTimeout: NodeJS.Timeout
|
||||
const handleScroll = () => {
|
||||
clearTimeout(scrollTimeout)
|
||||
scrollTimeout = setTimeout(() => {
|
||||
container.removeEventListener('scroll', handleScroll)
|
||||
requestAnimationFrame(() => createHighlightOverlay(element, container))
|
||||
}, 150)
|
||||
}
|
||||
|
||||
container.addEventListener('scroll', handleScroll)
|
||||
|
||||
// Fallback: if element already in view (no scroll happens)
|
||||
setTimeout(() => {
|
||||
const initialScrollTop = container.scrollTop
|
||||
setTimeout(() => {
|
||||
if (Math.abs(container.scrollTop - initialScrollTop) < 1) {
|
||||
container.removeEventListener('scroll', handleScroll)
|
||||
clearTimeout(scrollTimeout)
|
||||
requestAnimationFrame(() => createHighlightOverlay(element, container))
|
||||
}
|
||||
}, 200)
|
||||
}, 50)
|
||||
}
|
||||
|
||||
const RichEditor = ({
|
||||
ref,
|
||||
initialContent = '',
|
||||
@@ -372,6 +523,22 @@ const RichEditor = ({
|
||||
scrollContainerRef.current.scrollTop = value
|
||||
}
|
||||
},
|
||||
scrollToLine: (lineNumber: number, options?: { highlight?: boolean; lineContent?: string }) => {
|
||||
if (!editor || !scrollContainerRef.current) return
|
||||
|
||||
try {
|
||||
const element = findElementByLine(editor.view.dom, lineNumber, options?.lineContent)
|
||||
if (!element) return
|
||||
|
||||
if (options?.highlight) {
|
||||
scrollAndHighlight(element, scrollContainerRef.current)
|
||||
} else {
|
||||
element.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'nearest' })
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed in scrollToLine:', error as Error)
|
||||
}
|
||||
},
|
||||
// Dynamic command management
|
||||
registerCommand,
|
||||
registerToolbarCommand,
|
||||
|
||||
@@ -111,6 +111,8 @@ export interface RichEditorRef {
|
||||
getScrollTop: () => number
|
||||
/** Set scrollTop of the editor scroll container */
|
||||
setScrollTop: (value: number) => void
|
||||
/** Scroll to specific line number in markdown */
|
||||
scrollToLine: (lineNumber: number, options?: { highlight?: boolean; lineContent?: string }) => void
|
||||
// Dynamic command management
|
||||
/** Register a new command/toolbar item */
|
||||
registerCommand: (cmd: Command) => void
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'katex/dist/katex.min.css'
|
||||
|
||||
import { TableKit } from '@cherrystudio/extension-table-plus'
|
||||
import { loggerService } from '@logger'
|
||||
import { MARKDOWN_SOURCE_LINE_ATTR } from '@renderer/components/RichEditor/constants'
|
||||
import type { FormattingState } from '@renderer/components/RichEditor/types'
|
||||
import { useCodeStyle } from '@renderer/context/CodeStyleProvider'
|
||||
import {
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
markdownToPreviewText
|
||||
} from '@renderer/utils/markdownConverter'
|
||||
import type { Editor } from '@tiptap/core'
|
||||
import { Extension } from '@tiptap/core'
|
||||
import { TaskItem, TaskList } from '@tiptap/extension-list'
|
||||
import { migrateMathStrings } from '@tiptap/extension-mathematics'
|
||||
import Mention from '@tiptap/extension-mention'
|
||||
@@ -36,6 +38,31 @@ import { blobToArrayBuffer, compressImage, shouldCompressImage } from './helpers
|
||||
|
||||
const logger = loggerService.withContext('useRichEditor')
|
||||
|
||||
// Create extension to preserve data-source-line attribute
|
||||
const SourceLineAttribute = Extension.create({
|
||||
name: 'sourceLineAttribute',
|
||||
addGlobalAttributes() {
|
||||
return [
|
||||
{
|
||||
types: ['paragraph', 'heading', 'blockquote', 'bulletList', 'orderedList', 'listItem', 'horizontalRule'],
|
||||
attributes: {
|
||||
dataSourceLine: {
|
||||
default: null,
|
||||
parseHTML: (element) => {
|
||||
const value = element.getAttribute(MARKDOWN_SOURCE_LINE_ATTR)
|
||||
return value
|
||||
},
|
||||
renderHTML: (attributes) => {
|
||||
if (!attributes.dataSourceLine) return {}
|
||||
return { [MARKDOWN_SOURCE_LINE_ATTR]: attributes.dataSourceLine }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
export interface UseRichEditorOptions {
|
||||
/** Initial markdown content */
|
||||
initialContent?: string
|
||||
@@ -196,6 +223,7 @@ export const useRichEditor = (options: UseRichEditorOptions = {}): UseRichEditor
|
||||
// TipTap editor extensions
|
||||
const extensions = useMemo(
|
||||
() => [
|
||||
SourceLineAttribute,
|
||||
StarterKit.configure({
|
||||
heading: {
|
||||
levels: [1, 2, 3, 4, 5, 6]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { addToast, closeAll, closeToast, getToastQueue, isToastClosing } from '@heroui/toast'
|
||||
import { RequireSome } from '@renderer/types'
|
||||
import { t } from 'i18next'
|
||||
|
||||
type AddToastProps = Parameters<typeof addToast>[0]
|
||||
type ToastPropsColored = Omit<AddToastProps, 'color'>
|
||||
@@ -54,7 +55,7 @@ export const loading = (args: RequireSome<AddToastProps, 'promise'>) => {
|
||||
if (args.timeout === undefined) {
|
||||
args.timeout = 1
|
||||
}
|
||||
return addToast(args)
|
||||
return addToast({ title: t('common.loading'), ...args })
|
||||
}
|
||||
|
||||
export const getToastUtilities = () =>
|
||||
|
||||
@@ -74,7 +74,7 @@ const NavbarContainer = styled.div<{ $isFullScreen: boolean }>`
|
||||
`
|
||||
|
||||
const NavbarLeftContainer = styled.div`
|
||||
min-width: var(--assistants-width);
|
||||
min-width: ${isMac ? 'calc(var(--assistants-width) - 20px)' : 'var(--assistants-width)'};
|
||||
padding: 0 10px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
@@ -14,7 +14,7 @@ import { GEMINI_FLASH_MODEL_REGEX } from './websearch'
|
||||
|
||||
// Reasoning models
|
||||
export const REASONING_REGEX =
|
||||
/^(o\d+(?:-[\w-]+)?|.*\b(?:reasoning|reasoner|thinking)\b.*|.*-[rR]\d+.*|.*\bqwq(?:-[\w-]+)?\b.*|.*\bhunyuan-t1(?:-[\w-]+)?\b.*|.*\bglm-zero-preview\b.*|.*\bgrok-(?:3-mini|4)(?:-[\w-]+)?\b.*)$/i
|
||||
/^(?!.*-non-reasoning\b)(o\d+(?:-[\w-]+)?|.*\b(?:reasoning|reasoner|thinking)\b.*|.*-[rR]\d+.*|.*\bqwq(?:-[\w-]+)?\b.*|.*\bhunyuan-t1(?:-[\w-]+)?\b.*|.*\bglm-zero-preview\b.*|.*\bgrok-(?:3-mini|4|4-fast)(?:-[\w-]+)?\b.*)$/i
|
||||
|
||||
// 模型类型到支持的reasoning_effort的映射表
|
||||
// TODO: refactor this. too many identical options
|
||||
@@ -24,6 +24,7 @@ export const MODEL_SUPPORTED_REASONING_EFFORT: ReasoningEffortConfig = {
|
||||
gpt5: ['minimal', 'low', 'medium', 'high'] as const,
|
||||
gpt5_codex: ['low', 'medium', 'high'] as const,
|
||||
grok: ['low', 'high'] as const,
|
||||
grok4_fast: ['auto'] as const,
|
||||
gemini: ['low', 'medium', 'high', 'auto'] as const,
|
||||
gemini_pro: ['low', 'medium', 'high', 'auto'] as const,
|
||||
qwen: ['low', 'medium', 'high'] as const,
|
||||
@@ -43,6 +44,7 @@ export const MODEL_SUPPORTED_OPTIONS: ThinkingOptionConfig = {
|
||||
gpt5: [...MODEL_SUPPORTED_REASONING_EFFORT.gpt5] as const,
|
||||
gpt5_codex: MODEL_SUPPORTED_REASONING_EFFORT.gpt5_codex,
|
||||
grok: MODEL_SUPPORTED_REASONING_EFFORT.grok,
|
||||
grok4_fast: ['off', ...MODEL_SUPPORTED_REASONING_EFFORT.grok4_fast] as const,
|
||||
gemini: ['off', ...MODEL_SUPPORTED_REASONING_EFFORT.gemini] as const,
|
||||
gemini_pro: MODEL_SUPPORTED_REASONING_EFFORT.gemini_pro,
|
||||
qwen: ['off', ...MODEL_SUPPORTED_REASONING_EFFORT.qwen] as const,
|
||||
@@ -66,6 +68,8 @@ export const getThinkModelType = (model: Model): ThinkingModelType => {
|
||||
}
|
||||
} else if (isSupportedReasoningEffortOpenAIModel(model)) {
|
||||
thinkingModelType = 'o'
|
||||
} else if (isGrok4FastReasoningModel(model)) {
|
||||
thinkingModelType = 'grok4_fast'
|
||||
} else if (isSupportedThinkingTokenGeminiModel(model)) {
|
||||
if (GEMINI_FLASH_MODEL_REGEX.test(model.id)) {
|
||||
thinkingModelType = 'gemini'
|
||||
@@ -142,19 +146,46 @@ export function isSupportedReasoningEffortGrokModel(model?: Model): boolean {
|
||||
}
|
||||
|
||||
const modelId = getLowerBaseModelName(model.id)
|
||||
const providerId = model.provider.toLowerCase()
|
||||
if (modelId.includes('grok-3-mini')) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (providerId === 'openrouter' && modelId.includes('grok-4-fast')) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the model is Grok 4 Fast reasoning version
|
||||
* Explicitly excludes non-reasoning variants (models with 'non-reasoning' in their ID)
|
||||
*
|
||||
* Note: XAI official uses different model IDs for reasoning vs non-reasoning
|
||||
* Third-party providers like OpenRouter expose a single ID with reasoning parameters, while first-party providers require separate IDs. Only the OpenRouter variant supports toggling.
|
||||
*
|
||||
* @param model - The model to check
|
||||
* @returns true if the model is a reasoning-enabled Grok 4 Fast model
|
||||
*/
|
||||
export function isGrok4FastReasoningModel(model?: Model): boolean {
|
||||
if (!model) {
|
||||
return false
|
||||
}
|
||||
|
||||
const modelId = getLowerBaseModelName(model.id)
|
||||
return modelId.includes('grok-4-fast') && !modelId.includes('non-reasoning')
|
||||
}
|
||||
|
||||
export function isGrokReasoningModel(model?: Model): boolean {
|
||||
if (!model) {
|
||||
return false
|
||||
}
|
||||
const modelId = getLowerBaseModelName(model.id)
|
||||
if (isSupportedReasoningEffortGrokModel(model) || modelId.includes('grok-4')) {
|
||||
if (
|
||||
isSupportedReasoningEffortGrokModel(model) ||
|
||||
(modelId.includes('grok-4') && !modelId.includes('non-reasoning'))
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -265,7 +296,11 @@ export function isQwenAlwaysThinkModel(model?: Model): boolean {
|
||||
return false
|
||||
}
|
||||
const modelId = getLowerBaseModelName(model.id, '/')
|
||||
return modelId.startsWith('qwen3') && modelId.includes('thinking')
|
||||
// 包括 qwen3 开头的 thinking 模型和 qwen3-vl 的 thinking 模型
|
||||
return (
|
||||
(modelId.startsWith('qwen3') && modelId.includes('thinking')) ||
|
||||
(modelId.includes('qwen3-vl') && modelId.includes('thinking'))
|
||||
)
|
||||
}
|
||||
|
||||
// Doubao 支持思考模式的模型正则
|
||||
@@ -329,7 +364,10 @@ export const isPerplexityReasoningModel = (model?: Model): boolean => {
|
||||
}
|
||||
|
||||
const modelId = getLowerBaseModelName(model.id, '/')
|
||||
return isSupportedReasoningEffortPerplexityModel(model) || modelId.includes('reasoning')
|
||||
return (
|
||||
isSupportedReasoningEffortPerplexityModel(model) ||
|
||||
(modelId.includes('reasoning') && !modelId.includes('non-reasoning'))
|
||||
)
|
||||
}
|
||||
|
||||
export const isSupportedReasoningEffortPerplexityModel = (model: Model): boolean => {
|
||||
@@ -443,6 +481,8 @@ export const THINKING_TOKEN_MAP: Record<string, { min: number; max: number }> =
|
||||
// qwen-plus-x 系列自 qwen-plus-2025-07-28 后模型最长思维链变为 81_920, qwen-plus 模型于 2025.9.16 同步变更
|
||||
'qwen3-235b-a22b-thinking-2507$': { min: 0, max: 81_920 },
|
||||
'qwen3-30b-a3b-thinking-2507$': { min: 0, max: 81_920 },
|
||||
'qwen3-vl-235b-a22b-thinking$': { min: 0, max: 81_920 },
|
||||
'qwen3-vl-30b-a3b-thinking$': { min: 0, max: 81_920 },
|
||||
'qwen-plus-2025-07-14$': { min: 0, max: 38_912 },
|
||||
'qwen-plus-2025-04-28$': { min: 0, max: 38_912 },
|
||||
'qwen3-1\\.7b$': { min: 0, max: 30_720 },
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import OpenAI from '@cherrystudio/openai'
|
||||
import { isEmbeddingModel, isRerankModel } from '@renderer/config/models/embedding'
|
||||
import { Model } from '@renderer/types'
|
||||
import { getLowerBaseModelName } from '@renderer/utils'
|
||||
|
||||
import { WEB_SEARCH_PROMPT_FOR_OPENROUTER } from '../prompts'
|
||||
import { getWebSearchTools } from '../tools'
|
||||
import { isOpenAIReasoningModel } from './reasoning'
|
||||
import { isGenerateImageModel, isVisionModel } from './vision'
|
||||
import { isGenerateImageModel, isTextToImageModel, isVisionModel } from './vision'
|
||||
import { isOpenAIWebSearchChatCompletionOnlyModel } from './websearch'
|
||||
export const NOT_SUPPORTED_REGEX = /(?:^tts|whisper|speech)/i
|
||||
|
||||
@@ -246,3 +247,7 @@ export const isOpenAIOpenWeightModel = (model: Model) => {
|
||||
|
||||
// zhipu 视觉推理模型用这组 special token 标记推理结果
|
||||
export const ZHIPU_RESULT_TOKENS = ['<|begin_of_box|>', '<|end_of_box|>'] as const
|
||||
|
||||
export const agentModelFilter = (model: Model): boolean => {
|
||||
return !isEmbeddingModel(model) && !isRerankModel(model) && !isTextToImageModel(model)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ export const mockVideos: Video[] = [
|
||||
prompt: 'A beautiful sunset over the ocean with waves crashing',
|
||||
thumbnail: 'https://picsum.photos/200/200?random=1',
|
||||
fileId: 'file-001',
|
||||
providerId: 'openai',
|
||||
name: 'video-001',
|
||||
metadata: {
|
||||
id: 'video-001',
|
||||
object: 'video',
|
||||
@@ -35,6 +37,8 @@ export const mockVideos: Video[] = [
|
||||
status: 'in_progress',
|
||||
prompt: 'A cat playing with a ball of yarn in slow motion',
|
||||
progress: 65,
|
||||
providerId: 'openai',
|
||||
name: 'video-002',
|
||||
metadata: {
|
||||
id: 'video-002',
|
||||
object: 'video',
|
||||
@@ -55,6 +59,8 @@ export const mockVideos: Video[] = [
|
||||
type: 'openai',
|
||||
status: 'queued',
|
||||
prompt: 'Time-lapse of flowers blooming in a garden',
|
||||
providerId: 'openai',
|
||||
name: 'video-003',
|
||||
metadata: {
|
||||
id: 'video-003',
|
||||
object: 'video',
|
||||
@@ -77,6 +83,8 @@ export const mockVideos: Video[] = [
|
||||
status: 'downloading',
|
||||
progress: 80,
|
||||
thumbnail: 'https://picsum.photos/200/200?random=4',
|
||||
providerId: 'openai',
|
||||
name: 'video-004',
|
||||
metadata: {
|
||||
id: 'video-004',
|
||||
object: 'video',
|
||||
@@ -98,6 +106,8 @@ export const mockVideos: Video[] = [
|
||||
status: 'failed',
|
||||
error: { code: '400', message: 'Video generation failed' },
|
||||
prompt: 'Mountain landscape with snow peaks and forest',
|
||||
providerId: 'openai',
|
||||
name: 'video-005',
|
||||
metadata: {
|
||||
id: 'video-005',
|
||||
object: 'video',
|
||||
@@ -119,6 +129,8 @@ export const mockVideos: Video[] = [
|
||||
status: 'completed',
|
||||
thumbnail: 'https://picsum.photos/200/200?random=6',
|
||||
prompt: 'City street at night with neon lights reflecting on wet pavement',
|
||||
providerId: 'openai',
|
||||
name: 'video-006',
|
||||
metadata: {
|
||||
id: 'video-006',
|
||||
object: 'video',
|
||||
|
||||
@@ -24,7 +24,7 @@ const visionAllowedModels = [
|
||||
'qwen2.5-vl',
|
||||
'qwen3-vl',
|
||||
'qwen2.5-omni',
|
||||
'qwen3-omni',
|
||||
'qwen3-omni(?:-[\\w-]+)?',
|
||||
'qvq',
|
||||
'internvl2',
|
||||
'grok-vision-beta',
|
||||
@@ -82,14 +82,14 @@ export const IMAGE_ENHANCEMENT_MODELS = [
|
||||
'grok-2-image(?:-[\\w-]+)?',
|
||||
'qwen-image-edit',
|
||||
'gpt-image-1',
|
||||
'gemini-2.5-flash-image-preview',
|
||||
'gemini-2.5-flash-image',
|
||||
'gemini-2.0-flash-preview-image-generation'
|
||||
]
|
||||
|
||||
const IMAGE_ENHANCEMENT_MODELS_REGEX = new RegExp(IMAGE_ENHANCEMENT_MODELS.join('|'), 'i')
|
||||
|
||||
// Models that should auto-enable image generation button when selected
|
||||
export const AUTO_ENABLE_IMAGE_MODELS = ['gemini-2.5-flash-image-preview', ...DEDICATED_IMAGE_MODELS]
|
||||
export const AUTO_ENABLE_IMAGE_MODELS = ['gemini-2.5-flash-image', ...DEDICATED_IMAGE_MODELS]
|
||||
|
||||
export const OPENAI_TOOL_USE_IMAGE_GENERATION_MODELS = [
|
||||
'o3',
|
||||
@@ -107,7 +107,7 @@ export const GENERATE_IMAGE_MODELS = [
|
||||
'gemini-2.0-flash-exp',
|
||||
'gemini-2.0-flash-exp-image-generation',
|
||||
'gemini-2.0-flash-preview-image-generation',
|
||||
'gemini-2.5-flash-image-preview',
|
||||
'gemini-2.5-flash-image',
|
||||
...DEDICATED_IMAGE_MODELS
|
||||
]
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
BuiltinOcrProvider,
|
||||
BuiltinOcrProviderId,
|
||||
OcrOvProvider,
|
||||
OcrPpocrProvider,
|
||||
OcrProviderCapability,
|
||||
OcrSystemProvider,
|
||||
@@ -50,10 +51,23 @@ const ppocrOcr: OcrPpocrProvider = {
|
||||
}
|
||||
} as const
|
||||
|
||||
const ovOcr: OcrOvProvider = {
|
||||
id: 'ovocr',
|
||||
name: 'Intel OV(NPU) OCR',
|
||||
config: {
|
||||
langs: isWin ? ['en-us', 'zh-cn'] : undefined
|
||||
},
|
||||
capabilities: {
|
||||
image: true
|
||||
// pdf: true
|
||||
}
|
||||
} as const satisfies OcrOvProvider
|
||||
|
||||
export const BUILTIN_OCR_PROVIDERS_MAP = {
|
||||
tesseract,
|
||||
system: systemOcr,
|
||||
paddleocr: ppocrOcr
|
||||
paddleocr: ppocrOcr,
|
||||
ovocr: ovOcr
|
||||
} as const satisfies Record<BuiltinOcrProviderId, BuiltinOcrProvider>
|
||||
|
||||
export const BUILTIN_OCR_PROVIDERS: BuiltinOcrProvider[] = Object.values(BUILTIN_OCR_PROVIDERS_MAP)
|
||||
|
||||
@@ -58,7 +58,6 @@ import ZhipuProviderLogo from '@renderer/assets/images/providers/zhipu.png'
|
||||
import {
|
||||
AtLeast,
|
||||
isSystemProvider,
|
||||
Model,
|
||||
OpenAIServiceTiers,
|
||||
Provider,
|
||||
ProviderType,
|
||||
@@ -88,6 +87,7 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
|
||||
type: 'openai',
|
||||
apiKey: '',
|
||||
apiHost: 'https://open.cherryin.net',
|
||||
anthropicApiHost: 'https://open.cherryin.net',
|
||||
models: [],
|
||||
isSystem: true,
|
||||
enabled: true
|
||||
@@ -109,7 +109,6 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
|
||||
apiKey: '',
|
||||
apiHost: 'https://aihubmix.com',
|
||||
anthropicApiHost: 'https://aihubmix.com/anthropic',
|
||||
isAnthropicModel: (m: Model) => m.id.includes('claude'),
|
||||
models: SYSTEM_MODELS.aihubmix,
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
@@ -289,7 +288,7 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
|
||||
'new-api': {
|
||||
id: 'new-api',
|
||||
name: 'New API',
|
||||
type: 'openai',
|
||||
type: 'new-api',
|
||||
apiKey: '',
|
||||
apiHost: 'http://localhost:3000',
|
||||
anthropicApiHost: 'http://localhost:3000',
|
||||
@@ -1432,5 +1431,5 @@ export const isGeminiWebSearchProvider = (provider: Provider) => {
|
||||
}
|
||||
|
||||
export const isNewApiProvider = (provider: Provider) => {
|
||||
return ['new-api', 'cherryin'].includes(provider.id)
|
||||
return ['new-api', 'cherryin'].includes(provider.id) || provider.type === 'new-api'
|
||||
}
|
||||
|
||||
4
src/renderer/src/hooks/agents/types.ts
Normal file
4
src/renderer/src/hooks/agents/types.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export type UpdateAgentBaseOptions = {
|
||||
/** Whether to show success toast after updating. Defaults to true. */
|
||||
showSuccessToast?: boolean
|
||||
}
|
||||
8
src/renderer/src/hooks/agents/useActiveAgent.ts
Normal file
8
src/renderer/src/hooks/agents/useActiveAgent.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { useRuntime } from '../useRuntime'
|
||||
import { useAgent } from './useAgent'
|
||||
|
||||
export const useActiveAgent = () => {
|
||||
const { chat } = useRuntime()
|
||||
const { activeAgentId } = chat
|
||||
return useAgent(activeAgentId)
|
||||
}
|
||||
9
src/renderer/src/hooks/agents/useActiveSession.ts
Normal file
9
src/renderer/src/hooks/agents/useActiveSession.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { useRuntime } from '../useRuntime'
|
||||
import { useSession } from './useSession'
|
||||
|
||||
export const useActiveSession = () => {
|
||||
const { chat } = useRuntime()
|
||||
const { activeSessionIdMap, activeAgentId } = chat
|
||||
const activeSessionId = activeAgentId ? activeSessionIdMap[activeAgentId] : null
|
||||
return useSession(activeAgentId, activeSessionId)
|
||||
}
|
||||
@@ -1,18 +1,28 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useSWR from 'swr'
|
||||
|
||||
import { useApiServer } from '../useApiServer'
|
||||
import { useAgentClient } from './useAgentClient'
|
||||
|
||||
export const useAgent = (id: string | null) => {
|
||||
const { t } = useTranslation()
|
||||
const client = useAgentClient()
|
||||
const key = id ? client.agentPaths.withId(id) : null
|
||||
const { apiServerConfig, apiServerRunning } = useApiServer()
|
||||
const fetcher = useCallback(async () => {
|
||||
if (!id || id === 'fake') {
|
||||
return null
|
||||
if (!id) {
|
||||
throw new Error(t('agent.get.error.null_id'))
|
||||
}
|
||||
if (!apiServerConfig.enabled) {
|
||||
throw new Error(t('apiServer.messages.notEnabled'))
|
||||
}
|
||||
if (!apiServerRunning) {
|
||||
throw new Error(t('agent.server.error.not_running'))
|
||||
}
|
||||
const result = await client.getAgent(id)
|
||||
return result
|
||||
}, [client, id])
|
||||
}, [apiServerConfig.enabled, apiServerRunning, client, id, t])
|
||||
const { data, error, isLoading } = useSWR(key, id ? fetcher : null)
|
||||
|
||||
return {
|
||||
|
||||
@@ -17,7 +17,7 @@ export const useAgentSessionInitializer = () => {
|
||||
const dispatch = useAppDispatch()
|
||||
const client = useAgentClient()
|
||||
const { chat } = useRuntime()
|
||||
const { activeAgentId, activeSessionId } = chat
|
||||
const { activeAgentId, activeSessionIdMap } = chat
|
||||
|
||||
/**
|
||||
* Initialize session for the given agent by loading its sessions
|
||||
@@ -25,11 +25,11 @@ export const useAgentSessionInitializer = () => {
|
||||
*/
|
||||
const initializeAgentSession = useCallback(
|
||||
async (agentId: string) => {
|
||||
if (!agentId || agentId === 'fake') return
|
||||
if (!agentId) return
|
||||
|
||||
try {
|
||||
// Check if this agent already has an active session
|
||||
const currentSessionId = activeSessionId[agentId]
|
||||
const currentSessionId = activeSessionIdMap[agentId]
|
||||
if (currentSessionId) {
|
||||
// Session already exists, just switch to session view
|
||||
dispatch(setActiveTopicOrSessionAction('session'))
|
||||
@@ -58,21 +58,21 @@ export const useAgentSessionInitializer = () => {
|
||||
dispatch(setActiveTopicOrSessionAction('session'))
|
||||
}
|
||||
},
|
||||
[client, dispatch, activeSessionId]
|
||||
[client, dispatch, activeSessionIdMap]
|
||||
)
|
||||
|
||||
/**
|
||||
* Auto-initialize when activeAgentId changes
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (activeAgentId && activeAgentId !== 'fake') {
|
||||
if (activeAgentId) {
|
||||
// Check if we need to initialize this agent's session
|
||||
const hasActiveSession = activeSessionId[activeAgentId]
|
||||
const hasActiveSession = activeSessionIdMap[activeAgentId]
|
||||
if (!hasActiveSession) {
|
||||
initializeAgentSession(activeAgentId)
|
||||
}
|
||||
}
|
||||
}, [activeAgentId, activeSessionId, initializeAgentSession])
|
||||
}, [activeAgentId, activeSessionIdMap, initializeAgentSession])
|
||||
|
||||
return {
|
||||
initializeAgentSession
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useSWR from 'swr'
|
||||
|
||||
import { useApiServer } from '../useApiServer'
|
||||
import { useRuntime } from '../useRuntime'
|
||||
import { useAgentClient } from './useAgentClient'
|
||||
|
||||
@@ -23,11 +24,19 @@ export const useAgents = () => {
|
||||
const { t } = useTranslation()
|
||||
const client = useAgentClient()
|
||||
const key = client.agentPaths.base
|
||||
const { apiServerConfig, apiServerRunning } = useApiServer()
|
||||
const fetcher = useCallback(async () => {
|
||||
// API server will start on startup if enabled OR there are agents
|
||||
if (!apiServerConfig.enabled && !apiServerRunning) {
|
||||
throw new Error(t('apiServer.messages.notEnabled'))
|
||||
}
|
||||
if (!apiServerRunning) {
|
||||
throw new Error(t('agent.server.error.not_running'))
|
||||
}
|
||||
const result = await client.listAgents()
|
||||
// NOTE: We only use the array for now. useUpdateAgent depends on this behavior.
|
||||
return result.data
|
||||
}, [client])
|
||||
}, [apiServerConfig.enabled, apiServerRunning, client, t])
|
||||
const { data, error, isLoading, mutate } = useSWR(key, fetcher)
|
||||
const { chat } = useRuntime()
|
||||
const { activeAgentId } = chat
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk'
|
||||
import { UpdateSessionForm } from '@renderer/types'
|
||||
import { buildAgentSessionTopicId } from '@renderer/utils/agentSession'
|
||||
import { useCallback, useEffect, useMemo } from 'react'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useSWR from 'swr'
|
||||
|
||||
import { useAgentClient } from './useAgentClient'
|
||||
import { useUpdateSession } from './useUpdateSession'
|
||||
|
||||
export const useSession = (agentId: string, sessionId: string) => {
|
||||
export const useSession = (agentId: string | null, sessionId: string | null) => {
|
||||
const { t } = useTranslation()
|
||||
const client = useAgentClient()
|
||||
const key = client.getSessionPaths(agentId).withId(sessionId)
|
||||
const key = agentId && sessionId ? client.getSessionPaths(agentId).withId(sessionId) : null
|
||||
const dispatch = useAppDispatch()
|
||||
const sessionTopicId = useMemo(() => buildAgentSessionTopicId(sessionId), [sessionId])
|
||||
const sessionTopicId = useMemo(() => (sessionId ? buildAgentSessionTopicId(sessionId) : null), [sessionId])
|
||||
const { updateSession } = useUpdateSession(agentId)
|
||||
|
||||
const fetcher = async () => {
|
||||
if (!agentId) throw new Error(t('agent.get.error.null_id'))
|
||||
if (!sessionId) throw new Error(t('agent.session.get.error.null_id'))
|
||||
const data = await client.getSession(agentId, sessionId)
|
||||
return data
|
||||
}
|
||||
@@ -24,26 +27,13 @@ export const useSession = (agentId: string, sessionId: string) => {
|
||||
// Use loadTopicMessagesThunk to load messages (with caching mechanism)
|
||||
// This ensures messages are preserved when switching between sessions/tabs
|
||||
useEffect(() => {
|
||||
if (sessionId) {
|
||||
if (sessionTopicId) {
|
||||
// loadTopicMessagesThunk will check if messages already exist in Redux
|
||||
// and skip loading if they do (unless forceReload is true)
|
||||
dispatch(loadTopicMessagesThunk(sessionTopicId))
|
||||
}
|
||||
}, [dispatch, sessionId, sessionTopicId])
|
||||
|
||||
const updateSession = useCallback(
|
||||
async (form: UpdateSessionForm) => {
|
||||
if (!agentId) return
|
||||
try {
|
||||
const result = await client.updateSession(agentId, form)
|
||||
mutate(result)
|
||||
} catch (error) {
|
||||
window.toast.error(t('agent.session.update.error.failed'))
|
||||
}
|
||||
},
|
||||
[agentId, client, mutate, t]
|
||||
)
|
||||
|
||||
return {
|
||||
session: data,
|
||||
error,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CreateSessionForm } from '@renderer/types'
|
||||
import { CreateAgentSessionResponse, CreateSessionForm, GetAgentSessionResponse } from '@renderer/types'
|
||||
import { formatErrorMessageWithPrefix } from '@renderer/utils/error'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -6,46 +6,50 @@ import useSWR from 'swr'
|
||||
|
||||
import { useAgentClient } from './useAgentClient'
|
||||
|
||||
export const useSessions = (agentId: string) => {
|
||||
export const useSessions = (agentId: string | null) => {
|
||||
const { t } = useTranslation()
|
||||
const client = useAgentClient()
|
||||
const key = client.getSessionPaths(agentId).base
|
||||
const key = agentId ? client.getSessionPaths(agentId).base : null
|
||||
|
||||
const fetcher = async () => {
|
||||
if (!agentId) throw new Error('No active agent.')
|
||||
const data = await client.listSessions(agentId)
|
||||
return data.data
|
||||
}
|
||||
const { data, error, isLoading, mutate } = useSWR(key, fetcher)
|
||||
|
||||
const createSession = useCallback(
|
||||
async (form: CreateSessionForm) => {
|
||||
async (form: CreateSessionForm): Promise<CreateAgentSessionResponse | null> => {
|
||||
if (!agentId) return null
|
||||
try {
|
||||
const result = await client.createSession(agentId, form)
|
||||
await mutate((prev) => [...(prev ?? []), result], { revalidate: false })
|
||||
await mutate((prev) => [result, ...(prev ?? [])], { revalidate: false })
|
||||
return result
|
||||
} catch (error) {
|
||||
window.toast.error(formatErrorMessageWithPrefix(error, t('agent.session.create.error.failed')))
|
||||
return undefined
|
||||
return null
|
||||
}
|
||||
},
|
||||
[agentId, client, mutate, t]
|
||||
)
|
||||
|
||||
// TODO: including messages field
|
||||
const getSession = useCallback(
|
||||
async (id: string) => {
|
||||
async (id: string): Promise<GetAgentSessionResponse | null> => {
|
||||
if (!agentId) return null
|
||||
try {
|
||||
const result = await client.getSession(agentId, id)
|
||||
mutate((prev) => prev?.map((session) => (session.id === result.id ? result : session)))
|
||||
return result
|
||||
} catch (error) {
|
||||
window.toast.error(formatErrorMessageWithPrefix(error, t('agent.session.get.error.failed')))
|
||||
return null
|
||||
}
|
||||
},
|
||||
[agentId, client, mutate, t]
|
||||
)
|
||||
|
||||
const deleteSession = useCallback(
|
||||
async (id: string) => {
|
||||
async (id: string): Promise<boolean> => {
|
||||
if (!agentId) return false
|
||||
try {
|
||||
await client.deleteSession(agentId, id)
|
||||
|
||||
@@ -4,20 +4,16 @@ import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { mutate } from 'swr'
|
||||
|
||||
import { UpdateAgentBaseOptions } from './types'
|
||||
import { useAgentClient } from './useAgentClient'
|
||||
|
||||
export type UpdateAgentOptions = {
|
||||
/** Whether to show success toast after updating. Defaults to true. */
|
||||
showSuccessToast?: boolean
|
||||
}
|
||||
|
||||
export const useUpdateAgent = () => {
|
||||
const { t } = useTranslation()
|
||||
const client = useAgentClient()
|
||||
const listKey = client.agentPaths.base
|
||||
|
||||
const updateAgent = useCallback(
|
||||
async (form: UpdateAgentForm, options?: UpdateAgentOptions) => {
|
||||
async (form: UpdateAgentForm, options?: UpdateAgentBaseOptions) => {
|
||||
try {
|
||||
const itemKey = client.agentPaths.withId(form.id)
|
||||
// may change to optimistic update
|
||||
@@ -35,7 +31,7 @@ export const useUpdateAgent = () => {
|
||||
)
|
||||
|
||||
const updateModel = useCallback(
|
||||
async (agentId: string, modelId: string, options?: UpdateAgentOptions) => {
|
||||
async (agentId: string, modelId: string, options?: UpdateAgentBaseOptions) => {
|
||||
updateAgent({ id: agentId, model: modelId }, options)
|
||||
},
|
||||
[updateAgent]
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import { ListAgentSessionsResponse, UpdateSessionForm } from '@renderer/types'
|
||||
import { formatErrorMessageWithPrefix } from '@renderer/utils/error'
|
||||
import { getErrorMessage } from '@renderer/utils/error'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { mutate } from 'swr'
|
||||
|
||||
import { UpdateAgentBaseOptions } from './types'
|
||||
import { useAgentClient } from './useAgentClient'
|
||||
|
||||
export const useUpdateSession = (agentId: string) => {
|
||||
export const useUpdateSession = (agentId: string | null) => {
|
||||
const { t } = useTranslation()
|
||||
const client = useAgentClient()
|
||||
const paths = client.getSessionPaths(agentId)
|
||||
const listKey = paths.base
|
||||
|
||||
const updateSession = useCallback(
|
||||
async (form: UpdateSessionForm) => {
|
||||
async (form: UpdateSessionForm, options?: UpdateAgentBaseOptions) => {
|
||||
if (!agentId) return
|
||||
const paths = client.getSessionPaths(agentId)
|
||||
const listKey = paths.base
|
||||
const sessionId = form.id
|
||||
try {
|
||||
const itemKey = paths.withId(sessionId)
|
||||
@@ -24,13 +26,29 @@ export const useUpdateSession = (agentId: string) => {
|
||||
(prev) => prev?.map((session) => (session.id === result.id ? result : session)) ?? []
|
||||
)
|
||||
mutate(itemKey, result)
|
||||
window.toast.success(t('common.update_success'))
|
||||
if (options?.showSuccessToast ?? true) {
|
||||
window.toast.success(t('common.update_success'))
|
||||
}
|
||||
} catch (error) {
|
||||
window.toast.error(formatErrorMessageWithPrefix(error, t('agent.update.error.failed')))
|
||||
window.toast.error({ title: t('agent.session.update.error.failed'), description: getErrorMessage(error) })
|
||||
}
|
||||
},
|
||||
[agentId, client, listKey, paths, t]
|
||||
[agentId, client, t]
|
||||
)
|
||||
|
||||
return updateSession
|
||||
const updateModel = useCallback(
|
||||
async (sessionId: string, modelId: string, options?: UpdateAgentBaseOptions) => {
|
||||
if (!agentId) return
|
||||
return updateSession(
|
||||
{
|
||||
id: sessionId,
|
||||
model: modelId
|
||||
},
|
||||
options
|
||||
)
|
||||
},
|
||||
[agentId, updateSession]
|
||||
)
|
||||
|
||||
return { updateSession, updateModel }
|
||||
}
|
||||
|
||||
112
src/renderer/src/hooks/useApiServer.ts
Normal file
112
src/renderer/src/hooks/useApiServer.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { setApiServerEnabled as setApiServerEnabledAction } from '@renderer/store/settings'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const logger = loggerService.withContext('useApiServer')
|
||||
|
||||
export const useApiServer = () => {
|
||||
const { t } = useTranslation()
|
||||
// FIXME: We currently store two copies of the config data in both the renderer and the main processes,
|
||||
// which carries the risk of data inconsistency. This should be modified so that the main process stores
|
||||
// the data, and the renderer retrieves it.
|
||||
const apiServerConfig = useAppSelector((state) => state.settings.apiServer)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
// Optimistic initial state.
|
||||
const [apiServerRunning, setApiServerRunning] = useState(apiServerConfig.enabled)
|
||||
const [apiServerLoading, setApiServerLoading] = useState(true)
|
||||
|
||||
const setApiServerEnabled = useCallback(
|
||||
(enabled: boolean) => {
|
||||
dispatch(setApiServerEnabledAction(enabled))
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
// API Server functions
|
||||
const checkApiServerStatus = useCallback(async () => {
|
||||
setApiServerLoading(true)
|
||||
try {
|
||||
const status = await window.api.apiServer.getStatus()
|
||||
setApiServerRunning(status.running)
|
||||
} catch (error: any) {
|
||||
logger.error('Failed to check API server status:', error)
|
||||
} finally {
|
||||
setApiServerLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const startApiServer = useCallback(async () => {
|
||||
if (apiServerLoading) return
|
||||
|
||||
setApiServerLoading(true)
|
||||
try {
|
||||
const result = await window.api.apiServer.start()
|
||||
if (result.success) {
|
||||
setApiServerRunning(true)
|
||||
window.toast.success(t('apiServer.messages.startSuccess'))
|
||||
} else {
|
||||
window.toast.error(t('apiServer.messages.startError') + result.error)
|
||||
}
|
||||
} catch (error: any) {
|
||||
window.toast.error(t('apiServer.messages.startError') + (error.message || error))
|
||||
} finally {
|
||||
setApiServerLoading(false)
|
||||
}
|
||||
}, [apiServerLoading, t])
|
||||
|
||||
const stopApiServer = useCallback(async () => {
|
||||
if (apiServerLoading) return
|
||||
|
||||
setApiServerLoading(true)
|
||||
try {
|
||||
const result = await window.api.apiServer.stop()
|
||||
if (result.success) {
|
||||
setApiServerRunning(false)
|
||||
window.toast.success(t('apiServer.messages.stopSuccess'))
|
||||
} else {
|
||||
window.toast.error(t('apiServer.messages.stopError') + result.error)
|
||||
}
|
||||
} catch (error: any) {
|
||||
window.toast.error(t('apiServer.messages.stopError') + (error.message || error))
|
||||
} finally {
|
||||
setApiServerLoading(false)
|
||||
}
|
||||
}, [apiServerLoading, t])
|
||||
|
||||
const restartApiServer = useCallback(async () => {
|
||||
if (apiServerLoading) return
|
||||
|
||||
setApiServerLoading(true)
|
||||
try {
|
||||
const result = await window.api.apiServer.restart()
|
||||
if (result.success) {
|
||||
await checkApiServerStatus()
|
||||
window.toast.success(t('apiServer.messages.restartSuccess'))
|
||||
} else {
|
||||
window.toast.error(t('apiServer.messages.restartError') + result.error)
|
||||
}
|
||||
} catch (error) {
|
||||
window.toast.error(t('apiServer.messages.restartFailed') + (error as Error).message)
|
||||
} finally {
|
||||
setApiServerLoading(false)
|
||||
}
|
||||
}, [apiServerLoading, checkApiServerStatus, t])
|
||||
|
||||
useEffect(() => {
|
||||
checkApiServerStatus()
|
||||
}, [checkApiServerStatus])
|
||||
|
||||
return {
|
||||
apiServerConfig,
|
||||
apiServerRunning,
|
||||
apiServerLoading,
|
||||
startApiServer,
|
||||
stopApiServer,
|
||||
restartApiServer,
|
||||
checkApiServerStatus,
|
||||
setApiServerEnabled
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { loggerService } from '@logger'
|
||||
import IntelLogo from '@renderer/assets/images/providers/intel.png'
|
||||
import PaddleocrLogo from '@renderer/assets/images/providers/paddleocr.png'
|
||||
import TesseractLogo from '@renderer/assets/images/providers/Tesseract.js.png'
|
||||
import { BUILTIN_OCR_PROVIDERS_MAP, DEFAULT_OCR_PROVIDER } from '@renderer/config/ocr'
|
||||
@@ -83,6 +84,8 @@ export const useOcrProviders = () => {
|
||||
return <MonitorIcon size={size} />
|
||||
case 'paddleocr':
|
||||
return <Avatar size={size} src={PaddleocrLogo} />
|
||||
case 'ovocr':
|
||||
return <Avatar size={size} src={IntelLogo} />
|
||||
}
|
||||
}
|
||||
return <FileQuestionMarkIcon size={size} />
|
||||
|
||||
17
src/renderer/src/hooks/usePending.ts
Normal file
17
src/renderer/src/hooks/usePending.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setPendingAction } from '@renderer/store/runtime'
|
||||
import { useCallback } from 'react'
|
||||
|
||||
import { useRuntime } from './useRuntime'
|
||||
|
||||
export const usePending = () => {
|
||||
const { pendingMap } = useRuntime()
|
||||
const dispatch = useAppDispatch()
|
||||
const setPending = useCallback(
|
||||
(id: string, value: boolean | undefined) => {
|
||||
dispatch(setPendingAction({ id, value }))
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
return { pendingMap, setPending }
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import OpenAI from '@cherrystudio/openai'
|
||||
import { useCallback } from 'react'
|
||||
|
||||
import { useVideos } from './useVideos'
|
||||
import { useProviderVideos } from './useProviderVideos'
|
||||
|
||||
export const useAddOpenAIVideo = (providerId: string) => {
|
||||
const { addVideo } = useVideos(providerId)
|
||||
const { addVideo } = useProviderVideos(providerId)
|
||||
|
||||
const addOpenAIVideo = useCallback(
|
||||
(video: OpenAI.Videos.Video, prompt: string) => {
|
||||
@@ -12,6 +12,8 @@ export const useAddOpenAIVideo = (providerId: string) => {
|
||||
case 'queued':
|
||||
addVideo({
|
||||
id: video.id,
|
||||
name: video.id,
|
||||
providerId,
|
||||
status: video.status,
|
||||
type: 'openai',
|
||||
metadata: video,
|
||||
@@ -21,6 +23,8 @@ export const useAddOpenAIVideo = (providerId: string) => {
|
||||
case 'in_progress':
|
||||
addVideo({
|
||||
id: video.id,
|
||||
name: video.id,
|
||||
providerId,
|
||||
status: 'in_progress',
|
||||
type: 'openai',
|
||||
progress: video.progress,
|
||||
@@ -31,6 +35,8 @@ export const useAddOpenAIVideo = (providerId: string) => {
|
||||
case 'completed':
|
||||
addVideo({
|
||||
id: video.id,
|
||||
name: video.id,
|
||||
providerId,
|
||||
status: 'completed',
|
||||
type: 'openai',
|
||||
metadata: video,
|
||||
@@ -41,6 +47,8 @@ export const useAddOpenAIVideo = (providerId: string) => {
|
||||
case 'failed':
|
||||
addVideo({
|
||||
id: video.id,
|
||||
name: video.id,
|
||||
providerId,
|
||||
status: 'failed',
|
||||
type: 'openai',
|
||||
error: video.error,
|
||||
@@ -50,7 +58,7 @@ export const useAddOpenAIVideo = (providerId: string) => {
|
||||
break
|
||||
}
|
||||
},
|
||||
[addVideo]
|
||||
[addVideo, providerId]
|
||||
)
|
||||
|
||||
return addOpenAIVideo
|
||||
|
||||
@@ -1,26 +1,25 @@
|
||||
import { retrieveVideo } from '@renderer/services/ApiService'
|
||||
import { SystemProviderIds } from '@renderer/types'
|
||||
import { useEffect } from 'react'
|
||||
import useSWR, { SWRConfiguration, useSWRConfig } from 'swr'
|
||||
|
||||
import { useProvider } from '../useProvider'
|
||||
import { useAddOpenAIVideo } from './useAddOpenAIVideo'
|
||||
import { useVideo } from './useVideo'
|
||||
import { useVideos } from './useVideos'
|
||||
|
||||
export const useOpenAIVideo = (id: string) => {
|
||||
const providerId = SystemProviderIds.openai
|
||||
const { provider: openai } = useProvider(providerId)
|
||||
export const useOpenAIVideo = (providerId: string, id: string) => {
|
||||
const { provider } = useProvider(providerId)
|
||||
const fetcher = async () => {
|
||||
return retrieveVideo({
|
||||
type: 'openai',
|
||||
videoId: id,
|
||||
provider: openai
|
||||
})
|
||||
switch (provider.type) {
|
||||
case 'openai-response':
|
||||
return retrieveVideo({
|
||||
type: 'openai',
|
||||
videoId: id,
|
||||
provider
|
||||
})
|
||||
|
||||
default:
|
||||
throw new Error(`Unsupported provider type: ${provider.type}`)
|
||||
}
|
||||
}
|
||||
const video = useVideo(providerId, id)
|
||||
const { updateVideo } = useVideos(providerId)
|
||||
const addOpenAIVideo = useAddOpenAIVideo(providerId)
|
||||
let options: SWRConfiguration = {}
|
||||
switch (video?.status) {
|
||||
case 'queued':
|
||||
@@ -39,19 +38,6 @@ export const useOpenAIVideo = (id: string) => {
|
||||
const { mutate } = useSWRConfig()
|
||||
const revalidate = () => mutate(`video/openai/${id}`)
|
||||
|
||||
useEffect(() => {
|
||||
// queue -> in_progress / update progress
|
||||
if (data) {
|
||||
if (data.video.status === 'in_progress' && data.video.progress) {
|
||||
if (video) {
|
||||
updateVideo({ id: video.id, progress: data.video.progress })
|
||||
} else {
|
||||
addOpenAIVideo(data.video, 'Prompt lost')
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [addOpenAIVideo, data, updateVideo, video])
|
||||
|
||||
return {
|
||||
video: data,
|
||||
isLoading,
|
||||
|
||||
174
src/renderer/src/hooks/video/useProviderVideos.ts
Normal file
174
src/renderer/src/hooks/video/useProviderVideos.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { retrieveVideo } from '@renderer/services/ApiService'
|
||||
import { getProviderById } from '@renderer/services/ProviderService'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { addVideoAction, setVideoAction, setVideosAction, updateVideoAction } from '@renderer/store/video'
|
||||
import { Video } from '@renderer/types/video'
|
||||
import { getErrorMessage } from '@renderer/utils'
|
||||
import { useCallback, useEffect, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useSWR from 'swr'
|
||||
|
||||
import { useVideos } from './useVideos'
|
||||
import { useVideoThumbnail } from './useVideoThumbnail'
|
||||
|
||||
const logger = loggerService.withContext('useVideo')
|
||||
|
||||
export const useProviderVideos = (providerId: string) => {
|
||||
const { removeVideo } = useVideos()
|
||||
const videos = useAppSelector((state) => state.video.videoMap[providerId])
|
||||
const videosRef = useRef(videos)
|
||||
const dispatch = useAppDispatch()
|
||||
const { t } = useTranslation()
|
||||
|
||||
useEffect(() => {
|
||||
videosRef.current = videos
|
||||
}, [videos])
|
||||
|
||||
const getVideo = useCallback(
|
||||
(id: string) => {
|
||||
return videos?.find((v) => v.id === id)
|
||||
},
|
||||
[videos]
|
||||
)
|
||||
|
||||
const addVideo = useCallback(
|
||||
(video: Video) => {
|
||||
if (videos && videos.every((v) => v.id !== video.id)) {
|
||||
dispatch(addVideoAction({ providerId, video }))
|
||||
}
|
||||
},
|
||||
[dispatch, providerId, videos]
|
||||
)
|
||||
|
||||
const updateVideo = useCallback(
|
||||
(update: Partial<Omit<Video, 'status'>> & { id: string }) => {
|
||||
dispatch(updateVideoAction({ providerId, update }))
|
||||
},
|
||||
[dispatch, providerId]
|
||||
)
|
||||
|
||||
const setVideo = useCallback(
|
||||
(video: Video) => {
|
||||
dispatch(setVideoAction({ providerId, video }))
|
||||
},
|
||||
[dispatch, providerId]
|
||||
)
|
||||
|
||||
const setVideos = useCallback(
|
||||
(newVideos: Video[]) => {
|
||||
dispatch(setVideosAction({ providerId, videos: newVideos }))
|
||||
},
|
||||
[dispatch, providerId]
|
||||
)
|
||||
|
||||
const removeProviderVideo = useCallback(
|
||||
(videoId: string) => {
|
||||
removeVideo(videoId, providerId)
|
||||
},
|
||||
[providerId, removeVideo]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!videos) {
|
||||
setVideos([])
|
||||
}
|
||||
}, [setVideos, videos])
|
||||
|
||||
// update videos from api
|
||||
// NOTE: This provider should support openai videos endpoint. No runtime check here.
|
||||
const provider = getProviderById(providerId)
|
||||
const fetcher = async () => {
|
||||
if (!videos || !provider) return []
|
||||
if (provider.type === 'openai-response') {
|
||||
const openaiVideos = videos
|
||||
.filter((v) => v.type === 'openai')
|
||||
.filter((v) => v.status === 'queued' || v.status === 'in_progress')
|
||||
const jobs = openaiVideos.map((v) => retrieveVideo({ type: 'openai', videoId: v.id, provider }))
|
||||
const result = await Promise.allSettled(jobs)
|
||||
return result.filter((p) => p.status === 'fulfilled').map((p) => p.value)
|
||||
} else {
|
||||
throw new Error(`Provider type ${provider.type} is not supported for video status polling`)
|
||||
}
|
||||
}
|
||||
const { data, error } = useSWR('video/openai/videos', fetcher, { refreshInterval: 3000 })
|
||||
const { retrieveThumbnail } = useVideoThumbnail()
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
logger.error('Failed to fetch video status updates', error)
|
||||
return
|
||||
}
|
||||
if (!provider) {
|
||||
logger.warn(`Provider ${providerId} not found.`)
|
||||
return
|
||||
}
|
||||
const videos = videosRef.current
|
||||
|
||||
if (!data || !videos) return
|
||||
data.forEach((v) => {
|
||||
const retrievedVideo = v.video
|
||||
const storeVideo = videos.find((v) => v.id === retrievedVideo.id)
|
||||
if (!storeVideo) {
|
||||
logger.warn(`Try to update video ${retrievedVideo.id}, but it's not in the store.`)
|
||||
return
|
||||
}
|
||||
switch (retrievedVideo.status) {
|
||||
case 'in_progress':
|
||||
if (storeVideo.status === 'queued' || storeVideo.status === 'in_progress') {
|
||||
setVideo({
|
||||
...storeVideo,
|
||||
status: 'in_progress',
|
||||
progress: retrievedVideo.progress,
|
||||
metadata: retrievedVideo
|
||||
})
|
||||
}
|
||||
break
|
||||
case 'completed': {
|
||||
if (storeVideo.status === 'in_progress' || storeVideo.status === 'queued') {
|
||||
const newVideo = { ...storeVideo, status: 'completed', thumbnail: null, metadata: retrievedVideo } as const
|
||||
setVideo(newVideo)
|
||||
// Try to get thumbnail
|
||||
retrieveThumbnail(newVideo)
|
||||
.then((thumbnail) => {
|
||||
const latestVideo = videosRef.current?.find((v) => v.id === newVideo.id)
|
||||
if (
|
||||
thumbnail !== null &&
|
||||
latestVideo &&
|
||||
latestVideo.status !== 'queued' &&
|
||||
latestVideo.status !== 'in_progress' &&
|
||||
latestVideo.status !== 'failed'
|
||||
) {
|
||||
setVideo({
|
||||
...latestVideo,
|
||||
thumbnail
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
logger.error('Failed to get thumbnail', e as Error)
|
||||
window.toast.error({ title: t('video.thumbnail.error.get'), description: getErrorMessage(e) })
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'failed':
|
||||
setVideo({
|
||||
...storeVideo,
|
||||
status: 'failed',
|
||||
error: retrievedVideo.error,
|
||||
metadata: retrievedVideo
|
||||
})
|
||||
}
|
||||
})
|
||||
}, [data, error, provider, providerId, retrieveThumbnail, setVideo, t])
|
||||
|
||||
return {
|
||||
videos: videos ?? [],
|
||||
getVideo,
|
||||
addVideo,
|
||||
updateVideo,
|
||||
setVideos,
|
||||
setVideo,
|
||||
removeVideo: removeProviderVideo
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useVideos } from './useVideos'
|
||||
import { useProviderVideos } from './useProviderVideos'
|
||||
|
||||
export const useVideo = (providerId: string, id: string) => {
|
||||
const { videos } = useVideos(providerId)
|
||||
const { videos } = useProviderVideos(providerId)
|
||||
const video = videos.find((v) => v.id === id)
|
||||
return video
|
||||
}
|
||||
|
||||
86
src/renderer/src/hooks/video/useVideoThumbnail.ts
Normal file
86
src/renderer/src/hooks/video/useVideoThumbnail.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { retrieveVideoContent } from '@renderer/services/ApiService'
|
||||
import ImageStorage from '@renderer/services/ImageStorage'
|
||||
import { getProviderById } from '@renderer/services/ProviderService'
|
||||
import { Video } from '@renderer/types'
|
||||
import { useCallback } from 'react'
|
||||
|
||||
const logger = loggerService.withContext('useRetrieveThumbnail')
|
||||
|
||||
const pendingSet = new Set<string>()
|
||||
|
||||
export const useVideoThumbnail = () => {
|
||||
const getThumbnailKey = useCallback((id: string) => {
|
||||
return `video-thumbnail-${id}`
|
||||
}, [])
|
||||
|
||||
const retrieveThumbnail = useCallback(
|
||||
async (video: Video): Promise<string> => {
|
||||
const provider = getProviderById(video.providerId)
|
||||
if (!provider) {
|
||||
throw new Error(`Provider not found for id ${video.providerId}`)
|
||||
}
|
||||
const thumbnailKey = getThumbnailKey(video.id)
|
||||
if (pendingSet.has(thumbnailKey)) {
|
||||
throw new Error('Thumbnail retrieval already pending')
|
||||
}
|
||||
|
||||
pendingSet.add(thumbnailKey)
|
||||
try {
|
||||
const cachedThumbnail = await ImageStorage.get(thumbnailKey)
|
||||
if (cachedThumbnail) {
|
||||
return cachedThumbnail
|
||||
}
|
||||
|
||||
const result = await retrieveVideoContent({
|
||||
type: 'openai',
|
||||
provider,
|
||||
videoId: video.id,
|
||||
query: { variant: 'thumbnail' }
|
||||
})
|
||||
|
||||
const { response } = result
|
||||
if (!response.ok) {
|
||||
throw new Error(`Unexpected thumbnail status: ${response.status}`)
|
||||
}
|
||||
|
||||
const blob = await response.blob()
|
||||
if (!blob || blob.size === 0) {
|
||||
throw new Error('Thumbnail response body is empty')
|
||||
}
|
||||
|
||||
const base64 = await new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.onloadend = () => {
|
||||
if (typeof reader.result === 'string') {
|
||||
resolve(reader.result)
|
||||
} else {
|
||||
reject(new Error('Failed to convert thumbnail to base64'))
|
||||
}
|
||||
}
|
||||
reader.onerror = () => reject(reader.error ?? new Error('Failed to read thumbnail blob'))
|
||||
reader.readAsDataURL(blob)
|
||||
})
|
||||
|
||||
await ImageStorage.set(thumbnailKey, base64)
|
||||
return base64
|
||||
} catch (e) {
|
||||
logger.error(`Failed to get thumbnail for video ${video.id}`, e as Error)
|
||||
throw e
|
||||
} finally {
|
||||
pendingSet.delete(thumbnailKey)
|
||||
}
|
||||
},
|
||||
[getThumbnailKey]
|
||||
)
|
||||
|
||||
const removeThumbnail = useCallback(
|
||||
async (id: string) => {
|
||||
const key = getThumbnailKey(id)
|
||||
return ImageStorage.remove(key)
|
||||
},
|
||||
[getThumbnailKey]
|
||||
)
|
||||
|
||||
return { getThumbnailKey, retrieveThumbnail, removeThumbnail }
|
||||
}
|
||||
@@ -1,53 +1,48 @@
|
||||
import FileManager from '@renderer/services/FileManager'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { addVideoAction, removeVideoAction, setVideosAction, updateVideoAction } from '@renderer/store/video'
|
||||
import { Video } from '@renderer/types/video'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import { removeVideoAction } from '@renderer/store/video'
|
||||
import { objectValues } from '@renderer/types'
|
||||
import { useCallback } from 'react'
|
||||
|
||||
export const useVideos = (providerId: string) => {
|
||||
const videos = useAppSelector((state) => state.video.videoMap[providerId])
|
||||
import { useVideoThumbnail } from './useVideoThumbnail'
|
||||
|
||||
export const useVideos = () => {
|
||||
const videoMap = useAppSelector((state) => state.video.videoMap)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const addVideo = useCallback(
|
||||
(video: Video) => {
|
||||
if (videos && videos.every((v) => v.id !== video.id)) {
|
||||
dispatch(addVideoAction({ providerId, video }))
|
||||
}
|
||||
},
|
||||
[dispatch, providerId, videos]
|
||||
)
|
||||
const { removeThumbnail } = useVideoThumbnail()
|
||||
|
||||
const updateVideo = useCallback(
|
||||
(update: Partial<Video> & { id: string }) => {
|
||||
dispatch(updateVideoAction({ providerId, update }))
|
||||
},
|
||||
[dispatch, providerId]
|
||||
)
|
||||
const videos = objectValues(videoMap)
|
||||
.flat()
|
||||
.filter((v) => v !== undefined)
|
||||
|
||||
const setVideos = useCallback(
|
||||
(newVideos: Video[]) => {
|
||||
dispatch(setVideosAction({ providerId, videos: newVideos }))
|
||||
const getVideo = useCallback(
|
||||
(videoId: string) => {
|
||||
return videos.find((v) => v.id === videoId)
|
||||
},
|
||||
[dispatch, providerId]
|
||||
[videos]
|
||||
)
|
||||
|
||||
const removeVideo = useCallback(
|
||||
(videoId: string) => {
|
||||
(videoId: string, providerId?: string) => {
|
||||
const video = getVideo(videoId)
|
||||
if (!video) {
|
||||
return
|
||||
}
|
||||
if (!providerId) {
|
||||
providerId = video.providerId
|
||||
}
|
||||
// should delete from redux state, and related thumbnail image, video file
|
||||
if (video.thumbnail) {
|
||||
removeThumbnail(videoId)
|
||||
}
|
||||
if (video.fileId) {
|
||||
FileManager.deleteFile(video.fileId)
|
||||
}
|
||||
dispatch(removeVideoAction({ providerId, videoId }))
|
||||
},
|
||||
[dispatch, providerId]
|
||||
[dispatch, getVideo, removeThumbnail]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!videos) {
|
||||
setVideos([])
|
||||
}
|
||||
}, [setVideos, videos])
|
||||
|
||||
return {
|
||||
videos: videos ?? [],
|
||||
addVideo,
|
||||
updateVideo,
|
||||
setVideos,
|
||||
removeVideo
|
||||
}
|
||||
return { videos, getVideo, removeVideo }
|
||||
}
|
||||
|
||||
@@ -331,7 +331,8 @@ const builtInMcpDescriptionKeyMap: Record<BuiltinMCPServerName, string> = {
|
||||
[BuiltinMCPServerNames.fetch]: 'settings.mcp.builtinServersDescriptions.fetch',
|
||||
[BuiltinMCPServerNames.filesystem]: 'settings.mcp.builtinServersDescriptions.filesystem',
|
||||
[BuiltinMCPServerNames.difyKnowledge]: 'settings.mcp.builtinServersDescriptions.dify_knowledge',
|
||||
[BuiltinMCPServerNames.python]: 'settings.mcp.builtinServersDescriptions.python'
|
||||
[BuiltinMCPServerNames.python]: 'settings.mcp.builtinServersDescriptions.python',
|
||||
[BuiltinMCPServerNames.didiMCP]: 'settings.mcp.builtinServersDescriptions.didi_mcp'
|
||||
} as const
|
||||
|
||||
export const getBuiltInMcpServerDescriptionLabel = (key: string): string => {
|
||||
@@ -341,12 +342,14 @@ export const getBuiltInMcpServerDescriptionLabel = (key: string): string => {
|
||||
const builtinOcrProviderKeyMap = {
|
||||
system: 'ocr.builtin.system',
|
||||
tesseract: '',
|
||||
paddleocr: ''
|
||||
paddleocr: '',
|
||||
ovocr: ''
|
||||
} as const satisfies Record<BuiltinOcrProviderId, string>
|
||||
|
||||
export const getBuiltinOcrProviderLabel = (key: BuiltinOcrProviderId) => {
|
||||
if (key === 'tesseract') return 'Tesseract'
|
||||
else if (key == 'paddleocr') return 'PaddleOCR'
|
||||
else if (key == 'ovocr') return 'Intel OV(NPU) OCR'
|
||||
else return getLabel(builtinOcrProviderKeyMap, key)
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
},
|
||||
"get": {
|
||||
"error": {
|
||||
"failed": "Failed to get the agent."
|
||||
"failed": "Failed to get the agent.",
|
||||
"null_id": "Agent ID is null."
|
||||
}
|
||||
},
|
||||
"list": {
|
||||
@@ -30,6 +31,11 @@
|
||||
"failed": "Failed to list agents."
|
||||
}
|
||||
},
|
||||
"server": {
|
||||
"error": {
|
||||
"not_running": "The API server is enabled but not running properly."
|
||||
}
|
||||
},
|
||||
"session": {
|
||||
"accessible_paths": {
|
||||
"add": "Add directory",
|
||||
@@ -68,7 +74,8 @@
|
||||
},
|
||||
"get": {
|
||||
"error": {
|
||||
"failed": "Failed to get the session"
|
||||
"failed": "Failed to get the session",
|
||||
"null_id": "Session ID is null"
|
||||
}
|
||||
},
|
||||
"label_one": "Session",
|
||||
@@ -237,6 +244,7 @@
|
||||
"messages": {
|
||||
"apiKeyCopied": "API Key copied to clipboard",
|
||||
"apiKeyRegenerated": "API Key regenerated",
|
||||
"notEnabled": "The API Server is not enabled.",
|
||||
"operationFailed": "API Server operation failed: ",
|
||||
"restartError": "Failed to restart API Server: ",
|
||||
"restartFailed": "API Server restart failed: ",
|
||||
@@ -970,6 +978,7 @@
|
||||
"delete_confirm": "Are you sure you want to delete?",
|
||||
"delete_failed": "Failed to delete",
|
||||
"delete_success": "Deleted successfully",
|
||||
"deleting": "Deleting...",
|
||||
"description": "Description",
|
||||
"detail": "Detail",
|
||||
"disabled": "Disabled",
|
||||
@@ -1087,6 +1096,9 @@
|
||||
},
|
||||
"content": "Content",
|
||||
"data": "Data",
|
||||
"delete": {
|
||||
"failed": "Failed to delete."
|
||||
},
|
||||
"detail": "Error Details",
|
||||
"details": "Details",
|
||||
"errors": "Errors",
|
||||
@@ -1194,7 +1206,8 @@
|
||||
"size": "Size",
|
||||
"text": "Text",
|
||||
"title": "Files",
|
||||
"type": "Type"
|
||||
"type": "Type",
|
||||
"video": "Video"
|
||||
},
|
||||
"gpustack": {
|
||||
"keep_alive_time": {
|
||||
@@ -1954,6 +1967,14 @@
|
||||
"rename": "Rename",
|
||||
"rename_changed": "Due to security policies, the filename has been changed from {{original}} to {{final}}",
|
||||
"save": "Save to Notes",
|
||||
"search": {
|
||||
"both": "Name+Content",
|
||||
"content": "Content",
|
||||
"found_results": "Found {{count}} results (Name: {{nameCount}}, Content: {{contentCount}})",
|
||||
"more_matches": "more matches",
|
||||
"searching": "Searching...",
|
||||
"show_less": "Show less"
|
||||
},
|
||||
"settings": {
|
||||
"data": {
|
||||
"apply": "Apply",
|
||||
@@ -2038,6 +2059,7 @@
|
||||
"provider": {
|
||||
"cannot_remove_builtin": "Cannot delete built-in provider",
|
||||
"existing": "The provider already exists",
|
||||
"get_providers": "Failed to get available providers",
|
||||
"not_found": "OCR provider does not exist",
|
||||
"update_failed": "Failed to update configuration"
|
||||
},
|
||||
@@ -2099,8 +2121,10 @@
|
||||
"install_code_101": "Only supports Intel(R) Core(TM) Ultra CPU",
|
||||
"install_code_102": "Only supports Windows",
|
||||
"install_code_103": "Download OVMS runtime failed",
|
||||
"install_code_104": "Uncompress OVMS runtime failed",
|
||||
"install_code_105": "Clean OVMS runtime failed",
|
||||
"install_code_104": "Failed to install OVMS runtime",
|
||||
"install_code_105": "Failed to create ovdnd.exe",
|
||||
"install_code_106": "Failed to create run.bat",
|
||||
"install_code_110": "Failed to clean old OVMS runtime",
|
||||
"run": "Run OVMS failed:",
|
||||
"stop": "Stop OVMS failed:"
|
||||
},
|
||||
@@ -3571,6 +3595,7 @@
|
||||
"builtinServers": "Builtin Servers",
|
||||
"builtinServersDescriptions": {
|
||||
"brave_search": "An MCP server implementation integrating the Brave Search API, providing both web and local search functionalities. Requires configuring the BRAVE_API_KEY environment variable",
|
||||
"didi_mcp": "DiDi MCP server providing ride-hailing services including map search, price estimation, order management, and driver tracking. Only available in Mainland China. Requires configuring the DIDI_API_KEY environment variable",
|
||||
"dify_knowledge": "Dify's MCP server implementation provides a simple API to interact with Dify. Requires configuring the Dify Key",
|
||||
"fetch": "MCP server for retrieving URL web content",
|
||||
"filesystem": "A Node.js server implementing the Model Context Protocol (MCP) for file system operations. Requires configuration of directories allowed for access.",
|
||||
@@ -4076,7 +4101,7 @@
|
||||
"api_host_tooltip": "Override only when your provider requires a custom OpenAI-compatible endpoint.",
|
||||
"api_key": {
|
||||
"label": "API Key",
|
||||
"tip": "Multiple keys separated by commas or spaces"
|
||||
"tip": "Use commas to separate multiple keys"
|
||||
},
|
||||
"api_version": "API Version",
|
||||
"aws-bedrock": {
|
||||
@@ -4649,14 +4674,24 @@
|
||||
"title": "Update"
|
||||
},
|
||||
"video": {
|
||||
"delete": {
|
||||
"error": {
|
||||
"not_found": {
|
||||
"description": "The video was not found remotely. It will only be deleted locally.",
|
||||
"title": "Video not found."
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"create": "Failed to create video",
|
||||
"download": "Failed to download video.",
|
||||
"invalid": "Invalid video",
|
||||
"load": {
|
||||
"message": "Failed to load the video",
|
||||
"reason": "The file may be corrupted or has been deleted externally."
|
||||
}
|
||||
},
|
||||
"expired": "Expired",
|
||||
"input_reference": {
|
||||
"add": {
|
||||
"error": {
|
||||
@@ -4679,6 +4714,10 @@
|
||||
"queued": "Queued"
|
||||
},
|
||||
"thumbnail": {
|
||||
"error": {
|
||||
"get": "Failed to get thumbnail"
|
||||
},
|
||||
"get": "Get thumbnail",
|
||||
"placeholder": "No thumbnail"
|
||||
},
|
||||
"title": "Video",
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
},
|
||||
"get": {
|
||||
"error": {
|
||||
"failed": "获取智能体失败"
|
||||
"failed": "获取智能体失败",
|
||||
"null_id": "智能体 ID 为空。"
|
||||
}
|
||||
},
|
||||
"list": {
|
||||
@@ -30,6 +31,11 @@
|
||||
"failed": "获取智能体列表失败"
|
||||
}
|
||||
},
|
||||
"server": {
|
||||
"error": {
|
||||
"not_running": "API 服务器已启用但未正常运行。"
|
||||
}
|
||||
},
|
||||
"session": {
|
||||
"accessible_paths": {
|
||||
"add": "添加目录",
|
||||
@@ -68,7 +74,8 @@
|
||||
},
|
||||
"get": {
|
||||
"error": {
|
||||
"failed": "获取会话失败"
|
||||
"failed": "获取会话失败",
|
||||
"null_id": "会话 ID 为空"
|
||||
}
|
||||
},
|
||||
"label_one": "会话",
|
||||
@@ -237,6 +244,7 @@
|
||||
"messages": {
|
||||
"apiKeyCopied": "API 密钥已复制到剪贴板",
|
||||
"apiKeyRegenerated": "API 密钥已重新生成",
|
||||
"notEnabled": "API 服务器未启用。",
|
||||
"operationFailed": "API 服务器操作失败:",
|
||||
"restartError": "重启 API 服务器失败:",
|
||||
"restartFailed": "API 服务器重启失败:",
|
||||
@@ -970,6 +978,7 @@
|
||||
"delete_confirm": "确定要删除吗?",
|
||||
"delete_failed": "删除失败",
|
||||
"delete_success": "删除成功",
|
||||
"deleting": "删除中...",
|
||||
"description": "描述",
|
||||
"detail": "详情",
|
||||
"disabled": "已禁用",
|
||||
@@ -1087,6 +1096,9 @@
|
||||
},
|
||||
"content": "内容",
|
||||
"data": "数据",
|
||||
"delete": {
|
||||
"failed": "删除失败"
|
||||
},
|
||||
"detail": "错误详情",
|
||||
"details": "详细信息",
|
||||
"errors": "错误",
|
||||
@@ -1194,7 +1206,8 @@
|
||||
"size": "大小",
|
||||
"text": "文本",
|
||||
"title": "文件",
|
||||
"type": "类型"
|
||||
"type": "类型",
|
||||
"video": "视频"
|
||||
},
|
||||
"gpustack": {
|
||||
"keep_alive_time": {
|
||||
@@ -1954,6 +1967,14 @@
|
||||
"rename": "重命名",
|
||||
"rename_changed": "由于安全策略,文件名已从 {{original}} 更改为 {{final}}",
|
||||
"save": "保存到笔记",
|
||||
"search": {
|
||||
"both": "名称+内容",
|
||||
"content": "内容",
|
||||
"found_results": "找到 {{count}} 个结果 (名称: {{nameCount}}, 内容: {{contentCount}})",
|
||||
"more_matches": "个匹配",
|
||||
"searching": "搜索中...",
|
||||
"show_less": "收起"
|
||||
},
|
||||
"settings": {
|
||||
"data": {
|
||||
"apply": "应用",
|
||||
@@ -2038,6 +2059,7 @@
|
||||
"provider": {
|
||||
"cannot_remove_builtin": "不能删除内置提供商",
|
||||
"existing": "提供商已存在",
|
||||
"get_providers": "获取可用提供商失败",
|
||||
"not_found": "OCR 提供商不存在",
|
||||
"update_failed": "更新配置失败"
|
||||
},
|
||||
@@ -2074,7 +2096,7 @@
|
||||
"description": "<div><p>1. 下载 OV 模型.</p><p>2. 在 'Manager' 中添加模型.</p><p>仅支持 Windows!</p><p>OVMS 安装路径: '%USERPROFILE%\\.cherrystudio\\ovms' .</p><p>请参考 <a href=https://github.com/openvinotoolkit/model_server/blob/c55551763d02825829337b62c2dcef9339706f79/docs/deploying_server_baremetal.md>Intel OVMS 指南</a></p></dev>",
|
||||
"download": {
|
||||
"button": "下载",
|
||||
"error": "选择失败",
|
||||
"error": "下载失败",
|
||||
"model_id": {
|
||||
"label": "模型 ID",
|
||||
"model_id_pattern": "模型 ID 必须以 OpenVINO/ 开头",
|
||||
@@ -2099,8 +2121,10 @@
|
||||
"install_code_101": "仅支持 Intel(R) Core(TM) Ultra CPU",
|
||||
"install_code_102": "仅支持 Windows",
|
||||
"install_code_103": "下载 OVMS runtime 失败",
|
||||
"install_code_104": "解压 OVMS runtime 失败",
|
||||
"install_code_105": "清理 OVMS runtime 失败",
|
||||
"install_code_104": "安装 OVMS runtime 失败",
|
||||
"install_code_105": "创建 ovdnd.exe 失败",
|
||||
"install_code_106": "创建 run.bat 失败",
|
||||
"install_code_110": "清理旧 OVMS runtime 失败",
|
||||
"run": "运行 OVMS 失败:",
|
||||
"stop": "停止 OVMS 失败:"
|
||||
},
|
||||
@@ -3571,6 +3595,7 @@
|
||||
"builtinServers": "内置服务器",
|
||||
"builtinServersDescriptions": {
|
||||
"brave_search": "一个集成了Brave 搜索 API 的 MCP 服务器实现,提供网页与本地搜索双重功能。需要配置 BRAVE_API_KEY 环境变量",
|
||||
"didi_mcp": "一个集成了滴滴 MCP 服务器实现,提供网约车服务包括地图搜索、价格预估、订单管理和司机跟踪。仅支持中国大陆地区。需要配置 DIDI_API_KEY 环境变量",
|
||||
"dify_knowledge": "Dify 的 MCP 服务器实现,提供了一个简单的 API 来与 Dify 进行交互。需要配置 Dify Key",
|
||||
"fetch": "用于获取 URL 网页内容的 MCP 服务器",
|
||||
"filesystem": "实现文件系统操作的模型上下文协议(MCP)的 Node.js 服务器。需要配置允许访问的目录",
|
||||
@@ -4076,7 +4101,7 @@
|
||||
"api_host_tooltip": "仅在服务商需要自定义的 OpenAI 兼容地址时覆盖。",
|
||||
"api_key": {
|
||||
"label": "API 密钥",
|
||||
"tip": "多个密钥使用逗号或空格分隔"
|
||||
"tip": "多个密钥使用逗号分隔"
|
||||
},
|
||||
"api_version": "API 版本",
|
||||
"aws-bedrock": {
|
||||
@@ -4649,14 +4674,24 @@
|
||||
"title": "更新提示"
|
||||
},
|
||||
"video": {
|
||||
"delete": {
|
||||
"error": {
|
||||
"not_found": {
|
||||
"description": "远程未找到该视频,仅会删除本地记录。",
|
||||
"title": "视频未找到"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"create": "创建视频失败",
|
||||
"download": "视频下载失败",
|
||||
"invalid": "无效的视频",
|
||||
"load": {
|
||||
"message": "加载视频失败",
|
||||
"reason": "文件可能已损坏或已被外部删除。"
|
||||
}
|
||||
},
|
||||
"expired": "已过期",
|
||||
"input_reference": {
|
||||
"add": {
|
||||
"error": {
|
||||
@@ -4679,6 +4714,10 @@
|
||||
"queued": "排队中"
|
||||
},
|
||||
"thumbnail": {
|
||||
"error": {
|
||||
"get": "[to be translated]:Failed to get thumbnail"
|
||||
},
|
||||
"get": "[to be translated]:Get thumbnail",
|
||||
"placeholder": "无缩略图"
|
||||
},
|
||||
"title": "视频",
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
},
|
||||
"get": {
|
||||
"error": {
|
||||
"failed": "無法取得代理程式。"
|
||||
"failed": "無法取得代理程式。",
|
||||
"null_id": "代理程式 ID 為空。"
|
||||
}
|
||||
},
|
||||
"list": {
|
||||
@@ -30,6 +31,11 @@
|
||||
"failed": "無法列出代理程式。"
|
||||
}
|
||||
},
|
||||
"server": {
|
||||
"error": {
|
||||
"not_running": "API 伺服器已啟用,但運行不正常。"
|
||||
}
|
||||
},
|
||||
"session": {
|
||||
"accessible_paths": {
|
||||
"add": "新增目錄",
|
||||
@@ -68,7 +74,8 @@
|
||||
},
|
||||
"get": {
|
||||
"error": {
|
||||
"failed": "無法取得工作階段"
|
||||
"failed": "無法取得工作階段",
|
||||
"null_id": "工作階段 ID 為空"
|
||||
}
|
||||
},
|
||||
"label_one": "會議",
|
||||
@@ -237,6 +244,7 @@
|
||||
"messages": {
|
||||
"apiKeyCopied": "API 金鑰已複製到剪貼簿",
|
||||
"apiKeyRegenerated": "API 金鑰已重新生成",
|
||||
"notEnabled": "API 伺服器未啟用。",
|
||||
"operationFailed": "API 伺服器操作失敗:",
|
||||
"restartError": "重新啟動 API 伺服器失敗:",
|
||||
"restartFailed": "API 伺服器重新啟動失敗:",
|
||||
@@ -970,6 +978,7 @@
|
||||
"delete_confirm": "確定要刪除嗎?",
|
||||
"delete_failed": "刪除失敗",
|
||||
"delete_success": "刪除成功",
|
||||
"deleting": "[to be translated]:Deleting...",
|
||||
"description": "描述",
|
||||
"detail": "詳情",
|
||||
"disabled": "已停用",
|
||||
@@ -1087,6 +1096,9 @@
|
||||
},
|
||||
"content": "內容",
|
||||
"data": "数据",
|
||||
"delete": {
|
||||
"failed": "[to be translated]:Failed to delete."
|
||||
},
|
||||
"detail": "錯誤詳情",
|
||||
"details": "詳細信息",
|
||||
"errors": "錯誤",
|
||||
@@ -1194,7 +1206,8 @@
|
||||
"size": "大小",
|
||||
"text": "文字",
|
||||
"title": "檔案",
|
||||
"type": "類型"
|
||||
"type": "類型",
|
||||
"video": "[to be translated]:Video"
|
||||
},
|
||||
"gpustack": {
|
||||
"keep_alive_time": {
|
||||
@@ -1954,6 +1967,14 @@
|
||||
"rename": "重命名",
|
||||
"rename_changed": "由於安全策略,文件名已從 {{original}} 更改為 {{final}}",
|
||||
"save": "儲存到筆記",
|
||||
"search": {
|
||||
"both": "名稱+內容",
|
||||
"content": "內容",
|
||||
"found_results": "找到 {{count}} 個結果 (名稱: {{nameCount}}, 內容: {{contentCount}})",
|
||||
"more_matches": "個匹配",
|
||||
"searching": "搜索中...",
|
||||
"show_less": "收起"
|
||||
},
|
||||
"settings": {
|
||||
"data": {
|
||||
"apply": "應用",
|
||||
@@ -2037,8 +2058,9 @@
|
||||
"error": {
|
||||
"provider": {
|
||||
"cannot_remove_builtin": "不能刪除內建提供者",
|
||||
"existing": "提供商已存在",
|
||||
"not_found": "OCR 提供商不存在",
|
||||
"existing": "提供者已存在",
|
||||
"get_providers": "取得可用提供者失敗",
|
||||
"not_found": "OCR 提供者不存在",
|
||||
"update_failed": "更新配置失敗"
|
||||
},
|
||||
"unknown": "OCR過程發生錯誤"
|
||||
@@ -2074,7 +2096,7 @@
|
||||
"description": "<div><p>1. 下載 OV 模型。</p><p>2. 在 'Manager' 中新增模型。</p><p>僅支援 Windows!</p><p>OVMS 安裝路徑: '%USERPROFILE%\\.cherrystudio\\ovms' 。</p><p>請參考 <a href=https://github.com/openvinotoolkit/model_server/blob/c55551763d02825829337b62c2dcef9339706f79/docs/deploying_server_baremetal.md>Intel OVMS 指南</a></p></dev>",
|
||||
"download": {
|
||||
"button": "下載",
|
||||
"error": "選擇失敗",
|
||||
"error": "下載失敗",
|
||||
"model_id": {
|
||||
"label": "模型 ID",
|
||||
"model_id_pattern": "模型 ID 必須以 OpenVINO/ 開頭",
|
||||
@@ -2099,8 +2121,10 @@
|
||||
"install_code_101": "僅支援 Intel(R) Core(TM) Ultra CPU",
|
||||
"install_code_102": "僅支援 Windows",
|
||||
"install_code_103": "下載 OVMS runtime 失敗",
|
||||
"install_code_104": "解壓 OVMS runtime 失敗",
|
||||
"install_code_105": "清理 OVMS runtime 失敗",
|
||||
"install_code_104": "安裝 OVMS runtime 失敗",
|
||||
"install_code_105": "創建 ovdnd.exe 失敗",
|
||||
"install_code_106": "創建 run.bat 失敗",
|
||||
"install_code_110": "清理舊 OVMS runtime 失敗",
|
||||
"run": "執行 OVMS 失敗:",
|
||||
"stop": "停止 OVMS 失敗:"
|
||||
},
|
||||
@@ -3571,6 +3595,7 @@
|
||||
"builtinServers": "內置伺服器",
|
||||
"builtinServersDescriptions": {
|
||||
"brave_search": "一個集成了Brave 搜索 API 的 MCP 伺服器實現,提供網頁與本地搜尋雙重功能。需要配置 BRAVE_API_KEY 環境變數",
|
||||
"didi_mcp": "一個集成了滴滴 MCP 伺服器實現,提供網約車服務包括地圖搜尋、價格預估、訂單管理和司機追蹤。僅支援中國大陸地區。需要配置 DIDI_API_KEY 環境變數",
|
||||
"dify_knowledge": "Dify 的 MCP 伺服器實現,提供了一個簡單的 API 來與 Dify 進行互動。需要配置 Dify Key",
|
||||
"fetch": "用於獲取 URL 網頁內容的 MCP 伺服器",
|
||||
"filesystem": "實現文件系統操作的模型上下文協議(MCP)的 Node.js 伺服器。需要配置允許訪問的目錄",
|
||||
@@ -4076,7 +4101,7 @@
|
||||
"api_host_tooltip": "僅在服務商需要自訂的 OpenAI 相容端點時才覆蓋。",
|
||||
"api_key": {
|
||||
"label": "API 金鑰",
|
||||
"tip": "多個金鑰使用逗號或空格分隔"
|
||||
"tip": "多個金鑰使用逗號分隔"
|
||||
},
|
||||
"api_version": "API 版本",
|
||||
"aws-bedrock": {
|
||||
@@ -4649,14 +4674,24 @@
|
||||
"title": "更新提示"
|
||||
},
|
||||
"video": {
|
||||
"delete": {
|
||||
"error": {
|
||||
"not_found": {
|
||||
"description": "[to be translated]:The video was not found remotely. It will only be deleted locally.",
|
||||
"title": "[to be translated]:Video not found."
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"create": "[to be translated]:Failed to create video",
|
||||
"download": "[to be translated]:Failed to download video.",
|
||||
"invalid": "[to be translated]:Invalid video",
|
||||
"load": {
|
||||
"message": "[to be translated]:Failed to load the video",
|
||||
"reason": "[to be translated]:The file may be corrupted or has been deleted externally."
|
||||
}
|
||||
},
|
||||
"expired": "[to be translated]:Expired",
|
||||
"input_reference": {
|
||||
"add": {
|
||||
"error": {
|
||||
@@ -4679,6 +4714,10 @@
|
||||
"queued": "[to be translated]:Queued"
|
||||
},
|
||||
"thumbnail": {
|
||||
"error": {
|
||||
"get": "[to be translated]:Failed to get thumbnail"
|
||||
},
|
||||
"get": "[to be translated]:Get thumbnail",
|
||||
"placeholder": "[to be translated]:No thumbnail"
|
||||
},
|
||||
"title": "[to be translated]:Video",
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
},
|
||||
"get": {
|
||||
"error": {
|
||||
"failed": "Αποτυχία λήψης του πράκτορα."
|
||||
"failed": "Αποτυχία λήψης του πράκτορα.",
|
||||
"null_id": "Το ID του πράκτορα είναι null."
|
||||
}
|
||||
},
|
||||
"list": {
|
||||
@@ -30,6 +31,11 @@
|
||||
"failed": "Αποτυχία καταχώρησης πρακτόρων."
|
||||
}
|
||||
},
|
||||
"server": {
|
||||
"error": {
|
||||
"not_running": "Ο διακομιστής API είναι ενεργοποιημένος αλλά δεν λειτουργεί σωστά."
|
||||
}
|
||||
},
|
||||
"session": {
|
||||
"accessible_paths": {
|
||||
"add": "Προσθήκη καταλόγου",
|
||||
@@ -68,7 +74,8 @@
|
||||
},
|
||||
"get": {
|
||||
"error": {
|
||||
"failed": "Αποτυχία λήψης της συνεδρίας"
|
||||
"failed": "Αποτυχία λήψης της συνεδρίας",
|
||||
"null_id": "Το ID της συνεδρίας είναι null"
|
||||
}
|
||||
},
|
||||
"label_one": "Συνεδρία",
|
||||
@@ -237,6 +244,7 @@
|
||||
"messages": {
|
||||
"apiKeyCopied": "Το κλειδί API αντιγράφηκε στο πρόχειρο",
|
||||
"apiKeyRegenerated": "Το κλειδί API αναδημιουργήθηκε",
|
||||
"notEnabled": "Ο διακομιστής API δεν είναι ενεργοποιημένος.",
|
||||
"operationFailed": "Η λειτουργία του Διακομιστή API απέτυχε: ",
|
||||
"restartError": "Αποτυχία επανεκκίνησης του Διακομιστή API: ",
|
||||
"restartFailed": "Η επανεκκίνηση του Διακομιστή API απέτυχε: ",
|
||||
@@ -970,6 +978,7 @@
|
||||
"delete_confirm": "Είστε βέβαιοι ότι θέλετε να διαγράψετε;",
|
||||
"delete_failed": "Αποτυχία διαγραφής",
|
||||
"delete_success": "Η διαγραφή ήταν επιτυχής",
|
||||
"deleting": "[to be translated]:Deleting...",
|
||||
"description": "Περιγραφή",
|
||||
"detail": "Λεπτομέρειες",
|
||||
"disabled": "Απενεργοποιημένο",
|
||||
@@ -1087,6 +1096,9 @@
|
||||
},
|
||||
"content": "Περιεχόμενο",
|
||||
"data": "δεδομένα",
|
||||
"delete": {
|
||||
"failed": "[to be translated]:Failed to delete."
|
||||
},
|
||||
"detail": "Λεπτομέρειες σφάλματος",
|
||||
"details": "Λεπτομέρειες",
|
||||
"errors": "Λάθος",
|
||||
@@ -1194,7 +1206,8 @@
|
||||
"size": "Μέγεθος",
|
||||
"text": "Κείμενο",
|
||||
"title": "Αρχεία",
|
||||
"type": "Τύπος"
|
||||
"type": "Τύπος",
|
||||
"video": "[to be translated]:Video"
|
||||
},
|
||||
"gpustack": {
|
||||
"keep_alive_time": {
|
||||
@@ -2038,6 +2051,7 @@
|
||||
"provider": {
|
||||
"cannot_remove_builtin": "Δεν είναι δυνατή η διαγραφή του ενσωματωμένου παρόχου",
|
||||
"existing": "Ο πάροχος υπηρεσιών υπάρχει ήδη",
|
||||
"get_providers": "Αποτυχία λήψης διαθέσιμων παρόχων",
|
||||
"not_found": "Ο πάροχος OCR δεν υπάρχει",
|
||||
"update_failed": "Αποτυχία ενημέρωσης της διαμόρφωσης"
|
||||
},
|
||||
@@ -3571,6 +3585,7 @@
|
||||
"builtinServers": "Ενσωματωμένοι Διακομιστές",
|
||||
"builtinServersDescriptions": {
|
||||
"brave_search": "μια εφαρμογή διακομιστή MCP που ενσωματώνει το Brave Search API, παρέχοντας δυνατότητες αναζήτησης στον ιστό και τοπικής αναζήτησης. Απαιτείται η ρύθμιση της μεταβλητής περιβάλλοντος BRAVE_API_KEY",
|
||||
"didi_mcp": "Διακομιστής DiDi MCP που παρέχει υπηρεσίες μεταφοράς συμπεριλαμβανομένης της αναζήτησης χαρτών, εκτίμησης τιμών, διαχείρισης παραγγελιών και παρακολούθησης οδηγών. Διαθέσιμο μόνο στην ηπειρωτική Κίνα. Απαιτεί διαμόρφωση της μεταβλητής περιβάλλοντος DIDI_API_KEY",
|
||||
"dify_knowledge": "Η υλοποίηση του Dify για τον διακομιστή MCP, παρέχει μια απλή API για να αλληλεπιδρά με το Dify. Απαιτείται η ρύθμιση του κλειδιού Dify",
|
||||
"fetch": "Εξυπηρετητής MCP για λήψη περιεχομένου ιστοσελίδας URL",
|
||||
"filesystem": "Εξυπηρετητής Node.js για το πρωτόκολλο περιβάλλοντος μοντέλου (MCP) που εφαρμόζει λειτουργίες συστήματος αρχείων. Απαιτείται διαμόρφωση για την επιτροπή πρόσβασης σε καταλόγους",
|
||||
@@ -4649,14 +4664,24 @@
|
||||
"title": "Ενημέρωση"
|
||||
},
|
||||
"video": {
|
||||
"delete": {
|
||||
"error": {
|
||||
"not_found": {
|
||||
"description": "[to be translated]:The video was not found remotely. It will only be deleted locally.",
|
||||
"title": "[to be translated]:Video not found."
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"create": "[to be translated]:Failed to create video",
|
||||
"download": "[to be translated]:Failed to download video.",
|
||||
"invalid": "[to be translated]:Invalid video",
|
||||
"load": {
|
||||
"message": "[to be translated]:Failed to load the video",
|
||||
"reason": "[to be translated]:The file may be corrupted or has been deleted externally."
|
||||
}
|
||||
},
|
||||
"expired": "[to be translated]:Expired",
|
||||
"input_reference": {
|
||||
"add": {
|
||||
"error": {
|
||||
@@ -4679,6 +4704,10 @@
|
||||
"queued": "[to be translated]:Queued"
|
||||
},
|
||||
"thumbnail": {
|
||||
"error": {
|
||||
"get": "[to be translated]:Failed to get thumbnail"
|
||||
},
|
||||
"get": "[to be translated]:Get thumbnail",
|
||||
"placeholder": "[to be translated]:No thumbnail"
|
||||
},
|
||||
"title": "[to be translated]:Video",
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
},
|
||||
"get": {
|
||||
"error": {
|
||||
"failed": "No se pudo obtener el agente."
|
||||
"failed": "No se pudo obtener el agente.",
|
||||
"null_id": "El ID del agente es nulo."
|
||||
}
|
||||
},
|
||||
"list": {
|
||||
@@ -30,6 +31,11 @@
|
||||
"failed": "Error al listar agentes."
|
||||
}
|
||||
},
|
||||
"server": {
|
||||
"error": {
|
||||
"not_running": "El servidor de API está habilitado pero no funciona correctamente."
|
||||
}
|
||||
},
|
||||
"session": {
|
||||
"accessible_paths": {
|
||||
"add": "Agregar directorio",
|
||||
@@ -68,7 +74,8 @@
|
||||
},
|
||||
"get": {
|
||||
"error": {
|
||||
"failed": "Error al obtener la sesión"
|
||||
"failed": "Error al obtener la sesión",
|
||||
"null_id": "El ID de sesión es nulo"
|
||||
}
|
||||
},
|
||||
"label_one": "Sesión",
|
||||
@@ -237,6 +244,7 @@
|
||||
"messages": {
|
||||
"apiKeyCopied": "Clave API copiada al portapapeles",
|
||||
"apiKeyRegenerated": "Clave API regenerada",
|
||||
"notEnabled": "El servidor de API no está habilitado.",
|
||||
"operationFailed": "Falló la operación del Servidor API: ",
|
||||
"restartError": "Error al reiniciar el Servidor API: ",
|
||||
"restartFailed": "Falló el reinicio del Servidor API: ",
|
||||
@@ -970,6 +978,7 @@
|
||||
"delete_confirm": "¿Está seguro de que desea eliminarlo?",
|
||||
"delete_failed": "Error al eliminar",
|
||||
"delete_success": "Eliminación exitosa",
|
||||
"deleting": "[to be translated]:Deleting...",
|
||||
"description": "Descripción",
|
||||
"detail": "Detalles",
|
||||
"disabled": "Desactivado",
|
||||
@@ -1087,6 +1096,9 @@
|
||||
},
|
||||
"content": "contenido",
|
||||
"data": "datos",
|
||||
"delete": {
|
||||
"failed": "[to be translated]:Failed to delete."
|
||||
},
|
||||
"detail": "Detalles del error",
|
||||
"details": "Detalles",
|
||||
"errors": "error",
|
||||
@@ -1194,7 +1206,8 @@
|
||||
"size": "Tamaño",
|
||||
"text": "Texto",
|
||||
"title": "Archivo",
|
||||
"type": "Tipo"
|
||||
"type": "Tipo",
|
||||
"video": "[to be translated]:Video"
|
||||
},
|
||||
"gpustack": {
|
||||
"keep_alive_time": {
|
||||
@@ -2038,6 +2051,7 @@
|
||||
"provider": {
|
||||
"cannot_remove_builtin": "No se puede eliminar el proveedor integrado",
|
||||
"existing": "El proveedor ya existe",
|
||||
"get_providers": "Error al obtener proveedores disponibles",
|
||||
"not_found": "El proveedor de OCR no existe",
|
||||
"update_failed": "Actualización de la configuración fallida"
|
||||
},
|
||||
@@ -3571,6 +3585,7 @@
|
||||
"builtinServers": "Servidores integrados",
|
||||
"builtinServersDescriptions": {
|
||||
"brave_search": "Una implementación de servidor MCP que integra la API de búsqueda de Brave, proporcionando funciones de búsqueda web y búsqueda local. Requiere configurar la variable de entorno BRAVE_API_KEY",
|
||||
"didi_mcp": "Servidor DiDi MCP que proporciona servicios de transporte incluyendo búsqueda de mapas, estimación de precios, gestión de pedidos y seguimiento de conductores. Disponible solo en China Continental. Requiere configurar la variable de entorno DIDI_API_KEY",
|
||||
"dify_knowledge": "Implementación del servidor MCP de Dify, que proporciona una API sencilla para interactuar con Dify. Se requiere configurar la clave de Dify.",
|
||||
"fetch": "Servidor MCP para obtener el contenido de la página web de una URL",
|
||||
"filesystem": "Servidor Node.js que implementa el protocolo de contexto del modelo (MCP) para operaciones del sistema de archivos. Requiere configuración del directorio permitido para el acceso",
|
||||
@@ -4649,14 +4664,24 @@
|
||||
"title": "Actualización"
|
||||
},
|
||||
"video": {
|
||||
"delete": {
|
||||
"error": {
|
||||
"not_found": {
|
||||
"description": "[to be translated]:The video was not found remotely. It will only be deleted locally.",
|
||||
"title": "[to be translated]:Video not found."
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"create": "[to be translated]:Failed to create video",
|
||||
"download": "[to be translated]:Failed to download video.",
|
||||
"invalid": "[to be translated]:Invalid video",
|
||||
"load": {
|
||||
"message": "[to be translated]:Failed to load the video",
|
||||
"reason": "[to be translated]:The file may be corrupted or has been deleted externally."
|
||||
}
|
||||
},
|
||||
"expired": "[to be translated]:Expired",
|
||||
"input_reference": {
|
||||
"add": {
|
||||
"error": {
|
||||
@@ -4679,6 +4704,10 @@
|
||||
"queued": "[to be translated]:Queued"
|
||||
},
|
||||
"thumbnail": {
|
||||
"error": {
|
||||
"get": "[to be translated]:Failed to get thumbnail"
|
||||
},
|
||||
"get": "[to be translated]:Get thumbnail",
|
||||
"placeholder": "[to be translated]:No thumbnail"
|
||||
},
|
||||
"title": "[to be translated]:Video",
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
},
|
||||
"get": {
|
||||
"error": {
|
||||
"failed": "Échec de l'obtention de l'agent."
|
||||
"failed": "Échec de l'obtention de l'agent.",
|
||||
"null_id": "L'ID de l'agent est nul."
|
||||
}
|
||||
},
|
||||
"list": {
|
||||
@@ -30,6 +31,11 @@
|
||||
"failed": "Échec de la liste des agents."
|
||||
}
|
||||
},
|
||||
"server": {
|
||||
"error": {
|
||||
"not_running": "Le serveur API est activé mais ne fonctionne pas correctement."
|
||||
}
|
||||
},
|
||||
"session": {
|
||||
"accessible_paths": {
|
||||
"add": "Ajouter un répertoire",
|
||||
@@ -68,7 +74,8 @@
|
||||
},
|
||||
"get": {
|
||||
"error": {
|
||||
"failed": "Échec de l'obtention de la session"
|
||||
"failed": "Échec de l'obtention de la session",
|
||||
"null_id": "L'ID de session est nul"
|
||||
}
|
||||
},
|
||||
"label_one": "Session",
|
||||
@@ -237,6 +244,7 @@
|
||||
"messages": {
|
||||
"apiKeyCopied": "Clé API copiée dans le presse-papiers",
|
||||
"apiKeyRegenerated": "Clé API régénérée",
|
||||
"notEnabled": "Le serveur API n'est pas activé.",
|
||||
"operationFailed": "Opération du Serveur API échouée : ",
|
||||
"restartError": "Échec du redémarrage du Serveur API : ",
|
||||
"restartFailed": "Redémarrage du Serveur API échoué : ",
|
||||
@@ -970,6 +978,7 @@
|
||||
"delete_confirm": "Êtes-vous sûr de vouloir supprimer ?",
|
||||
"delete_failed": "Échec de la suppression",
|
||||
"delete_success": "Suppression réussie",
|
||||
"deleting": "[to be translated]:Deleting...",
|
||||
"description": "Description",
|
||||
"detail": "détails",
|
||||
"disabled": "Désactivé",
|
||||
@@ -1087,6 +1096,9 @@
|
||||
},
|
||||
"content": "suivre l'instruction du système",
|
||||
"data": "données",
|
||||
"delete": {
|
||||
"failed": "[to be translated]:Failed to delete."
|
||||
},
|
||||
"detail": "Détails de l'erreur",
|
||||
"details": "Informations détaillées",
|
||||
"errors": "erreur",
|
||||
@@ -1194,7 +1206,8 @@
|
||||
"size": "Taille",
|
||||
"text": "Texte",
|
||||
"title": "Fichier",
|
||||
"type": "Type"
|
||||
"type": "Type",
|
||||
"video": "[to be translated]:Video"
|
||||
},
|
||||
"gpustack": {
|
||||
"keep_alive_time": {
|
||||
@@ -2038,6 +2051,7 @@
|
||||
"provider": {
|
||||
"cannot_remove_builtin": "Impossible de supprimer le fournisseur intégré",
|
||||
"existing": "Le fournisseur existe déjà",
|
||||
"get_providers": "Échec de l'obtention des fournisseurs disponibles",
|
||||
"not_found": "Le fournisseur OCR n'existe pas",
|
||||
"update_failed": "Échec de la mise à jour de la configuration"
|
||||
},
|
||||
@@ -3571,6 +3585,7 @@
|
||||
"builtinServers": "Serveurs intégrés",
|
||||
"builtinServersDescriptions": {
|
||||
"brave_search": "Une implémentation de serveur MCP intégrant l'API de recherche Brave, offrant des fonctionnalités de recherche web et locale. Nécessite la configuration de la variable d'environnement BRAVE_API_KEY",
|
||||
"didi_mcp": "Serveur DiDi MCP fournissant des services de transport incluant la recherche de cartes, l'estimation des prix, la gestion des commandes et le suivi des conducteurs. Disponible uniquement en Chine continentale. Nécessite la configuration de la variable d'environnement DIDI_API_KEY",
|
||||
"dify_knowledge": "Implémentation du serveur MCP de Dify, fournissant une API simple pour interagir avec Dify. Nécessite la configuration de la clé Dify",
|
||||
"fetch": "serveur MCP utilisé pour récupérer le contenu des pages web URL",
|
||||
"filesystem": "Serveur Node.js implémentant le protocole de contexte de modèle (MCP) pour les opérations de système de fichiers. Nécessite une configuration des répertoires autorisés à être accédés.",
|
||||
@@ -4649,14 +4664,24 @@
|
||||
"title": "Mise à jour"
|
||||
},
|
||||
"video": {
|
||||
"delete": {
|
||||
"error": {
|
||||
"not_found": {
|
||||
"description": "[to be translated]:The video was not found remotely. It will only be deleted locally.",
|
||||
"title": "[to be translated]:Video not found."
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"create": "[to be translated]:Failed to create video",
|
||||
"download": "[to be translated]:Failed to download video.",
|
||||
"invalid": "[to be translated]:Invalid video",
|
||||
"load": {
|
||||
"message": "[to be translated]:Failed to load the video",
|
||||
"reason": "[to be translated]:The file may be corrupted or has been deleted externally."
|
||||
}
|
||||
},
|
||||
"expired": "[to be translated]:Expired",
|
||||
"input_reference": {
|
||||
"add": {
|
||||
"error": {
|
||||
@@ -4679,6 +4704,10 @@
|
||||
"queued": "[to be translated]:Queued"
|
||||
},
|
||||
"thumbnail": {
|
||||
"error": {
|
||||
"get": "[to be translated]:Failed to get thumbnail"
|
||||
},
|
||||
"get": "[to be translated]:Get thumbnail",
|
||||
"placeholder": "[to be translated]:No thumbnail"
|
||||
},
|
||||
"title": "[to be translated]:Video",
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
},
|
||||
"get": {
|
||||
"error": {
|
||||
"failed": "エージェントの取得に失敗しました。"
|
||||
"failed": "エージェントの取得に失敗しました。",
|
||||
"null_id": "エージェント ID が null です。"
|
||||
}
|
||||
},
|
||||
"list": {
|
||||
@@ -30,6 +31,11 @@
|
||||
"failed": "エージェントの一覧取得に失敗しました。"
|
||||
}
|
||||
},
|
||||
"server": {
|
||||
"error": {
|
||||
"not_running": "APIサーバーは有効になっていますが、正常に動作していません。"
|
||||
}
|
||||
},
|
||||
"session": {
|
||||
"accessible_paths": {
|
||||
"add": "ディレクトリを追加",
|
||||
@@ -68,7 +74,8 @@
|
||||
},
|
||||
"get": {
|
||||
"error": {
|
||||
"failed": "セッションの取得に失敗しました"
|
||||
"failed": "セッションの取得に失敗しました",
|
||||
"null_id": "セッション ID が null です"
|
||||
}
|
||||
},
|
||||
"label_one": "セッション",
|
||||
@@ -237,6 +244,7 @@
|
||||
"messages": {
|
||||
"apiKeyCopied": "API キーがクリップボードにコピーされました",
|
||||
"apiKeyRegenerated": "API キーが再生成されました",
|
||||
"notEnabled": "APIサーバーが有効になっていません。",
|
||||
"operationFailed": "API サーバーの操作に失敗しました:",
|
||||
"restartError": "API サーバーの再起動に失敗しました:",
|
||||
"restartFailed": "API サーバーの再起動に失敗しました:",
|
||||
@@ -970,6 +978,7 @@
|
||||
"delete_confirm": "削除してもよろしいですか?",
|
||||
"delete_failed": "削除に失敗しました",
|
||||
"delete_success": "削除に成功しました",
|
||||
"deleting": "[to be translated]:Deleting...",
|
||||
"description": "説明",
|
||||
"detail": "詳細",
|
||||
"disabled": "無効",
|
||||
@@ -1087,6 +1096,9 @@
|
||||
},
|
||||
"content": "内容",
|
||||
"data": "データ",
|
||||
"delete": {
|
||||
"failed": "[to be translated]:Failed to delete."
|
||||
},
|
||||
"detail": "エラーの詳細",
|
||||
"details": "詳細",
|
||||
"errors": "エラー",
|
||||
@@ -1194,7 +1206,8 @@
|
||||
"size": "サイズ",
|
||||
"text": "テキスト",
|
||||
"title": "ファイル",
|
||||
"type": "タイプ"
|
||||
"type": "タイプ",
|
||||
"video": "[to be translated]:Video"
|
||||
},
|
||||
"gpustack": {
|
||||
"keep_alive_time": {
|
||||
@@ -2038,6 +2051,7 @@
|
||||
"provider": {
|
||||
"cannot_remove_builtin": "組み込みプロバイダーは削除できません",
|
||||
"existing": "プロバイダーはすでに存在します",
|
||||
"get_providers": "利用可能なプロバイダーの取得に失敗しました",
|
||||
"not_found": "OCRプロバイダーが存在しません",
|
||||
"update_failed": "更新構成に失敗しました"
|
||||
},
|
||||
@@ -3571,6 +3585,7 @@
|
||||
"builtinServers": "組み込みサーバー",
|
||||
"builtinServersDescriptions": {
|
||||
"brave_search": "Brave検索APIを統合したMCPサーバーの実装で、ウェブ検索とローカル検索の両機能を提供します。BRAVE_API_KEY環境変数の設定が必要です",
|
||||
"didi_mcp": "DiDi MCPサーバーは、地図検索、料金見積もり、注文管理、ドライバー追跡を含むライドシェアサービスを提供します。中国本土でのみ利用可能です。DIDI_API_KEY環境変数の設定が必要です",
|
||||
"dify_knowledge": "DifyのMCPサーバー実装は、Difyと対話するためのシンプルなAPIを提供します。Dify Keyの設定が必要です。",
|
||||
"fetch": "URLのウェブページコンテンツを取得するためのMCPサーバー",
|
||||
"filesystem": "Node.jsサーバーによるファイルシステム操作を実現するモデルコンテキストプロトコル(MCP)。アクセスを許可するディレクトリの設定が必要です",
|
||||
@@ -4649,14 +4664,24 @@
|
||||
"title": "更新"
|
||||
},
|
||||
"video": {
|
||||
"delete": {
|
||||
"error": {
|
||||
"not_found": {
|
||||
"description": "[to be translated]:The video was not found remotely. It will only be deleted locally.",
|
||||
"title": "[to be translated]:Video not found."
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"create": "[to be translated]:Failed to create video",
|
||||
"download": "[to be translated]:Failed to download video.",
|
||||
"invalid": "[to be translated]:Invalid video",
|
||||
"load": {
|
||||
"message": "[to be translated]:Failed to load the video",
|
||||
"reason": "[to be translated]:The file may be corrupted or has been deleted externally."
|
||||
}
|
||||
},
|
||||
"expired": "[to be translated]:Expired",
|
||||
"input_reference": {
|
||||
"add": {
|
||||
"error": {
|
||||
@@ -4679,6 +4704,10 @@
|
||||
"queued": "[to be translated]:Queued"
|
||||
},
|
||||
"thumbnail": {
|
||||
"error": {
|
||||
"get": "[to be translated]:Failed to get thumbnail"
|
||||
},
|
||||
"get": "[to be translated]:Get thumbnail",
|
||||
"placeholder": "[to be translated]:No thumbnail"
|
||||
},
|
||||
"title": "[to be translated]:Video",
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
},
|
||||
"get": {
|
||||
"error": {
|
||||
"failed": "Falha ao obter o agente."
|
||||
"failed": "Falha ao obter o agente.",
|
||||
"null_id": "O ID do agente é nulo."
|
||||
}
|
||||
},
|
||||
"list": {
|
||||
@@ -30,6 +31,11 @@
|
||||
"failed": "Falha ao listar agentes."
|
||||
}
|
||||
},
|
||||
"server": {
|
||||
"error": {
|
||||
"not_running": "O servidor de API está habilitado, mas não está funcionando corretamente."
|
||||
}
|
||||
},
|
||||
"session": {
|
||||
"accessible_paths": {
|
||||
"add": "Adicionar diretório",
|
||||
@@ -68,7 +74,8 @@
|
||||
},
|
||||
"get": {
|
||||
"error": {
|
||||
"failed": "Falha ao obter a sessão"
|
||||
"failed": "Falha ao obter a sessão",
|
||||
"null_id": "O ID da sessão é nulo"
|
||||
}
|
||||
},
|
||||
"label_one": "Sessão",
|
||||
@@ -237,6 +244,7 @@
|
||||
"messages": {
|
||||
"apiKeyCopied": "Chave API copiada para a área de transferência",
|
||||
"apiKeyRegenerated": "Chave API regenerada",
|
||||
"notEnabled": "O Servidor de API não está habilitado.",
|
||||
"operationFailed": "Operação do Servidor API falhou: ",
|
||||
"restartError": "Falha ao reiniciar o Servidor API: ",
|
||||
"restartFailed": "Reinício do Servidor API falhou: ",
|
||||
@@ -970,6 +978,7 @@
|
||||
"delete_confirm": "Tem certeza de que deseja excluir?",
|
||||
"delete_failed": "Falha ao excluir",
|
||||
"delete_success": "Excluído com sucesso",
|
||||
"deleting": "[to be translated]:Deleting...",
|
||||
"description": "Descrição",
|
||||
"detail": "detalhes",
|
||||
"disabled": "Desativado",
|
||||
@@ -1087,6 +1096,9 @@
|
||||
},
|
||||
"content": "conteúdo",
|
||||
"data": "dados",
|
||||
"delete": {
|
||||
"failed": "[to be translated]:Failed to delete."
|
||||
},
|
||||
"detail": "Detalhes do erro",
|
||||
"details": "Detalhes",
|
||||
"errors": "erro",
|
||||
@@ -1194,7 +1206,8 @@
|
||||
"size": "Tamanho",
|
||||
"text": "Texto",
|
||||
"title": "Arquivo",
|
||||
"type": "Tipo"
|
||||
"type": "Tipo",
|
||||
"video": "[to be translated]:Video"
|
||||
},
|
||||
"gpustack": {
|
||||
"keep_alive_time": {
|
||||
@@ -2038,6 +2051,7 @@
|
||||
"provider": {
|
||||
"cannot_remove_builtin": "Não é possível excluir o provedor integrado",
|
||||
"existing": "O provedor já existe",
|
||||
"get_providers": "Falha ao obter provedores disponíveis",
|
||||
"not_found": "O provedor OCR não existe",
|
||||
"update_failed": "Falha ao atualizar a configuração"
|
||||
},
|
||||
@@ -3571,6 +3585,7 @@
|
||||
"builtinServers": "Servidores integrados",
|
||||
"builtinServersDescriptions": {
|
||||
"brave_search": "uma implementação de servidor MCP integrada com a API de pesquisa Brave, fornecendo funcionalidades de pesquisa web e local. Requer a configuração da variável de ambiente BRAVE_API_KEY",
|
||||
"didi_mcp": "Servidor DiDi MCP que fornece serviços de transporte incluindo pesquisa de mapas, estimativa de preços, gestão de pedidos e rastreamento de motoristas. Disponível apenas na China Continental. Requer configuração da variável de ambiente DIDI_API_KEY",
|
||||
"dify_knowledge": "Implementação do servidor MCP do Dify, que fornece uma API simples para interagir com o Dify. Requer a configuração da chave Dify",
|
||||
"fetch": "servidor MCP para obter o conteúdo da página web do URL",
|
||||
"filesystem": "Servidor Node.js do protocolo de contexto de modelo (MCP) para implementar operações de sistema de ficheiros. Requer configuração do diretório permitido para acesso",
|
||||
@@ -4649,14 +4664,24 @@
|
||||
"title": "Atualização"
|
||||
},
|
||||
"video": {
|
||||
"delete": {
|
||||
"error": {
|
||||
"not_found": {
|
||||
"description": "[to be translated]:The video was not found remotely. It will only be deleted locally.",
|
||||
"title": "[to be translated]:Video not found."
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"create": "[to be translated]:Failed to create video",
|
||||
"download": "[to be translated]:Failed to download video.",
|
||||
"invalid": "[to be translated]:Invalid video",
|
||||
"load": {
|
||||
"message": "[to be translated]:Failed to load the video",
|
||||
"reason": "[to be translated]:The file may be corrupted or has been deleted externally."
|
||||
}
|
||||
},
|
||||
"expired": "[to be translated]:Expired",
|
||||
"input_reference": {
|
||||
"add": {
|
||||
"error": {
|
||||
@@ -4679,6 +4704,10 @@
|
||||
"queued": "[to be translated]:Queued"
|
||||
},
|
||||
"thumbnail": {
|
||||
"error": {
|
||||
"get": "[to be translated]:Failed to get thumbnail"
|
||||
},
|
||||
"get": "[to be translated]:Get thumbnail",
|
||||
"placeholder": "[to be translated]:No thumbnail"
|
||||
},
|
||||
"title": "[to be translated]:Video",
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
},
|
||||
"get": {
|
||||
"error": {
|
||||
"failed": "Не удалось получить агента."
|
||||
"failed": "Не удалось получить агента.",
|
||||
"null_id": "ID агента равен null."
|
||||
}
|
||||
},
|
||||
"list": {
|
||||
@@ -30,6 +31,11 @@
|
||||
"failed": "Не удалось получить список агентов."
|
||||
}
|
||||
},
|
||||
"server": {
|
||||
"error": {
|
||||
"not_running": "API-сервер включен, но работает неправильно."
|
||||
}
|
||||
},
|
||||
"session": {
|
||||
"accessible_paths": {
|
||||
"add": "Добавить каталог",
|
||||
@@ -68,7 +74,8 @@
|
||||
},
|
||||
"get": {
|
||||
"error": {
|
||||
"failed": "Не удалось получить сеанс"
|
||||
"failed": "Не удалось получить сеанс",
|
||||
"null_id": "ID сессии равен null"
|
||||
}
|
||||
},
|
||||
"label_one": "Сессия",
|
||||
@@ -237,6 +244,7 @@
|
||||
"messages": {
|
||||
"apiKeyCopied": "API ключ скопирован в буфер обмена",
|
||||
"apiKeyRegenerated": "API ключ перегенерирован",
|
||||
"notEnabled": "API-сервер не включен.",
|
||||
"operationFailed": "Операция API сервера не удалась: ",
|
||||
"restartError": "Не удалось перезапустить API сервер: ",
|
||||
"restartFailed": "Перезапуск API сервера не удался: ",
|
||||
@@ -970,6 +978,7 @@
|
||||
"delete_confirm": "Вы уверены, что хотите удалить?",
|
||||
"delete_failed": "Не удалось удалить",
|
||||
"delete_success": "Удаление выполнено успешно",
|
||||
"deleting": "[to be translated]:Deleting...",
|
||||
"description": "Описание",
|
||||
"detail": "Подробности",
|
||||
"disabled": "Отключено",
|
||||
@@ -1087,6 +1096,9 @@
|
||||
},
|
||||
"content": "Содержание",
|
||||
"data": "данные",
|
||||
"delete": {
|
||||
"failed": "[to be translated]:Failed to delete."
|
||||
},
|
||||
"detail": "Детали ошибки",
|
||||
"details": "Подробности",
|
||||
"errors": "ошибка",
|
||||
@@ -1194,7 +1206,8 @@
|
||||
"size": "Размер",
|
||||
"text": "Текст",
|
||||
"title": "Файлы",
|
||||
"type": "Тип"
|
||||
"type": "Тип",
|
||||
"video": "[to be translated]:Video"
|
||||
},
|
||||
"gpustack": {
|
||||
"keep_alive_time": {
|
||||
@@ -2038,6 +2051,7 @@
|
||||
"provider": {
|
||||
"cannot_remove_builtin": "Не удается удалить встроенного поставщика",
|
||||
"existing": "Поставщик уже существует",
|
||||
"get_providers": "Не удалось получить доступных поставщиков",
|
||||
"not_found": "Поставщик OCR отсутствует",
|
||||
"update_failed": "Обновление конфигурации не удалось"
|
||||
},
|
||||
@@ -3571,6 +3585,7 @@
|
||||
"builtinServers": "Встроенные серверы",
|
||||
"builtinServersDescriptions": {
|
||||
"brave_search": "реализация сервера MCP с интеграцией API поиска Brave, обеспечивающая функции веб-поиска и локального поиска. Требуется настройка переменной среды BRAVE_API_KEY",
|
||||
"didi_mcp": "Сервер DiDi MCP, предоставляющий услуги такси, включая поиск на карте, оценку стоимости, управление заказами и отслеживание водителей. Доступен только в материковом Китае. Требует настройки переменной окружения DIDI_API_KEY",
|
||||
"dify_knowledge": "Реализация сервера MCP Dify, предоставляющая простой API для взаимодействия с Dify. Требуется настройка ключа Dify",
|
||||
"fetch": "MCP-сервер для получения содержимого веб-страниц по URL",
|
||||
"filesystem": "Node.js-сервер протокола контекста модели (MCP) для реализации операций файловой системы. Требуется настройка каталогов, к которым разрешён доступ",
|
||||
@@ -4649,14 +4664,24 @@
|
||||
"title": "Обновление"
|
||||
},
|
||||
"video": {
|
||||
"delete": {
|
||||
"error": {
|
||||
"not_found": {
|
||||
"description": "[to be translated]:The video was not found remotely. It will only be deleted locally.",
|
||||
"title": "[to be translated]:Video not found."
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"create": "[to be translated]:Failed to create video",
|
||||
"download": "[to be translated]:Failed to download video.",
|
||||
"invalid": "[to be translated]:Invalid video",
|
||||
"load": {
|
||||
"message": "[to be translated]:Failed to load the video",
|
||||
"reason": "[to be translated]:The file may be corrupted or has been deleted externally."
|
||||
}
|
||||
},
|
||||
"expired": "[to be translated]:Expired",
|
||||
"input_reference": {
|
||||
"add": {
|
||||
"error": {
|
||||
@@ -4679,6 +4704,10 @@
|
||||
"queued": "[to be translated]:Queued"
|
||||
},
|
||||
"thumbnail": {
|
||||
"error": {
|
||||
"get": "[to be translated]:Failed to get thumbnail"
|
||||
},
|
||||
"get": "[to be translated]:Get thumbnail",
|
||||
"placeholder": "[to be translated]:No thumbnail"
|
||||
},
|
||||
"title": "[to be translated]:Video",
|
||||
|
||||
@@ -19,7 +19,8 @@ import {
|
||||
File as FileIcon,
|
||||
FileImage,
|
||||
FileText,
|
||||
FileType as FileTypeIcon
|
||||
FileType as FileTypeIcon,
|
||||
FileVideo
|
||||
} from 'lucide-react'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -138,6 +139,7 @@ const FilesPage: FC = () => {
|
||||
{ key: FileTypes.DOCUMENT, label: t('files.document'), icon: <FileIcon size={16} /> },
|
||||
{ key: FileTypes.IMAGE, label: t('files.image'), icon: <FileImage size={16} /> },
|
||||
{ key: FileTypes.TEXT, label: t('files.text'), icon: <FileTypeIcon size={16} /> },
|
||||
{ key: FileTypes.VIDEO, label: t('files.video'), icon: <FileVideo size={16} /> },
|
||||
{ key: 'all', label: t('files.all'), icon: <FileText size={16} /> }
|
||||
]
|
||||
|
||||
|
||||
@@ -49,7 +49,8 @@ const Chat: FC<Props> = (props) => {
|
||||
const { isTopNavbar } = useNavbarPosition()
|
||||
const chatMaxWidth = useChatMaxWidth()
|
||||
const { chat } = useRuntime()
|
||||
const { activeTopicOrSession, activeAgentId, activeSessionId } = chat
|
||||
const { activeTopicOrSession, activeAgentId, activeSessionIdMap } = chat
|
||||
const activeSessionId = activeAgentId ? activeSessionIdMap[activeAgentId] : null
|
||||
const { apiServer } = useSettings()
|
||||
|
||||
const mainRef = React.useRef<HTMLDivElement>(null)
|
||||
@@ -147,8 +148,7 @@ const Chat: FC<Props> = (props) => {
|
||||
if (activeAgentId === null) {
|
||||
return () => <div> Active Agent ID is invalid.</div>
|
||||
}
|
||||
const sessionId = activeSessionId[activeAgentId]
|
||||
if (!sessionId) {
|
||||
if (!activeSessionId) {
|
||||
return () => <div> Active Session ID is invalid.</div>
|
||||
}
|
||||
if (!apiServer.enabled) {
|
||||
@@ -158,18 +158,17 @@ const Chat: FC<Props> = (props) => {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return () => <AgentSessionMessages agentId={activeAgentId} sessionId={sessionId} />
|
||||
return () => <AgentSessionMessages agentId={activeAgentId} sessionId={activeSessionId} />
|
||||
}, [activeAgentId, activeSessionId, apiServer.enabled, t])
|
||||
|
||||
const SessionInputBar = useMemo(() => {
|
||||
if (activeAgentId === null) {
|
||||
return () => <div> Active Agent ID is invalid.</div>
|
||||
}
|
||||
const sessionId = activeSessionId[activeAgentId]
|
||||
if (!sessionId) {
|
||||
if (!activeSessionId) {
|
||||
return () => <div> Active Session ID is invalid.</div>
|
||||
}
|
||||
return () => <AgentSessionInputbar agentId={activeAgentId} sessionId={sessionId} />
|
||||
return () => <AgentSessionInputbar agentId={activeAgentId} sessionId={activeSessionId} />
|
||||
}, [activeAgentId, activeSessionId])
|
||||
|
||||
// TODO: more info
|
||||
@@ -235,10 +234,8 @@ const Chat: FC<Props> = (props) => {
|
||||
</>
|
||||
)}
|
||||
{activeTopicOrSession === 'session' && !activeAgentId && <AgentInvalid />}
|
||||
{activeTopicOrSession === 'session' && activeAgentId && !activeSessionId[activeAgentId] && (
|
||||
<SessionInvalid />
|
||||
)}
|
||||
{activeTopicOrSession === 'session' && activeAgentId && activeSessionId[activeAgentId] && (
|
||||
{activeTopicOrSession === 'session' && activeAgentId && !activeSessionId && <SessionInvalid />}
|
||||
{activeTopicOrSession === 'session' && activeAgentId && activeSessionId && (
|
||||
<>
|
||||
<SessionMessages />
|
||||
<SessionInputBar />
|
||||
|
||||
@@ -1,34 +1,24 @@
|
||||
import { BreadcrumbItem, Breadcrumbs, Chip, cn } from '@heroui/react'
|
||||
import { NavbarHeader } from '@renderer/components/app/Navbar'
|
||||
import HorizontalScrollContainer from '@renderer/components/HorizontalScrollContainer'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import SearchPopup from '@renderer/components/Popups/SearchPopup'
|
||||
import { permissionModeCards } from '@renderer/constants/permissionModes'
|
||||
import { useAgent } from '@renderer/hooks/agents/useAgent'
|
||||
import { useSession } from '@renderer/hooks/agents/useSession'
|
||||
import { useUpdateAgent } from '@renderer/hooks/agents/useUpdateAgent'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { modelGenerating, useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { modelGenerating } from '@renderer/hooks/useRuntime'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useShortcut } from '@renderer/hooks/useShortcuts'
|
||||
import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setNarrowMode } from '@renderer/store/settings'
|
||||
import { ApiModel, Assistant, PermissionMode, Topic } from '@renderer/types'
|
||||
import { formatErrorMessageWithPrefix } from '@renderer/utils/error'
|
||||
import { Assistant, Topic } from '@renderer/types'
|
||||
import { Tooltip } from 'antd'
|
||||
import { t } from 'i18next'
|
||||
import { Menu, PanelLeftClose, PanelRightClose, Search } from 'lucide-react'
|
||||
import { AnimatePresence, motion } from 'motion/react'
|
||||
import React, { FC, ReactNode, useCallback } from 'react'
|
||||
import { FC } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { AgentSettingsPopup } from '../settings/AgentSettings'
|
||||
import { AgentLabel } from '../settings/AgentSettings/shared'
|
||||
import AssistantsDrawer from './components/AssistantsDrawer'
|
||||
import SelectAgentModelButton from './components/SelectAgentModelButton'
|
||||
import SelectModelButton from './components/SelectModelButton'
|
||||
import ChatNavbarContent from './components/ChatNavbarContent'
|
||||
import UpdateAppButton from './components/UpdateAppButton'
|
||||
|
||||
interface Props {
|
||||
@@ -45,11 +35,6 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTo
|
||||
const { topicPosition, narrowMode } = useSettings()
|
||||
const { showTopics, toggleShowTopics } = useShowTopics()
|
||||
const dispatch = useAppDispatch()
|
||||
const { chat } = useRuntime()
|
||||
const { activeTopicOrSession, activeAgentId } = chat
|
||||
const sessionId = activeAgentId ? (chat.activeSessionId[activeAgentId] ?? null) : null
|
||||
const { agent } = useAgent(activeAgentId)
|
||||
const { updateModel } = useUpdateAgent()
|
||||
|
||||
useShortcut('toggle_show_assistants', toggleShowAssistants)
|
||||
|
||||
@@ -79,13 +64,13 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTo
|
||||
})
|
||||
}
|
||||
|
||||
const handleUpdateModel = useCallback(
|
||||
async (model: ApiModel) => {
|
||||
if (!agent) return
|
||||
return updateModel(agent.id, model.id, { showSuccessToast: false })
|
||||
},
|
||||
[agent, updateModel]
|
||||
)
|
||||
// const handleUpdateModel = useCallback(
|
||||
// async (model: ApiModel) => {
|
||||
// if (!activeSession || !activeAgent) return
|
||||
// return updateModel(activeSession.id, model.id, { showSuccessToast: false })
|
||||
// },
|
||||
// [activeAgent, activeSession, updateModel]
|
||||
// )
|
||||
|
||||
return (
|
||||
<NavbarHeader className="home-navbar">
|
||||
@@ -117,38 +102,7 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTo
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
{activeTopicOrSession === 'topic' && <SelectModelButton assistant={assistant} />}
|
||||
{activeTopicOrSession === 'session' && agent && (
|
||||
<HorizontalScrollContainer>
|
||||
<Breadcrumbs
|
||||
classNames={{
|
||||
base: 'flex',
|
||||
list: 'flex-nowrap'
|
||||
}}>
|
||||
<BreadcrumbItem
|
||||
onPress={() => AgentSettingsPopup.show({ agentId: agent.id })}
|
||||
classNames={{
|
||||
base: 'self-stretch',
|
||||
item: 'h-full'
|
||||
}}>
|
||||
<Chip size="md" variant="light" className="h-full transition-background hover:bg-foreground-100">
|
||||
<AgentLabel
|
||||
agent={agent}
|
||||
classNames={{ name: 'max-w-50 font-bold text-xs', avatar: 'h-4.5 w-4.5', container: 'gap-1.5' }}
|
||||
/>
|
||||
</Chip>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbItem>
|
||||
<SelectAgentModelButton agent={agent} onSelect={handleUpdateModel} />
|
||||
</BreadcrumbItem>
|
||||
{activeAgentId && sessionId && (
|
||||
<BreadcrumbItem>
|
||||
<SessionWorkspaceMeta agentId={activeAgentId} sessionId={sessionId} />
|
||||
</BreadcrumbItem>
|
||||
)}
|
||||
</Breadcrumbs>
|
||||
</HorizontalScrollContainer>
|
||||
)}
|
||||
<ChatNavbarContent assistant={assistant} />
|
||||
</div>
|
||||
<HStack alignItems="center" gap={8}>
|
||||
<UpdateAppButton />
|
||||
@@ -181,74 +135,6 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTo
|
||||
)
|
||||
}
|
||||
|
||||
const SessionWorkspaceMeta: FC<{ agentId: string; sessionId: string }> = ({ agentId, sessionId }) => {
|
||||
const { agent } = useAgent(agentId)
|
||||
const { session } = useSession(agentId, sessionId)
|
||||
if (!session || !agent) {
|
||||
return null
|
||||
}
|
||||
|
||||
const firstAccessiblePath = session.accessible_paths?.[0]
|
||||
const permissionMode = (session.configuration?.permission_mode ?? 'default') as PermissionMode
|
||||
const permissionModeCard = permissionModeCards.find((card) => card.mode === permissionMode)
|
||||
const permissionModeLabel = permissionModeCard
|
||||
? t(permissionModeCard.titleKey, permissionModeCard.titleFallback)
|
||||
: permissionMode
|
||||
|
||||
const infoItems: ReactNode[] = []
|
||||
|
||||
const InfoTag = ({
|
||||
text,
|
||||
className,
|
||||
onClick
|
||||
}: {
|
||||
text: string
|
||||
className?: string
|
||||
classNames?: {}
|
||||
onClick?: (e: React.MouseEvent) => void
|
||||
}) => (
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-medium border border-default-200 px-2 py-1 text-foreground-500 text-xs dark:text-foreground-400',
|
||||
onClick !== undefined ? 'cursor-pointer' : undefined,
|
||||
className
|
||||
)}
|
||||
title={text}
|
||||
onClick={onClick}>
|
||||
<span className="block truncate">{text}</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
// infoItems.push(<InfoTag key="name" text={agent.name ?? ''} className="max-w-60" />)
|
||||
|
||||
if (firstAccessiblePath) {
|
||||
infoItems.push(
|
||||
<InfoTag
|
||||
key="path"
|
||||
text={firstAccessiblePath}
|
||||
className="max-w-60 transition-colors hover:border-primary hover:text-primary"
|
||||
onClick={() => {
|
||||
window.api.file
|
||||
.openPath(firstAccessiblePath)
|
||||
.catch((e) =>
|
||||
window.toast.error(
|
||||
formatErrorMessageWithPrefix(e, t('files.error.open_path', { path: firstAccessiblePath }))
|
||||
)
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
infoItems.push(<InfoTag key="permission-mode" text={permissionModeLabel} className="max-w-50" />)
|
||||
|
||||
if (infoItems.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <div className="ml-2 flex items-center gap-2">{infoItems}</div>
|
||||
}
|
||||
|
||||
export const NavbarIcon = styled.div`
|
||||
-webkit-app-region: none;
|
||||
border-radius: 8px;
|
||||
|
||||
@@ -117,7 +117,7 @@ const HomePage: FC = () => {
|
||||
type: 'chat'
|
||||
})
|
||||
} else if (activeTopicOrSession === 'topic') {
|
||||
dispatch(setActiveAgentId('fake'))
|
||||
dispatch(setActiveAgentId(null))
|
||||
}
|
||||
}, [activeTopicOrSession, dispatch, setActiveAssistant])
|
||||
|
||||
@@ -130,6 +130,7 @@ const HomePage: FC = () => {
|
||||
setActiveTopic={setActiveTopic}
|
||||
setActiveAssistant={setActiveAssistant}
|
||||
position="left"
|
||||
activeTopicOrSession={activeTopicOrSession}
|
||||
/>
|
||||
)}
|
||||
<ContentContainer id={isLeftNavbar ? 'content-container' : undefined}>
|
||||
|
||||
@@ -262,7 +262,9 @@ const AgentSessionInputbar: FC<Props> = ({ agentId, sessionId }) => {
|
||||
value={text}
|
||||
onChange={onChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={t('chat.input.placeholder', { key: getSendMessageShortcutLabel(sendMessageShortcut) })}
|
||||
placeholder={t('chat.input.placeholder_without_triggers', {
|
||||
key: getSendMessageShortcutLabel(sendMessageShortcut)
|
||||
})}
|
||||
autoFocus
|
||||
variant="borderless"
|
||||
spellCheck={enableSpellCheck}
|
||||
|
||||
@@ -2,7 +2,7 @@ import Favicon from '@renderer/components/Icons/FallbackFavicon'
|
||||
import { Tooltip } from 'antd'
|
||||
import React, { memo, useCallback, useMemo } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { z } from 'zod'
|
||||
import * as z from 'zod'
|
||||
|
||||
export const CitationSchema = z.object({
|
||||
url: z.url(),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Button } from '@heroui/button'
|
||||
import CodeViewer from '@renderer/components/CodeViewer'
|
||||
import { useCodeStyle } from '@renderer/context/CodeStyleProvider'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import { getHttpMessageLabel, getProviderLabel } from '@renderer/i18n/label'
|
||||
import { getProviderById } from '@renderer/services/ProviderService'
|
||||
@@ -35,7 +36,7 @@ import {
|
||||
import type { ErrorMessageBlock, Message } from '@renderer/types/newMessage'
|
||||
import { formatAiSdkError, formatError, safeToString } from '@renderer/utils/error'
|
||||
import { Alert as AntdAlert, Modal } from 'antd'
|
||||
import React, { useState } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { Link } from 'react-router-dom'
|
||||
import styled from 'styled-components'
|
||||
@@ -305,14 +306,36 @@ const BuiltinError = ({ error }: { error: SerializedError }) => {
|
||||
// 作为 base,渲染公共字段,应当在 ErrorDetailList 中渲染
|
||||
const AiSdkErrorBase = ({ error }: { error: SerializedAiSdkError }) => {
|
||||
const { t } = useTranslation()
|
||||
const { highlightCode } = useCodeStyle()
|
||||
const [highlightedString, setHighlightedString] = useState('')
|
||||
const cause = error.cause
|
||||
|
||||
useEffect(() => {
|
||||
const highlight = async () => {
|
||||
try {
|
||||
const result = await highlightCode(JSON.stringify(JSON.parse(cause || '{}'), null, 2), 'json')
|
||||
setHighlightedString(result)
|
||||
} catch {
|
||||
setHighlightedString(cause || '')
|
||||
}
|
||||
}
|
||||
const timer = setTimeout(highlight, 0)
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}, [highlightCode, cause])
|
||||
|
||||
return (
|
||||
<>
|
||||
<BuiltinError error={error} />
|
||||
{cause && (
|
||||
<ErrorDetailItem>
|
||||
<ErrorDetailLabel>{t('error.cause')}:</ErrorDetailLabel>
|
||||
<ErrorDetailValue>{error.cause}</ErrorDetailValue>
|
||||
<ErrorDetailValue>
|
||||
<div
|
||||
className="markdown [&_pre]:!bg-transparent [&_pre_span]:whitespace-pre-wrap"
|
||||
dangerouslySetInnerHTML={{ __html: highlightedString }}
|
||||
/>
|
||||
</ErrorDetailValue>
|
||||
</ErrorDetailItem>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -337,29 +337,18 @@ const GroupContainer = styled.div`
|
||||
const GridContainer = styled(Scrollbar)<{ $count: number; $gridColumns: number }>`
|
||||
width: 100%;
|
||||
display: grid;
|
||||
overflow-y: visible;
|
||||
gap: 16px;
|
||||
|
||||
&.horizontal {
|
||||
padding-bottom: 4px;
|
||||
grid-template-columns: repeat(${({ $count }) => $count}, minmax(420px, 1fr));
|
||||
overflow-y: hidden;
|
||||
overflow-x: auto;
|
||||
&::-webkit-scrollbar {
|
||||
height: 6px;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--color-scrollbar-thumb);
|
||||
border-radius: var(--scrollbar-thumb-radius);
|
||||
}
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color-scrollbar-thumb-hover);
|
||||
}
|
||||
}
|
||||
&.fold,
|
||||
&.vertical {
|
||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
&.grid {
|
||||
grid-template-columns: repeat(
|
||||
@@ -367,15 +356,11 @@ const GridContainer = styled(Scrollbar)<{ $count: number; $gridColumns: number }
|
||||
minmax(0, 1fr)
|
||||
);
|
||||
grid-template-rows: auto;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
&.multi-select-mode {
|
||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
.grid {
|
||||
height: auto;
|
||||
}
|
||||
@@ -401,7 +386,7 @@ interface MessageWrapperProps {
|
||||
const MessageWrapper = styled.div<MessageWrapperProps>`
|
||||
&.horizontal {
|
||||
padding: 1px;
|
||||
/* overflow-y: auto; */
|
||||
overflow-y: auto;
|
||||
.message {
|
||||
height: 100%;
|
||||
border: 0.5px solid var(--color-border);
|
||||
@@ -423,7 +408,7 @@ const MessageWrapper = styled.div<MessageWrapperProps>`
|
||||
&.grid {
|
||||
display: block;
|
||||
height: 300px;
|
||||
overflow: hidden;
|
||||
overflow-y: hidden;
|
||||
border: 0.5px solid var(--color-border);
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Navbar, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar'
|
||||
import { Navbar, NavbarCenter, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import SearchPopup from '@renderer/components/Popups/SearchPopup'
|
||||
import { isLinux, isMac, isWin } from '@renderer/config/constant'
|
||||
@@ -7,6 +7,8 @@ import { modelGenerating } from '@renderer/hooks/useRuntime'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useShortcut } from '@renderer/hooks/useShortcuts'
|
||||
import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore'
|
||||
import { useChatMaxWidth } from '@renderer/pages/home/Chat'
|
||||
import ChatNavbarContent from '@renderer/pages/home/components/ChatNavbarContent'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setNarrowMode } from '@renderer/store/settings'
|
||||
@@ -15,7 +17,7 @@ import { Tooltip } from 'antd'
|
||||
import { t } from 'i18next'
|
||||
import { Menu, PanelLeftClose, PanelRightClose, Search } from 'lucide-react'
|
||||
import { AnimatePresence, motion } from 'motion/react'
|
||||
import { FC } from 'react'
|
||||
import React, { FC } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import AssistantsDrawer from './components/AssistantsDrawer'
|
||||
@@ -28,13 +30,21 @@ interface Props {
|
||||
setActiveTopic: (topic: Topic) => void
|
||||
setActiveAssistant: (assistant: Assistant) => void
|
||||
position: 'left' | 'right'
|
||||
activeTopicOrSession?: 'topic' | 'session'
|
||||
}
|
||||
|
||||
const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTopic, setActiveTopic }) => {
|
||||
const HeaderNavbar: FC<Props> = ({
|
||||
activeAssistant,
|
||||
setActiveAssistant,
|
||||
activeTopic,
|
||||
setActiveTopic,
|
||||
activeTopicOrSession
|
||||
}) => {
|
||||
const { assistant } = useAssistant(activeAssistant.id)
|
||||
const { showAssistants, toggleShowAssistants } = useShowAssistants()
|
||||
const { topicPosition, narrowMode } = useSettings()
|
||||
const { showTopics, toggleShowTopics } = useShowTopics()
|
||||
const chatMaxWidth = useChatMaxWidth()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
useShortcut('toggle_show_assistants', toggleShowAssistants)
|
||||
@@ -113,15 +123,29 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTo
|
||||
</AnimatePresence>
|
||||
</NavbarLeft>
|
||||
)}
|
||||
<HStack alignItems="center" gap={6} ml={!isMac ? 16 : 0}>
|
||||
<SelectModelButton assistant={assistant} />
|
||||
</HStack>
|
||||
<NavbarCenter>
|
||||
{activeTopicOrSession === 'topic' ? (
|
||||
<HStack alignItems="center" gap={6} ml={!isMac ? 16 : 0}>
|
||||
<SelectModelButton assistant={assistant} />
|
||||
</HStack>
|
||||
) : (
|
||||
<ChatNavbarContainer
|
||||
style={{
|
||||
maxWidth: chatMaxWidth,
|
||||
marginLeft: !isMac ? 16 : 0
|
||||
}}>
|
||||
<ChatNavbarContent assistant={assistant} />
|
||||
</ChatNavbarContainer>
|
||||
)}
|
||||
</NavbarCenter>
|
||||
|
||||
<NavbarRight
|
||||
style={{
|
||||
justifyContent: 'flex-end',
|
||||
flex: 1,
|
||||
flex: activeTopicOrSession === 'topic' ? 1 : 'none',
|
||||
position: 'relative',
|
||||
paddingRight: isWin || isLinux ? '144px' : '15px'
|
||||
paddingRight: isWin || isLinux ? '144px' : '15px',
|
||||
minWidth: activeTopicOrSession === 'topic' ? '' : 'auto'
|
||||
}}
|
||||
className="home-navbar-right">
|
||||
<HStack alignItems="center" gap={6}>
|
||||
@@ -196,4 +220,15 @@ const NarrowIcon = styled(NavbarIcon)`
|
||||
}
|
||||
`
|
||||
|
||||
const ChatNavbarContainer: React.FC<{ children: React.ReactNode; style?: React.CSSProperties }> = ({
|
||||
children,
|
||||
style
|
||||
}) => {
|
||||
return (
|
||||
<div className="nodrag flex min-w-0 flex-1 items-center justify-start gap-1.5 overflow-hidden" style={style}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default HeaderNavbar
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { Alert, Spinner } from '@heroui/react'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { useAgents } from '@renderer/hooks/agents/useAgents'
|
||||
import { useApiServer } from '@renderer/hooks/useApiServer'
|
||||
import { useAssistants } from '@renderer/hooks/useAssistant'
|
||||
import { useAssistantPresets } from '@renderer/hooks/useAssistantPresets'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useAssistantsTabSortType } from '@renderer/hooks/useStore'
|
||||
import { useTags } from '@renderer/hooks/useTags'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { addIknowAction } from '@renderer/store/runtime'
|
||||
import { Assistant, AssistantsSortType } from '@renderer/types'
|
||||
import { FC, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { getErrorMessage } from '@renderer/utils'
|
||||
import { FC, useCallback, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
@@ -35,7 +36,8 @@ const AssistantsTab: FC<AssistantsTabProps> = (props) => {
|
||||
const { activeAssistant, setActiveAssistant, onCreateAssistant, onCreateDefaultAssistant } = props
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const { t } = useTranslation()
|
||||
const { apiServer } = useSettings()
|
||||
const { apiServerConfig, apiServerRunning } = useApiServer()
|
||||
const apiServerEnabled = apiServerConfig.enabled
|
||||
const { iknow, chat } = useRuntime()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
@@ -55,7 +57,7 @@ const AssistantsTab: FC<AssistantsTabProps> = (props) => {
|
||||
const { unifiedItems, handleUnifiedListReorder } = useUnifiedItems({
|
||||
agents,
|
||||
assistants,
|
||||
apiServerEnabled: apiServer.enabled,
|
||||
apiServerEnabled,
|
||||
agentsLoading,
|
||||
agentsError,
|
||||
updateAssistants
|
||||
@@ -72,18 +74,12 @@ const AssistantsTab: FC<AssistantsTabProps> = (props) => {
|
||||
unifiedItems,
|
||||
assistants,
|
||||
agents,
|
||||
apiServerEnabled: apiServer.enabled,
|
||||
apiServerEnabled,
|
||||
agentsLoading,
|
||||
agentsError,
|
||||
updateAssistants
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!agentsLoading && agents.length > 0 && !activeAgentId && apiServer.enabled) {
|
||||
setActiveAgentId(agents[0].id)
|
||||
}
|
||||
}, [agentsLoading, agents, activeAgentId, setActiveAgentId, apiServer.enabled])
|
||||
|
||||
const onDeleteAssistant = useCallback(
|
||||
(assistant: Assistant) => {
|
||||
const remaining = assistants.filter((a) => a.id !== assistant.id)
|
||||
@@ -105,7 +101,7 @@ const AssistantsTab: FC<AssistantsTabProps> = (props) => {
|
||||
|
||||
return (
|
||||
<Container className="assistants-tab" ref={containerRef}>
|
||||
{!apiServer.enabled && !iknow[ALERT_KEY] && (
|
||||
{!apiServerConfig.enabled && !apiServerRunning && !iknow[ALERT_KEY] && (
|
||||
<Alert
|
||||
color="warning"
|
||||
title={t('agent.warning.enable_server')}
|
||||
@@ -113,13 +109,22 @@ const AssistantsTab: FC<AssistantsTabProps> = (props) => {
|
||||
onClose={() => {
|
||||
dispatch(addIknowAction(ALERT_KEY))
|
||||
}}
|
||||
className="mb-2"
|
||||
/>
|
||||
)}
|
||||
|
||||
<UnifiedAddButton onCreateAssistant={onCreateAssistant} />
|
||||
|
||||
{agentsLoading && <Spinner />}
|
||||
{apiServer.enabled && agentsError && <Alert color="danger" title={t('agent.list.error.failed')} />}
|
||||
{apiServerConfig.enabled && !apiServerRunning && (
|
||||
<Alert color="danger" title={t('agent.server.error.not_running')} isClosable className="mb-2" />
|
||||
)}
|
||||
{apiServerRunning && agentsError && (
|
||||
<Alert
|
||||
color="danger"
|
||||
title={t('agent.list.error.failed')}
|
||||
description={getErrorMessage(agentsError)}
|
||||
className="mb-2"
|
||||
/>
|
||||
)}
|
||||
|
||||
{assistantsTabSortType === 'tags' ? (
|
||||
<UnifiedTagGroups
|
||||
@@ -165,6 +170,8 @@ const AssistantsTab: FC<AssistantsTabProps> = (props) => {
|
||||
/>
|
||||
)}
|
||||
|
||||
<UnifiedAddButton onCreateAssistant={onCreateAssistant} />
|
||||
|
||||
{!dragging && <div style={{ minHeight: 10 }}></div>}
|
||||
</Container>
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user