Compare commits
128 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ca44133e90 | |||
| b4810bb487 | |||
| dc0f9c5f08 | |||
| 595fd878a6 | |||
| 9d45991181 | |||
| cf2f2fd707 | |||
| d4b1db0407 | |||
| 8470e252d6 | |||
| 131444ac52 | |||
| ab3083f943 | |||
| 1e1d5c4a14 | |||
| c8ab0b9428 | |||
| 33ce41704d | |||
| 4eb3aa31ee | |||
| d1a9dfa3e6 | |||
| 0e5ebcfd00 | |||
| c4e0a6acfe | |||
| 2243bb2862 | |||
| 1f7d2fa93f | |||
| fb680ce764 | |||
| dc5bc64040 | |||
| 1c2ce7e0aa | |||
| a290ee7f39 | |||
| 79c697c34d | |||
| 76271cbf77 | |||
| 51dcdf94fb | |||
| 254051cf62 | |||
| 24d2e6e6ce | |||
| cf9bfce43c | |||
| 9e0ee24fd7 | |||
| 5eb2772d53 | |||
| f943f05cb1 | |||
| 96ce645064 | |||
| 1a972ac0e0 | |||
| 2e173631a0 | |||
| c457d4a868 | |||
| b74655651d | |||
| f27a481c3c | |||
| 4028b26c1d | |||
| 011b6f2df1 | |||
| 7b3b73d390 | |||
| 004d6d8201 | |||
| 7cf57adceb | |||
| 76bf78b810 | |||
| f4441e2a55 | |||
| 84f590ec7b | |||
| a5865cfd01 | |||
| 4e7c714ea2 | |||
| d2c4231458 | |||
| b5004e2a51 | |||
| e0c334b5ed | |||
| d482e661fb | |||
| 3ac1caca69 | |||
| 94c112c066 | |||
| 2e694a87f8 | |||
| 4ae30db53a | |||
| f4a6dd91cf | |||
| c08a570c27 | |||
| 9c318c9526 | |||
| 4cee09870a | |||
| 866e8e8734 | |||
| 80e1784777 | |||
| 0d760ffa2e | |||
| 88f7e6a854 | |||
| de37e2355d | |||
| f27b04c5b0 | |||
| a02b8f4609 | |||
| 7b90dfb46c | |||
| 26a9dba01a | |||
| a176814ad1 | |||
| ea51439aac | |||
| 162e33f478 | |||
| ee4c310725 | |||
| a000ff2a1a | |||
| 2f9576b2ae | |||
| 92554dd398 | |||
| 9473ddc762 | |||
| 5f469a71f3 | |||
| 87bac60afc | |||
| 704339e835 | |||
| c8ab7180ba | |||
| 11757546c3 | |||
| 420b9ec2f2 | |||
| 1c73271e33 | |||
| acdbe6b9ed | |||
| 6c201228d9 | |||
| 73b2a375ad | |||
| 89bb830b60 | |||
| 2399db4944 | |||
| 62774b34d3 | |||
| 654f19eaa9 | |||
| ce642f17d9 | |||
| d7bcd5a20e | |||
| 27903e7d9d | |||
| a8c0d0a684 | |||
| 5e33c89fe7 | |||
| 42849e4586 | |||
| 6a8544fb0e | |||
| 37f7042f0f | |||
| 65d066cbef | |||
| 504531d4d5 | |||
| d4b3428160 | |||
| cd881ceb34 | |||
| 68b37e66e9 | |||
| d6e7ed81ee | |||
| a9843b4128 | |||
| d4c6131fa3 | |||
| d2d5064eed | |||
| 8bec7640fa | |||
| fcf53f06ef | |||
| 2048f210e7 | |||
| 78eacccf6e | |||
| a436ab1d78 | |||
| 2aedbf5702 | |||
| b7e7174f3d | |||
| e7e5c0456f | |||
| 53e38ed1aa | |||
| f91e7da0a1 | |||
| 74db4c4646 | |||
| 1e4902b267 | |||
| 932b1d529a | |||
| 53046460ec | |||
| 38ac42af8c | |||
| 538291c03f | |||
| 142ad9e41e | |||
| 7250ce3514 | |||
| 02cf012671 | |||
| d11a2cd95c |
@@ -26,7 +26,7 @@ jobs:
|
|||||||
ref: ${{ github.event.pull_request.head.ref }}
|
ref: ${{ github.event.pull_request.head.ref }}
|
||||||
|
|
||||||
- name: 📦 Setting Node.js
|
- name: 📦 Setting Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v5
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
|
|||||||
@@ -16,10 +16,13 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
translate:
|
translate:
|
||||||
if: |
|
if: |
|
||||||
(github.event_name == 'issues') ||
|
(github.event_name == 'issues')
|
||||||
(github.event_name == 'issue_comment' && github.event.sender.type != 'Bot') ||
|
|| (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 == '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
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -29,7 +32,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
@@ -42,7 +45,7 @@ jobs:
|
|||||||
# See: https://github.com/anthropics/claude-code-action/blob/main/docs/security.md
|
# See: https://github.com/anthropics/claude-code-action/blob/main/docs/security.md
|
||||||
github_token: ${{ secrets.TOKEN_GITHUB_WRITE }}
|
github_token: ${{ secrets.TOKEN_GITHUB_WRITE }}
|
||||||
allowed_non_write_users: "*"
|
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/*)"
|
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: |
|
prompt: |
|
||||||
你是一个多语言翻译助手。你需要响应 GitHub Webhooks 中的以下四种事件:
|
你是一个多语言翻译助手。你需要响应 GitHub Webhooks 中的以下四种事件:
|
||||||
@@ -105,3 +108,5 @@ jobs:
|
|||||||
|
|
||||||
使用以下命令获取完整信息:
|
使用以下命令获取完整信息:
|
||||||
gh issue view ${{ github.event.issue.number }} --json title,body,comments
|
gh issue view ${{ github.event.issue.number }} --json title,body,comments
|
||||||
|
env:
|
||||||
|
ANTHROPIC_BASE_URL: ${{ secrets.CLAUDE_TRANSLATOR_BASEURL }}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ jobs:
|
|||||||
actions: read # Required for Claude to read CI results on PRs
|
actions: read # Required for Claude to read CI results on PRs
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ jobs:
|
|||||||
if: github.event.pull_request.merged == true && github.event.pull_request.head.repo.full_name == github.repository
|
if: github.event.pull_request.merged == true && github.event.pull_request.head.repo.full_name == github.repository
|
||||||
steps:
|
steps:
|
||||||
- name: Delete merged branch
|
- name: Delete merged branch
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@v8
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
github.rest.git.deleteRef({
|
github.rest.git.deleteRef({
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ jobs:
|
|||||||
ref: main
|
ref: main
|
||||||
|
|
||||||
- name: Install Node.js
|
- name: Install Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v5
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Install Node.js
|
- name: Install Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v5
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ jobs:
|
|||||||
npm version "$VERSION" --no-git-tag-version --allow-same-version
|
npm version "$VERSION" --no-git-tag-version --allow-same-version
|
||||||
|
|
||||||
- name: Install Node.js
|
- name: Install Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v5
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
diff --git a/dist/index.mjs b/dist/index.mjs
|
diff --git a/dist/index.mjs b/dist/index.mjs
|
||||||
index 110f37ec18c98b1d55ae2b73cc716194e6f9094d..17e109b7778cbebb904f1919e768d21a2833d965 100644
|
index 69ab1599c76801dc1167551b6fa283dded123466..f0af43bba7ad1196fe05338817e65b4ebda40955 100644
|
||||||
--- a/dist/index.mjs
|
--- a/dist/index.mjs
|
||||||
+++ b/dist/index.mjs
|
+++ b/dist/index.mjs
|
||||||
@@ -448,7 +448,7 @@ function convertToGoogleGenerativeAIMessages(prompt, options) {
|
@@ -477,7 +477,7 @@ function convertToGoogleGenerativeAIMessages(prompt, options) {
|
||||||
|
|
||||||
// src/get-model-path.ts
|
// src/get-model-path.ts
|
||||||
function getModelPath(modelId) {
|
function getModelPath(modelId) {
|
||||||
@@ -4,12 +4,12 @@ index 461e9a2ba246778261108a682762ffcf26f7224e..44bd667d9f591969d36a105ba5eb8b47
|
|||||||
+++ b/sdk.mjs
|
+++ b/sdk.mjs
|
||||||
@@ -6215,7 +6215,7 @@ function createAbortController(maxListeners = DEFAULT_MAX_LISTENERS) {
|
@@ -6215,7 +6215,7 @@ function createAbortController(maxListeners = DEFAULT_MAX_LISTENERS) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ../src/transport/ProcessTransport.ts
|
// ../src/transport/ProcessTransport.ts
|
||||||
-import { spawn } from "child_process";
|
-import { spawn } from "child_process";
|
||||||
+import { fork } from "child_process";
|
+import { fork } from "child_process";
|
||||||
import { createInterface } from "readline";
|
import { createInterface } from "readline";
|
||||||
|
|
||||||
// ../src/utils/fsOperations.ts
|
// ../src/utils/fsOperations.ts
|
||||||
@@ -6473,14 +6473,11 @@ class ProcessTransport {
|
@@ -6473,14 +6473,11 @@ class ProcessTransport {
|
||||||
const errorMessage = isNativeBinary(pathToClaudeCodeExecutable) ? `Claude Code native binary not found at ${pathToClaudeCodeExecutable}. Please ensure Claude Code is installed via native installer or specify a valid path with options.pathToClaudeCodeExecutable.` : `Claude Code executable not found at ${pathToClaudeCodeExecutable}. Is options.pathToClaudeCodeExecutable set?`;
|
const errorMessage = isNativeBinary(pathToClaudeCodeExecutable) ? `Claude Code native binary not found at ${pathToClaudeCodeExecutable}. Please ensure Claude Code is installed via native installer or specify a valid path with options.pathToClaudeCodeExecutable.` : `Claude Code executable not found at ${pathToClaudeCodeExecutable}. Is options.pathToClaudeCodeExecutable set?`;
|
||||||
@@ -19,7 +19,7 @@ index 461e9a2ba246778261108a682762ffcf26f7224e..44bd667d9f591969d36a105ba5eb8b47
|
|||||||
- const spawnCommand = isNative ? pathToClaudeCodeExecutable : executable;
|
- const spawnCommand = isNative ? pathToClaudeCodeExecutable : executable;
|
||||||
- const spawnArgs = isNative ? args : [...executableArgs, pathToClaudeCodeExecutable, ...args];
|
- const spawnArgs = isNative ? args : [...executableArgs, pathToClaudeCodeExecutable, ...args];
|
||||||
- this.logForDebugging(isNative ? `Spawning Claude Code native binary: ${pathToClaudeCodeExecutable} ${args.join(" ")}` : `Spawning Claude Code process: ${executable} ${[...executableArgs, pathToClaudeCodeExecutable, ...args].join(" ")}`);
|
- this.logForDebugging(isNative ? `Spawning Claude Code native binary: ${pathToClaudeCodeExecutable} ${args.join(" ")}` : `Spawning Claude Code process: ${executable} ${[...executableArgs, pathToClaudeCodeExecutable, ...args].join(" ")}`);
|
||||||
+ this.logDebug(`Forking Claude Code Node.js process: ${pathToClaudeCodeExecutable} ${args.join(" ")}`);
|
+ this.logForDebugging(`Forking Claude Code Node.js process: ${pathToClaudeCodeExecutable} ${args.join(" ")}`);
|
||||||
const stderrMode = env.DEBUG || stderr ? "pipe" : "ignore";
|
const stderrMode = env.DEBUG || stderr ? "pipe" : "ignore";
|
||||||
- this.child = spawn(spawnCommand, spawnArgs, {
|
- this.child = spawn(spawnCommand, spawnArgs, {
|
||||||
+ this.child = fork(pathToClaudeCodeExecutable, args, {
|
+ this.child = fork(pathToClaudeCodeExecutable, args, {
|
||||||
|
|||||||
@@ -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,7 +125,61 @@ afterSign: scripts/notarize.js
|
|||||||
artifactBuildCompleted: scripts/artifact-build-completed.js
|
artifactBuildCompleted: scripts/artifact-build-completed.js
|
||||||
releaseInfo:
|
releaseInfo:
|
||||||
releaseNotes: |
|
releaseNotes: |
|
||||||
Optimized note-taking feature, now able to quickly rename by modifying the title
|
<!--LANG:en-->
|
||||||
Fixed issue where CherryAI free model could not be used
|
What's New in v1.7.0-beta.2
|
||||||
Fixed issue where VertexAI proxy address could not be called normally
|
|
||||||
Fixed issue where built-in tools from service providers could not be called normally
|
New Features:
|
||||||
|
- Session Settings: Manage session-specific settings and model configurations independently
|
||||||
|
- Notes Full-Text Search: Search across all notes with match highlighting
|
||||||
|
- Built-in DiDi MCP Server: Integration with DiDi ride-hailing services (China only)
|
||||||
|
- Intel OV OCR: Hardware-accelerated OCR using Intel NPU
|
||||||
|
- Auto-start API Server: Automatically starts when agents exist
|
||||||
|
|
||||||
|
Improvements:
|
||||||
|
- Agent model selection now requires explicit user choice
|
||||||
|
- Added Mistral AI provider support
|
||||||
|
- Added NewAPI generic provider support
|
||||||
|
- Improved navbar layout consistency across different modes
|
||||||
|
- Enhanced chat component responsiveness
|
||||||
|
- Better code block display on small screens
|
||||||
|
- Updated OVMS to 2025.3 official release
|
||||||
|
- Added Greek language support
|
||||||
|
|
||||||
|
Bug Fixes:
|
||||||
|
- Fixed GitHub Copilot gpt-5-codex streaming issues
|
||||||
|
- Fixed assistant creation failures
|
||||||
|
- Fixed translate auto-copy functionality
|
||||||
|
- Fixed miniapps external link opening
|
||||||
|
- Fixed message layout and overflow issues
|
||||||
|
- Fixed API key parsing to preserve spaces
|
||||||
|
- Fixed agent display in different navbar layouts
|
||||||
|
|
||||||
|
<!--LANG:zh-CN-->
|
||||||
|
v1.7.0-beta.2 新特性
|
||||||
|
|
||||||
|
新功能:
|
||||||
|
- 会话设置:独立管理会话特定的设置和模型配置
|
||||||
|
- 笔记全文搜索:跨所有笔记搜索并高亮匹配内容
|
||||||
|
- 内置滴滴 MCP 服务器:集成滴滴打车服务(仅限中国地区)
|
||||||
|
- Intel OV OCR:使用 Intel NPU 的硬件加速 OCR
|
||||||
|
- 自动启动 API 服务器:当存在 Agent 时自动启动
|
||||||
|
|
||||||
|
改进:
|
||||||
|
- Agent 模型选择现在需要用户显式选择
|
||||||
|
- 添加 Mistral AI 提供商支持
|
||||||
|
- 添加 NewAPI 通用提供商支持
|
||||||
|
- 改进不同模式下的导航栏布局一致性
|
||||||
|
- 增强聊天组件响应式设计
|
||||||
|
- 优化小屏幕代码块显示
|
||||||
|
- 更新 OVMS 至 2025.3 正式版
|
||||||
|
- 添加希腊语支持
|
||||||
|
|
||||||
|
问题修复:
|
||||||
|
- 修复 GitHub Copilot gpt-5-codex 流式传输问题
|
||||||
|
- 修复助手创建失败
|
||||||
|
- 修复翻译自动复制功能
|
||||||
|
- 修复小程序外部链接打开
|
||||||
|
- 修复消息布局和溢出问题
|
||||||
|
- 修复 API 密钥解析以保留空格
|
||||||
|
- 修复不同导航栏布局中的 Agent 显示
|
||||||
|
<!--LANG:END-->
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import tseslint from '@electron-toolkit/eslint-config-ts'
|
|||||||
import eslint from '@eslint/js'
|
import eslint from '@eslint/js'
|
||||||
import eslintReact from '@eslint-react/eslint-plugin'
|
import eslintReact from '@eslint-react/eslint-plugin'
|
||||||
import { defineConfig } from 'eslint/config'
|
import { defineConfig } from 'eslint/config'
|
||||||
|
import importZod from 'eslint-plugin-import-zod'
|
||||||
import oxlint from 'eslint-plugin-oxlint'
|
import oxlint from 'eslint-plugin-oxlint'
|
||||||
import reactHooks from 'eslint-plugin-react-hooks'
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
import simpleImportSort from 'eslint-plugin-simple-import-sort'
|
import simpleImportSort from 'eslint-plugin-simple-import-sort'
|
||||||
@@ -11,11 +12,12 @@ export default defineConfig([
|
|||||||
eslint.configs.recommended,
|
eslint.configs.recommended,
|
||||||
tseslint.configs.recommended,
|
tseslint.configs.recommended,
|
||||||
eslintReact.configs['recommended-typescript'],
|
eslintReact.configs['recommended-typescript'],
|
||||||
reactHooks.configs['recommended-latest'],
|
reactHooks.configs.flat.recommended,
|
||||||
{
|
{
|
||||||
plugins: {
|
plugins: {
|
||||||
'simple-import-sort': simpleImportSort,
|
'simple-import-sort': simpleImportSort,
|
||||||
'unused-imports': unusedImports
|
'unused-imports': unusedImports,
|
||||||
|
'import-zod': importZod
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
@@ -25,6 +27,7 @@ export default defineConfig([
|
|||||||
'simple-import-sort/exports': 'error',
|
'simple-import-sort/exports': 'error',
|
||||||
'unused-imports/no-unused-imports': 'error',
|
'unused-imports/no-unused-imports': 'error',
|
||||||
'@eslint-react/no-prop-types': 'error',
|
'@eslint-react/no-prop-types': 'error',
|
||||||
|
'import-zod/prefer-zod-namespace': 'error'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Configuration for ensuring compatibility with the original ESLint(8.x) rules
|
// Configuration for ensuring compatibility with the original ESLint(8.x) rules
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "CherryStudio",
|
"name": "CherryStudio",
|
||||||
"version": "1.7.0-alpha.4",
|
"version": "1.7.0-beta.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "A powerful AI assistant for producer.",
|
"description": "A powerful AI assistant for producer.",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
@@ -83,6 +83,7 @@
|
|||||||
"@libsql/win32-x64-msvc": "^0.4.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",
|
"@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",
|
"@strongtz/win32-arm64-msvc": "^0.4.7",
|
||||||
|
"express": "^5.1.0",
|
||||||
"font-list": "^2.0.0",
|
"font-list": "^2.0.0",
|
||||||
"graceful-fs": "^4.2.11",
|
"graceful-fs": "^4.2.11",
|
||||||
"jsdom": "26.1.0",
|
"jsdom": "26.1.0",
|
||||||
@@ -92,6 +93,7 @@
|
|||||||
"selection-hook": "^1.0.12",
|
"selection-hook": "^1.0.12",
|
||||||
"sharp": "^0.34.3",
|
"sharp": "^0.34.3",
|
||||||
"swagger-jsdoc": "^6.2.8",
|
"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",
|
"tesseract.js": "patch:tesseract.js@npm%3A6.0.1#~/.yarn/patches/tesseract.js-npm-6.0.1-2562a7e46d.patch",
|
||||||
"turndown": "7.2.0"
|
"turndown": "7.2.0"
|
||||||
},
|
},
|
||||||
@@ -99,10 +101,10 @@
|
|||||||
"@agentic/exa": "^7.3.3",
|
"@agentic/exa": "^7.3.3",
|
||||||
"@agentic/searxng": "^7.3.3",
|
"@agentic/searxng": "^7.3.3",
|
||||||
"@agentic/tavily": "^7.3.3",
|
"@agentic/tavily": "^7.3.3",
|
||||||
"@ai-sdk/amazon-bedrock": "^3.0.21",
|
"@ai-sdk/amazon-bedrock": "^3.0.35",
|
||||||
"@ai-sdk/google-vertex": "^3.0.27",
|
"@ai-sdk/google-vertex": "^3.0.40",
|
||||||
"@ai-sdk/mistral": "^2.0.14",
|
"@ai-sdk/mistral": "^2.0.19",
|
||||||
"@ai-sdk/perplexity": "^2.0.9",
|
"@ai-sdk/perplexity": "^2.0.13",
|
||||||
"@ant-design/v5-patch-for-react-19": "^1.0.3",
|
"@ant-design/v5-patch-for-react-19": "^1.0.3",
|
||||||
"@anthropic-ai/sdk": "^0.41.0",
|
"@anthropic-ai/sdk": "^0.41.0",
|
||||||
"@anthropic-ai/vertex-sdk": "patch:@anthropic-ai/vertex-sdk@npm%3A0.11.4#~/.yarn/patches/@anthropic-ai-vertex-sdk-npm-0.11.4-c19cb41edb.patch",
|
"@anthropic-ai/vertex-sdk": "patch:@anthropic-ai/vertex-sdk@npm%3A0.11.4#~/.yarn/patches/@anthropic-ai-vertex-sdk-npm-0.11.4-c19cb41edb.patch",
|
||||||
@@ -152,6 +154,7 @@
|
|||||||
"@opentelemetry/sdk-trace-base": "^2.0.0",
|
"@opentelemetry/sdk-trace-base": "^2.0.0",
|
||||||
"@opentelemetry/sdk-trace-node": "^2.0.0",
|
"@opentelemetry/sdk-trace-node": "^2.0.0",
|
||||||
"@opentelemetry/sdk-trace-web": "^2.0.0",
|
"@opentelemetry/sdk-trace-web": "^2.0.0",
|
||||||
|
"@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",
|
"@playwright/test": "^1.52.0",
|
||||||
"@radix-ui/react-context-menu": "^2.2.16",
|
"@radix-ui/react-context-menu": "^2.2.16",
|
||||||
"@reduxjs/toolkit": "^2.2.5",
|
"@reduxjs/toolkit": "^2.2.5",
|
||||||
@@ -219,7 +222,7 @@
|
|||||||
"@viz-js/lang-dot": "^1.0.5",
|
"@viz-js/lang-dot": "^1.0.5",
|
||||||
"@viz-js/viz": "^3.14.0",
|
"@viz-js/viz": "^3.14.0",
|
||||||
"@xyflow/react": "^12.4.4",
|
"@xyflow/react": "^12.4.4",
|
||||||
"ai": "^5.0.44",
|
"ai": "^5.0.68",
|
||||||
"antd": "patch:antd@npm%3A5.27.0#~/.yarn/patches/antd-npm-5.27.0-aa91c36546.patch",
|
"antd": "patch:antd@npm%3A5.27.0#~/.yarn/patches/antd-npm-5.27.0-aa91c36546.patch",
|
||||||
"archiver": "^7.0.1",
|
"archiver": "^7.0.1",
|
||||||
"async-mutex": "^0.5.0",
|
"async-mutex": "^0.5.0",
|
||||||
@@ -244,7 +247,7 @@
|
|||||||
"dotenv-cli": "^7.4.2",
|
"dotenv-cli": "^7.4.2",
|
||||||
"drizzle-kit": "^0.31.4",
|
"drizzle-kit": "^0.31.4",
|
||||||
"drizzle-orm": "^0.44.5",
|
"drizzle-orm": "^0.44.5",
|
||||||
"electron": "37.4.0",
|
"electron": "37.6.0",
|
||||||
"electron-builder": "26.0.15",
|
"electron-builder": "26.0.15",
|
||||||
"electron-devtools-installer": "^3.2.0",
|
"electron-devtools-installer": "^3.2.0",
|
||||||
"electron-reload": "^2.0.0-alpha.1",
|
"electron-reload": "^2.0.0-alpha.1",
|
||||||
@@ -256,11 +259,11 @@
|
|||||||
"emoji-picker-element": "^1.22.1",
|
"emoji-picker-element": "^1.22.1",
|
||||||
"epub": "patch:epub@npm%3A1.3.0#~/.yarn/patches/epub-npm-1.3.0-8325494ffe.patch",
|
"epub": "patch:epub@npm%3A1.3.0#~/.yarn/patches/epub-npm-1.3.0-8325494ffe.patch",
|
||||||
"eslint": "^9.22.0",
|
"eslint": "^9.22.0",
|
||||||
|
"eslint-plugin-import-zod": "^1.2.0",
|
||||||
"eslint-plugin-oxlint": "^1.15.0",
|
"eslint-plugin-oxlint": "^1.15.0",
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^7.0.0",
|
||||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||||
"eslint-plugin-unused-imports": "^4.1.4",
|
"eslint-plugin-unused-imports": "^4.1.4",
|
||||||
"express": "^5.1.0",
|
|
||||||
"express-validator": "^7.2.1",
|
"express-validator": "^7.2.1",
|
||||||
"fast-diff": "^1.3.0",
|
"fast-diff": "^1.3.0",
|
||||||
"fast-xml-parser": "^5.2.0",
|
"fast-xml-parser": "^5.2.0",
|
||||||
@@ -294,15 +297,15 @@
|
|||||||
"notion-helper": "^1.3.22",
|
"notion-helper": "^1.3.22",
|
||||||
"npx-scope-finder": "^1.2.0",
|
"npx-scope-finder": "^1.2.0",
|
||||||
"openai": "patch:openai@npm%3A5.12.2#~/.yarn/patches/openai-npm-5.12.2-30b075401c.patch",
|
"openai": "patch:openai@npm%3A5.12.2#~/.yarn/patches/openai-npm-5.12.2-30b075401c.patch",
|
||||||
"oxlint": "^1.15.0",
|
"oxlint": "^1.22.0",
|
||||||
"oxlint-tsgolint": "^0.2.0",
|
"oxlint-tsgolint": "^0.2.0",
|
||||||
"p-queue": "^8.1.0",
|
"p-queue": "^8.1.0",
|
||||||
"pdf-lib": "^1.17.1",
|
"pdf-lib": "^1.17.1",
|
||||||
"pdf-parse": "^1.1.1",
|
"pdf-parse": "^1.1.1",
|
||||||
"playwright": "^1.52.0",
|
"playwright": "^1.52.0",
|
||||||
"proxy-agent": "^6.5.0",
|
"proxy-agent": "^6.5.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-error-boundary": "^6.0.0",
|
"react-error-boundary": "^6.0.0",
|
||||||
"react-hotkeys-hook": "^4.6.1",
|
"react-hotkeys-hook": "^4.6.1",
|
||||||
"react-i18next": "^14.1.2",
|
"react-i18next": "^14.1.2",
|
||||||
@@ -334,7 +337,6 @@
|
|||||||
"string-width": "^7.2.0",
|
"string-width": "^7.2.0",
|
||||||
"striptags": "^3.2.0",
|
"striptags": "^3.2.0",
|
||||||
"styled-components": "^6.1.11",
|
"styled-components": "^6.1.11",
|
||||||
"swagger-ui-express": "^5.0.1",
|
|
||||||
"swr": "^2.3.6",
|
"swr": "^2.3.6",
|
||||||
"tailwindcss": "^4.1.13",
|
"tailwindcss": "^4.1.13",
|
||||||
"tar": "^7.4.3",
|
"tar": "^7.4.3",
|
||||||
@@ -370,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.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",
|
"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",
|
"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",
|
"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",
|
"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",
|
"node-abi": "4.12.0",
|
||||||
@@ -377,10 +380,11 @@
|
|||||||
"openai@npm:^4.87.3": "patch:openai@npm%3A5.12.2#~/.yarn/patches/openai-npm-5.12.2-30b075401c.patch",
|
"openai@npm:^4.87.3": "patch:openai@npm%3A5.12.2#~/.yarn/patches/openai-npm-5.12.2-30b075401c.patch",
|
||||||
"pdf-parse@npm:1.1.1": "patch:pdf-parse@npm%3A1.1.1#~/.yarn/patches/pdf-parse-npm-1.1.1-04a6109b2a.patch",
|
"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",
|
"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",
|
"undici": "6.21.2",
|
||||||
"vite": "npm:rolldown-vite@latest",
|
"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",
|
"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.14": "patch:@ai-sdk/google@npm%3A2.0.14#~/.yarn/patches/@ai-sdk-google-npm-2.0.14-376d8b03cc.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",
|
"packageManager": "yarn@4.9.1",
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@cherrystudio/ai-core",
|
"name": "@cherrystudio/ai-core",
|
||||||
"version": "1.0.0-alpha.18",
|
"version": "1.0.1",
|
||||||
"description": "Cherry Studio AI Core - Unified AI Provider Interface Based on Vercel AI SDK",
|
"description": "Cherry Studio AI Core - Unified AI Provider Interface Based on Vercel AI SDK",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"module": "dist/index.mjs",
|
"module": "dist/index.mjs",
|
||||||
@@ -36,14 +36,14 @@
|
|||||||
"ai": "^5.0.26"
|
"ai": "^5.0.26"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "^2.0.17",
|
"@ai-sdk/anthropic": "^2.0.27",
|
||||||
"@ai-sdk/azure": "^2.0.30",
|
"@ai-sdk/azure": "^2.0.49",
|
||||||
"@ai-sdk/deepseek": "^1.0.17",
|
"@ai-sdk/deepseek": "^1.0.23",
|
||||||
"@ai-sdk/openai": "^2.0.30",
|
"@ai-sdk/openai": "^2.0.48",
|
||||||
"@ai-sdk/openai-compatible": "^1.0.17",
|
"@ai-sdk/openai-compatible": "^1.0.22",
|
||||||
"@ai-sdk/provider": "^2.0.0",
|
"@ai-sdk/provider": "^2.0.0",
|
||||||
"@ai-sdk/provider-utils": "^3.0.9",
|
"@ai-sdk/provider-utils": "^3.0.12",
|
||||||
"@ai-sdk/xai": "^2.0.18",
|
"@ai-sdk/xai": "^2.0.26",
|
||||||
"zod": "^4.1.5"
|
"zod": "^4.1.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { anthropic } from '@ai-sdk/anthropic'
|
import { anthropic } from '@ai-sdk/anthropic'
|
||||||
import { google } from '@ai-sdk/google'
|
import { google } from '@ai-sdk/google'
|
||||||
import { openai } from '@ai-sdk/openai'
|
import { openai } from '@ai-sdk/openai'
|
||||||
import { InferToolInput, InferToolOutput } from 'ai'
|
import { InferToolInput, InferToolOutput, type Tool } from 'ai'
|
||||||
|
|
||||||
import { ProviderOptionsMap } from '../../../options/types'
|
import { ProviderOptionsMap } from '../../../options/types'
|
||||||
import { OpenRouterSearchConfig } from './openrouter'
|
import { OpenRouterSearchConfig } from './openrouter'
|
||||||
@@ -15,6 +15,13 @@ export type AnthropicSearchConfig = NonNullable<Parameters<typeof anthropic.tool
|
|||||||
export type GoogleSearchConfig = NonNullable<Parameters<typeof google.tools.googleSearch>[0]>
|
export type GoogleSearchConfig = NonNullable<Parameters<typeof google.tools.googleSearch>[0]>
|
||||||
export type XAISearchConfig = NonNullable<ProviderOptionsMap['xai']['searchParameters']>
|
export type XAISearchConfig = NonNullable<ProviderOptionsMap['xai']['searchParameters']>
|
||||||
|
|
||||||
|
type NormalizeTool<T> = T extends Tool<infer INPUT, infer OUTPUT> ? Tool<INPUT, OUTPUT> : Tool<any, any>
|
||||||
|
|
||||||
|
type AnthropicWebSearchTool = NormalizeTool<ReturnType<typeof anthropic.tools.webSearch_20250305>>
|
||||||
|
type OpenAIWebSearchTool = NormalizeTool<ReturnType<typeof openai.tools.webSearch>>
|
||||||
|
type OpenAIChatWebSearchTool = NormalizeTool<ReturnType<typeof openai.tools.webSearchPreview>>
|
||||||
|
type GoogleWebSearchTool = NormalizeTool<ReturnType<typeof google.tools.googleSearch>>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 插件初始化时接收的完整配置对象
|
* 插件初始化时接收的完整配置对象
|
||||||
*
|
*
|
||||||
@@ -59,7 +66,7 @@ export const DEFAULT_WEB_SEARCH_CONFIG: WebSearchPluginConfig = {
|
|||||||
|
|
||||||
export type WebSearchToolOutputSchema = {
|
export type WebSearchToolOutputSchema = {
|
||||||
// Anthropic 工具 - 手动定义
|
// Anthropic 工具 - 手动定义
|
||||||
anthropic: InferToolOutput<ReturnType<typeof anthropic.tools.webSearch_20250305>>
|
anthropic: InferToolOutput<AnthropicWebSearchTool>
|
||||||
|
|
||||||
// OpenAI 工具 - 基于实际输出
|
// OpenAI 工具 - 基于实际输出
|
||||||
// TODO: 上游定义不规范,是unknown
|
// TODO: 上游定义不规范,是unknown
|
||||||
@@ -82,8 +89,8 @@ export type WebSearchToolOutputSchema = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type WebSearchToolInputSchema = {
|
export type WebSearchToolInputSchema = {
|
||||||
anthropic: InferToolInput<ReturnType<typeof anthropic.tools.webSearch_20250305>>
|
anthropic: InferToolInput<AnthropicWebSearchTool>
|
||||||
openai: InferToolInput<ReturnType<typeof openai.tools.webSearch>>
|
openai: InferToolInput<OpenAIWebSearchTool>
|
||||||
google: InferToolInput<ReturnType<typeof google.tools.googleSearch>>
|
google: InferToolInput<GoogleWebSearchTool>
|
||||||
'openai-chat': InferToolInput<ReturnType<typeof openai.tools.webSearchPreview>>
|
'openai-chat': InferToolInput<OpenAIChatWebSearchTool>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { LanguageModelV2 } from '@ai-sdk/provider'
|
|||||||
import { createXai } from '@ai-sdk/xai'
|
import { createXai } from '@ai-sdk/xai'
|
||||||
import { createOpenRouter } from '@openrouter/ai-sdk-provider'
|
import { createOpenRouter } from '@openrouter/ai-sdk-provider'
|
||||||
import { customProvider, Provider } from 'ai'
|
import { customProvider, Provider } from 'ai'
|
||||||
import { z } from 'zod'
|
import * as z from 'zod'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 基础 Provider IDs
|
* 基础 Provider IDs
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ export enum IpcChannel {
|
|||||||
App_SetLanguage = 'app:set-language',
|
App_SetLanguage = 'app:set-language',
|
||||||
App_SetEnableSpellCheck = 'app:set-enable-spell-check',
|
App_SetEnableSpellCheck = 'app:set-enable-spell-check',
|
||||||
App_SetSpellCheckLanguages = 'app:set-spell-check-languages',
|
App_SetSpellCheckLanguages = 'app:set-spell-check-languages',
|
||||||
App_ShowUpdateDialog = 'app:show-update-dialog',
|
|
||||||
App_CheckForUpdate = 'app:check-for-update',
|
App_CheckForUpdate = 'app:check-for-update',
|
||||||
|
App_QuitAndInstall = 'app:quit-and-install',
|
||||||
App_Reload = 'app:reload',
|
App_Reload = 'app:reload',
|
||||||
App_Quit = 'app:quit',
|
App_Quit = 'app:quit',
|
||||||
App_Info = 'app:info',
|
App_Info = 'app:info',
|
||||||
@@ -53,6 +53,7 @@ export enum IpcChannel {
|
|||||||
|
|
||||||
Webview_SetOpenLinkExternal = 'webview:set-open-link-external',
|
Webview_SetOpenLinkExternal = 'webview:set-open-link-external',
|
||||||
Webview_SetSpellCheckEnabled = 'webview:set-spell-check-enabled',
|
Webview_SetSpellCheckEnabled = 'webview:set-spell-check-enabled',
|
||||||
|
Webview_SearchHotkey = 'webview:search-hotkey',
|
||||||
|
|
||||||
// Open
|
// Open
|
||||||
Open_Path = 'open:path',
|
Open_Path = 'open:path',
|
||||||
@@ -234,7 +235,6 @@ export enum IpcChannel {
|
|||||||
// events
|
// events
|
||||||
BackupProgress = 'backup-progress',
|
BackupProgress = 'backup-progress',
|
||||||
ThemeUpdated = 'theme:updated',
|
ThemeUpdated = 'theme:updated',
|
||||||
UpdateDownloadedCancelled = 'update-downloaded-cancelled',
|
|
||||||
RestoreProgress = 'restore-progress',
|
RestoreProgress = 'restore-progress',
|
||||||
UpdateError = 'update-error',
|
UpdateError = 'update-error',
|
||||||
UpdateAvailable = 'update-available',
|
UpdateAvailable = 'update-available',
|
||||||
@@ -317,6 +317,7 @@ export enum IpcChannel {
|
|||||||
ApiServer_Stop = 'api-server:stop',
|
ApiServer_Stop = 'api-server:stop',
|
||||||
ApiServer_Restart = 'api-server:restart',
|
ApiServer_Restart = 'api-server:restart',
|
||||||
ApiServer_GetStatus = 'api-server:get-status',
|
ApiServer_GetStatus = 'api-server:get-status',
|
||||||
|
// NOTE: This api is not be used.
|
||||||
ApiServer_GetConfig = 'api-server:get-config',
|
ApiServer_GetConfig = 'api-server:get-config',
|
||||||
|
|
||||||
// Anthropic OAuth
|
// Anthropic OAuth
|
||||||
@@ -336,6 +337,7 @@ export enum IpcChannel {
|
|||||||
|
|
||||||
// OCR
|
// OCR
|
||||||
OCR_ocr = 'ocr:ocr',
|
OCR_ocr = 'ocr:ocr',
|
||||||
|
OCR_ListProviders = 'ocr:list-providers',
|
||||||
|
|
||||||
// OVMS
|
// OVMS
|
||||||
Ovms_AddModel = 'ovms:add-model',
|
Ovms_AddModel = 'ovms:add-model',
|
||||||
|
|||||||
@@ -12,8 +12,19 @@ import Anthropic from '@anthropic-ai/sdk'
|
|||||||
import { TextBlockParam } from '@anthropic-ai/sdk/resources'
|
import { TextBlockParam } from '@anthropic-ai/sdk/resources'
|
||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
import { Provider } from '@types'
|
import { Provider } from '@types'
|
||||||
|
import type { ModelMessage } from 'ai'
|
||||||
|
|
||||||
const logger = loggerService.withContext('anthropic-sdk')
|
const logger = loggerService.withContext('anthropic-sdk')
|
||||||
|
|
||||||
|
const defaultClaudeCodeSystemPrompt = `You are Claude Code, Anthropic's official CLI for Claude.`
|
||||||
|
|
||||||
|
const defaultClaudeCodeSystem: Array<TextBlockParam> = [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: defaultClaudeCodeSystemPrompt
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates and configures an Anthropic SDK client based on the provider configuration.
|
* Creates and configures an Anthropic SDK client based on the provider configuration.
|
||||||
*
|
*
|
||||||
@@ -44,7 +55,11 @@ const logger = loggerService.withContext('anthropic-sdk')
|
|||||||
* const apiKeyClient = getSdkClient(apiKeyProvider);
|
* const apiKeyClient = getSdkClient(apiKeyProvider);
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export function getSdkClient(provider: Provider, oauthToken?: string | null): Anthropic {
|
export function getSdkClient(
|
||||||
|
provider: Provider,
|
||||||
|
oauthToken?: string | null,
|
||||||
|
extraHeaders?: Record<string, string | string[]>
|
||||||
|
): Anthropic {
|
||||||
if (provider.authType === 'oauth') {
|
if (provider.authType === 'oauth') {
|
||||||
if (!oauthToken) {
|
if (!oauthToken) {
|
||||||
throw new Error('OAuth token is not available')
|
throw new Error('OAuth token is not available')
|
||||||
@@ -68,7 +83,8 @@ export function getSdkClient(provider: Provider, oauthToken?: string | null): An
|
|||||||
'x-stainless-os': 'MacOS',
|
'x-stainless-os': 'MacOS',
|
||||||
'x-stainless-arch': 'arm64',
|
'x-stainless-arch': 'arm64',
|
||||||
'x-stainless-runtime': 'node',
|
'x-stainless-runtime': 'node',
|
||||||
'x-stainless-runtime-version': 'v22.18.0'
|
'x-stainless-runtime-version': 'v22.18.0',
|
||||||
|
...extraHeaders
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -87,7 +103,8 @@ export function getSdkClient(provider: Provider, oauthToken?: string | null): An
|
|||||||
defaultHeaders: {
|
defaultHeaders: {
|
||||||
'anthropic-beta': 'output-128k-2025-02-19',
|
'anthropic-beta': 'output-128k-2025-02-19',
|
||||||
'APP-Code': 'MLTG2087',
|
'APP-Code': 'MLTG2087',
|
||||||
...provider.extra_headers
|
...provider.extra_headers,
|
||||||
|
...extraHeaders
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -118,53 +135,36 @@ export function getSdkClient(provider: Provider, oauthToken?: string | null): An
|
|||||||
* @param system - Optional user-provided system message (string or TextBlockParam array)
|
* @param system - Optional user-provided system message (string or TextBlockParam array)
|
||||||
* @returns Combined system message with Claude Code prompt prepended
|
* @returns Combined system message with Claude Code prompt prepended
|
||||||
*
|
*
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* // No system message
|
|
||||||
* const result1 = buildClaudeCodeSystemMessage();
|
|
||||||
* // Returns: "You are Claude Code, Anthropic's official CLI for Claude."
|
|
||||||
*
|
|
||||||
* // String system message
|
|
||||||
* const result2 = buildClaudeCodeSystemMessage("You are a helpful assistant.");
|
|
||||||
* // Returns: [
|
|
||||||
* // { type: 'text', text: "You are Claude Code, Anthropic's official CLI for Claude." },
|
|
||||||
* // { type: 'text', text: "You are a helpful assistant." }
|
|
||||||
* // ]
|
|
||||||
*
|
|
||||||
* // Array system message
|
|
||||||
* const systemArray = [{ type: 'text', text: 'Custom instructions' }];
|
|
||||||
* const result3 = buildClaudeCodeSystemMessage(systemArray);
|
|
||||||
* // Returns: Array with Claude Code message prepended
|
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export function buildClaudeCodeSystemMessage(system?: string | Array<TextBlockParam>): string | Array<TextBlockParam> {
|
export function buildClaudeCodeSystemMessage(system?: string | Array<TextBlockParam>): Array<TextBlockParam> {
|
||||||
const defaultClaudeCodeSystem = `You are Claude Code, Anthropic's official CLI for Claude.`
|
|
||||||
if (!system) {
|
if (!system) {
|
||||||
return defaultClaudeCodeSystem
|
return defaultClaudeCodeSystem
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof system === 'string') {
|
if (typeof system === 'string') {
|
||||||
if (system.trim() === defaultClaudeCodeSystem) {
|
if (system.trim() === defaultClaudeCodeSystemPrompt || system.trim() === '') {
|
||||||
return system
|
return defaultClaudeCodeSystem
|
||||||
|
} else {
|
||||||
|
return [...defaultClaudeCodeSystem, { type: 'text', text: system }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Array.isArray(system)) {
|
||||||
|
const firstSystem = system[0]
|
||||||
|
if (firstSystem.type === 'text' && firstSystem.text.trim() === defaultClaudeCodeSystemPrompt) {
|
||||||
|
return system
|
||||||
|
} else {
|
||||||
|
return [...defaultClaudeCodeSystem, ...system]
|
||||||
}
|
}
|
||||||
return [
|
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
text: defaultClaudeCodeSystem
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
text: system
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (system[0].text.trim() != defaultClaudeCodeSystem) {
|
return defaultClaudeCodeSystem
|
||||||
system.unshift({
|
}
|
||||||
type: 'text',
|
|
||||||
text: defaultClaudeCodeSystem
|
export function buildClaudeCodeSystemModelMessage(system?: string | Array<TextBlockParam>): Array<ModelMessage> {
|
||||||
})
|
const textBlocks = buildClaudeCodeSystemMessage(system)
|
||||||
}
|
return textBlocks.map((block) => ({
|
||||||
|
role: 'system',
|
||||||
return system
|
content: block.text
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -217,7 +217,8 @@ export enum codeTools {
|
|||||||
claudeCode = 'claude-code',
|
claudeCode = 'claude-code',
|
||||||
geminiCli = 'gemini-cli',
|
geminiCli = 'gemini-cli',
|
||||||
openaiCodex = 'openai-codex',
|
openaiCodex = 'openai-codex',
|
||||||
iFlowCli = 'iflow-cli'
|
iFlowCli = 'iflow-cli',
|
||||||
|
githubCopilotCli = 'github-copilot-cli'
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum terminalApps {
|
export enum terminalApps {
|
||||||
|
|||||||
@@ -22,3 +22,12 @@ export type MCPProgressEvent = {
|
|||||||
callId: string
|
callId: string
|
||||||
progress: number // 0-1 range
|
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')
|
const { downloadWithPowerShell } = require('./download')
|
||||||
|
|
||||||
// Base URL for downloading OVMS binaries
|
// Base URL for downloading OVMS binaries
|
||||||
const OVMS_PKG_NAME = 'ovms250911.zip'
|
const OVMS_RELEASE_BASE_URL =
|
||||||
const OVMS_RELEASE_BASE_URL = [`https://gitcode.com/gcw_ggDjjkY3/kjfile/releases/download/download/${OVMS_PKG_NAME}`]
|
'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')
|
const csDir = path.join(os.homedir(), '.cherrystudio')
|
||||||
|
|
||||||
// Ensure directories exist
|
|
||||||
fs.mkdirSync(csDir, { recursive: true })
|
|
||||||
|
|
||||||
const csOvmsDir = path.join(csDir, 'ovms')
|
const csOvmsDir = path.join(csDir, 'ovms')
|
||||||
// Delete existing OVMS directory if it exists
|
|
||||||
if (fs.existsSync(csOvmsDir)) {
|
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 {
|
try {
|
||||||
console.log(`Downloading OVMS from ${downloadUrl} to ${tempFilename}...`)
|
fs.rmSync(csOvmsDir, { recursive: true })
|
||||||
|
|
||||||
// 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
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`Download failed from ${downloadUrl}: ${error.message}`)
|
console.warn(`Failed to clean up old OVMS installation: ${error.message}`)
|
||||||
lastError = error
|
return 110
|
||||||
|
|
||||||
// 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...`)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if any download succeeded
|
return 0
|
||||||
if (!downloadSuccess) {
|
}
|
||||||
console.error(`All download URLs failed. Last error: ${lastError?.message || 'Unknown error'}`)
|
|
||||||
|
/**
|
||||||
|
* 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
|
return 103
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// unzip the base package to the target directory
|
||||||
console.log(`Extracting to ${csDir}...`)
|
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
|
// Use tar.exe to extract the ZIP file
|
||||||
console.log(`Extracting OVMS to ${csDir}...`)
|
console.log(`Extracting OVMS Base to ${csOvmsDir}...`)
|
||||||
execSync(`tar -xf ${tempFilename} -C ${csDir}`, { stdio: 'inherit' })
|
execSync(`tar -xf ${tempFilename} -C ${csOvmsDir}`, { stdio: 'inherit' })
|
||||||
console.log(`OVMS extracted to ${csDir}`)
|
console.log(`OVMS extracted to ${csOvmsDir}`)
|
||||||
|
|
||||||
// Clean up temporary file
|
// Clean up temporary file
|
||||||
fs.unlinkSync(tempFilename)
|
fs.unlinkSync(tempFilename)
|
||||||
console.log(`Installation directory: ${csDir}`)
|
console.log(`Installation directory: ${csDir}`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error installing OVMS: ${error.message}`)
|
console.error(`Error installing OVMS: ${error.message}`)
|
||||||
if (fs.existsSync(tempFilename)) {
|
fs.unlinkSync(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
|
|
||||||
}
|
|
||||||
|
|
||||||
return 104
|
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
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,7 +224,27 @@ async function installOvms() {
|
|||||||
return 102
|
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
|
// Run the installation
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { MessageCreateParams } from '@anthropic-ai/sdk/resources'
|
import { MessageCreateParams } from '@anthropic-ai/sdk/resources'
|
||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
|
import { Provider } from '@types'
|
||||||
import express, { Request, Response } from 'express'
|
import express, { Request, Response } from 'express'
|
||||||
|
|
||||||
import { Provider } from '../../../renderer/src/types/provider'
|
import { messagesService } from '../services/messages'
|
||||||
import { MessagesService, messagesService } from '../services/messages'
|
|
||||||
import { getProviderById, validateModelId } from '../utils'
|
import { getProviderById, validateModelId } from '../utils'
|
||||||
|
|
||||||
const logger = loggerService.withContext('ApiServerMessagesRoutes')
|
const logger = loggerService.withContext('ApiServerMessagesRoutes')
|
||||||
@@ -11,7 +11,7 @@ const logger = loggerService.withContext('ApiServerMessagesRoutes')
|
|||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
const providerRouter = express.Router({ mergeParams: true })
|
const providerRouter = express.Router({ mergeParams: true })
|
||||||
|
|
||||||
// Helper functions for shared logic
|
// Helper function for basic request validation
|
||||||
async function validateRequestBody(req: Request): Promise<{ valid: boolean; error?: any }> {
|
async function validateRequestBody(req: Request): Promise<{ valid: boolean; error?: any }> {
|
||||||
const request: MessageCreateParams = req.body
|
const request: MessageCreateParams = req.body
|
||||||
|
|
||||||
@@ -31,157 +31,53 @@ async function validateRequestBody(req: Request): Promise<{ valid: boolean; erro
|
|||||||
return { valid: true }
|
return { valid: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleStreamingResponse(
|
interface HandleMessageProcessingOptions {
|
||||||
res: Response,
|
req: Request
|
||||||
request: MessageCreateParams,
|
res: Response
|
||||||
provider: Provider,
|
provider: Provider
|
||||||
messagesService: MessagesService
|
request: MessageCreateParams
|
||||||
): Promise<void> {
|
|
||||||
res.setHeader('Content-Type', 'text/event-stream; charset=utf-8')
|
|
||||||
res.setHeader('Cache-Control', 'no-cache, no-transform')
|
|
||||||
res.setHeader('Connection', 'keep-alive')
|
|
||||||
res.setHeader('X-Accel-Buffering', 'no')
|
|
||||||
res.flushHeaders()
|
|
||||||
const flushableResponse = res as Response & { flush?: () => void }
|
|
||||||
const flushStream = () => {
|
|
||||||
if (typeof flushableResponse.flush !== 'function') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
flushableResponse.flush()
|
|
||||||
} catch (flushError: unknown) {
|
|
||||||
logger.warn('Failed to flush streaming response', {
|
|
||||||
error: flushError
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
for await (const chunk of messagesService.processStreamingMessage(request, provider)) {
|
|
||||||
res.write(`event: ${chunk.type}\n`)
|
|
||||||
res.write(`data: ${JSON.stringify(chunk)}\n\n`)
|
|
||||||
flushStream()
|
|
||||||
}
|
|
||||||
res.write('data: [DONE]\n\n')
|
|
||||||
flushStream()
|
|
||||||
} catch (streamError: any) {
|
|
||||||
logger.error('Stream error', {
|
|
||||||
error: streamError,
|
|
||||||
provider: provider.id,
|
|
||||||
model: request.model,
|
|
||||||
apiHost: provider.apiHost,
|
|
||||||
anthropicApiHost: provider.anthropicApiHost
|
|
||||||
})
|
|
||||||
res.write(
|
|
||||||
`data: ${JSON.stringify({
|
|
||||||
type: 'error',
|
|
||||||
error: {
|
|
||||||
type: 'api_error',
|
|
||||||
message: 'Stream processing error'
|
|
||||||
}
|
|
||||||
})}\n\n`
|
|
||||||
)
|
|
||||||
} finally {
|
|
||||||
res.end()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleErrorResponse(res: Response, error: any): Response {
|
|
||||||
logger.error('Message processing error', { error })
|
|
||||||
|
|
||||||
let statusCode = 500
|
|
||||||
let errorType = 'api_error'
|
|
||||||
let errorMessage = 'Internal server error'
|
|
||||||
|
|
||||||
const anthropicStatus = typeof error?.status === 'number' ? error.status : undefined
|
|
||||||
const anthropicError = error?.error
|
|
||||||
|
|
||||||
if (anthropicStatus) {
|
|
||||||
statusCode = anthropicStatus
|
|
||||||
}
|
|
||||||
|
|
||||||
if (anthropicError?.type) {
|
|
||||||
errorType = anthropicError.type
|
|
||||||
}
|
|
||||||
|
|
||||||
if (anthropicError?.message) {
|
|
||||||
errorMessage = anthropicError.message
|
|
||||||
} else if (error instanceof Error && error.message) {
|
|
||||||
errorMessage = error.message
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!anthropicStatus && error instanceof Error) {
|
|
||||||
if (error.message.includes('API key') || error.message.includes('authentication')) {
|
|
||||||
statusCode = 401
|
|
||||||
errorType = 'authentication_error'
|
|
||||||
} else if (error.message.includes('rate limit') || error.message.includes('quota')) {
|
|
||||||
statusCode = 429
|
|
||||||
errorType = 'rate_limit_error'
|
|
||||||
} else if (error.message.includes('timeout') || error.message.includes('connection')) {
|
|
||||||
statusCode = 502
|
|
||||||
errorType = 'api_error'
|
|
||||||
} else if (error.message.includes('validation') || error.message.includes('invalid')) {
|
|
||||||
statusCode = 400
|
|
||||||
errorType = 'invalid_request_error'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(statusCode).json({
|
|
||||||
type: 'error',
|
|
||||||
error: {
|
|
||||||
type: errorType,
|
|
||||||
message: errorMessage,
|
|
||||||
requestId: error?.request_id
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function processMessageRequest(
|
|
||||||
req: Request,
|
|
||||||
res: Response,
|
|
||||||
provider: Provider,
|
|
||||||
modelId?: string
|
modelId?: string
|
||||||
): Promise<Response | void> {
|
}
|
||||||
|
|
||||||
|
async function handleMessageProcessing({
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
provider,
|
||||||
|
request,
|
||||||
|
modelId
|
||||||
|
}: HandleMessageProcessingOptions): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const request: MessageCreateParams = req.body
|
|
||||||
|
|
||||||
// Use provided modelId or keep original model
|
|
||||||
if (modelId) {
|
|
||||||
request.model = modelId
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate request
|
|
||||||
const validation = messagesService.validateRequest(request)
|
const validation = messagesService.validateRequest(request)
|
||||||
if (!validation.isValid) {
|
if (!validation.isValid) {
|
||||||
return res.status(400).json({
|
res.status(400).json({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
error: {
|
error: {
|
||||||
type: 'invalid_request_error',
|
type: 'invalid_request_error',
|
||||||
message: validation.errors.join('; ')
|
message: validation.errors.join('; ')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
logger.info('Processing anthropic messages request', {
|
|
||||||
provider: provider.id,
|
|
||||||
apiHost: provider.apiHost,
|
|
||||||
anthropicApiHost: provider.anthropicApiHost,
|
|
||||||
model: request.model,
|
|
||||||
stream: request.stream,
|
|
||||||
thinking: request.thinking
|
|
||||||
})
|
|
||||||
|
|
||||||
// Handle streaming
|
|
||||||
if (request.stream) {
|
|
||||||
await handleStreamingResponse(res, request, provider, messagesService)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle non-streaming
|
const extraHeaders = messagesService.prepareHeaders(req.headers)
|
||||||
const response = await messagesService.processMessage(request, provider)
|
const { client, anthropicRequest } = await messagesService.processMessage({
|
||||||
return res.json(response)
|
provider,
|
||||||
|
request,
|
||||||
|
extraHeaders,
|
||||||
|
modelId
|
||||||
|
})
|
||||||
|
|
||||||
|
if (request.stream) {
|
||||||
|
await messagesService.handleStreaming(client, anthropicRequest, { response: res }, provider)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await client.messages.create(anthropicRequest)
|
||||||
|
res.json(response)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return handleErrorResponse(res, error)
|
logger.error('Message processing error', { error })
|
||||||
|
const { statusCode, errorResponse } = messagesService.transformError(error)
|
||||||
|
res.status(statusCode).json(errorResponse)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,10 +234,11 @@ router.post('/', async (req: Request, res: Response) => {
|
|||||||
const provider = modelValidation.provider!
|
const provider = modelValidation.provider!
|
||||||
const modelId = modelValidation.modelId!
|
const modelId = modelValidation.modelId!
|
||||||
|
|
||||||
// Use shared processing function
|
return handleMessageProcessing({ req, res, provider, request, modelId })
|
||||||
return await processMessageRequest(req, res, provider, modelId)
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return handleErrorResponse(res, error)
|
logger.error('Message processing error', { error })
|
||||||
|
const { statusCode, errorResponse } = messagesService.transformError(error)
|
||||||
|
return res.status(statusCode).json(errorResponse)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -493,10 +390,13 @@ providerRouter.post('/', async (req: Request, res: Response) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use shared processing function (no modelId override needed)
|
const request: MessageCreateParams = req.body
|
||||||
return await processMessageRequest(req, res, provider)
|
|
||||||
|
return handleMessageProcessing({ req, res, provider, request })
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return handleErrorResponse(res, error)
|
logger.error('Message processing error', { error })
|
||||||
|
const { statusCode, errorResponse } = messagesService.transformError(error)
|
||||||
|
return res.status(statusCode).json(errorResponse)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { createServer } from 'node:http'
|
import { createServer } from 'node:http'
|
||||||
|
|
||||||
|
import { loggerService } from '@logger'
|
||||||
|
|
||||||
import { agentService } from '../services/agents'
|
import { agentService } from '../services/agents'
|
||||||
import { loggerService } from '../services/LoggerService'
|
|
||||||
import { app } from './app'
|
import { app } from './app'
|
||||||
import { config } from './config'
|
import { config } from './config'
|
||||||
|
|
||||||
@@ -15,11 +16,17 @@ export class ApiServer {
|
|||||||
private server: ReturnType<typeof createServer> | null = null
|
private server: ReturnType<typeof createServer> | null = null
|
||||||
|
|
||||||
async start(): Promise<void> {
|
async start(): Promise<void> {
|
||||||
if (this.server) {
|
if (this.server && this.server.listening) {
|
||||||
logger.warn('Server already running')
|
logger.warn('Server already running')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clean up any failed server instance
|
||||||
|
if (this.server && !this.server.listening) {
|
||||||
|
logger.warn('Cleaning up failed server instance')
|
||||||
|
this.server = null
|
||||||
|
}
|
||||||
|
|
||||||
// Load config
|
// Load config
|
||||||
const { port, host } = await config.load()
|
const { port, host } = await config.load()
|
||||||
|
|
||||||
@@ -39,7 +46,11 @@ export class ApiServer {
|
|||||||
resolve()
|
resolve()
|
||||||
})
|
})
|
||||||
|
|
||||||
this.server!.on('error', reject)
|
this.server!.on('error', (error) => {
|
||||||
|
// Clean up the server instance if listen fails
|
||||||
|
this.server = null
|
||||||
|
reject(error)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,33 +1,93 @@
|
|||||||
import Anthropic from '@anthropic-ai/sdk'
|
import Anthropic from '@anthropic-ai/sdk'
|
||||||
import { Message, MessageCreateParams, RawMessageStreamEvent } from '@anthropic-ai/sdk/resources'
|
import { MessageCreateParams, MessageStreamEvent } from '@anthropic-ai/sdk/resources'
|
||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
import anthropicService from '@main/services/AnthropicService'
|
import anthropicService from '@main/services/AnthropicService'
|
||||||
import { buildClaudeCodeSystemMessage, getSdkClient } from '@shared/anthropic'
|
import { buildClaudeCodeSystemMessage, getSdkClient } from '@shared/anthropic'
|
||||||
import { Provider } from '@types'
|
import { Provider } from '@types'
|
||||||
|
import { Response } from 'express'
|
||||||
|
|
||||||
const logger = loggerService.withContext('MessagesService')
|
const logger = loggerService.withContext('MessagesService')
|
||||||
|
const EXCLUDED_FORWARD_HEADERS: ReadonlySet<string> = new Set([
|
||||||
|
'host',
|
||||||
|
'x-api-key',
|
||||||
|
'authorization',
|
||||||
|
'sentry-trace',
|
||||||
|
'baggage',
|
||||||
|
'content-length',
|
||||||
|
'connection'
|
||||||
|
])
|
||||||
|
|
||||||
export interface ValidationResult {
|
export interface ValidationResult {
|
||||||
isValid: boolean
|
isValid: boolean
|
||||||
errors: string[]
|
errors: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ErrorResponse {
|
||||||
|
type: 'error'
|
||||||
|
error: {
|
||||||
|
type: string
|
||||||
|
message: string
|
||||||
|
requestId?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StreamConfig {
|
||||||
|
response: Response
|
||||||
|
onChunk?: (chunk: MessageStreamEvent) => void
|
||||||
|
onError?: (error: any) => void
|
||||||
|
onComplete?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProcessMessageOptions {
|
||||||
|
provider: Provider
|
||||||
|
request: MessageCreateParams
|
||||||
|
extraHeaders?: Record<string, string | string[]>
|
||||||
|
modelId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProcessMessageResult {
|
||||||
|
client: Anthropic
|
||||||
|
anthropicRequest: MessageCreateParams
|
||||||
|
}
|
||||||
|
|
||||||
export class MessagesService {
|
export class MessagesService {
|
||||||
// oxlint-disable-next-line no-unused-vars
|
|
||||||
validateRequest(request: MessageCreateParams): ValidationResult {
|
validateRequest(request: MessageCreateParams): ValidationResult {
|
||||||
// TODO: Implement comprehensive request validation
|
// TODO: Implement comprehensive request validation
|
||||||
const errors: string[] = []
|
const errors: string[] = []
|
||||||
|
|
||||||
if (!request.model) {
|
if (!request.model || typeof request.model !== 'string') {
|
||||||
errors.push('Model is required')
|
errors.push('Model is required')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!request.max_tokens || request.max_tokens < 1) {
|
if (typeof request.max_tokens !== 'number' || !Number.isFinite(request.max_tokens) || request.max_tokens < 1) {
|
||||||
errors.push('max_tokens is required and must be at least 1')
|
errors.push('max_tokens is required and must be a positive number')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!request.messages || !Array.isArray(request.messages) || request.messages.length === 0) {
|
if (!request.messages || !Array.isArray(request.messages) || request.messages.length === 0) {
|
||||||
errors.push('messages is required and must be a non-empty array')
|
errors.push('messages is required and must be a non-empty array')
|
||||||
|
} else {
|
||||||
|
request.messages.forEach((message, index) => {
|
||||||
|
if (!message || typeof message !== 'object') {
|
||||||
|
errors.push(`messages[${index}] must be an object`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!('role' in message) || typeof message.role !== 'string' || message.role.trim().length === 0) {
|
||||||
|
errors.push(`messages[${index}].role is required`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const content: unknown = message.content
|
||||||
|
if (content === undefined || content === null) {
|
||||||
|
errors.push(`messages[${index}].content is required`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof content === 'string' && content.trim().length === 0) {
|
||||||
|
errors.push(`messages[${index}].content cannot be empty`)
|
||||||
|
} else if (Array.isArray(content) && content.length === 0) {
|
||||||
|
errors.push(`messages[${index}].content must include at least one item when using an array`)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -36,79 +96,224 @@ export class MessagesService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getClient(provider: Provider): Promise<Anthropic> {
|
async getClient(provider: Provider, extraHeaders?: Record<string, string | string[]>): Promise<Anthropic> {
|
||||||
// Create Anthropic client for the provider
|
// Create Anthropic client for the provider
|
||||||
if (provider.authType === 'oauth') {
|
if (provider.authType === 'oauth') {
|
||||||
const oauthToken = await anthropicService.getValidAccessToken()
|
const oauthToken = await anthropicService.getValidAccessToken()
|
||||||
return getSdkClient(provider, oauthToken)
|
return getSdkClient(provider, oauthToken, extraHeaders)
|
||||||
}
|
}
|
||||||
return getSdkClient(provider)
|
return getSdkClient(provider, null, extraHeaders)
|
||||||
}
|
}
|
||||||
|
|
||||||
async processMessage(request: MessageCreateParams, provider: Provider): Promise<Message> {
|
prepareHeaders(headers: Record<string, string | string[] | undefined>): Record<string, string | string[]> {
|
||||||
logger.debug('Preparing Anthropic message request', {
|
const extraHeaders: Record<string, string | string[]> = {}
|
||||||
model: request.model,
|
|
||||||
messageCount: request.messages.length,
|
|
||||||
stream: request.stream,
|
|
||||||
maxTokens: request.max_tokens,
|
|
||||||
provider: provider.id
|
|
||||||
})
|
|
||||||
|
|
||||||
// Create Anthropic client for the provider
|
for (const [key, value] of Object.entries(headers)) {
|
||||||
const client = await this.getClient(provider)
|
if (value === undefined) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// Prepare request with the actual model ID
|
const normalizedKey = key.toLowerCase()
|
||||||
|
if (EXCLUDED_FORWARD_HEADERS.has(normalizedKey)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
extraHeaders[normalizedKey] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
return extraHeaders
|
||||||
|
}
|
||||||
|
|
||||||
|
createAnthropicRequest(request: MessageCreateParams, provider: Provider, modelId?: string): MessageCreateParams {
|
||||||
const anthropicRequest: MessageCreateParams = {
|
const anthropicRequest: MessageCreateParams = {
|
||||||
...request,
|
...request,
|
||||||
stream: false
|
stream: !!request.stream
|
||||||
}
|
}
|
||||||
|
|
||||||
if (provider.authType === 'oauth') {
|
// Override model if provided
|
||||||
anthropicRequest.system = buildClaudeCodeSystemMessage(request.system || '')
|
if (modelId) {
|
||||||
|
anthropicRequest.model = modelId
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await client.messages.create(anthropicRequest)
|
// Add Claude Code system message for OAuth providers
|
||||||
|
if (provider.type === 'anthropic' && provider.authType === 'oauth') {
|
||||||
|
anthropicRequest.system = buildClaudeCodeSystemMessage(request.system)
|
||||||
|
}
|
||||||
|
|
||||||
logger.info('Anthropic message completed', {
|
return anthropicRequest
|
||||||
model: request.model,
|
|
||||||
provider: provider.id
|
|
||||||
})
|
|
||||||
return response
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async *processStreamingMessage(
|
async handleStreaming(
|
||||||
|
client: Anthropic,
|
||||||
request: MessageCreateParams,
|
request: MessageCreateParams,
|
||||||
|
config: StreamConfig,
|
||||||
provider: Provider
|
provider: Provider
|
||||||
): AsyncIterable<RawMessageStreamEvent> {
|
): Promise<void> {
|
||||||
logger.debug('Preparing streaming Anthropic message request', {
|
const { response, onChunk, onError, onComplete } = config
|
||||||
model: request.model,
|
|
||||||
messageCount: request.messages.length,
|
// Set streaming headers
|
||||||
provider: provider.id
|
response.setHeader('Content-Type', 'text/event-stream; charset=utf-8')
|
||||||
|
response.setHeader('Cache-Control', 'no-cache, no-transform')
|
||||||
|
response.setHeader('Connection', 'keep-alive')
|
||||||
|
response.setHeader('X-Accel-Buffering', 'no')
|
||||||
|
response.flushHeaders()
|
||||||
|
|
||||||
|
const flushableResponse = response as Response & { flush?: () => void }
|
||||||
|
const flushStream = () => {
|
||||||
|
if (typeof flushableResponse.flush !== 'function') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
flushableResponse.flush()
|
||||||
|
} catch (flushError: unknown) {
|
||||||
|
logger.warn('Failed to flush streaming response', { error: flushError })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const writeSse = (eventType: string | undefined, payload: unknown) => {
|
||||||
|
if (response.writableEnded || response.destroyed) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventType) {
|
||||||
|
response.write(`event: ${eventType}\n`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = typeof payload === 'string' ? payload : JSON.stringify(payload)
|
||||||
|
response.write(`data: ${data}\n\n`)
|
||||||
|
flushStream()
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stream = client.messages.stream(request)
|
||||||
|
for await (const chunk of stream) {
|
||||||
|
if (response.writableEnded || response.destroyed) {
|
||||||
|
logger.warn('Streaming response ended before stream completion', {
|
||||||
|
provider: provider.id,
|
||||||
|
model: request.model
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
writeSse(chunk.type, chunk)
|
||||||
|
|
||||||
|
if (onChunk) {
|
||||||
|
onChunk(chunk)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writeSse(undefined, '[DONE]')
|
||||||
|
|
||||||
|
if (onComplete) {
|
||||||
|
onComplete()
|
||||||
|
}
|
||||||
|
} catch (streamError: any) {
|
||||||
|
logger.error('Stream error', {
|
||||||
|
error: streamError,
|
||||||
|
provider: provider.id,
|
||||||
|
model: request.model,
|
||||||
|
apiHost: provider.apiHost,
|
||||||
|
anthropicApiHost: provider.anthropicApiHost
|
||||||
|
})
|
||||||
|
writeSse(undefined, {
|
||||||
|
type: 'error',
|
||||||
|
error: {
|
||||||
|
type: 'api_error',
|
||||||
|
message: 'Stream processing error'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (onError) {
|
||||||
|
onError(streamError)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (!response.writableEnded) {
|
||||||
|
response.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
transformError(error: any): { statusCode: number; errorResponse: ErrorResponse } {
|
||||||
|
let statusCode = 500
|
||||||
|
let errorType = 'api_error'
|
||||||
|
let errorMessage = 'Internal server error'
|
||||||
|
|
||||||
|
const anthropicStatus = typeof error?.status === 'number' ? error.status : undefined
|
||||||
|
const anthropicError = error?.error
|
||||||
|
|
||||||
|
if (anthropicStatus) {
|
||||||
|
statusCode = anthropicStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anthropicError?.type) {
|
||||||
|
errorType = anthropicError.type
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anthropicError?.message) {
|
||||||
|
errorMessage = anthropicError.message
|
||||||
|
} else if (error instanceof Error && error.message) {
|
||||||
|
errorMessage = error.message
|
||||||
|
}
|
||||||
|
|
||||||
|
// Infer error type from message if not from Anthropic API
|
||||||
|
if (!anthropicStatus && error instanceof Error) {
|
||||||
|
const errorMessageText = error.message ?? ''
|
||||||
|
|
||||||
|
if (errorMessageText.includes('API key') || errorMessageText.includes('authentication')) {
|
||||||
|
statusCode = 401
|
||||||
|
errorType = 'authentication_error'
|
||||||
|
} else if (errorMessageText.includes('rate limit') || errorMessageText.includes('quota')) {
|
||||||
|
statusCode = 429
|
||||||
|
errorType = 'rate_limit_error'
|
||||||
|
} else if (errorMessageText.includes('timeout') || errorMessageText.includes('connection')) {
|
||||||
|
statusCode = 502
|
||||||
|
errorType = 'api_error'
|
||||||
|
} else if (errorMessageText.includes('validation') || errorMessageText.includes('invalid')) {
|
||||||
|
statusCode = 400
|
||||||
|
errorType = 'invalid_request_error'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const safeErrorMessage =
|
||||||
|
typeof errorMessage === 'string' && errorMessage.length > 0 ? errorMessage : 'Internal server error'
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode,
|
||||||
|
errorResponse: {
|
||||||
|
type: 'error',
|
||||||
|
error: {
|
||||||
|
type: errorType,
|
||||||
|
message: safeErrorMessage,
|
||||||
|
requestId: error?.request_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async processMessage(options: ProcessMessageOptions): Promise<ProcessMessageResult> {
|
||||||
|
const { provider, request, extraHeaders, modelId } = options
|
||||||
|
|
||||||
|
const client = await this.getClient(provider, extraHeaders)
|
||||||
|
const anthropicRequest = this.createAnthropicRequest(request, provider, modelId)
|
||||||
|
|
||||||
|
const messageCount = Array.isArray(request.messages) ? request.messages.length : 0
|
||||||
|
|
||||||
|
logger.info('Processing anthropic messages request', {
|
||||||
|
provider: provider.id,
|
||||||
|
apiHost: provider.apiHost,
|
||||||
|
anthropicApiHost: provider.anthropicApiHost,
|
||||||
|
model: anthropicRequest.model,
|
||||||
|
stream: !!anthropicRequest.stream,
|
||||||
|
// systemPrompt: JSON.stringify(!!request.system),
|
||||||
|
// messages: JSON.stringify(request.messages),
|
||||||
|
messageCount,
|
||||||
|
toolCount: Array.isArray(request.tools) ? request.tools.length : 0
|
||||||
})
|
})
|
||||||
|
|
||||||
// Create Anthropic client for the provider
|
// Return client and request for route layer to handle streaming/non-streaming
|
||||||
const client = await this.getClient(provider)
|
return {
|
||||||
|
client,
|
||||||
// Prepare streaming request
|
anthropicRequest
|
||||||
const streamingRequest: MessageCreateParams = {
|
|
||||||
...request,
|
|
||||||
stream: true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (provider.authType === 'oauth') {
|
|
||||||
streamingRequest.system = buildClaudeCodeSystemMessage(request.system || '')
|
|
||||||
}
|
|
||||||
|
|
||||||
const stream = client.messages.stream(streamingRequest)
|
|
||||||
|
|
||||||
for await (const chunk of stream) {
|
|
||||||
yield chunk
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info('Completed streaming Anthropic message', {
|
|
||||||
model: request.model,
|
|
||||||
provider: provider.id
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
|
import { isEmpty } from 'lodash'
|
||||||
|
|
||||||
import { ApiModel, ApiModelsFilter, ApiModelsResponse } from '../../../renderer/src/types/apiModels'
|
import { ApiModel, ApiModelsFilter, ApiModelsResponse } from '../../../renderer/src/types/apiModels'
|
||||||
import { loggerService } from '../../services/LoggerService'
|
import { loggerService } from '../../services/LoggerService'
|
||||||
import { getAvailableProviders, listAllAvailableModels, transformModelToOpenAI } from '../utils'
|
import {
|
||||||
|
getAvailableProviders,
|
||||||
|
getProviderAnthropicModelChecker,
|
||||||
|
listAllAvailableModels,
|
||||||
|
transformModelToOpenAI
|
||||||
|
} from '../utils'
|
||||||
|
|
||||||
const logger = loggerService.withContext('ModelsService')
|
const logger = loggerService.withContext('ModelsService')
|
||||||
|
|
||||||
@@ -16,9 +23,7 @@ export class ModelsService {
|
|||||||
let providers = await getAvailableProviders()
|
let providers = await getAvailableProviders()
|
||||||
|
|
||||||
if (filter.providerType === 'anthropic') {
|
if (filter.providerType === 'anthropic') {
|
||||||
providers = providers.filter(
|
providers = providers.filter((p) => p.type === 'anthropic' || !isEmpty(p.anthropicApiHost?.trim()))
|
||||||
(p) => p.type === 'anthropic' || (p.anthropicApiHost !== undefined && p.anthropicApiHost.trim() !== '')
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const models = await listAllAvailableModels(providers)
|
const models = await listAllAvailableModels(providers)
|
||||||
@@ -27,18 +32,18 @@ export class ModelsService {
|
|||||||
|
|
||||||
for (const model of models) {
|
for (const model of models) {
|
||||||
const provider = providers.find((p) => p.id === model.provider)
|
const provider = providers.find((p) => p.id === model.provider)
|
||||||
logger.debug(`Processing model ${model.id} from provider ${model.provider}`, {
|
logger.debug(`Processing model ${model.id}`)
|
||||||
isAnthropicModel: provider?.isAnthropicModel
|
if (!provider) {
|
||||||
})
|
logger.debug(`Skipping model ${model.id} . Reason: Provider not found.`)
|
||||||
if (
|
|
||||||
!provider ||
|
|
||||||
(filter.providerType === 'anthropic' && provider.isAnthropicModel && !provider.isAnthropicModel(model))
|
|
||||||
) {
|
|
||||||
continue
|
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')) {
|
if (filter.providerType === 'anthropic') {
|
||||||
continue
|
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)
|
const openAIModel = transformModelToOpenAI(model, provider)
|
||||||
|
|||||||
@@ -279,3 +279,16 @@ export function validateProvider(provider: Provider): boolean {
|
|||||||
return false
|
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 { registerShortcuts } from './services/ShortcutService'
|
||||||
import { TrayService } from './services/TrayService'
|
import { TrayService } from './services/TrayService'
|
||||||
import { windowService } from './services/WindowService'
|
import { windowService } from './services/WindowService'
|
||||||
|
import { initWebviewHotkeys } from './services/WebviewService'
|
||||||
|
|
||||||
const logger = loggerService.withContext('MainEntry')
|
const logger = loggerService.withContext('MainEntry')
|
||||||
|
|
||||||
@@ -108,6 +109,7 @@ if (!app.requestSingleInstanceLock()) {
|
|||||||
// Some APIs can only be used after this event occurs.
|
// Some APIs can only be used after this event occurs.
|
||||||
|
|
||||||
app.whenReady().then(async () => {
|
app.whenReady().then(async () => {
|
||||||
|
initWebviewHotkeys()
|
||||||
// Set app user model id for windows
|
// Set app user model id for windows
|
||||||
electronApp.setAppUserModelId(import.meta.env.VITE_MAIN_BUNDLE_ID || 'com.kangfenmao.CherryStudio')
|
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)
|
logger.error('Failed to initialize Agent service:', error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start API server if enabled
|
// Start API server if enabled or if agents exist
|
||||||
try {
|
try {
|
||||||
const config = await apiServerService.getCurrentConfig()
|
const config = await apiServerService.getCurrentConfig()
|
||||||
logger.info('API server config:', config)
|
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()
|
await apiServerService.start()
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
|||||||
ipcMain.handle(IpcChannel.Open_Website, (_, url: string) => shell.openExternal(url))
|
ipcMain.handle(IpcChannel.Open_Website, (_, url: string) => shell.openExternal(url))
|
||||||
|
|
||||||
// Update
|
// Update
|
||||||
ipcMain.handle(IpcChannel.App_ShowUpdateDialog, () => appUpdater.showUpdateDialog(mainWindow))
|
ipcMain.handle(IpcChannel.App_QuitAndInstall, () => appUpdater.quitAndInstall())
|
||||||
|
|
||||||
// language
|
// language
|
||||||
ipcMain.handle(IpcChannel.App_SetLanguage, (_, language) => {
|
ipcMain.handle(IpcChannel.App_SetLanguage, (_, language) => {
|
||||||
@@ -786,7 +786,6 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
|||||||
ipcMain.handle(IpcChannel.Webview_SetOpenLinkExternal, (_, webviewId: number, isExternal: boolean) =>
|
ipcMain.handle(IpcChannel.Webview_SetOpenLinkExternal, (_, webviewId: number, isExternal: boolean) =>
|
||||||
setOpenLinkExternal(webviewId, isExternal)
|
setOpenLinkExternal(webviewId, isExternal)
|
||||||
)
|
)
|
||||||
|
|
||||||
ipcMain.handle(IpcChannel.Webview_SetSpellCheckEnabled, (_, webviewId: number, isEnable: boolean) => {
|
ipcMain.handle(IpcChannel.Webview_SetSpellCheckEnabled, (_, webviewId: number, isEnable: boolean) => {
|
||||||
const webview = webContents.fromId(webviewId)
|
const webview = webContents.fromId(webviewId)
|
||||||
if (!webview) return
|
if (!webview) return
|
||||||
@@ -876,6 +875,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
|||||||
ipcMain.handle(IpcChannel.OCR_ocr, (_, file: SupportedOcrFile, provider: OcrProvider) =>
|
ipcMain.handle(IpcChannel.OCR_ocr, (_, file: SupportedOcrFile, provider: OcrProvider) =>
|
||||||
ocrService.ocr(file, provider)
|
ocrService.ocr(file, provider)
|
||||||
)
|
)
|
||||||
|
ipcMain.handle(IpcChannel.OCR_ListProviders, () => ocrService.listProviderIds())
|
||||||
|
|
||||||
// OVMS
|
// OVMS
|
||||||
ipcMain.handle(IpcChannel.Ovms_AddModel, (_, modelName: string, modelId: string, modelSource: string, task: string) =>
|
ipcMain.handle(IpcChannel.Ovms_AddModel, (_, modelName: string, modelId: string, modelSource: string, task: string) =>
|
||||||
|
|||||||
@@ -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 { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
||||||
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
|
||||||
import { net } from 'electron'
|
import { net } from 'electron'
|
||||||
import { z } from 'zod'
|
import * as z from 'zod'
|
||||||
|
|
||||||
const logger = loggerService.withContext('DifyKnowledgeServer')
|
const logger = loggerService.withContext('DifyKnowledgeServer')
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
|||||||
import { BuiltinMCPServerName, BuiltinMCPServerNames } from '@types'
|
import { BuiltinMCPServerName, BuiltinMCPServerNames } from '@types'
|
||||||
|
|
||||||
import BraveSearchServer from './brave-search'
|
import BraveSearchServer from './brave-search'
|
||||||
|
import DiDiMcpServer from './didi-mcp'
|
||||||
import DifyKnowledgeServer from './dify-knowledge'
|
import DifyKnowledgeServer from './dify-knowledge'
|
||||||
import FetchServer from './fetch'
|
import FetchServer from './fetch'
|
||||||
import FileSystemServer from './filesystem'
|
import FileSystemServer from './filesystem'
|
||||||
@@ -42,6 +43,10 @@ export function createInMemoryMCPServer(
|
|||||||
case BuiltinMCPServerNames.python: {
|
case BuiltinMCPServerNames.python: {
|
||||||
return new PythonServer().server
|
return new PythonServer().server
|
||||||
}
|
}
|
||||||
|
case BuiltinMCPServerNames.didiMCP: {
|
||||||
|
const apiKey = envs.DIDI_API_KEY
|
||||||
|
return new DiDiMcpServer(apiKey).server
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown in-memory MCP server: ${name}`)
|
throw new Error(`Unknown in-memory MCP server: ${name}`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprot
|
|||||||
import { net } from 'electron'
|
import { net } from 'electron'
|
||||||
import { JSDOM } from 'jsdom'
|
import { JSDOM } from 'jsdom'
|
||||||
import TurndownService from 'turndown'
|
import TurndownService from 'turndown'
|
||||||
import { z } from 'zod'
|
import * as z from 'zod'
|
||||||
|
|
||||||
export const RequestPayloadSchema = z.object({
|
export const RequestPayloadSchema = z.object({
|
||||||
url: z.url(),
|
url: z.url(),
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import fs from 'fs/promises'
|
|||||||
import { minimatch } from 'minimatch'
|
import { minimatch } from 'minimatch'
|
||||||
import os from 'os'
|
import os from 'os'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { z } from 'zod'
|
import * as z from 'zod'
|
||||||
|
|
||||||
const logger = loggerService.withContext('MCP:FileSystemServer')
|
const logger = loggerService.withContext('MCP:FileSystemServer')
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import { IpcChannel } from '@shared/IpcChannel'
|
import { IpcChannel } from '@shared/IpcChannel'
|
||||||
import { ApiServerConfig } from '@types'
|
import {
|
||||||
|
ApiServerConfig,
|
||||||
|
GetApiServerStatusResult,
|
||||||
|
RestartApiServerStatusResult,
|
||||||
|
StartApiServerStatusResult,
|
||||||
|
StopApiServerStatusResult
|
||||||
|
} from '@types'
|
||||||
import { ipcMain } from 'electron'
|
import { ipcMain } from 'electron'
|
||||||
|
|
||||||
import { apiServer } from '../apiServer'
|
import { apiServer } from '../apiServer'
|
||||||
@@ -52,7 +58,7 @@ export class ApiServerService {
|
|||||||
|
|
||||||
registerIpcHandlers(): void {
|
registerIpcHandlers(): void {
|
||||||
// API Server
|
// API Server
|
||||||
ipcMain.handle(IpcChannel.ApiServer_Start, async () => {
|
ipcMain.handle(IpcChannel.ApiServer_Start, async (): Promise<StartApiServerStatusResult> => {
|
||||||
try {
|
try {
|
||||||
await this.start()
|
await this.start()
|
||||||
return { success: true }
|
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 {
|
try {
|
||||||
await this.stop()
|
await this.stop()
|
||||||
return { success: true }
|
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 {
|
try {
|
||||||
await this.restart()
|
await this.restart()
|
||||||
return { success: true }
|
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 {
|
try {
|
||||||
const config = await this.getCurrentConfig()
|
const config = await this.getCurrentConfig()
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
import { isWin } from '@main/constant'
|
import { isWin } from '@main/constant'
|
||||||
import { getIpCountry } from '@main/utils/ipService'
|
import { getIpCountry } from '@main/utils/ipService'
|
||||||
import { locales } from '@main/utils/locales'
|
|
||||||
import { generateUserAgent } from '@main/utils/systemInfo'
|
import { generateUserAgent } from '@main/utils/systemInfo'
|
||||||
import { FeedUrl, UpgradeChannel } from '@shared/config/constant'
|
import { FeedUrl, UpgradeChannel } from '@shared/config/constant'
|
||||||
import { IpcChannel } from '@shared/IpcChannel'
|
import { IpcChannel } from '@shared/IpcChannel'
|
||||||
import { CancellationToken, UpdateInfo } from 'builder-util-runtime'
|
import { CancellationToken, UpdateInfo } from 'builder-util-runtime'
|
||||||
import { app, BrowserWindow, dialog, net } from 'electron'
|
import { app, net } from 'electron'
|
||||||
import { AppUpdater as _AppUpdater, autoUpdater, Logger, NsisUpdater, UpdateCheckResult } from 'electron-updater'
|
import { AppUpdater as _AppUpdater, autoUpdater, Logger, NsisUpdater, UpdateCheckResult } from 'electron-updater'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import semver from 'semver'
|
import semver from 'semver'
|
||||||
|
|
||||||
import icon from '../../../build/icon.png?asset'
|
|
||||||
import { configManager } from './ConfigManager'
|
import { configManager } from './ConfigManager'
|
||||||
import { windowService } from './WindowService'
|
import { windowService } from './WindowService'
|
||||||
|
|
||||||
@@ -26,7 +24,6 @@ const LANG_MARKERS = {
|
|||||||
|
|
||||||
export default class AppUpdater {
|
export default class AppUpdater {
|
||||||
autoUpdater: _AppUpdater = autoUpdater
|
autoUpdater: _AppUpdater = autoUpdater
|
||||||
private releaseInfo: UpdateInfo | undefined
|
|
||||||
private cancellationToken: CancellationToken = new CancellationToken()
|
private cancellationToken: CancellationToken = new CancellationToken()
|
||||||
private updateCheckResult: UpdateCheckResult | null = null
|
private updateCheckResult: UpdateCheckResult | null = null
|
||||||
|
|
||||||
@@ -66,7 +63,6 @@ export default class AppUpdater {
|
|||||||
autoUpdater.on('update-downloaded', (releaseInfo: UpdateInfo) => {
|
autoUpdater.on('update-downloaded', (releaseInfo: UpdateInfo) => {
|
||||||
const processedReleaseInfo = this.processReleaseInfo(releaseInfo)
|
const processedReleaseInfo = this.processReleaseInfo(releaseInfo)
|
||||||
windowService.getMainWindow()?.webContents.send(IpcChannel.UpdateDownloaded, processedReleaseInfo)
|
windowService.getMainWindow()?.webContents.send(IpcChannel.UpdateDownloaded, processedReleaseInfo)
|
||||||
this.releaseInfo = processedReleaseInfo
|
|
||||||
logger.info('update downloaded', processedReleaseInfo)
|
logger.info('update downloaded', processedReleaseInfo)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -247,37 +243,9 @@ export default class AppUpdater {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async showUpdateDialog(mainWindow: BrowserWindow) {
|
public quitAndInstall() {
|
||||||
if (!this.releaseInfo) {
|
app.isQuitting = true
|
||||||
return
|
setImmediate(() => autoUpdater.quitAndInstall())
|
||||||
}
|
|
||||||
const locale = locales[configManager.getLanguage()]
|
|
||||||
const { update: updateLocale } = locale.translation
|
|
||||||
|
|
||||||
let detail = this.formatReleaseNotes(this.releaseInfo.releaseNotes)
|
|
||||||
if (detail === '') {
|
|
||||||
detail = updateLocale.noReleaseNotes
|
|
||||||
}
|
|
||||||
|
|
||||||
dialog
|
|
||||||
.showMessageBox({
|
|
||||||
type: 'info',
|
|
||||||
title: updateLocale.title,
|
|
||||||
icon,
|
|
||||||
message: updateLocale.message.replace('{{version}}', this.releaseInfo.version),
|
|
||||||
detail,
|
|
||||||
buttons: [updateLocale.later, updateLocale.install],
|
|
||||||
defaultId: 1,
|
|
||||||
cancelId: 0
|
|
||||||
})
|
|
||||||
.then(({ response }) => {
|
|
||||||
if (response === 1) {
|
|
||||||
app.isQuitting = true
|
|
||||||
setImmediate(() => autoUpdater.quitAndInstall())
|
|
||||||
} else {
|
|
||||||
mainWindow.webContents.send(IpcChannel.UpdateDownloadedCancelled)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -349,38 +317,9 @@ export default class AppUpdater {
|
|||||||
|
|
||||||
return processedInfo
|
return processedInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Format release notes for display
|
|
||||||
* @param releaseNotes - Release notes in various formats
|
|
||||||
* @returns Formatted string for display
|
|
||||||
*/
|
|
||||||
private formatReleaseNotes(releaseNotes: string | ReleaseNoteInfo[] | null | undefined): string {
|
|
||||||
if (!releaseNotes) {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof releaseNotes === 'string') {
|
|
||||||
// Check if it contains multi-language markers
|
|
||||||
if (this.hasMultiLanguageMarkers(releaseNotes)) {
|
|
||||||
return this.parseMultiLangReleaseNotes(releaseNotes)
|
|
||||||
}
|
|
||||||
return releaseNotes
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(releaseNotes)) {
|
|
||||||
return releaseNotes.map((note) => note.note).join('\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
interface GithubReleaseInfo {
|
interface GithubReleaseInfo {
|
||||||
draft: boolean
|
draft: boolean
|
||||||
prerelease: boolean
|
prerelease: boolean
|
||||||
tag_name: string
|
tag_name: string
|
||||||
}
|
}
|
||||||
interface ReleaseNoteInfo {
|
|
||||||
readonly version: string
|
|
||||||
readonly note: string | null
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -31,7 +31,10 @@ interface VersionInfo {
|
|||||||
|
|
||||||
class CodeToolsService {
|
class CodeToolsService {
|
||||||
private versionCache: Map<string, { version: string; timestamp: number }> = new Map()
|
private versionCache: Map<string, { version: string; timestamp: number }> = new Map()
|
||||||
private terminalsCache: { terminals: TerminalConfig[]; timestamp: number } | null = null
|
private terminalsCache: {
|
||||||
|
terminals: TerminalConfig[]
|
||||||
|
timestamp: number
|
||||||
|
} | null = null
|
||||||
private customTerminalPaths: Map<string, string> = new Map() // Store user-configured terminal paths
|
private customTerminalPaths: Map<string, string> = new Map() // Store user-configured terminal paths
|
||||||
private readonly CACHE_DURATION = 1000 * 60 * 30 // 30 minutes cache
|
private readonly CACHE_DURATION = 1000 * 60 * 30 // 30 minutes cache
|
||||||
private readonly TERMINALS_CACHE_DURATION = 1000 * 60 * 5 // 5 minutes cache for terminals
|
private readonly TERMINALS_CACHE_DURATION = 1000 * 60 * 5 // 5 minutes cache for terminals
|
||||||
@@ -73,7 +76,7 @@ class CodeToolsService {
|
|||||||
public async getPackageName(cliTool: string) {
|
public async getPackageName(cliTool: string) {
|
||||||
switch (cliTool) {
|
switch (cliTool) {
|
||||||
case codeTools.claudeCode:
|
case codeTools.claudeCode:
|
||||||
return '@anthropic-ai/claude-agent-sdk'
|
return '@anthropic-ai/claude-code'
|
||||||
case codeTools.geminiCli:
|
case codeTools.geminiCli:
|
||||||
return '@google/gemini-cli'
|
return '@google/gemini-cli'
|
||||||
case codeTools.openaiCodex:
|
case codeTools.openaiCodex:
|
||||||
@@ -82,6 +85,8 @@ class CodeToolsService {
|
|||||||
return '@qwen-code/qwen-code'
|
return '@qwen-code/qwen-code'
|
||||||
case codeTools.iFlowCli:
|
case codeTools.iFlowCli:
|
||||||
return '@iflow-ai/iflow-cli'
|
return '@iflow-ai/iflow-cli'
|
||||||
|
case codeTools.githubCopilotCli:
|
||||||
|
return '@github/copilot'
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unsupported CLI tool: ${cliTool}`)
|
throw new Error(`Unsupported CLI tool: ${cliTool}`)
|
||||||
}
|
}
|
||||||
@@ -99,6 +104,8 @@ class CodeToolsService {
|
|||||||
return 'qwen'
|
return 'qwen'
|
||||||
case codeTools.iFlowCli:
|
case codeTools.iFlowCli:
|
||||||
return 'iflow'
|
return 'iflow'
|
||||||
|
case codeTools.githubCopilotCli:
|
||||||
|
return 'copilot'
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unsupported CLI tool: ${cliTool}`)
|
throw new Error(`Unsupported CLI tool: ${cliTool}`)
|
||||||
}
|
}
|
||||||
@@ -144,7 +151,9 @@ class CodeToolsService {
|
|||||||
case terminalApps.powershell:
|
case terminalApps.powershell:
|
||||||
// Check for PowerShell in PATH
|
// Check for PowerShell in PATH
|
||||||
try {
|
try {
|
||||||
await execAsync('powershell -Command "Get-Host"', { timeout: 3000 })
|
await execAsync('powershell -Command "Get-Host"', {
|
||||||
|
timeout: 3000
|
||||||
|
})
|
||||||
return terminal
|
return terminal
|
||||||
} catch {
|
} catch {
|
||||||
try {
|
try {
|
||||||
@@ -384,7 +393,9 @@ class CodeToolsService {
|
|||||||
const binDir = path.join(os.homedir(), '.cherrystudio', 'bin')
|
const binDir = path.join(os.homedir(), '.cherrystudio', 'bin')
|
||||||
const executablePath = path.join(binDir, executableName + (isWin ? '.exe' : ''))
|
const executablePath = path.join(binDir, executableName + (isWin ? '.exe' : ''))
|
||||||
|
|
||||||
const { stdout } = await execAsync(`"${executablePath}" --version`, { timeout: 10000 })
|
const { stdout } = await execAsync(`"${executablePath}" --version`, {
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
// Extract version number from output (format may vary by tool)
|
// Extract version number from output (format may vary by tool)
|
||||||
const versionMatch = stdout.trim().match(/\d+\.\d+\.\d+/)
|
const versionMatch = stdout.trim().match(/\d+\.\d+\.\d+/)
|
||||||
installedVersion = versionMatch ? versionMatch[0] : stdout.trim().split(' ')[0]
|
installedVersion = versionMatch ? versionMatch[0] : stdout.trim().split(' ')[0]
|
||||||
@@ -425,7 +436,10 @@ class CodeToolsService {
|
|||||||
logger.info(`${packageName} latest version: ${latestVersion}`)
|
logger.info(`${packageName} latest version: ${latestVersion}`)
|
||||||
|
|
||||||
// Cache the result
|
// Cache the result
|
||||||
this.versionCache.set(cacheKey, { version: latestVersion!, timestamp: now })
|
this.versionCache.set(cacheKey, {
|
||||||
|
version: latestVersion!,
|
||||||
|
timestamp: now
|
||||||
|
})
|
||||||
logger.debug(`Cached latest version for ${packageName}`)
|
logger.debug(`Cached latest version for ${packageName}`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn(`Failed to get latest version for ${packageName}:`, error as Error)
|
logger.warn(`Failed to get latest version for ${packageName}:`, error as Error)
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import Reranker from '@main/knowledge/reranker/Reranker'
|
|||||||
import { fileStorage } from '@main/services/FileStorage'
|
import { fileStorage } from '@main/services/FileStorage'
|
||||||
import { windowService } from '@main/services/WindowService'
|
import { windowService } from '@main/services/WindowService'
|
||||||
import { getDataPath } from '@main/utils'
|
import { getDataPath } from '@main/utils'
|
||||||
import { getAllFiles } from '@main/utils/file'
|
import { getAllFiles, sanitizeFilename } from '@main/utils/file'
|
||||||
import { TraceMethod } from '@mcp-trace/trace-core'
|
import { TraceMethod } from '@mcp-trace/trace-core'
|
||||||
import { MB } from '@shared/config/constant'
|
import { MB } from '@shared/config/constant'
|
||||||
import type { LoaderReturn } from '@shared/config/types'
|
import type { LoaderReturn } from '@shared/config/types'
|
||||||
@@ -147,11 +147,16 @@ class KnowledgeService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getDbPath = (id: string): string => {
|
||||||
|
// 消除网络搜索requestI d中的特殊字符
|
||||||
|
return path.join(this.storageDir, sanitizeFilename(id, '_'))
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete knowledge base file
|
* Delete knowledge base file
|
||||||
*/
|
*/
|
||||||
private deleteKnowledgeFile = (id: string): boolean => {
|
private deleteKnowledgeFile = (id: string): boolean => {
|
||||||
const dbPath = path.join(this.storageDir, id)
|
const dbPath = this.getDbPath(id)
|
||||||
if (fs.existsSync(dbPath)) {
|
if (fs.existsSync(dbPath)) {
|
||||||
try {
|
try {
|
||||||
fs.rmSync(dbPath, { recursive: true })
|
fs.rmSync(dbPath, { recursive: true })
|
||||||
@@ -244,7 +249,8 @@ class KnowledgeService {
|
|||||||
dimensions
|
dimensions
|
||||||
})
|
})
|
||||||
try {
|
try {
|
||||||
const libSqlDb = new LibSqlDb({ path: path.join(this.storageDir, id) })
|
const dbPath = this.getDbPath(id)
|
||||||
|
const libSqlDb = new LibSqlDb({ path: dbPath })
|
||||||
// Save database instance for later closing
|
// Save database instance for later closing
|
||||||
this.dbInstances.set(id, libSqlDb)
|
this.dbInstances.set(id, libSqlDb)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
* 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -274,46 +274,4 @@ describe('AppUpdater', () => {
|
|||||||
expect(result.releaseNotes).toBeNull()
|
expect(result.releaseNotes).toBeNull()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('formatReleaseNotes', () => {
|
|
||||||
it('should format string release notes with markers', () => {
|
|
||||||
vi.mocked(configManager.getLanguage).mockReturnValue('en-US')
|
|
||||||
const notes = `<!--LANG:en-->English<!--LANG:zh-CN-->中文<!--LANG:END-->`
|
|
||||||
|
|
||||||
const result = (appUpdater as any).formatReleaseNotes(notes)
|
|
||||||
|
|
||||||
expect(result).toBe('English')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should format string release notes without markers', () => {
|
|
||||||
const notes = 'Simple notes'
|
|
||||||
|
|
||||||
const result = (appUpdater as any).formatReleaseNotes(notes)
|
|
||||||
|
|
||||||
expect(result).toBe('Simple notes')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should format array release notes', () => {
|
|
||||||
const notes = [
|
|
||||||
{ version: '1.0.0', note: 'Note 1' },
|
|
||||||
{ version: '1.0.1', note: 'Note 2' }
|
|
||||||
]
|
|
||||||
|
|
||||||
const result = (appUpdater as any).formatReleaseNotes(notes)
|
|
||||||
|
|
||||||
expect(result).toBe('Note 1\nNote 2')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle null release notes', () => {
|
|
||||||
const result = (appUpdater as any).formatReleaseNotes(null)
|
|
||||||
|
|
||||||
expect(result).toBe('')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle undefined release notes', () => {
|
|
||||||
const result = (appUpdater as any).formatReleaseNotes(undefined)
|
|
||||||
|
|
||||||
expect(result).toBe('')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,341 +0,0 @@
|
|||||||
# Agent Message Architecture Design Document
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
This document describes the architecture for handling agent messages in Cherry Studio, including how agent-specific messages are generated, transformed to AI SDK format, stored, and sent to the UI. The system is designed to be agent-agnostic, allowing multiple agent types (Claude Code, OpenAI, etc.) to integrate seamlessly.
|
|
||||||
|
|
||||||
## Core Design Principles
|
|
||||||
|
|
||||||
1. **Agent Agnosticism**: The core message handling system should work with any agent type without modification
|
|
||||||
2. **Data Preservation**: All raw agent data must be preserved alongside transformed UI-friendly formats
|
|
||||||
3. **Streaming First**: Support real-time streaming of agent responses to the UI
|
|
||||||
4. **Type Safety**: Strong TypeScript interfaces ensure consistency across the pipeline
|
|
||||||
|
|
||||||
## Architecture Components
|
|
||||||
|
|
||||||
### 1. Agent Service Layer
|
|
||||||
|
|
||||||
Each agent (e.g., ClaudeCodeService) implements the `AgentServiceInterface`:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface AgentServiceInterface {
|
|
||||||
invoke(prompt: string, cwd: string, sessionId?: string, options?: any): AgentStream
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Responsibilities:
|
|
||||||
- Spawn and manage agent-specific processes (e.g., Claude Code CLI)
|
|
||||||
- Parse agent-specific output formats (e.g., SDKMessage for Claude Code)
|
|
||||||
- Transform agent messages to AI SDK format
|
|
||||||
- Emit standardized `AgentStreamEvent` objects
|
|
||||||
|
|
||||||
### 2. Agent Stream Events
|
|
||||||
|
|
||||||
The standardized event interface that all agents emit:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface AgentStreamEvent {
|
|
||||||
type: 'chunk' | 'error' | 'complete'
|
|
||||||
chunk?: UIMessageChunk // AI SDK format for UI
|
|
||||||
rawAgentMessage?: any // Agent-specific raw message
|
|
||||||
error?: Error
|
|
||||||
agentResult?: any // Complete agent-specific result
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Session Message Service
|
|
||||||
|
|
||||||
The `SessionMessageService` acts as the orchestration layer:
|
|
||||||
|
|
||||||
#### Responsibilities:
|
|
||||||
- Manages session lifecycle and persistence
|
|
||||||
- Collects streaming chunks and raw agent messages
|
|
||||||
- Stores structured data in the database
|
|
||||||
- Forwards events to the API layer
|
|
||||||
|
|
||||||
### 4. Database Storage
|
|
||||||
|
|
||||||
Session messages are stored with complete structured data:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface SessionMessageContent {
|
|
||||||
aiSDKChunks: UIMessageChunk[] // UI-friendly format
|
|
||||||
rawAgentMessages: any[] // Original agent messages
|
|
||||||
agentResult?: any // Complete agent result
|
|
||||||
agentType: string // Agent identifier
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Data Flow
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph TD
|
|
||||||
A[User Input] --> B[API Handler]
|
|
||||||
B --> C[SessionMessageService]
|
|
||||||
C --> D[Agent Service]
|
|
||||||
D --> E[Agent Process]
|
|
||||||
E --> F[Raw Agent Output]
|
|
||||||
F --> G[Transform to AI SDK]
|
|
||||||
G --> H[Emit AgentStreamEvent]
|
|
||||||
H --> I[SessionMessageService]
|
|
||||||
I --> J[Store in Database]
|
|
||||||
I --> K[Forward to Client]
|
|
||||||
K --> L[UI Rendering]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Message Transformation Process
|
|
||||||
|
|
||||||
### Step 1: Raw Agent Message Generation
|
|
||||||
|
|
||||||
Each agent generates messages in its native format:
|
|
||||||
|
|
||||||
**Claude Code Example:**
|
|
||||||
```typescript
|
|
||||||
// SDKMessage from Claude Code CLI
|
|
||||||
{
|
|
||||||
type: 'assistant',
|
|
||||||
uuid: 'msg_123',
|
|
||||||
session_id: 'session_456',
|
|
||||||
message: {
|
|
||||||
role: 'assistant',
|
|
||||||
content: [
|
|
||||||
{ type: 'text', text: 'Hello, I can help...' },
|
|
||||||
{ type: 'tool_use', id: 'tool_1', name: 'read_file', input: {...} }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 2: Transformation to AI SDK Format
|
|
||||||
|
|
||||||
The agent service transforms native messages to AI SDK `UIMessageChunk`:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// In ClaudeCodeService
|
|
||||||
const emitChunks = (sdkMessage: SDKMessage) => {
|
|
||||||
// Transform to AI SDK format
|
|
||||||
const chunks = transformSDKMessageToUIChunk(sdkMessage)
|
|
||||||
|
|
||||||
for (const chunk of chunks) {
|
|
||||||
stream.emit('data', {
|
|
||||||
type: 'chunk',
|
|
||||||
chunk, // AI SDK format
|
|
||||||
rawAgentMessage: sdkMessage // Preserve original
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Transformed AI SDK Chunk:**
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
type: 'text-delta',
|
|
||||||
id: 'msg_123',
|
|
||||||
delta: 'Hello, I can help...',
|
|
||||||
providerMetadata: {
|
|
||||||
claudeCode: {
|
|
||||||
originalSDKMessage: {...},
|
|
||||||
uuid: 'msg_123',
|
|
||||||
session_id: 'session_456'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 3: Session Message Processing
|
|
||||||
|
|
||||||
The SessionMessageService collects and processes events:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Collect streaming data
|
|
||||||
const streamedChunks: UIMessageChunk[] = []
|
|
||||||
const rawAgentMessages: any[] = []
|
|
||||||
|
|
||||||
claudeStream.on('data', async (event: AgentStreamEvent) => {
|
|
||||||
switch (event.type) {
|
|
||||||
case 'chunk':
|
|
||||||
streamedChunks.push(event.chunk)
|
|
||||||
if (event.rawAgentMessage) {
|
|
||||||
rawAgentMessages.push(event.rawAgentMessage)
|
|
||||||
}
|
|
||||||
// Forward to client
|
|
||||||
sessionStream.emit('data', { type: 'chunk', chunk: event.chunk })
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'complete':
|
|
||||||
// Store complete structured data
|
|
||||||
const content = {
|
|
||||||
aiSDKChunks: streamedChunks,
|
|
||||||
rawAgentMessages: rawAgentMessages,
|
|
||||||
agentResult: event.agentResult,
|
|
||||||
agentType: event.agentResult?.agentType || 'unknown'
|
|
||||||
}
|
|
||||||
// Save to database...
|
|
||||||
break
|
|
||||||
}
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 4: Client Streaming
|
|
||||||
|
|
||||||
The API handler converts events to Server-Sent Events (SSE):
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// In API handler
|
|
||||||
messageStream.on('data', (event: any) => {
|
|
||||||
switch (event.type) {
|
|
||||||
case 'chunk':
|
|
||||||
// Send AI SDK chunk as SSE
|
|
||||||
res.write(`data: ${JSON.stringify(event.chunk)}\n\n`)
|
|
||||||
break
|
|
||||||
case 'complete':
|
|
||||||
res.write('data: [DONE]\n\n')
|
|
||||||
res.end()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
## Adding New Agent Types
|
|
||||||
|
|
||||||
To add support for a new agent (e.g., OpenAI):
|
|
||||||
|
|
||||||
### 1. Create Agent Service
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
class OpenAIService implements AgentServiceInterface {
|
|
||||||
invokeStream(prompt: string, cwd: string, sessionId?: string, options?: any): AgentStream {
|
|
||||||
const stream = new OpenAIStream()
|
|
||||||
|
|
||||||
// Call OpenAI API
|
|
||||||
const openaiResponse = await openai.chat.completions.create({
|
|
||||||
messages: [{ role: 'user', content: prompt }],
|
|
||||||
stream: true
|
|
||||||
})
|
|
||||||
|
|
||||||
// Transform OpenAI format to AI SDK
|
|
||||||
for await (const chunk of openaiResponse) {
|
|
||||||
const aiSDKChunk = transformOpenAIToAISDK(chunk)
|
|
||||||
stream.emit('data', {
|
|
||||||
type: 'chunk',
|
|
||||||
chunk: aiSDKChunk,
|
|
||||||
rawAgentMessage: chunk // Preserve OpenAI format
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return stream
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Create Transform Function
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
function transformOpenAIToAISDK(openaiChunk: OpenAIChunk): UIMessageChunk {
|
|
||||||
return {
|
|
||||||
type: 'text-delta',
|
|
||||||
id: openaiChunk.id,
|
|
||||||
delta: openaiChunk.choices[0].delta.content,
|
|
||||||
providerMetadata: {
|
|
||||||
openai: {
|
|
||||||
original: openaiChunk,
|
|
||||||
model: openaiChunk.model
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Register Agent Type
|
|
||||||
|
|
||||||
Update the agent type enum and factory:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export type AgentType = 'claude-code' | 'openai' | 'anthropic-api'
|
|
||||||
|
|
||||||
function createAgentService(type: AgentType): AgentServiceInterface {
|
|
||||||
switch (type) {
|
|
||||||
case 'claude-code':
|
|
||||||
return new ClaudeCodeService()
|
|
||||||
case 'openai':
|
|
||||||
return new OpenAIService()
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Benefits of This Architecture
|
|
||||||
|
|
||||||
1. **Extensibility**: Easy to add new agent types without modifying core logic
|
|
||||||
2. **Data Integrity**: Raw agent data is never lost during transformation
|
|
||||||
3. **Debugging**: Complete message history available for troubleshooting
|
|
||||||
4. **Performance**: Streaming support for real-time responses
|
|
||||||
5. **Type Safety**: Strong interfaces prevent runtime errors
|
|
||||||
6. **UI Consistency**: All agents provide data in standard AI SDK format
|
|
||||||
|
|
||||||
## Key Interfaces Reference
|
|
||||||
|
|
||||||
### AgentStreamEvent
|
|
||||||
```typescript
|
|
||||||
interface AgentStreamEvent {
|
|
||||||
type: 'chunk' | 'error' | 'complete'
|
|
||||||
chunk?: UIMessageChunk
|
|
||||||
rawAgentMessage?: any
|
|
||||||
error?: Error
|
|
||||||
agentResult?: any
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### SessionMessageEntity
|
|
||||||
```typescript
|
|
||||||
interface SessionMessageEntity {
|
|
||||||
id: number
|
|
||||||
session_id: string
|
|
||||||
parent_id?: number
|
|
||||||
role: 'user' | 'assistant' | 'system' | 'tool'
|
|
||||||
type: string
|
|
||||||
content: string | SessionMessageContent
|
|
||||||
metadata?: Record<string, any>
|
|
||||||
created_at: string
|
|
||||||
updated_at: string
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### SessionMessageContent
|
|
||||||
```typescript
|
|
||||||
interface SessionMessageContent {
|
|
||||||
aiSDKChunks: UIMessageChunk[]
|
|
||||||
rawAgentMessages: any[]
|
|
||||||
agentResult?: any
|
|
||||||
agentType: string
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing Strategy
|
|
||||||
|
|
||||||
### Unit Tests
|
|
||||||
- Test each transform function independently
|
|
||||||
- Verify event emission sequences
|
|
||||||
- Validate data structure preservation
|
|
||||||
|
|
||||||
### Integration Tests
|
|
||||||
- Test complete flow from input to database
|
|
||||||
- Verify streaming behavior
|
|
||||||
- Test error handling and recovery
|
|
||||||
|
|
||||||
### Agent-Specific Tests
|
|
||||||
- Validate agent-specific transformations
|
|
||||||
- Test edge cases for each agent type
|
|
||||||
- Verify metadata preservation
|
|
||||||
|
|
||||||
## Future Enhancements
|
|
||||||
|
|
||||||
1. **Message Replay**: Ability to replay sessions from stored raw messages
|
|
||||||
2. **Format Migration**: Tools to migrate between agent formats
|
|
||||||
3. **Analytics**: Aggregate metrics from raw agent data
|
|
||||||
4. **Caching**: Cache transformed chunks for performance
|
|
||||||
5. **Compression**: Compress raw messages for storage efficiency
|
|
||||||
|
|
||||||
## Conclusion
|
|
||||||
|
|
||||||
This architecture provides a robust, extensible foundation for handling messages from multiple AI agents while maintaining data integrity and providing a consistent interface for the UI. The separation of concerns between agent-specific logic and core message handling ensures the system can evolve to support new agents and features without breaking existing functionality.
|
|
||||||
@@ -84,9 +84,13 @@ class ClaudeCodeService implements AgentServiceInterface {
|
|||||||
|
|
||||||
const env = {
|
const env = {
|
||||||
...loginShellEnvWithoutProxies,
|
...loginShellEnvWithoutProxies,
|
||||||
ANTHROPIC_API_KEY: apiConfig.apiKey,
|
// TODO: fix the proxy api server
|
||||||
ANTHROPIC_AUTH_TOKEN: apiConfig.apiKey,
|
// ANTHROPIC_API_KEY: apiConfig.apiKey,
|
||||||
ANTHROPIC_BASE_URL: `http://${apiConfig.host}:${apiConfig.port}/${modelInfo.provider.id}`,
|
// ANTHROPIC_AUTH_TOKEN: apiConfig.apiKey,
|
||||||
|
// ANTHROPIC_BASE_URL: `http://${apiConfig.host}:${apiConfig.port}/${modelInfo.provider.id}`,
|
||||||
|
ANTHROPIC_API_KEY: modelInfo.provider.apiKey,
|
||||||
|
ANTHROPIC_AUTH_TOKEN: modelInfo.provider.apiKey,
|
||||||
|
ANTHROPIC_BASE_URL: modelInfo.provider.anthropicApiHost?.trim() || modelInfo.provider.apiHost,
|
||||||
ANTHROPIC_MODEL: modelInfo.modelId,
|
ANTHROPIC_MODEL: modelInfo.modelId,
|
||||||
ANTHROPIC_SMALL_FAST_MODEL: modelInfo.modelId,
|
ANTHROPIC_SMALL_FAST_MODEL: modelInfo.modelId,
|
||||||
ELECTRON_RUN_AS_NODE: '1',
|
ELECTRON_RUN_AS_NODE: '1',
|
||||||
@@ -106,7 +110,13 @@ class ClaudeCodeService implements AgentServiceInterface {
|
|||||||
logger.warn('claude stderr', { chunk })
|
logger.warn('claude stderr', { chunk })
|
||||||
errorChunks.push(chunk)
|
errorChunks.push(chunk)
|
||||||
},
|
},
|
||||||
systemPrompt: session.instructions ? session.instructions : { type: 'preset', preset: 'claude_code' },
|
systemPrompt: session.instructions
|
||||||
|
? {
|
||||||
|
type: 'preset',
|
||||||
|
preset: 'claude_code',
|
||||||
|
append: session.instructions
|
||||||
|
}
|
||||||
|
: { type: 'preset', preset: 'claude_code' },
|
||||||
settingSources: ['project'],
|
settingSources: ['project'],
|
||||||
includePartialMessages: true,
|
includePartialMessages: true,
|
||||||
permissionMode: session.configuration?.permission_mode,
|
permissionMode: session.configuration?.permission_mode,
|
||||||
@@ -136,6 +146,8 @@ class ClaudeCodeService implements AgentServiceInterface {
|
|||||||
|
|
||||||
if (lastAgentSessionId) {
|
if (lastAgentSessionId) {
|
||||||
options.resume = lastAgentSessionId
|
options.resume = lastAgentSessionId
|
||||||
|
// TODO: use fork session when we support branching sessions
|
||||||
|
// options.forkSession = true
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info('Starting Claude Code SDK query', {
|
logger.info('Starting Claude Code SDK query', {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
OAuthTokens
|
OAuthTokens
|
||||||
} from '@modelcontextprotocol/sdk/shared/auth.js'
|
} from '@modelcontextprotocol/sdk/shared/auth.js'
|
||||||
import EventEmitter from 'events'
|
import EventEmitter from 'events'
|
||||||
import { z } from 'zod'
|
import * as z from 'zod'
|
||||||
|
|
||||||
export interface OAuthStorageData {
|
export interface OAuthStorageData {
|
||||||
clientInfo?: OAuthClientInformation
|
clientInfo?: OAuthClientInformation
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { loggerService } from '@logger'
|
|||||||
import { isLinux } from '@main/constant'
|
import { isLinux } from '@main/constant'
|
||||||
import { BuiltinOcrProviderIds, OcrHandler, OcrProvider, OcrResult, SupportedOcrFile } from '@types'
|
import { BuiltinOcrProviderIds, OcrHandler, OcrProvider, OcrResult, SupportedOcrFile } from '@types'
|
||||||
|
|
||||||
|
import { ovOcrService } from './builtin/OvOcrService'
|
||||||
import { ppocrService } from './builtin/PpocrService'
|
import { ppocrService } from './builtin/PpocrService'
|
||||||
import { systemOcrService } from './builtin/SystemOcrService'
|
import { systemOcrService } from './builtin/SystemOcrService'
|
||||||
import { tesseractService } from './builtin/TesseractService'
|
import { tesseractService } from './builtin/TesseractService'
|
||||||
@@ -22,6 +23,10 @@ export class OcrService {
|
|||||||
this.registry.delete(providerId)
|
this.registry.delete(providerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public listProviderIds(): string[] {
|
||||||
|
return Array.from(this.registry.keys())
|
||||||
|
}
|
||||||
|
|
||||||
public async ocr(file: SupportedOcrFile, provider: OcrProvider): Promise<OcrResult> {
|
public async ocr(file: SupportedOcrFile, provider: OcrProvider): Promise<OcrResult> {
|
||||||
const handler = this.registry.get(provider.id)
|
const handler = this.registry.get(provider.id)
|
||||||
if (!handler) {
|
if (!handler) {
|
||||||
@@ -39,3 +44,5 @@ ocrService.register(BuiltinOcrProviderIds.tesseract, tesseractService.ocr.bind(t
|
|||||||
!isLinux && ocrService.register(BuiltinOcrProviderIds.system, systemOcrService.ocr.bind(systemOcrService))
|
!isLinux && ocrService.register(BuiltinOcrProviderIds.system, systemOcrService.ocr.bind(systemOcrService))
|
||||||
|
|
||||||
ocrService.register(BuiltinOcrProviderIds.paddleocr, ppocrService.ocr.bind(ppocrService))
|
ocrService.register(BuiltinOcrProviderIds.paddleocr, ppocrService.ocr.bind(ppocrService))
|
||||||
|
|
||||||
|
ovOcrService.isAvailable() && ocrService.register(BuiltinOcrProviderIds.ovocr, ovOcrService.ocr.bind(ovOcrService))
|
||||||
|
|||||||
@@ -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 { loadOcrImage } from '@main/utils/ocr'
|
||||||
import { ImageFileMetadata, isImageFileMetadata, OcrPpocrConfig, OcrResult, SupportedOcrFile } from '@types'
|
import { ImageFileMetadata, isImageFileMetadata, OcrPpocrConfig, OcrResult, SupportedOcrFile } from '@types'
|
||||||
import { net } from 'electron'
|
import { net } from 'electron'
|
||||||
import { z } from 'zod'
|
import * as z from 'zod'
|
||||||
|
|
||||||
import { OcrBaseService } from './OcrBaseService'
|
import { OcrBaseService } from './OcrBaseService'
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { SpanEntity, TokenUsage } from '@mcp-trace/trace-core'
|
|||||||
import { SpanContext } from '@opentelemetry/api'
|
import { SpanContext } from '@opentelemetry/api'
|
||||||
import { TerminalConfig, UpgradeChannel } from '@shared/config/constant'
|
import { TerminalConfig, UpgradeChannel } from '@shared/config/constant'
|
||||||
import type { LogLevel, LogSourceWithContext } from '@shared/config/logger'
|
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 { IpcChannel } from '@shared/IpcChannel'
|
||||||
import type { Notification } from '@types'
|
import type { Notification } from '@types'
|
||||||
import {
|
import {
|
||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
FileListResponse,
|
FileListResponse,
|
||||||
FileMetadata,
|
FileMetadata,
|
||||||
FileUploadResponse,
|
FileUploadResponse,
|
||||||
|
GetApiServerStatusResult,
|
||||||
KnowledgeBaseParams,
|
KnowledgeBaseParams,
|
||||||
KnowledgeItem,
|
KnowledgeItem,
|
||||||
KnowledgeSearchResult,
|
KnowledgeSearchResult,
|
||||||
@@ -22,8 +23,11 @@ import {
|
|||||||
OcrProvider,
|
OcrProvider,
|
||||||
OcrResult,
|
OcrResult,
|
||||||
Provider,
|
Provider,
|
||||||
|
RestartApiServerStatusResult,
|
||||||
S3Config,
|
S3Config,
|
||||||
Shortcut,
|
Shortcut,
|
||||||
|
StartApiServerStatusResult,
|
||||||
|
StopApiServerStatusResult,
|
||||||
SupportedOcrFile,
|
SupportedOcrFile,
|
||||||
ThemeMode,
|
ThemeMode,
|
||||||
WebDavConfig
|
WebDavConfig
|
||||||
@@ -51,7 +55,7 @@ const api = {
|
|||||||
setProxy: (proxy: string | undefined, bypassRules?: string) =>
|
setProxy: (proxy: string | undefined, bypassRules?: string) =>
|
||||||
ipcRenderer.invoke(IpcChannel.App_Proxy, proxy, bypassRules),
|
ipcRenderer.invoke(IpcChannel.App_Proxy, proxy, bypassRules),
|
||||||
checkForUpdate: () => ipcRenderer.invoke(IpcChannel.App_CheckForUpdate),
|
checkForUpdate: () => ipcRenderer.invoke(IpcChannel.App_CheckForUpdate),
|
||||||
showUpdateDialog: () => ipcRenderer.invoke(IpcChannel.App_ShowUpdateDialog),
|
quitAndInstall: () => ipcRenderer.invoke(IpcChannel.App_QuitAndInstall),
|
||||||
setLanguage: (lang: string) => ipcRenderer.invoke(IpcChannel.App_SetLanguage, lang),
|
setLanguage: (lang: string) => ipcRenderer.invoke(IpcChannel.App_SetLanguage, lang),
|
||||||
setEnableSpellCheck: (isEnable: boolean) => ipcRenderer.invoke(IpcChannel.App_SetEnableSpellCheck, isEnable),
|
setEnableSpellCheck: (isEnable: boolean) => ipcRenderer.invoke(IpcChannel.App_SetEnableSpellCheck, isEnable),
|
||||||
setSpellCheckLanguages: (languages: string[]) => ipcRenderer.invoke(IpcChannel.App_SetSpellCheckLanguages, languages),
|
setSpellCheckLanguages: (languages: string[]) => ipcRenderer.invoke(IpcChannel.App_SetSpellCheckLanguages, languages),
|
||||||
@@ -223,7 +227,7 @@ const api = {
|
|||||||
create: (base: KnowledgeBaseParams, context?: SpanContext) =>
|
create: (base: KnowledgeBaseParams, context?: SpanContext) =>
|
||||||
tracedInvoke(IpcChannel.KnowledgeBase_Create, context, base),
|
tracedInvoke(IpcChannel.KnowledgeBase_Create, context, base),
|
||||||
reset: (base: KnowledgeBaseParams) => ipcRenderer.invoke(IpcChannel.KnowledgeBase_Reset, base),
|
reset: (base: KnowledgeBaseParams) => ipcRenderer.invoke(IpcChannel.KnowledgeBase_Reset, base),
|
||||||
delete: (base: KnowledgeBaseParams, id: string) => ipcRenderer.invoke(IpcChannel.KnowledgeBase_Delete, base, id),
|
delete: (id: string) => ipcRenderer.invoke(IpcChannel.KnowledgeBase_Delete, id),
|
||||||
add: ({
|
add: ({
|
||||||
base,
|
base,
|
||||||
item,
|
item,
|
||||||
@@ -390,7 +394,16 @@ const api = {
|
|||||||
setOpenLinkExternal: (webviewId: number, isExternal: boolean) =>
|
setOpenLinkExternal: (webviewId: number, isExternal: boolean) =>
|
||||||
ipcRenderer.invoke(IpcChannel.Webview_SetOpenLinkExternal, webviewId, isExternal),
|
ipcRenderer.invoke(IpcChannel.Webview_SetOpenLinkExternal, webviewId, isExternal),
|
||||||
setSpellCheckEnabled: (webviewId: number, isEnable: boolean) =>
|
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: {
|
storeSync: {
|
||||||
subscribe: () => ipcRenderer.invoke(IpcChannel.StoreSync_Subscribe),
|
subscribe: () => ipcRenderer.invoke(IpcChannel.StoreSync_Subscribe),
|
||||||
@@ -467,7 +480,8 @@ const api = {
|
|||||||
},
|
},
|
||||||
ocr: {
|
ocr: {
|
||||||
ocr: (file: SupportedOcrFile, provider: OcrProvider): Promise<OcrResult> =>
|
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: {
|
cherryai: {
|
||||||
generateSignature: (params: { method: string; path: string; query: string; body: Record<string, any> }) =>
|
generateSignature: (params: { method: string; path: string; query: string; body: Record<string, any> }) =>
|
||||||
@@ -487,6 +501,12 @@ const api = {
|
|||||||
ipcRenderer.removeListener(channel, listener)
|
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,9 +6,11 @@
|
|||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
import { AISDKWebSearchResult, MCPTool, WebSearchResults, WebSearchSource } from '@renderer/types'
|
import { AISDKWebSearchResult, MCPTool, WebSearchResults, WebSearchSource } from '@renderer/types'
|
||||||
import { Chunk, ChunkType } from '@renderer/types/chunk'
|
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 { convertLinks, flushLinkConverterBuffer } from '@renderer/utils/linkConverter'
|
||||||
import type { ClaudeCodeRawValue } from '@shared/agents/claudecode/types'
|
import type { ClaudeCodeRawValue } from '@shared/agents/claudecode/types'
|
||||||
import type { TextStreamPart, ToolSet } from 'ai'
|
import { AISDKError, type TextStreamPart, type ToolSet } from 'ai'
|
||||||
|
|
||||||
import { ToolCallChunkHandler } from './handleToolCallChunk'
|
import { ToolCallChunkHandler } from './handleToolCallChunk'
|
||||||
|
|
||||||
@@ -24,6 +26,8 @@ export class AiSdkToChunkAdapter {
|
|||||||
private isFirstChunk = true
|
private isFirstChunk = true
|
||||||
private enableWebSearch: boolean = false
|
private enableWebSearch: boolean = false
|
||||||
private onSessionUpdate?: (sessionId: string) => void
|
private onSessionUpdate?: (sessionId: string) => void
|
||||||
|
private responseStartTimestamp: number | null = null
|
||||||
|
private firstTokenTimestamp: number | null = null
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private onChunk: (chunk: Chunk) => void,
|
private onChunk: (chunk: Chunk) => void,
|
||||||
@@ -38,6 +42,17 @@ export class AiSdkToChunkAdapter {
|
|||||||
this.onSessionUpdate = onSessionUpdate
|
this.onSessionUpdate = onSessionUpdate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private markFirstTokenIfNeeded() {
|
||||||
|
if (this.firstTokenTimestamp === null && this.responseStartTimestamp !== null) {
|
||||||
|
this.firstTokenTimestamp = Date.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private resetTimingState() {
|
||||||
|
this.responseStartTimestamp = null
|
||||||
|
this.firstTokenTimestamp = null
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理 AI SDK 流结果
|
* 处理 AI SDK 流结果
|
||||||
* @param aiSdkResult AI SDK 的流结果对象
|
* @param aiSdkResult AI SDK 的流结果对象
|
||||||
@@ -65,6 +80,8 @@ export class AiSdkToChunkAdapter {
|
|||||||
webSearchResults: [],
|
webSearchResults: [],
|
||||||
reasoningId: ''
|
reasoningId: ''
|
||||||
}
|
}
|
||||||
|
this.resetTimingState()
|
||||||
|
this.responseStartTimestamp = Date.now()
|
||||||
// Reset link converter state at the start of stream
|
// Reset link converter state at the start of stream
|
||||||
this.isFirstChunk = true
|
this.isFirstChunk = true
|
||||||
|
|
||||||
@@ -77,6 +94,7 @@ export class AiSdkToChunkAdapter {
|
|||||||
if (this.enableWebSearch) {
|
if (this.enableWebSearch) {
|
||||||
const remainingText = flushLinkConverterBuffer()
|
const remainingText = flushLinkConverterBuffer()
|
||||||
if (remainingText) {
|
if (remainingText) {
|
||||||
|
this.markFirstTokenIfNeeded()
|
||||||
this.onChunk({
|
this.onChunk({
|
||||||
type: ChunkType.TEXT_DELTA,
|
type: ChunkType.TEXT_DELTA,
|
||||||
text: remainingText
|
text: remainingText
|
||||||
@@ -91,6 +109,7 @@ export class AiSdkToChunkAdapter {
|
|||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
reader.releaseLock()
|
reader.releaseLock()
|
||||||
|
this.resetTimingState()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,6 +171,7 @@ export class AiSdkToChunkAdapter {
|
|||||||
|
|
||||||
// Only emit chunk if there's text to send
|
// Only emit chunk if there's text to send
|
||||||
if (finalText) {
|
if (finalText) {
|
||||||
|
this.markFirstTokenIfNeeded()
|
||||||
this.onChunk({
|
this.onChunk({
|
||||||
type: ChunkType.TEXT_DELTA,
|
type: ChunkType.TEXT_DELTA,
|
||||||
text: this.accumulate ? final.text : finalText
|
text: this.accumulate ? final.text : finalText
|
||||||
@@ -176,6 +196,9 @@ export class AiSdkToChunkAdapter {
|
|||||||
break
|
break
|
||||||
case 'reasoning-delta':
|
case 'reasoning-delta':
|
||||||
final.reasoningContent += chunk.text || ''
|
final.reasoningContent += chunk.text || ''
|
||||||
|
if (chunk.text) {
|
||||||
|
this.markFirstTokenIfNeeded()
|
||||||
|
}
|
||||||
this.onChunk({
|
this.onChunk({
|
||||||
type: ChunkType.THINKING_DELTA,
|
type: ChunkType.THINKING_DELTA,
|
||||||
text: final.reasoningContent || ''
|
text: final.reasoningContent || ''
|
||||||
@@ -275,44 +298,37 @@ export class AiSdkToChunkAdapter {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'finish':
|
case 'finish': {
|
||||||
|
const usage = {
|
||||||
|
completion_tokens: chunk.totalUsage?.outputTokens || 0,
|
||||||
|
prompt_tokens: chunk.totalUsage?.inputTokens || 0,
|
||||||
|
total_tokens: chunk.totalUsage?.totalTokens || 0
|
||||||
|
}
|
||||||
|
const metrics = this.buildMetrics(chunk.totalUsage)
|
||||||
|
const baseResponse = {
|
||||||
|
text: final.text || '',
|
||||||
|
reasoning_content: final.reasoningContent || ''
|
||||||
|
}
|
||||||
|
|
||||||
this.onChunk({
|
this.onChunk({
|
||||||
type: ChunkType.BLOCK_COMPLETE,
|
type: ChunkType.BLOCK_COMPLETE,
|
||||||
response: {
|
response: {
|
||||||
text: final.text || '',
|
...baseResponse,
|
||||||
reasoning_content: final.reasoningContent || '',
|
usage: { ...usage },
|
||||||
usage: {
|
metrics: metrics ? { ...metrics } : undefined
|
||||||
completion_tokens: chunk.totalUsage.outputTokens || 0,
|
|
||||||
prompt_tokens: chunk.totalUsage.inputTokens || 0,
|
|
||||||
total_tokens: chunk.totalUsage.totalTokens || 0
|
|
||||||
},
|
|
||||||
metrics: chunk.totalUsage
|
|
||||||
? {
|
|
||||||
completion_tokens: chunk.totalUsage.outputTokens || 0,
|
|
||||||
time_completion_millsec: 0
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
this.onChunk({
|
this.onChunk({
|
||||||
type: ChunkType.LLM_RESPONSE_COMPLETE,
|
type: ChunkType.LLM_RESPONSE_COMPLETE,
|
||||||
response: {
|
response: {
|
||||||
text: final.text || '',
|
...baseResponse,
|
||||||
reasoning_content: final.reasoningContent || '',
|
usage: { ...usage },
|
||||||
usage: {
|
metrics: metrics ? { ...metrics } : undefined
|
||||||
completion_tokens: chunk.totalUsage.outputTokens || 0,
|
|
||||||
prompt_tokens: chunk.totalUsage.inputTokens || 0,
|
|
||||||
total_tokens: chunk.totalUsage.totalTokens || 0
|
|
||||||
},
|
|
||||||
metrics: chunk.totalUsage
|
|
||||||
? {
|
|
||||||
completion_tokens: chunk.totalUsage.outputTokens || 0,
|
|
||||||
time_completion_millsec: 0
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
this.resetTimingState()
|
||||||
break
|
break
|
||||||
|
}
|
||||||
|
|
||||||
// === 源和文件相关事件 ===
|
// === 源和文件相关事件 ===
|
||||||
case 'source':
|
case 'source':
|
||||||
@@ -341,13 +357,48 @@ export class AiSdkToChunkAdapter {
|
|||||||
case 'error':
|
case 'error':
|
||||||
this.onChunk({
|
this.onChunk({
|
||||||
type: ChunkType.ERROR,
|
type: ChunkType.ERROR,
|
||||||
error: chunk.error as Record<string, any>
|
error:
|
||||||
|
chunk.error instanceof AISDKError
|
||||||
|
? chunk.error
|
||||||
|
: new ProviderSpecificError({
|
||||||
|
message: formatErrorMessage(chunk.error),
|
||||||
|
provider: 'unknown',
|
||||||
|
cause: chunk.error
|
||||||
|
})
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
|
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private buildMetrics(totalUsage?: {
|
||||||
|
inputTokens?: number | null
|
||||||
|
outputTokens?: number | null
|
||||||
|
totalTokens?: number | null
|
||||||
|
}) {
|
||||||
|
if (!totalUsage) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const completionTokens = totalUsage.outputTokens ?? 0
|
||||||
|
const now = Date.now()
|
||||||
|
const start = this.responseStartTimestamp ?? now
|
||||||
|
const firstToken = this.firstTokenTimestamp
|
||||||
|
const timeFirstToken = Math.max(firstToken != null ? firstToken - start : 0, 0)
|
||||||
|
const baseForCompletion = firstToken ?? start
|
||||||
|
let timeCompletion = Math.max(now - baseForCompletion, 0)
|
||||||
|
|
||||||
|
if (timeCompletion === 0 && completionTokens > 0) {
|
||||||
|
timeCompletion = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
completion_tokens: completionTokens,
|
||||||
|
time_first_token_millsec: timeFirstToken,
|
||||||
|
time_completion_millsec: timeCompletion
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AiSdkToChunkAdapter
|
export default AiSdkToChunkAdapter
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { addSpan, endSpan } from '@renderer/services/SpanManagerService'
|
|||||||
import { StartSpanParams } from '@renderer/trace/types/ModelSpanEntity'
|
import { StartSpanParams } from '@renderer/trace/types/ModelSpanEntity'
|
||||||
import type { Assistant, GenerateImageParams, Model, Provider } from '@renderer/types'
|
import type { Assistant, GenerateImageParams, Model, Provider } from '@renderer/types'
|
||||||
import type { AiSdkModel, StreamTextParams } from '@renderer/types/aiCoreTypes'
|
import type { AiSdkModel, StreamTextParams } from '@renderer/types/aiCoreTypes'
|
||||||
|
import { buildClaudeCodeSystemModelMessage } from '@shared/anthropic'
|
||||||
import { type ImageModel, type LanguageModel, type Provider as AiSdkProvider, wrapLanguageModel } from 'ai'
|
import { type ImageModel, type LanguageModel, type Provider as AiSdkProvider, wrapLanguageModel } from 'ai'
|
||||||
|
|
||||||
import AiSdkToChunkAdapter from './chunk/AiSdkToChunkAdapter'
|
import AiSdkToChunkAdapter from './chunk/AiSdkToChunkAdapter'
|
||||||
@@ -21,7 +22,6 @@ import LegacyAiProvider from './legacy/index'
|
|||||||
import { CompletionsParams, CompletionsResult } from './legacy/middleware/schemas'
|
import { CompletionsParams, CompletionsResult } from './legacy/middleware/schemas'
|
||||||
import { AiSdkMiddlewareConfig, buildAiSdkMiddlewares } from './middleware/AiSdkMiddlewareBuilder'
|
import { AiSdkMiddlewareConfig, buildAiSdkMiddlewares } from './middleware/AiSdkMiddlewareBuilder'
|
||||||
import { buildPlugins } from './plugins/PluginBuilder'
|
import { buildPlugins } from './plugins/PluginBuilder'
|
||||||
import { buildClaudeCodeSystemMessage } from './provider/config/anthropic'
|
|
||||||
import { createAiSdkProvider } from './provider/factory'
|
import { createAiSdkProvider } from './provider/factory'
|
||||||
import {
|
import {
|
||||||
getActualProvider,
|
getActualProvider,
|
||||||
@@ -83,10 +83,8 @@ export default class ModernAiProvider {
|
|||||||
throw new Error('Model is required for completions. Please use constructor with model parameter.')
|
throw new Error('Model is required for completions. Please use constructor with model parameter.')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确保配置存在
|
// 每次请求时重新生成配置以确保API key轮换生效
|
||||||
if (!this.config) {
|
this.config = providerToAiSdkConfig(this.actualProvider, this.model)
|
||||||
this.config = providerToAiSdkConfig(this.actualProvider, this.model)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 准备特殊配置
|
// 准备特殊配置
|
||||||
await prepareSpecialProviderConfig(this.actualProvider, this.config)
|
await prepareSpecialProviderConfig(this.actualProvider, this.config)
|
||||||
@@ -122,13 +120,9 @@ export default class ModernAiProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.actualProvider.id === 'anthropic' && this.actualProvider.authType === 'oauth') {
|
if (this.actualProvider.id === 'anthropic' && this.actualProvider.authType === 'oauth') {
|
||||||
const claudeCodeSystemMessage = buildClaudeCodeSystemMessage(params.system)
|
const claudeCodeSystemMessage = buildClaudeCodeSystemModelMessage(params.system)
|
||||||
params.system = undefined // 清除原有system,避免重复
|
params.system = undefined // 清除原有system,避免重复
|
||||||
if (Array.isArray(params.messages)) {
|
params.messages = [...claudeCodeSystemMessage, ...(params.messages || [])]
|
||||||
params.messages = [...claudeCodeSystemMessage, ...params.messages]
|
|
||||||
} else {
|
|
||||||
params.messages = claudeCodeSystemMessage
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.topicId && getEnableDeveloperMode()) {
|
if (config.topicId && getEnableDeveloperMode()) {
|
||||||
|
|||||||
@@ -70,13 +70,19 @@ export abstract class BaseApiClient<
|
|||||||
{
|
{
|
||||||
public provider: Provider
|
public provider: Provider
|
||||||
protected host: string
|
protected host: string
|
||||||
protected apiKey: string
|
|
||||||
protected sdkInstance?: TSdkInstance
|
protected sdkInstance?: TSdkInstance
|
||||||
|
|
||||||
constructor(provider: Provider) {
|
constructor(provider: Provider) {
|
||||||
this.provider = provider
|
this.provider = provider
|
||||||
this.host = this.getBaseURL()
|
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,4 +1,5 @@
|
|||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
|
import { COPILOT_DEFAULT_HEADERS } from '@renderer/aiCore/provider/constants'
|
||||||
import {
|
import {
|
||||||
isClaudeReasoningModel,
|
isClaudeReasoningModel,
|
||||||
isOpenAIReasoningModel,
|
isOpenAIReasoningModel,
|
||||||
@@ -167,8 +168,7 @@ export abstract class OpenAIBaseClient<
|
|||||||
defaultHeaders: {
|
defaultHeaders: {
|
||||||
...this.defaultHeaders(),
|
...this.defaultHeaders(),
|
||||||
...this.provider.extra_headers,
|
...this.provider.extra_headers,
|
||||||
...(this.provider.id === 'copilot' ? { 'editor-version': 'vscode/1.97.2' } : {}),
|
...(this.provider.id === 'copilot' ? COPILOT_DEFAULT_HEADERS : {})
|
||||||
...(this.provider.id === 'copilot' ? { 'copilot-vision-request': 'true' } : {})
|
|
||||||
}
|
}
|
||||||
}) as TSdkInstance
|
}) as TSdkInstance
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import type { MCPTool, Message, Model, Provider } from '@renderer/types'
|
|||||||
import type { Chunk } from '@renderer/types/chunk'
|
import type { Chunk } from '@renderer/types/chunk'
|
||||||
import { extractReasoningMiddleware, LanguageModelMiddleware, simulateStreamingMiddleware } from 'ai'
|
import { extractReasoningMiddleware, LanguageModelMiddleware, simulateStreamingMiddleware } from 'ai'
|
||||||
|
|
||||||
|
import { noThinkMiddleware } from './noThinkMiddleware'
|
||||||
|
|
||||||
const logger = loggerService.withContext('AiSdkMiddlewareBuilder')
|
const logger = loggerService.withContext('AiSdkMiddlewareBuilder')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -186,6 +188,14 @@ function addProviderSpecificMiddlewares(builder: AiSdkMiddlewareBuilder, config:
|
|||||||
// 其他provider的通用处理
|
// 其他provider的通用处理
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OVMS+MCP's specific middleware
|
||||||
|
if (config.provider.id === 'ovms' && config.mcpTools && config.mcpTools.length > 0) {
|
||||||
|
builder.add({
|
||||||
|
name: 'no-think',
|
||||||
|
middleware: noThinkMiddleware()
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ import { CherryWebSearchConfig } from '@renderer/store/websearch'
|
|||||||
import { type Assistant, type MCPTool, type Provider } from '@renderer/types'
|
import { type Assistant, type MCPTool, type Provider } from '@renderer/types'
|
||||||
import type { StreamTextParams } from '@renderer/types/aiCoreTypes'
|
import type { StreamTextParams } from '@renderer/types/aiCoreTypes'
|
||||||
import { mapRegexToPatterns } from '@renderer/utils/blacklistMatchPattern'
|
import { mapRegexToPatterns } from '@renderer/utils/blacklistMatchPattern'
|
||||||
|
import { replacePromptVariables } from '@renderer/utils/prompt'
|
||||||
import type { ModelMessage, Tool } from 'ai'
|
import type { ModelMessage, Tool } from 'ai'
|
||||||
import { stepCountIs } from 'ai'
|
import { stepCountIs } from 'ai'
|
||||||
|
|
||||||
@@ -159,14 +160,14 @@ export async function buildStreamTextParams(
|
|||||||
abortSignal: options.requestOptions?.signal,
|
abortSignal: options.requestOptions?.signal,
|
||||||
headers: options.requestOptions?.headers,
|
headers: options.requestOptions?.headers,
|
||||||
providerOptions,
|
providerOptions,
|
||||||
stopWhen: stepCountIs(10),
|
stopWhen: stepCountIs(20),
|
||||||
maxRetries: 0
|
maxRetries: 0
|
||||||
}
|
}
|
||||||
if (tools) {
|
if (tools) {
|
||||||
params.tools = tools
|
params.tools = tools
|
||||||
}
|
}
|
||||||
if (assistant.prompt) {
|
if (assistant.prompt) {
|
||||||
params.system = assistant.prompt
|
params.system = await replacePromptVariables(assistant.prompt, model.name)
|
||||||
}
|
}
|
||||||
logger.debug('params', params)
|
logger.debug('params', params)
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
|
vi.mock('@renderer/services/LoggerService', () => ({
|
||||||
|
loggerService: {
|
||||||
|
withContext: () => ({
|
||||||
|
debug: vi.fn(),
|
||||||
|
info: vi.fn(),
|
||||||
|
warn: vi.fn(),
|
||||||
|
error: vi.fn()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@renderer/services/AssistantService', () => ({
|
||||||
|
getProviderByModel: vi.fn()
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@renderer/store', () => ({
|
||||||
|
default: {
|
||||||
|
getState: () => ({ copilot: { defaultHeaders: {} } })
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
import type { Model, Provider } from '@renderer/types'
|
||||||
|
|
||||||
|
import { COPILOT_DEFAULT_HEADERS, COPILOT_EDITOR_VERSION, isCopilotResponsesModel } from '../constants'
|
||||||
|
import { providerToAiSdkConfig } from '../providerConfig'
|
||||||
|
|
||||||
|
const createWindowKeyv = () => {
|
||||||
|
const store = new Map<string, string>()
|
||||||
|
return {
|
||||||
|
get: (key: string) => store.get(key),
|
||||||
|
set: (key: string, value: string) => {
|
||||||
|
store.set(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createCopilotProvider = (): Provider => ({
|
||||||
|
id: 'copilot',
|
||||||
|
type: 'openai',
|
||||||
|
name: 'GitHub Copilot',
|
||||||
|
apiKey: 'test-key',
|
||||||
|
apiHost: 'https://api.githubcopilot.com',
|
||||||
|
models: [],
|
||||||
|
isSystem: true
|
||||||
|
})
|
||||||
|
|
||||||
|
const createModel = (id: string, name = id): Model => ({
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
provider: 'copilot',
|
||||||
|
group: 'copilot'
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Copilot responses routing', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
;(globalThis as any).window = {
|
||||||
|
...(globalThis as any).window,
|
||||||
|
keyv: createWindowKeyv()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('detects official GPT-5 Codex identifiers case-insensitively', () => {
|
||||||
|
expect(isCopilotResponsesModel(createModel('gpt-5-codex', 'gpt-5-codex'))).toBe(true)
|
||||||
|
expect(isCopilotResponsesModel(createModel('GPT-5-CODEX', 'GPT-5-CODEX'))).toBe(true)
|
||||||
|
expect(isCopilotResponsesModel(createModel('gpt-5-codex', 'custom-name'))).toBe(true)
|
||||||
|
expect(isCopilotResponsesModel(createModel('custom-id', 'custom-name'))).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('configures gpt-5-codex with the Copilot provider', () => {
|
||||||
|
const provider = createCopilotProvider()
|
||||||
|
const config = providerToAiSdkConfig(provider, createModel('gpt-5-codex', 'GPT-5-CODEX'))
|
||||||
|
|
||||||
|
expect(config.providerId).toBe('github-copilot-openai-compatible')
|
||||||
|
expect(config.options.headers?.['Editor-Version']).toBe(COPILOT_EDITOR_VERSION)
|
||||||
|
expect(config.options.headers?.['Copilot-Integration-Id']).toBe(COPILOT_DEFAULT_HEADERS['Copilot-Integration-Id'])
|
||||||
|
expect(config.options.headers?.['copilot-vision-request']).toBe('true')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses the Copilot provider for other models and keeps headers', () => {
|
||||||
|
const provider = createCopilotProvider()
|
||||||
|
const config = providerToAiSdkConfig(provider, createModel('gpt-4'))
|
||||||
|
|
||||||
|
expect(config.providerId).toBe('github-copilot-openai-compatible')
|
||||||
|
expect(config.options.headers?.['Editor-Version']).toBe(COPILOT_DEFAULT_HEADERS['Editor-Version'])
|
||||||
|
expect(config.options.headers?.['Copilot-Integration-Id']).toBe(COPILOT_DEFAULT_HEADERS['Copilot-Integration-Id'])
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import { SystemModelMessage } from 'ai'
|
|
||||||
|
|
||||||
export function buildClaudeCodeSystemMessage(system?: string): Array<SystemModelMessage> {
|
|
||||||
const defaultClaudeCodeSystem = `You are Claude Code, Anthropic's official CLI for Claude.`
|
|
||||||
if (!system || system.trim() === defaultClaudeCodeSystem) {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
role: 'system',
|
|
||||||
content: defaultClaudeCodeSystem
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
role: 'system',
|
|
||||||
content: defaultClaudeCodeSystem
|
|
||||||
},
|
|
||||||
{
|
|
||||||
role: 'system',
|
|
||||||
content: system
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import type { Model } from '@renderer/types'
|
||||||
|
|
||||||
|
export const COPILOT_EDITOR_VERSION = 'vscode/1.104.1'
|
||||||
|
export const COPILOT_PLUGIN_VERSION = 'copilot-chat/0.26.7'
|
||||||
|
export const COPILOT_INTEGRATION_ID = 'vscode-chat'
|
||||||
|
export const COPILOT_USER_AGENT = 'GitHubCopilotChat/0.26.7'
|
||||||
|
|
||||||
|
export const COPILOT_DEFAULT_HEADERS = {
|
||||||
|
'Copilot-Integration-Id': COPILOT_INTEGRATION_ID,
|
||||||
|
'User-Agent': COPILOT_USER_AGENT,
|
||||||
|
'Editor-Version': COPILOT_EDITOR_VERSION,
|
||||||
|
'Editor-Plugin-Version': COPILOT_PLUGIN_VERSION,
|
||||||
|
'editor-version': COPILOT_EDITOR_VERSION,
|
||||||
|
'editor-plugin-version': COPILOT_PLUGIN_VERSION,
|
||||||
|
'copilot-vision-request': 'true'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
// Models that require the OpenAI Responses endpoint when routed through GitHub Copilot (#10560)
|
||||||
|
const COPILOT_RESPONSES_MODEL_IDS = ['gpt-5-codex']
|
||||||
|
|
||||||
|
export function isCopilotResponsesModel(model: Model): boolean {
|
||||||
|
const normalizedId = model.id?.trim().toLowerCase()
|
||||||
|
const normalizedName = model.name?.trim().toLowerCase()
|
||||||
|
return COPILOT_RESPONSES_MODEL_IDS.some((target) => normalizedId === target || normalizedName === target)
|
||||||
|
}
|
||||||
@@ -28,7 +28,8 @@ const STATIC_PROVIDER_MAPPING: Record<string, ProviderId> = {
|
|||||||
gemini: 'google', // Google Gemini -> google
|
gemini: 'google', // Google Gemini -> google
|
||||||
'azure-openai': 'azure', // Azure OpenAI -> azure
|
'azure-openai': 'azure', // Azure OpenAI -> azure
|
||||||
'openai-response': 'openai', // OpenAI Responses -> openai
|
'openai-response': 'openai', // OpenAI Responses -> openai
|
||||||
grok: 'xai' // Grok -> xai
|
grok: 'xai', // Grok -> xai
|
||||||
|
copilot: 'github-copilot-openai-compatible'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { formatApiHost } from '@renderer/utils/api'
|
|||||||
import { cloneDeep, trim } from 'lodash'
|
import { cloneDeep, trim } from 'lodash'
|
||||||
|
|
||||||
import { aihubmixProviderCreator, newApiResolverCreator, vertexAnthropicProviderCreator } from './config'
|
import { aihubmixProviderCreator, newApiResolverCreator, vertexAnthropicProviderCreator } from './config'
|
||||||
|
import { COPILOT_DEFAULT_HEADERS } from './constants'
|
||||||
import { getAiSdkProviderId } from './factory'
|
import { getAiSdkProviderId } from './factory'
|
||||||
|
|
||||||
const logger = loggerService.withContext('ProviderConfigProcessor')
|
const logger = loggerService.withContext('ProviderConfigProcessor')
|
||||||
@@ -62,13 +63,14 @@ function handleSpecialProviders(model: Model, provider: Provider): Provider {
|
|||||||
// return createVertexProvider(provider)
|
// return createVertexProvider(provider)
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
if (isNewApiProvider(provider)) {
|
||||||
|
return newApiResolverCreator(model, provider)
|
||||||
|
}
|
||||||
|
|
||||||
if (isSystemProvider(provider)) {
|
if (isSystemProvider(provider)) {
|
||||||
if (provider.id === 'aihubmix') {
|
if (provider.id === 'aihubmix') {
|
||||||
return aihubmixProviderCreator(model, provider)
|
return aihubmixProviderCreator(model, provider)
|
||||||
}
|
}
|
||||||
if (isNewApiProvider(provider)) {
|
|
||||||
return newApiResolverCreator(model, provider)
|
|
||||||
}
|
|
||||||
if (provider.id === 'vertexai') {
|
if (provider.id === 'vertexai') {
|
||||||
return vertexAnthropicProviderCreator(model, provider)
|
return vertexAnthropicProviderCreator(model, provider)
|
||||||
}
|
}
|
||||||
@@ -109,6 +111,9 @@ function formatProviderApiHost(provider: Provider): Provider {
|
|||||||
if (!formatted.anthropicApiHost) {
|
if (!formatted.anthropicApiHost) {
|
||||||
formatted.anthropicApiHost = formatted.apiHost
|
formatted.anthropicApiHost = formatted.apiHost
|
||||||
}
|
}
|
||||||
|
} else if (formatted.id === 'copilot') {
|
||||||
|
const trimmed = trim(formatted.apiHost)
|
||||||
|
formatted.apiHost = trimmed.endsWith('/') ? trimmed.slice(0, -1) : trimmed
|
||||||
} else if (formatted.type === 'gemini') {
|
} else if (formatted.type === 'gemini') {
|
||||||
formatted.apiHost = formatApiHost(formatted.apiHost, 'v1beta')
|
formatted.apiHost = formatApiHost(formatted.apiHost, 'v1beta')
|
||||||
} else {
|
} else {
|
||||||
@@ -151,6 +156,26 @@ export function providerToAiSdkConfig(
|
|||||||
baseURL: trim(actualProvider.apiHost),
|
baseURL: trim(actualProvider.apiHost),
|
||||||
apiKey: getRotatedApiKey(actualProvider)
|
apiKey: getRotatedApiKey(actualProvider)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isCopilotProvider = actualProvider.id === 'copilot'
|
||||||
|
if (isCopilotProvider) {
|
||||||
|
const storedHeaders = store.getState().copilot.defaultHeaders ?? {}
|
||||||
|
const options = ProviderConfigFactory.fromProvider('github-copilot-openai-compatible', baseConfig, {
|
||||||
|
headers: {
|
||||||
|
...COPILOT_DEFAULT_HEADERS,
|
||||||
|
...storedHeaders,
|
||||||
|
...actualProvider.extra_headers
|
||||||
|
},
|
||||||
|
name: actualProvider.id,
|
||||||
|
includeUsage: true
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
providerId: 'github-copilot-openai-compatible',
|
||||||
|
options
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 处理OpenAI模式
|
// 处理OpenAI模式
|
||||||
const extraOptions: any = {}
|
const extraOptions: any = {}
|
||||||
if (actualProvider.type === 'openai-response' && !isOpenAIChatCompletionOnlyModel(model)) {
|
if (actualProvider.type === 'openai-response' && !isOpenAIChatCompletionOnlyModel(model)) {
|
||||||
@@ -172,15 +197,6 @@ export function providerToAiSdkConfig(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// copilot
|
|
||||||
if (actualProvider.id === 'copilot') {
|
|
||||||
extraOptions.headers = {
|
|
||||||
...extraOptions.headers,
|
|
||||||
'editor-version': 'vscode/1.97.2',
|
|
||||||
'copilot-vision-request': 'true'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// azure
|
// azure
|
||||||
if (aiSdkProviderId === 'azure' || actualProvider.type === 'azure-openai') {
|
if (aiSdkProviderId === 'azure' || actualProvider.type === 'azure-openai') {
|
||||||
extraOptions.apiVersion = actualProvider.apiVersion
|
extraOptions.apiVersion = actualProvider.apiVersion
|
||||||
@@ -229,7 +245,6 @@ export function providerToAiSdkConfig(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果AI SDK支持该provider,使用原生配置
|
|
||||||
if (hasProviderConfig(aiSdkProviderId) && aiSdkProviderId !== 'openai-compatible') {
|
if (hasProviderConfig(aiSdkProviderId) && aiSdkProviderId !== 'openai-compatible') {
|
||||||
const options = ProviderConfigFactory.fromProvider(aiSdkProviderId, baseConfig, extraOptions)
|
const options = ProviderConfigFactory.fromProvider(aiSdkProviderId, baseConfig, extraOptions)
|
||||||
return {
|
return {
|
||||||
@@ -277,9 +292,17 @@ export async function prepareSpecialProviderConfig(
|
|||||||
) {
|
) {
|
||||||
switch (provider.id) {
|
switch (provider.id) {
|
||||||
case 'copilot': {
|
case 'copilot': {
|
||||||
const defaultHeaders = store.getState().copilot.defaultHeaders
|
const defaultHeaders = store.getState().copilot.defaultHeaders ?? {}
|
||||||
const { token } = await window.api.copilot.getToken(defaultHeaders)
|
const headers = {
|
||||||
|
...COPILOT_DEFAULT_HEADERS,
|
||||||
|
...defaultHeaders
|
||||||
|
}
|
||||||
|
const { token } = await window.api.copilot.getToken(headers)
|
||||||
config.options.apiKey = token
|
config.options.apiKey = token
|
||||||
|
config.options.headers = {
|
||||||
|
...headers,
|
||||||
|
...config.options.headers
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'cherryai': {
|
case 'cherryai': {
|
||||||
|
|||||||
@@ -32,6 +32,14 @@ export const NEW_PROVIDER_CONFIGS: ProviderConfig[] = [
|
|||||||
supportsImageGeneration: true,
|
supportsImageGeneration: true,
|
||||||
aliases: ['vertexai-anthropic']
|
aliases: ['vertexai-anthropic']
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'github-copilot-openai-compatible',
|
||||||
|
name: 'GitHub Copilot OpenAI Compatible',
|
||||||
|
import: () => import('@opeoginni/github-copilot-openai-compatible'),
|
||||||
|
creatorFunctionName: 'createGitHubCopilotOpenAICompatible',
|
||||||
|
supportsImageGeneration: false,
|
||||||
|
aliases: ['copilot', 'github-copilot']
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'bedrock',
|
id: 'bedrock',
|
||||||
name: 'Amazon Bedrock',
|
name: 'Amazon Bedrock',
|
||||||
@@ -47,6 +55,14 @@ export const NEW_PROVIDER_CONFIGS: ProviderConfig[] = [
|
|||||||
creatorFunctionName: 'createPerplexity',
|
creatorFunctionName: 'createPerplexity',
|
||||||
supportsImageGeneration: false,
|
supportsImageGeneration: false,
|
||||||
aliases: ['perplexity']
|
aliases: ['perplexity']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'mistral',
|
||||||
|
name: 'Mistral',
|
||||||
|
import: () => import('@ai-sdk/mistral'),
|
||||||
|
creatorFunctionName: 'createMistral',
|
||||||
|
supportsImageGeneration: false,
|
||||||
|
aliases: ['mistral']
|
||||||
}
|
}
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { Assistant, KnowledgeReference } from '@renderer/types'
|
|||||||
import { ExtractResults, KnowledgeExtractResults } from '@renderer/utils/extract'
|
import { ExtractResults, KnowledgeExtractResults } from '@renderer/utils/extract'
|
||||||
import { type InferToolInput, type InferToolOutput, tool } from 'ai'
|
import { type InferToolInput, type InferToolOutput, tool } from 'ai'
|
||||||
import { isEmpty } from 'lodash'
|
import { isEmpty } from 'lodash'
|
||||||
import { z } from 'zod'
|
import * as z from 'zod'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 知识库搜索工具
|
* 知识库搜索工具
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import store from '@renderer/store'
|
import store from '@renderer/store'
|
||||||
import { selectCurrentUserId, selectGlobalMemoryEnabled, selectMemoryConfig } from '@renderer/store/memory'
|
import { selectCurrentUserId, selectGlobalMemoryEnabled, selectMemoryConfig } from '@renderer/store/memory'
|
||||||
import { type InferToolInput, type InferToolOutput, tool } from 'ai'
|
import { type InferToolInput, type InferToolOutput, tool } from 'ai'
|
||||||
import { z } from 'zod'
|
import * as z from 'zod'
|
||||||
|
|
||||||
import { MemoryProcessor } from '../../services/MemoryProcessor'
|
import { MemoryProcessor } from '../../services/MemoryProcessor'
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import WebSearchService from '@renderer/services/WebSearchService'
|
|||||||
import { WebSearchProvider, WebSearchProviderResponse } from '@renderer/types'
|
import { WebSearchProvider, WebSearchProviderResponse } from '@renderer/types'
|
||||||
import { ExtractResults } from '@renderer/utils/extract'
|
import { ExtractResults } from '@renderer/utils/extract'
|
||||||
import { type InferToolInput, type InferToolOutput, tool } from 'ai'
|
import { type InferToolInput, type InferToolOutput, tool } from 'ai'
|
||||||
import { z } from 'zod'
|
import * as z from 'zod'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 使用预提取关键词的网络搜索工具
|
* 使用预提取关键词的网络搜索工具
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
getThinkModelType,
|
getThinkModelType,
|
||||||
isDeepSeekHybridInferenceModel,
|
isDeepSeekHybridInferenceModel,
|
||||||
isDoubaoThinkingAutoModel,
|
isDoubaoThinkingAutoModel,
|
||||||
|
isGrok4FastReasoningModel,
|
||||||
isGrokReasoningModel,
|
isGrokReasoningModel,
|
||||||
isOpenAIReasoningModel,
|
isOpenAIReasoningModel,
|
||||||
isQwenAlwaysThinkModel,
|
isQwenAlwaysThinkModel,
|
||||||
@@ -52,7 +53,12 @@ export function getReasoningEffort(assistant: Assistant, model: Model): Reasonin
|
|||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
// Don't disable reasoning for models that require it
|
// 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 {}
|
||||||
}
|
}
|
||||||
return { reasoning: { enabled: false, exclude: true } }
|
return { reasoning: { enabled: false, exclude: true } }
|
||||||
@@ -100,6 +106,7 @@ export function getReasoningEffort(assistant: Assistant, model: Model): Reasonin
|
|||||||
// reasoningEffort有效的情况
|
// reasoningEffort有效的情况
|
||||||
// DeepSeek hybrid inference models, v3.1 and maybe more in the future
|
// DeepSeek hybrid inference models, v3.1 and maybe more in the future
|
||||||
// 不同的 provider 有不同的思考控制方式,在这里统一解决
|
// 不同的 provider 有不同的思考控制方式,在这里统一解决
|
||||||
|
|
||||||
if (isDeepSeekHybridInferenceModel(model)) {
|
if (isDeepSeekHybridInferenceModel(model)) {
|
||||||
if (isSystemProvider(provider)) {
|
if (isSystemProvider(provider)) {
|
||||||
switch (provider.id) {
|
switch (provider.id) {
|
||||||
@@ -142,6 +149,16 @@ export function getReasoningEffort(assistant: Assistant, model: Model): Reasonin
|
|||||||
|
|
||||||
// OpenRouter models
|
// OpenRouter models
|
||||||
if (model.provider === SystemProviderIds.openrouter) {
|
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)) {
|
if (isSupportedReasoningEffortModel(model) || isSupportedThinkingTokenModel(model)) {
|
||||||
return {
|
return {
|
||||||
reasoning: {
|
reasoning: {
|
||||||
@@ -412,6 +429,13 @@ export function getGeminiReasoningParams(assistant: Assistant, model: Model): Re
|
|||||||
return {}
|
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> {
|
export function getXAIReasoningParams(assistant: Assistant, model: Model): Record<string, any> {
|
||||||
if (!isSupportedReasoningEffortGrokModel(model)) {
|
if (!isSupportedReasoningEffortGrokModel(model)) {
|
||||||
return {}
|
return {}
|
||||||
@@ -419,6 +443,11 @@ export function getXAIReasoningParams(assistant: Assistant, model: Model): Recor
|
|||||||
|
|
||||||
const { reasoning_effort: reasoningEffort } = getAssistantSettings(assistant)
|
const { reasoning_effort: reasoningEffort } = getAssistantSettings(assistant)
|
||||||
|
|
||||||
|
if (!reasoningEffort) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For XAI provider Grok models, use reasoningEffort parameter directly
|
||||||
return {
|
return {
|
||||||
reasoningEffort
|
reasoningEffort
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import {
|
|||||||
CreateAgentRequest,
|
CreateAgentRequest,
|
||||||
CreateAgentResponse,
|
CreateAgentResponse,
|
||||||
CreateAgentResponseSchema,
|
CreateAgentResponseSchema,
|
||||||
|
CreateAgentSessionResponse,
|
||||||
|
CreateAgentSessionResponseSchema,
|
||||||
CreateSessionForm,
|
CreateSessionForm,
|
||||||
CreateSessionRequest,
|
CreateSessionRequest,
|
||||||
GetAgentResponse,
|
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
|
const url = this.getSessionPaths(agentId).base
|
||||||
try {
|
try {
|
||||||
const payload = session satisfies CreateSessionRequest
|
const payload = session satisfies CreateSessionRequest
|
||||||
const response = await this.axios.post(url, payload)
|
const response = await this.axios.post(url, payload)
|
||||||
const data = GetAgentSessionResponseSchema.parse(response.data)
|
const data = CreateAgentSessionResponseSchema.parse(response.data)
|
||||||
return data
|
return data
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw processError(error, 'Failed to add session.')
|
throw processError(error, 'Failed to add session.')
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
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 在气泡样式下的用户消息中无法撑开气泡,
|
* 一是 CodeViewer 在气泡样式下的用户消息中无法撑开气泡,
|
||||||
* 二是 代码块内容过少时 toolbar 会和 title 重叠。
|
* 二是 代码块内容过少时 toolbar 会和 title 重叠。
|
||||||
*/
|
*/
|
||||||
min-width: 45ch;
|
min-width: 35ch;
|
||||||
|
|
||||||
.code-toolbar {
|
.code-toolbar {
|
||||||
background-color: ${(props) => (props.$isInSpecialView ? 'transparent' : 'var(--color-background-mute)')};
|
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 { EditorView } from '@codemirror/view'
|
||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
import { Extension, keymap } from '@uiw/react-codemirror'
|
import { Extension, keymap } from '@uiw/react-codemirror'
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
|
|
||||||
import { getNormalizedExtension } from './utils'
|
import { getNormalizedExtension } from './utils'
|
||||||
|
|
||||||
@@ -203,3 +203,80 @@ export function useHeightListener({ onHeightChange }: UseHeightListenerProps) {
|
|||||||
})
|
})
|
||||||
}, [onHeightChange])
|
}, [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 { useCallback, useEffect, useImperativeHandle, useMemo, useRef } from 'react'
|
||||||
import { memo } 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>()
|
const External = Annotation.define<boolean>()
|
||||||
|
|
||||||
export interface CodeEditorHandles {
|
export interface CodeEditorHandles {
|
||||||
save?: () => void
|
save?: () => void
|
||||||
|
scrollToLine?: (lineNumber: number, options?: { highlight?: boolean }) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CodeEditorProps {
|
export interface CodeEditorProps {
|
||||||
@@ -75,10 +76,15 @@ export interface CodeEditorProps {
|
|||||||
/** CSS class name appended to the default `code-editor` class. */
|
/** CSS class name appended to the default `code-editor` class. */
|
||||||
className?: string
|
className?: string
|
||||||
/**
|
/**
|
||||||
* Whether the editor is editable.
|
* Whether the editor view is editable.
|
||||||
* @default true
|
* @default true
|
||||||
*/
|
*/
|
||||||
editable?: boolean
|
editable?: boolean
|
||||||
|
/**
|
||||||
|
* Set the editor state to read only but keep some user interactions, e.g., keymaps.
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
readOnly?: boolean
|
||||||
/**
|
/**
|
||||||
* Whether the editor is expanded.
|
* Whether the editor is expanded.
|
||||||
* If true, the height and maxHeight props are ignored.
|
* If true, the height and maxHeight props are ignored.
|
||||||
@@ -114,6 +120,7 @@ const CodeEditor = ({
|
|||||||
style,
|
style,
|
||||||
className,
|
className,
|
||||||
editable = true,
|
editable = true,
|
||||||
|
readOnly = false,
|
||||||
expanded = true,
|
expanded = true,
|
||||||
wrapped = true
|
wrapped = true
|
||||||
}: CodeEditorProps) => {
|
}: CodeEditorProps) => {
|
||||||
@@ -175,8 +182,11 @@ const CodeEditor = ({
|
|||||||
].flat()
|
].flat()
|
||||||
}, [extensions, langExtensions, wrapped, saveKeymapExtension, blurExtension, heightListenerExtension])
|
}, [extensions, langExtensions, wrapped, saveKeymapExtension, blurExtension, heightListenerExtension])
|
||||||
|
|
||||||
|
const scrollToLine = useScrollToLine(editorViewRef)
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
save: handleSave
|
save: handleSave,
|
||||||
|
scrollToLine
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -189,6 +199,7 @@ const CodeEditor = ({
|
|||||||
maxHeight={expanded ? undefined : maxHeight}
|
maxHeight={expanded ? undefined : maxHeight}
|
||||||
minHeight={minHeight}
|
minHeight={minHeight}
|
||||||
editable={editable}
|
editable={editable}
|
||||||
|
readOnly={readOnly}
|
||||||
// @ts-ignore 强制使用,见 react-codemirror 的 Example.tsx
|
// @ts-ignore 强制使用,见 react-codemirror 的 Example.tsx
|
||||||
theme={activeCmTheme}
|
theme={activeCmTheme}
|
||||||
extensions={customExtensions}
|
extensions={customExtensions}
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { cn } from '@heroui/react'
|
||||||
import Scrollbar from '@renderer/components/Scrollbar'
|
import Scrollbar from '@renderer/components/Scrollbar'
|
||||||
import { ChevronRight } from 'lucide-react'
|
import { ChevronRight } from 'lucide-react'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
@@ -17,6 +18,10 @@ export interface HorizontalScrollContainerProps {
|
|||||||
dependencies?: readonly unknown[]
|
dependencies?: readonly unknown[]
|
||||||
scrollDistance?: number
|
scrollDistance?: number
|
||||||
className?: string
|
className?: string
|
||||||
|
classNames?: {
|
||||||
|
container?: string
|
||||||
|
content?: string
|
||||||
|
}
|
||||||
gap?: string
|
gap?: string
|
||||||
expandable?: boolean
|
expandable?: boolean
|
||||||
}
|
}
|
||||||
@@ -26,6 +31,7 @@ const HorizontalScrollContainer: React.FC<HorizontalScrollContainerProps> = ({
|
|||||||
dependencies = [],
|
dependencies = [],
|
||||||
scrollDistance = 200,
|
scrollDistance = 200,
|
||||||
className,
|
className,
|
||||||
|
classNames,
|
||||||
gap = '8px',
|
gap = '8px',
|
||||||
expandable = false
|
expandable = false
|
||||||
}) => {
|
}) => {
|
||||||
@@ -95,11 +101,16 @@ const HorizontalScrollContainer: React.FC<HorizontalScrollContainerProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Container
|
<Container
|
||||||
className={className}
|
className={cn(className, classNames?.container)}
|
||||||
$expandable={expandable}
|
$expandable={expandable}
|
||||||
$disableHoverButton={isScrolledToEnd}
|
$disableHoverButton={isScrolledToEnd}
|
||||||
onClick={expandable ? handleContainerClick : undefined}>
|
onClick={expandable ? handleContainerClick : undefined}>
|
||||||
<ScrollContent ref={scrollRef} $gap={gap} $isExpanded={isExpanded} $expandable={expandable}>
|
<ScrollContent
|
||||||
|
ref={scrollRef}
|
||||||
|
$gap={gap}
|
||||||
|
$isExpanded={isExpanded}
|
||||||
|
$expandable={expandable}
|
||||||
|
className={cn(classNames?.content)}>
|
||||||
{children}
|
{children}
|
||||||
</ScrollContent>
|
</ScrollContent>
|
||||||
{canScroll && !isExpanded && !isScrolledToEnd && (
|
{canScroll && !isExpanded && !isScrolledToEnd && (
|
||||||
|
|||||||
@@ -549,7 +549,7 @@ const MinappPopupContainer: React.FC = () => {
|
|||||||
{/* 在所有小程序中显示GoogleLoginTip */}
|
{/* 在所有小程序中显示GoogleLoginTip */}
|
||||||
<GoogleLoginTip isReady={isReady} currentUrl={currentUrl} currentAppId={currentMinappId} />
|
<GoogleLoginTip isReady={isReady} currentUrl={currentUrl} currentAppId={currentMinappId} />
|
||||||
{!isReady && (
|
{!isReady && (
|
||||||
<EmptyView>
|
<EmptyView style={{ backgroundColor: 'var(--color-background-soft)' }}>
|
||||||
<Avatar
|
<Avatar
|
||||||
src={currentAppInfo?.logo}
|
src={currentAppInfo?.logo}
|
||||||
size={80}
|
size={80}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const WebviewContainer = memo(
|
|||||||
onNavigateCallback: (appid: string, url: string) => void
|
onNavigateCallback: (appid: string, url: string) => void
|
||||||
}) => {
|
}) => {
|
||||||
const webviewRef = useRef<WebviewTag | null>(null)
|
const webviewRef = useRef<WebviewTag | null>(null)
|
||||||
const { enableSpellCheck } = useSettings()
|
const { enableSpellCheck, minappsOpenLinkExternal } = useSettings()
|
||||||
|
|
||||||
const setRef = (appid: string) => {
|
const setRef = (appid: string) => {
|
||||||
onSetRefCallback(appid, null)
|
onSetRefCallback(appid, null)
|
||||||
@@ -76,6 +76,8 @@ const WebviewContainer = memo(
|
|||||||
const webviewId = webviewRef.current?.getWebContentsId()
|
const webviewId = webviewRef.current?.getWebContentsId()
|
||||||
if (webviewId) {
|
if (webviewId) {
|
||||||
window.api?.webview?.setSpellCheckEnabled?.(webviewId, enableSpellCheck)
|
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
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [appid, url])
|
}, [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 = {
|
const WebviewStyle: React.CSSProperties = {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ interface PopupContainerProps {
|
|||||||
message?: Message
|
message?: Message
|
||||||
messages?: Message[]
|
messages?: Message[]
|
||||||
topic?: Topic
|
topic?: Topic
|
||||||
|
rawContent?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// 转换文件信息数组为树形结构
|
// 转换文件信息数组为树形结构
|
||||||
@@ -140,7 +141,8 @@ const PopupContainer: React.FC<PopupContainerProps> = ({
|
|||||||
resolve,
|
resolve,
|
||||||
message,
|
message,
|
||||||
messages,
|
messages,
|
||||||
topic
|
topic,
|
||||||
|
rawContent
|
||||||
}) => {
|
}) => {
|
||||||
const defaultObsidianVault = store.getState().settings.defaultObsidianVault
|
const defaultObsidianVault = store.getState().settings.defaultObsidianVault
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
@@ -229,7 +231,9 @@ const PopupContainer: React.FC<PopupContainerProps> = ({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
let markdown = ''
|
let markdown = ''
|
||||||
if (topic) {
|
if (rawContent) {
|
||||||
|
markdown = rawContent
|
||||||
|
} else if (topic) {
|
||||||
markdown = await topicToMarkdown(topic, exportReasoning)
|
markdown = await topicToMarkdown(topic, exportReasoning)
|
||||||
} else if (messages && messages.length > 0) {
|
} else if (messages && messages.length > 0) {
|
||||||
markdown = messagesToMarkdown(messages, exportReasoning)
|
markdown = messagesToMarkdown(messages, exportReasoning)
|
||||||
@@ -299,7 +303,6 @@ const PopupContainer: React.FC<PopupContainerProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={i18n.t('chat.topics.export.obsidian_atributes')}
|
title={i18n.t('chat.topics.export.obsidian_atributes')}
|
||||||
@@ -410,9 +413,11 @@ const PopupContainer: React.FC<PopupContainerProps> = ({
|
|||||||
</Option>
|
</Option>
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={i18n.t('chat.topics.export.obsidian_reasoning')}>
|
{!rawContent && (
|
||||||
<Switch checked={exportReasoning} onChange={setExportReasoning} />
|
<Form.Item label={i18n.t('chat.topics.export.obsidian_reasoning')}>
|
||||||
</Form.Item>
|
<Switch checked={exportReasoning} onChange={setExportReasoning} />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ interface ShowParams {
|
|||||||
providerId: string
|
providerId: string
|
||||||
title?: string
|
title?: string
|
||||||
showHealthCheck?: boolean
|
showHealthCheck?: boolean
|
||||||
|
providerType?: 'llm' | 'webSearch' | 'preprocess'
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props extends ShowParams {
|
interface Props extends ShowParams {
|
||||||
@@ -19,7 +20,7 @@ interface Props extends ShowParams {
|
|||||||
/**
|
/**
|
||||||
* API Key 列表弹窗容器组件
|
* API Key 列表弹窗容器组件
|
||||||
*/
|
*/
|
||||||
const PopupContainer: React.FC<Props> = ({ providerId, title, resolve, showHealthCheck = true }) => {
|
const PopupContainer: React.FC<Props> = ({ providerId, title, resolve, showHealthCheck = true, providerType }) => {
|
||||||
const [open, setOpen] = useState(true)
|
const [open, setOpen] = useState(true)
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
@@ -32,14 +33,20 @@ const PopupContainer: React.FC<Props> = ({ providerId, title, resolve, showHealt
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ListComponent = useMemo(() => {
|
const ListComponent = useMemo(() => {
|
||||||
if (isWebSearchProviderId(providerId)) {
|
const type =
|
||||||
return <WebSearchApiKeyList providerId={providerId} showHealthCheck={showHealthCheck} />
|
providerType ||
|
||||||
|
(isWebSearchProviderId(providerId) ? 'webSearch' : isPreprocessProviderId(providerId) ? 'preprocess' : 'llm')
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'webSearch':
|
||||||
|
return <WebSearchApiKeyList providerId={providerId as any} showHealthCheck={showHealthCheck} />
|
||||||
|
case 'preprocess':
|
||||||
|
return <DocPreprocessApiKeyList providerId={providerId as any} showHealthCheck={showHealthCheck} />
|
||||||
|
case 'llm':
|
||||||
|
default:
|
||||||
|
return <LlmApiKeyList providerId={providerId} showHealthCheck={showHealthCheck} />
|
||||||
}
|
}
|
||||||
if (isPreprocessProviderId(providerId)) {
|
}, [providerId, showHealthCheck, providerType])
|
||||||
return <DocPreprocessApiKeyList providerId={providerId} showHealthCheck={showHealthCheck} />
|
|
||||||
}
|
|
||||||
return <LlmApiKeyList providerId={providerId} showHealthCheck={showHealthCheck} />
|
|
||||||
}, [providerId, showHealthCheck])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ interface ObsidianExportOptions {
|
|||||||
topic?: Topic
|
topic?: Topic
|
||||||
message?: Message
|
message?: Message
|
||||||
messages?: Message[]
|
messages?: Message[]
|
||||||
|
rawContent?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class ObsidianExportPopup {
|
export default class ObsidianExportPopup {
|
||||||
@@ -24,6 +25,7 @@ export default class ObsidianExportPopup {
|
|||||||
topic={options.topic}
|
topic={options.topic}
|
||||||
message={options.message}
|
message={options.message}
|
||||||
messages={options.messages}
|
messages={options.messages}
|
||||||
|
rawContent={options.rawContent}
|
||||||
obsidianTags={''}
|
obsidianTags={''}
|
||||||
open={true}
|
open={true}
|
||||||
resolve={(v) => {
|
resolve={(v) => {
|
||||||
|
|||||||
@@ -55,12 +55,15 @@ const PopupContainer: React.FC<Props> = ({ text, title, extension, resolve }) =>
|
|||||||
footer={null}>
|
footer={null}>
|
||||||
{extension !== undefined ? (
|
{extension !== undefined ? (
|
||||||
<Editor
|
<Editor
|
||||||
editable={false}
|
readOnly={true}
|
||||||
expanded={false}
|
expanded={false}
|
||||||
height="100%"
|
height="100%"
|
||||||
style={{ height: '100%' }}
|
style={{ height: '100%' }}
|
||||||
value={text}
|
value={text}
|
||||||
language={extension}
|
language={extension}
|
||||||
|
options={{
|
||||||
|
keymap: true
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Text>{text}</Text>
|
<Text>{text}</Text>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
cn,
|
|
||||||
Form,
|
Form,
|
||||||
Input,
|
Input,
|
||||||
Modal,
|
Modal,
|
||||||
@@ -17,7 +16,7 @@ import {
|
|||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
import type { Selection } from '@react-types/shared'
|
import type { Selection } from '@react-types/shared'
|
||||||
import ClaudeIcon from '@renderer/assets/images/models/claude.png'
|
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 { permissionModeCards } from '@renderer/constants/permissionModes'
|
||||||
import { useAgents } from '@renderer/hooks/agents/useAgents'
|
import { useAgents } from '@renderer/hooks/agents/useAgents'
|
||||||
import { useApiModels } from '@renderer/hooks/agents/useModels'
|
import { useApiModels } from '@renderer/hooks/agents/useModels'
|
||||||
@@ -34,7 +33,7 @@ import {
|
|||||||
UpdateAgentForm
|
UpdateAgentForm
|
||||||
} from '@renderer/types'
|
} from '@renderer/types'
|
||||||
import { AlertTriangleIcon } from 'lucide-react'
|
import { AlertTriangleIcon } from 'lucide-react'
|
||||||
import { ChangeEvent, FormEvent, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import { ChangeEvent, FormEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { ErrorBoundary } from '../../ErrorBoundary'
|
import { ErrorBoundary } from '../../ErrorBoundary'
|
||||||
@@ -57,43 +56,30 @@ const buildAgentForm = (existing?: AgentWithTools): BaseAgentForm => ({
|
|||||||
name: existing?.name ?? 'Claude Code',
|
name: existing?.name ?? 'Claude Code',
|
||||||
description: existing?.description,
|
description: existing?.description,
|
||||||
instructions: existing?.instructions,
|
instructions: existing?.instructions,
|
||||||
model: existing?.model ?? 'claude-4-sonnet',
|
model: existing?.model ?? '',
|
||||||
accessible_paths: existing?.accessible_paths ? [...existing.accessible_paths] : [],
|
accessible_paths: existing?.accessible_paths ? [...existing.accessible_paths] : [],
|
||||||
allowed_tools: existing?.allowed_tools ? [...existing.allowed_tools] : [],
|
allowed_tools: existing?.allowed_tools ? [...existing.allowed_tools] : [],
|
||||||
mcps: existing?.mcps ? [...existing.mcps] : [],
|
mcps: existing?.mcps ? [...existing.mcps] : [],
|
||||||
configuration: AgentConfigurationSchema.parse(existing?.configuration ?? {})
|
configuration: AgentConfigurationSchema.parse(existing?.configuration ?? {})
|
||||||
})
|
})
|
||||||
|
|
||||||
interface BaseProps {
|
type Props = {
|
||||||
agent?: AgentWithTools
|
agent?: AgentWithTools
|
||||||
}
|
|
||||||
|
|
||||||
interface TriggerProps extends BaseProps {
|
|
||||||
trigger: { content: ReactNode; className?: string }
|
|
||||||
isOpen?: never
|
|
||||||
onClose?: never
|
|
||||||
}
|
|
||||||
|
|
||||||
interface StateProps extends BaseProps {
|
|
||||||
trigger?: never
|
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = TriggerProps | StateProps
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modal component for creating or editing an agent.
|
* Modal component for creating or editing an agent.
|
||||||
*
|
*
|
||||||
* Either trigger or isOpen and onClose is given.
|
* Either trigger or isOpen and onClose is given.
|
||||||
* @param agent - Optional agent entity for editing mode.
|
* @param agent - Optional agent entity for editing mode.
|
||||||
* @param trigger - Optional trigger element that opens the modal. It MUST propagate the click event to trigger the modal.
|
|
||||||
* @param isOpen - Optional controlled modal open state. From useDisclosure.
|
* @param isOpen - Optional controlled modal open state. From useDisclosure.
|
||||||
* @param onClose - Optional callback when modal closes. From useDisclosure.
|
* @param onClose - Optional callback when modal closes. From useDisclosure.
|
||||||
* @returns Modal component for agent creation/editing
|
* @returns Modal component for agent creation/editing
|
||||||
*/
|
*/
|
||||||
export const AgentModal: React.FC<Props> = ({ agent, trigger, isOpen: _isOpen, onClose: _onClose }) => {
|
export const AgentModal: React.FC<Props> = ({ agent, isOpen: _isOpen, onClose: _onClose }) => {
|
||||||
const { isOpen, onClose, onOpen } = useDisclosure({ isOpen: _isOpen, onClose: _onClose })
|
const { isOpen, onClose } = useDisclosure({ isOpen: _isOpen, onClose: _onClose })
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const loadingRef = useRef(false)
|
const loadingRef = useRef(false)
|
||||||
// const { setTimeoutTimer } = useTimer()
|
// const { setTimeoutTimer } = useTimer()
|
||||||
@@ -245,14 +231,23 @@ export const AgentModal: React.FC<Props> = ({ agent, trigger, isOpen: _isOpen, o
|
|||||||
|
|
||||||
const modelOptions = useMemo(() => {
|
const modelOptions = useMemo(() => {
|
||||||
// mocked data. not final version
|
// mocked data. not final version
|
||||||
return (models ?? []).map((model) => ({
|
return (models ?? [])
|
||||||
type: 'model',
|
.filter((m) =>
|
||||||
key: model.id,
|
agentModelFilter({
|
||||||
label: model.name,
|
id: m.id,
|
||||||
avatar: getModelLogo(model.id),
|
provider: m.provider || '',
|
||||||
providerId: model.provider,
|
name: m.name,
|
||||||
providerName: model.provider_name
|
group: ''
|
||||||
})) satisfies ModelOption[]
|
})
|
||||||
|
)
|
||||||
|
.map((model) => ({
|
||||||
|
type: 'model',
|
||||||
|
key: model.id,
|
||||||
|
label: model.name,
|
||||||
|
avatar: getModelLogo(model.id),
|
||||||
|
providerId: model.provider,
|
||||||
|
providerName: model.provider_name
|
||||||
|
})) satisfies ModelOption[]
|
||||||
}, [models])
|
}, [models])
|
||||||
|
|
||||||
const onModelChange = useCallback((e: ChangeEvent<HTMLSelectElement>) => {
|
const onModelChange = useCallback((e: ChangeEvent<HTMLSelectElement>) => {
|
||||||
@@ -350,23 +345,6 @@ export const AgentModal: React.FC<Props> = ({ agent, trigger, isOpen: _isOpen, o
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
{/* NOTE: Hero UI Modal Pattern: Combine the Button and Modal components into a single
|
|
||||||
encapsulated component. This is because the Modal component needs to bind the onOpen
|
|
||||||
event handler to the Button for proper focus management.
|
|
||||||
|
|
||||||
Or just use external isOpen/onOpen/onClose to control modal state.
|
|
||||||
*/}
|
|
||||||
|
|
||||||
{trigger && (
|
|
||||||
<div
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
onOpen()
|
|
||||||
}}
|
|
||||||
className={cn('w-full', trigger.className)}>
|
|
||||||
{trigger.content}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ export const SessionModal: React.FC<Props> = ({
|
|||||||
const loadingRef = useRef(false)
|
const loadingRef = useRef(false)
|
||||||
// const { setTimeoutTimer } = useTimer()
|
// const { setTimeoutTimer } = useTimer()
|
||||||
const { createSession } = useSessions(agentId)
|
const { createSession } = useSessions(agentId)
|
||||||
const updateSession = useUpdateSession(agentId)
|
const { updateSession } = useUpdateSession(agentId)
|
||||||
const { agent } = useAgent(agentId)
|
const { agent } = useAgent(agentId)
|
||||||
const isEditing = (session?: AgentSessionEntity) => session !== undefined
|
const isEditing = (session?: AgentSessionEntity) => session !== undefined
|
||||||
|
|
||||||
|
|||||||
@@ -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 { loggerService } from '@logger'
|
||||||
import { ContentSearch, type ContentSearchRef } from '@renderer/components/ContentSearch'
|
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 DragHandle from '@tiptap/extension-drag-handle-react'
|
||||||
import { EditorContent } from '@tiptap/react'
|
import { EditorContent } from '@tiptap/react'
|
||||||
import { Tooltip } from 'antd'
|
import { Tooltip } from 'antd'
|
||||||
@@ -29,6 +30,156 @@ import type { FormattingCommand, RichEditorProps, RichEditorRef } from './types'
|
|||||||
import { useRichEditor } from './useRichEditor'
|
import { useRichEditor } from './useRichEditor'
|
||||||
const logger = loggerService.withContext('RichEditor')
|
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 = ({
|
const RichEditor = ({
|
||||||
ref,
|
ref,
|
||||||
initialContent = '',
|
initialContent = '',
|
||||||
@@ -48,7 +199,8 @@ const RichEditor = ({
|
|||||||
enableContentSearch = false,
|
enableContentSearch = false,
|
||||||
isFullWidth = false,
|
isFullWidth = false,
|
||||||
fontFamily = 'default',
|
fontFamily = 'default',
|
||||||
fontSize = 16
|
fontSize = 16,
|
||||||
|
enableSpellCheck = false
|
||||||
// toolbarItems: _toolbarItems // TODO: Implement custom toolbar items
|
// toolbarItems: _toolbarItems // TODO: Implement custom toolbar items
|
||||||
}: RichEditorProps & { ref?: React.RefObject<RichEditorRef | null> }) => {
|
}: RichEditorProps & { ref?: React.RefObject<RichEditorRef | null> }) => {
|
||||||
// Use the rich editor hook for complete editor management
|
// Use the rich editor hook for complete editor management
|
||||||
@@ -71,6 +223,7 @@ const RichEditor = ({
|
|||||||
onBlur,
|
onBlur,
|
||||||
placeholder,
|
placeholder,
|
||||||
editable,
|
editable,
|
||||||
|
enableSpellCheck,
|
||||||
scrollParent: () => scrollContainerRef.current,
|
scrollParent: () => scrollContainerRef.current,
|
||||||
onShowTableActionMenu: ({ position, actions }) => {
|
onShowTableActionMenu: ({ position, actions }) => {
|
||||||
const iconMap: Record<string, React.ReactNode> = {
|
const iconMap: Record<string, React.ReactNode> = {
|
||||||
@@ -370,6 +523,22 @@ const RichEditor = ({
|
|||||||
scrollContainerRef.current.scrollTop = value
|
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
|
// Dynamic command management
|
||||||
registerCommand,
|
registerCommand,
|
||||||
registerToolbarCommand,
|
registerToolbarCommand,
|
||||||
|
|||||||
@@ -14,6 +14,31 @@ export const RichEditorWrapper = styled.div<{
|
|||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background: var(--color-background);
|
background: var(--color-background);
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
|
.ProseMirror table,
|
||||||
|
.tiptap table {
|
||||||
|
table-layout: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror table th,
|
||||||
|
.ProseMirror table td,
|
||||||
|
.tiptap th,
|
||||||
|
.tiptap td {
|
||||||
|
white-space: normal !important;
|
||||||
|
word-wrap: break-word !important;
|
||||||
|
word-break: break-word !important;
|
||||||
|
overflow-wrap: break-word !important;
|
||||||
|
overflow: visible !important;
|
||||||
|
text-overflow: clip !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror table th > *,
|
||||||
|
.ProseMirror table td > *,
|
||||||
|
.tiptap td > *,
|
||||||
|
.tiptap th > * {
|
||||||
|
white-space: normal !important;
|
||||||
|
overflow: visible !important;
|
||||||
|
text-overflow: clip !important;
|
||||||
|
}
|
||||||
width: ${({ $isFullWidth }) => ($isFullWidth ? '100%' : '60%')};
|
width: ${({ $isFullWidth }) => ($isFullWidth ? '100%' : '60%')};
|
||||||
margin: ${({ $isFullWidth }) => ($isFullWidth ? '0' : '0 auto')};
|
margin: ${({ $isFullWidth }) => ($isFullWidth ? '0' : '0 auto')};
|
||||||
font-family: ${({ $fontFamily }) => ($fontFamily === 'serif' ? 'var(--font-family-serif)' : 'var(--font-family)')};
|
font-family: ${({ $fontFamily }) => ($fontFamily === 'serif' ? 'var(--font-family-serif)' : 'var(--font-family)')};
|
||||||
@@ -21,6 +46,7 @@ export const RichEditorWrapper = styled.div<{
|
|||||||
|
|
||||||
${({ $minHeight }) => $minHeight && `min-height: ${$minHeight}px;`}
|
${({ $minHeight }) => $minHeight && `min-height: ${$minHeight}px;`}
|
||||||
${({ $maxHeight }) => $maxHeight && `max-height: ${$maxHeight}px;`}
|
${({ $maxHeight }) => $maxHeight && `max-height: ${$maxHeight}px;`}
|
||||||
|
|
||||||
`
|
`
|
||||||
|
|
||||||
export const ToolbarWrapper = styled.div`
|
export const ToolbarWrapper = styled.div`
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ export interface RichEditorProps {
|
|||||||
fontFamily?: 'default' | 'serif'
|
fontFamily?: 'default' | 'serif'
|
||||||
/** Font size in pixels */
|
/** Font size in pixels */
|
||||||
fontSize?: number
|
fontSize?: number
|
||||||
|
/** Whether to enable spell check */
|
||||||
|
enableSpellCheck?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ToolbarItem {
|
export interface ToolbarItem {
|
||||||
@@ -109,6 +111,8 @@ export interface RichEditorRef {
|
|||||||
getScrollTop: () => number
|
getScrollTop: () => number
|
||||||
/** Set scrollTop of the editor scroll container */
|
/** Set scrollTop of the editor scroll container */
|
||||||
setScrollTop: (value: number) => void
|
setScrollTop: (value: number) => void
|
||||||
|
/** Scroll to specific line number in markdown */
|
||||||
|
scrollToLine: (lineNumber: number, options?: { highlight?: boolean; lineContent?: string }) => void
|
||||||
// Dynamic command management
|
// Dynamic command management
|
||||||
/** Register a new command/toolbar item */
|
/** Register a new command/toolbar item */
|
||||||
registerCommand: (cmd: Command) => void
|
registerCommand: (cmd: Command) => void
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'katex/dist/katex.min.css'
|
|||||||
|
|
||||||
import { TableKit } from '@cherrystudio/extension-table-plus'
|
import { TableKit } from '@cherrystudio/extension-table-plus'
|
||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
|
import { MARKDOWN_SOURCE_LINE_ATTR } from '@renderer/components/RichEditor/constants'
|
||||||
import type { FormattingState } from '@renderer/components/RichEditor/types'
|
import type { FormattingState } from '@renderer/components/RichEditor/types'
|
||||||
import { useCodeStyle } from '@renderer/context/CodeStyleProvider'
|
import { useCodeStyle } from '@renderer/context/CodeStyleProvider'
|
||||||
import {
|
import {
|
||||||
@@ -11,6 +12,7 @@ import {
|
|||||||
markdownToPreviewText
|
markdownToPreviewText
|
||||||
} from '@renderer/utils/markdownConverter'
|
} from '@renderer/utils/markdownConverter'
|
||||||
import type { Editor } from '@tiptap/core'
|
import type { Editor } from '@tiptap/core'
|
||||||
|
import { Extension } from '@tiptap/core'
|
||||||
import { TaskItem, TaskList } from '@tiptap/extension-list'
|
import { TaskItem, TaskList } from '@tiptap/extension-list'
|
||||||
import { migrateMathStrings } from '@tiptap/extension-mathematics'
|
import { migrateMathStrings } from '@tiptap/extension-mathematics'
|
||||||
import Mention from '@tiptap/extension-mention'
|
import Mention from '@tiptap/extension-mention'
|
||||||
@@ -36,6 +38,31 @@ import { blobToArrayBuffer, compressImage, shouldCompressImage } from './helpers
|
|||||||
|
|
||||||
const logger = loggerService.withContext('useRichEditor')
|
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 {
|
export interface UseRichEditorOptions {
|
||||||
/** Initial markdown content */
|
/** Initial markdown content */
|
||||||
initialContent?: string
|
initialContent?: string
|
||||||
@@ -57,6 +84,8 @@ export interface UseRichEditorOptions {
|
|||||||
editable?: boolean
|
editable?: boolean
|
||||||
/** Whether to enable table of contents functionality */
|
/** Whether to enable table of contents functionality */
|
||||||
enableTableOfContents?: boolean
|
enableTableOfContents?: boolean
|
||||||
|
/** Whether to enable spell check */
|
||||||
|
enableSpellCheck?: boolean
|
||||||
/** Show table action menu (row/column) with concrete actions and position */
|
/** Show table action menu (row/column) with concrete actions and position */
|
||||||
onShowTableActionMenu?: (payload: {
|
onShowTableActionMenu?: (payload: {
|
||||||
type: 'row' | 'column'
|
type: 'row' | 'column'
|
||||||
@@ -126,6 +155,7 @@ export const useRichEditor = (options: UseRichEditorOptions = {}): UseRichEditor
|
|||||||
previewLength = 50,
|
previewLength = 50,
|
||||||
placeholder = '',
|
placeholder = '',
|
||||||
editable = true,
|
editable = true,
|
||||||
|
enableSpellCheck = false,
|
||||||
onShowTableActionMenu,
|
onShowTableActionMenu,
|
||||||
scrollParent
|
scrollParent
|
||||||
} = options
|
} = options
|
||||||
@@ -193,6 +223,7 @@ export const useRichEditor = (options: UseRichEditorOptions = {}): UseRichEditor
|
|||||||
// TipTap editor extensions
|
// TipTap editor extensions
|
||||||
const extensions = useMemo(
|
const extensions = useMemo(
|
||||||
() => [
|
() => [
|
||||||
|
SourceLineAttribute,
|
||||||
StarterKit.configure({
|
StarterKit.configure({
|
||||||
heading: {
|
heading: {
|
||||||
levels: [1, 2, 3, 4, 5, 6]
|
levels: [1, 2, 3, 4, 5, 6]
|
||||||
@@ -410,7 +441,9 @@ export const useRichEditor = (options: UseRichEditorOptions = {}): UseRichEditor
|
|||||||
// Allow text selection even when not editable
|
// Allow text selection even when not editable
|
||||||
style: editable
|
style: editable
|
||||||
? ''
|
? ''
|
||||||
: 'user-select: text; -webkit-user-select: text; -moz-user-select: text; -ms-user-select: text;'
|
: 'user-select: text; -webkit-user-select: text; -moz-user-select: text; -ms-user-select: text;',
|
||||||
|
// Set spellcheck attribute on the contenteditable element
|
||||||
|
spellcheck: enableSpellCheck ? 'true' : 'false'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onUpdate: ({ editor }) => {
|
onUpdate: ({ editor }) => {
|
||||||
|
|||||||
@@ -237,7 +237,17 @@ const TabsContainer: React.FC<TabsContainerProps> = ({ children }) => {
|
|||||||
onSortEnd={onSortEnd}
|
onSortEnd={onSortEnd}
|
||||||
className="tabs-sortable"
|
className="tabs-sortable"
|
||||||
renderItem={(tab) => (
|
renderItem={(tab) => (
|
||||||
<Tab key={tab.id} active={tab.id === activeTabId} onClick={() => handleTabClick(tab)}>
|
<Tab
|
||||||
|
key={tab.id}
|
||||||
|
active={tab.id === activeTabId}
|
||||||
|
onClick={() => handleTabClick(tab)}
|
||||||
|
onAuxClick={(e) => {
|
||||||
|
if (e.button === 1 && tab.id !== 'home') {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
closeTab(tab.id)
|
||||||
|
}
|
||||||
|
}}>
|
||||||
<TabHeader>
|
<TabHeader>
|
||||||
{tab.id && <TabIcon>{getTabIcon(tab.id, minapps, minAppsCache)}</TabIcon>}
|
{tab.id && <TabIcon>{getTabIcon(tab.id, minapps, minAppsCache)}</TabIcon>}
|
||||||
<TabTitle>{getTabTitle(tab.id)}</TabTitle>
|
<TabTitle>{getTabTitle(tab.id)}</TabTitle>
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader, ScrollShadow } from '@heroui/react'
|
||||||
|
import { loggerService } from '@logger'
|
||||||
|
import { handleSaveData } from '@renderer/store'
|
||||||
|
import { ReleaseNoteInfo, UpdateInfo } from 'builder-util-runtime'
|
||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import Markdown from 'react-markdown'
|
||||||
|
|
||||||
|
const logger = loggerService.withContext('UpdateDialog')
|
||||||
|
|
||||||
|
interface UpdateDialogProps {
|
||||||
|
isOpen: boolean
|
||||||
|
onClose: () => void
|
||||||
|
releaseInfo: UpdateInfo | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const UpdateDialog: React.FC<UpdateDialogProps> = ({ isOpen, onClose, releaseInfo }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [isInstalling, setIsInstalling] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen && releaseInfo) {
|
||||||
|
logger.info('Update dialog opened', { version: releaseInfo.version })
|
||||||
|
}
|
||||||
|
}, [isOpen, releaseInfo])
|
||||||
|
|
||||||
|
const handleInstall = async () => {
|
||||||
|
setIsInstalling(true)
|
||||||
|
try {
|
||||||
|
await handleSaveData()
|
||||||
|
await window.api.quitAndInstall()
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to save data before update', error as Error)
|
||||||
|
setIsInstalling(false)
|
||||||
|
window.toast.error(t('update.saveDataError'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const releaseNotes = releaseInfo?.releaseNotes
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={onClose}
|
||||||
|
size="2xl"
|
||||||
|
scrollBehavior="inside"
|
||||||
|
classNames={{
|
||||||
|
base: 'max-h-[85vh]',
|
||||||
|
header: 'border-b border-divider',
|
||||||
|
footer: 'border-t border-divider'
|
||||||
|
}}>
|
||||||
|
<ModalContent>
|
||||||
|
{(onModalClose) => (
|
||||||
|
<>
|
||||||
|
<ModalHeader className="flex flex-col gap-1">
|
||||||
|
<h3 className="font-semibold text-lg">{t('update.title')}</h3>
|
||||||
|
<p className="text-default-500 text-small">
|
||||||
|
{t('update.message').replace('{{version}}', releaseInfo?.version || '')}
|
||||||
|
</p>
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<ScrollShadow className="max-h-[450px]" hideScrollBar>
|
||||||
|
<div className="markdown rounded-lg bg-default-50 p-4">
|
||||||
|
<Markdown>
|
||||||
|
{typeof releaseNotes === 'string'
|
||||||
|
? releaseNotes
|
||||||
|
: Array.isArray(releaseNotes)
|
||||||
|
? releaseNotes
|
||||||
|
.map((note: ReleaseNoteInfo) => note.note)
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('\n\n')
|
||||||
|
: t('update.noReleaseNotes')}
|
||||||
|
</Markdown>
|
||||||
|
</div>
|
||||||
|
</ScrollShadow>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button variant="light" onPress={onModalClose} isDisabled={isInstalling}>
|
||||||
|
{t('update.later')}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
onPress={async () => {
|
||||||
|
await handleInstall()
|
||||||
|
onModalClose()
|
||||||
|
}}
|
||||||
|
isLoading={isInstalling}>
|
||||||
|
{t('update.install')}
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UpdateDialog
|
||||||
@@ -67,14 +67,14 @@ const NavbarContainer = styled.div<{ $isFullScreen: boolean }>`
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
min-height: ${isMac ? 'env(titlebar-area-height)' : 'var(--navbar-height)'};
|
min-height: ${isMac ? 'env(titlebar-area-height)' : 'var(--navbar-height)'};
|
||||||
max-height: var(--navbar-height);
|
max-height: var(--navbar-height);
|
||||||
margin-left: ${isMac ? 'calc(var(--sidebar-width) * -1)' : 0};
|
margin-left: ${isMac ? 'calc(var(--sidebar-width) * -1 + 2px)' : 0};
|
||||||
padding-left: ${({ $isFullScreen }) =>
|
padding-left: ${({ $isFullScreen }) =>
|
||||||
isMac ? ($isFullScreen ? 'var(--sidebar-width)' : 'env(titlebar-area-x)') : 0};
|
isMac ? ($isFullScreen ? 'var(--sidebar-width)' : 'env(titlebar-area-x)') : 0};
|
||||||
-webkit-app-region: drag;
|
-webkit-app-region: drag;
|
||||||
`
|
`
|
||||||
|
|
||||||
const NavbarLeftContainer = styled.div`
|
const NavbarLeftContainer = styled.div`
|
||||||
min-width: var(--assistants-width);
|
/* min-width: ${isMac ? 'calc(var(--assistants-width) - 20px)' : 'var(--assistants-width)'}; */
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|||||||
@@ -22,8 +22,6 @@ import GithubCopilotLogo from '@renderer/assets/images/apps/github-copilot.webp?
|
|||||||
import GoogleAppLogo from '@renderer/assets/images/apps/google.svg?url'
|
import GoogleAppLogo from '@renderer/assets/images/apps/google.svg?url'
|
||||||
import GrokAppLogo from '@renderer/assets/images/apps/grok.png?url'
|
import GrokAppLogo from '@renderer/assets/images/apps/grok.png?url'
|
||||||
import GrokXAppLogo from '@renderer/assets/images/apps/grok-x.png?url'
|
import GrokXAppLogo from '@renderer/assets/images/apps/grok-x.png?url'
|
||||||
import HikaLogo from '@renderer/assets/images/apps/hika.webp?url'
|
|
||||||
import HuggingChatLogo from '@renderer/assets/images/apps/huggingchat.svg?url'
|
|
||||||
import KimiAppLogo from '@renderer/assets/images/apps/kimi.webp?url'
|
import KimiAppLogo from '@renderer/assets/images/apps/kimi.webp?url'
|
||||||
import LambdaChatLogo from '@renderer/assets/images/apps/lambdachat.webp?url'
|
import LambdaChatLogo from '@renderer/assets/images/apps/lambdachat.webp?url'
|
||||||
import LeChatLogo from '@renderer/assets/images/apps/lechat.png?url'
|
import LeChatLogo from '@renderer/assets/images/apps/lechat.png?url'
|
||||||
@@ -32,13 +30,13 @@ import MetasoAppLogo from '@renderer/assets/images/apps/metaso.webp?url'
|
|||||||
import MonicaLogo from '@renderer/assets/images/apps/monica.webp?url'
|
import MonicaLogo from '@renderer/assets/images/apps/monica.webp?url'
|
||||||
import n8nLogo from '@renderer/assets/images/apps/n8n.svg?url'
|
import n8nLogo from '@renderer/assets/images/apps/n8n.svg?url'
|
||||||
import NamiAiLogo from '@renderer/assets/images/apps/nm.png?url'
|
import NamiAiLogo from '@renderer/assets/images/apps/nm.png?url'
|
||||||
import NamiAiSearchLogo from '@renderer/assets/images/apps/nm-search.webp?url'
|
|
||||||
import NotebookLMAppLogo from '@renderer/assets/images/apps/notebooklm.svg?url'
|
import NotebookLMAppLogo from '@renderer/assets/images/apps/notebooklm.svg?url'
|
||||||
import PerplexityAppLogo from '@renderer/assets/images/apps/perplexity.webp?url'
|
import PerplexityAppLogo from '@renderer/assets/images/apps/perplexity.webp?url'
|
||||||
import PoeAppLogo from '@renderer/assets/images/apps/poe.webp?url'
|
import PoeAppLogo from '@renderer/assets/images/apps/poe.webp?url'
|
||||||
import QwenlmAppLogo from '@renderer/assets/images/apps/qwenlm.webp?url'
|
import QwenlmAppLogo from '@renderer/assets/images/apps/qwenlm.webp?url'
|
||||||
import SensetimeAppLogo from '@renderer/assets/images/apps/sensetime.png?url'
|
import SensetimeAppLogo from '@renderer/assets/images/apps/sensetime.png?url'
|
||||||
import SparkDeskAppLogo from '@renderer/assets/images/apps/sparkdesk.webp?url'
|
import SparkDeskAppLogo from '@renderer/assets/images/apps/sparkdesk.webp?url'
|
||||||
|
import StepfunAppLogo from '@renderer/assets/images/apps/stepfun.png?url'
|
||||||
import ThinkAnyLogo from '@renderer/assets/images/apps/thinkany.webp?url'
|
import ThinkAnyLogo from '@renderer/assets/images/apps/thinkany.webp?url'
|
||||||
import TiangongAiLogo from '@renderer/assets/images/apps/tiangong.png?url'
|
import TiangongAiLogo from '@renderer/assets/images/apps/tiangong.png?url'
|
||||||
import WanZhiAppLogo from '@renderer/assets/images/apps/wanzhi.jpg?url'
|
import WanZhiAppLogo from '@renderer/assets/images/apps/wanzhi.jpg?url'
|
||||||
@@ -46,7 +44,6 @@ import WPSLingXiLogo from '@renderer/assets/images/apps/wpslingxi.webp?url'
|
|||||||
import XiaoYiAppLogo from '@renderer/assets/images/apps/xiaoyi.webp?url'
|
import XiaoYiAppLogo from '@renderer/assets/images/apps/xiaoyi.webp?url'
|
||||||
import YouLogo from '@renderer/assets/images/apps/you.jpg?url'
|
import YouLogo from '@renderer/assets/images/apps/you.jpg?url'
|
||||||
import TencentYuanbaoAppLogo from '@renderer/assets/images/apps/yuanbao.webp?url'
|
import TencentYuanbaoAppLogo from '@renderer/assets/images/apps/yuanbao.webp?url'
|
||||||
import YuewenAppLogo from '@renderer/assets/images/apps/yuewen.png?url'
|
|
||||||
import ZaiAppLogo from '@renderer/assets/images/apps/zai.png?url'
|
import ZaiAppLogo from '@renderer/assets/images/apps/zai.png?url'
|
||||||
import ZhihuAppLogo from '@renderer/assets/images/apps/zhihu.png?url'
|
import ZhihuAppLogo from '@renderer/assets/images/apps/zhihu.png?url'
|
||||||
import ClaudeAppLogo from '@renderer/assets/images/models/claude.png?url'
|
import ClaudeAppLogo from '@renderer/assets/images/models/claude.png?url'
|
||||||
@@ -145,14 +142,14 @@ const ORIGIN_DEFAULT_MIN_APPS: MinAppType[] = [
|
|||||||
{
|
{
|
||||||
id: 'dashscope',
|
id: 'dashscope',
|
||||||
name: i18n.t('minapps.qwen'),
|
name: i18n.t('minapps.qwen'),
|
||||||
url: 'https://tongyi.aliyun.com/qianwen/',
|
url: 'https://www.tongyi.com/',
|
||||||
logo: QwenModelLogo
|
logo: QwenModelLogo
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'stepfun',
|
id: 'stepfun',
|
||||||
name: i18n.t('minapps.yuewen'),
|
name: i18n.t('minapps.stepfun'),
|
||||||
url: 'https://yuewen.cn/chats/new',
|
url: 'https://stepfun.com',
|
||||||
logo: YuewenAppLogo,
|
logo: StepfunAppLogo,
|
||||||
bodered: true
|
bodered: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -263,13 +260,6 @@ const ORIGIN_DEFAULT_MIN_APPS: MinAppType[] = [
|
|||||||
url: 'https://www.tiangong.cn/',
|
url: 'https://www.tiangong.cn/',
|
||||||
bodered: true
|
bodered: true
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'hugging-chat',
|
|
||||||
name: 'HuggingChat',
|
|
||||||
logo: HuggingChatLogo,
|
|
||||||
url: 'https://huggingface.co/chat/',
|
|
||||||
bodered: true
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'Felo',
|
id: 'Felo',
|
||||||
name: 'Felo',
|
name: 'Felo',
|
||||||
@@ -297,13 +287,6 @@ const ORIGIN_DEFAULT_MIN_APPS: MinAppType[] = [
|
|||||||
url: 'https://bot.n.cn/',
|
url: 'https://bot.n.cn/',
|
||||||
bodered: true
|
bodered: true
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'nm-search',
|
|
||||||
name: i18n.t('minapps.nami-ai-search'),
|
|
||||||
logo: NamiAiSearchLogo,
|
|
||||||
url: 'https://www.n.cn/',
|
|
||||||
bodered: true
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'thinkany',
|
id: 'thinkany',
|
||||||
name: 'ThinkAny',
|
name: 'ThinkAny',
|
||||||
@@ -314,13 +297,6 @@ const ORIGIN_DEFAULT_MIN_APPS: MinAppType[] = [
|
|||||||
padding: 5
|
padding: 5
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'hika',
|
|
||||||
name: 'Hika',
|
|
||||||
logo: HikaLogo,
|
|
||||||
url: 'https://hika.fyi/',
|
|
||||||
bodered: true
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'github-copilot',
|
id: 'github-copilot',
|
||||||
name: 'GitHub Copilot',
|
name: 'GitHub Copilot',
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export const SYSTEM_MODELS: Record<SystemProviderId | 'defaultModel', Model[]> =
|
|||||||
// Default quick assistant model
|
// Default quick assistant model
|
||||||
glm45FlashModel
|
glm45FlashModel
|
||||||
],
|
],
|
||||||
// cherryin: [],
|
cherryin: [],
|
||||||
vertexai: [],
|
vertexai: [],
|
||||||
'302ai': [
|
'302ai': [
|
||||||
{
|
{
|
||||||
@@ -430,6 +430,18 @@ export const SYSTEM_MODELS: Record<SystemProviderId | 'defaultModel', Model[]> =
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
anthropic: [
|
anthropic: [
|
||||||
|
{
|
||||||
|
id: 'claude-haiku-4-5-20251001',
|
||||||
|
provider: 'anthropic',
|
||||||
|
name: 'Claude Haiku 4.5',
|
||||||
|
group: 'Claude 4.5'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'claude-sonnet-4-5-20250929',
|
||||||
|
provider: 'anthropic',
|
||||||
|
name: 'Claude Sonnet 4.5',
|
||||||
|
group: 'Claude 4.5'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'claude-sonnet-4-20250514',
|
id: 'claude-sonnet-4-20250514',
|
||||||
provider: 'anthropic',
|
provider: 'anthropic',
|
||||||
@@ -698,6 +710,12 @@ export const SYSTEM_MODELS: Record<SystemProviderId | 'defaultModel', Model[]> =
|
|||||||
name: 'GLM-4.5-Flash',
|
name: 'GLM-4.5-Flash',
|
||||||
group: 'GLM-4.5'
|
group: 'GLM-4.5'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'glm-4.6',
|
||||||
|
provider: 'zhipu',
|
||||||
|
name: 'GLM-4.6',
|
||||||
|
group: 'GLM-4.6'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'glm-4.5',
|
id: 'glm-4.5',
|
||||||
provider: 'zhipu',
|
provider: 'zhipu',
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { GEMINI_FLASH_MODEL_REGEX } from './websearch'
|
|||||||
|
|
||||||
// Reasoning models
|
// Reasoning models
|
||||||
export const REASONING_REGEX =
|
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的映射表
|
// 模型类型到支持的reasoning_effort的映射表
|
||||||
// TODO: refactor this. too many identical options
|
// 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: ['minimal', 'low', 'medium', 'high'] as const,
|
||||||
gpt5_codex: ['low', 'medium', 'high'] as const,
|
gpt5_codex: ['low', 'medium', 'high'] as const,
|
||||||
grok: ['low', 'high'] as const,
|
grok: ['low', 'high'] as const,
|
||||||
|
grok4_fast: ['auto'] as const,
|
||||||
gemini: ['low', 'medium', 'high', 'auto'] as const,
|
gemini: ['low', 'medium', 'high', 'auto'] as const,
|
||||||
gemini_pro: ['low', 'medium', 'high', 'auto'] as const,
|
gemini_pro: ['low', 'medium', 'high', 'auto'] as const,
|
||||||
qwen: ['low', 'medium', 'high'] 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: [...MODEL_SUPPORTED_REASONING_EFFORT.gpt5] as const,
|
||||||
gpt5_codex: MODEL_SUPPORTED_REASONING_EFFORT.gpt5_codex,
|
gpt5_codex: MODEL_SUPPORTED_REASONING_EFFORT.gpt5_codex,
|
||||||
grok: MODEL_SUPPORTED_REASONING_EFFORT.grok,
|
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: ['off', ...MODEL_SUPPORTED_REASONING_EFFORT.gemini] as const,
|
||||||
gemini_pro: MODEL_SUPPORTED_REASONING_EFFORT.gemini_pro,
|
gemini_pro: MODEL_SUPPORTED_REASONING_EFFORT.gemini_pro,
|
||||||
qwen: ['off', ...MODEL_SUPPORTED_REASONING_EFFORT.qwen] as const,
|
qwen: ['off', ...MODEL_SUPPORTED_REASONING_EFFORT.qwen] as const,
|
||||||
@@ -66,6 +68,8 @@ export const getThinkModelType = (model: Model): ThinkingModelType => {
|
|||||||
}
|
}
|
||||||
} else if (isSupportedReasoningEffortOpenAIModel(model)) {
|
} else if (isSupportedReasoningEffortOpenAIModel(model)) {
|
||||||
thinkingModelType = 'o'
|
thinkingModelType = 'o'
|
||||||
|
} else if (isGrok4FastReasoningModel(model)) {
|
||||||
|
thinkingModelType = 'grok4_fast'
|
||||||
} else if (isSupportedThinkingTokenGeminiModel(model)) {
|
} else if (isSupportedThinkingTokenGeminiModel(model)) {
|
||||||
if (GEMINI_FLASH_MODEL_REGEX.test(model.id)) {
|
if (GEMINI_FLASH_MODEL_REGEX.test(model.id)) {
|
||||||
thinkingModelType = 'gemini'
|
thinkingModelType = 'gemini'
|
||||||
@@ -142,19 +146,46 @@ export function isSupportedReasoningEffortGrokModel(model?: Model): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const modelId = getLowerBaseModelName(model.id)
|
const modelId = getLowerBaseModelName(model.id)
|
||||||
|
const providerId = model.provider.toLowerCase()
|
||||||
if (modelId.includes('grok-3-mini')) {
|
if (modelId.includes('grok-3-mini')) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (providerId === 'openrouter' && modelId.includes('grok-4-fast')) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
return false
|
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 {
|
export function isGrokReasoningModel(model?: Model): boolean {
|
||||||
if (!model) {
|
if (!model) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
const modelId = getLowerBaseModelName(model.id)
|
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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,9 +209,13 @@ export function isGeminiReasoningModel(model?: Model): boolean {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Gemini 支持思考模式的模型正则
|
||||||
|
export const GEMINI_THINKING_MODEL_REGEX =
|
||||||
|
/gemini-(?:2\.5.*(?:-latest)?|flash-latest|pro-latest|flash-lite-latest)(?:-[\w-]+)*$/i
|
||||||
|
|
||||||
export const isSupportedThinkingTokenGeminiModel = (model: Model): boolean => {
|
export const isSupportedThinkingTokenGeminiModel = (model: Model): boolean => {
|
||||||
const modelId = getLowerBaseModelName(model.id, '/')
|
const modelId = getLowerBaseModelName(model.id, '/')
|
||||||
if (modelId.includes('gemini-2.5')) {
|
if (GEMINI_THINKING_MODEL_REGEX.test(modelId)) {
|
||||||
if (modelId.includes('image') || modelId.includes('tts')) {
|
if (modelId.includes('image') || modelId.includes('tts')) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -261,7 +296,11 @@ export function isQwenAlwaysThinkModel(model?: Model): boolean {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
const modelId = getLowerBaseModelName(model.id, '/')
|
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 支持思考模式的模型正则
|
// Doubao 支持思考模式的模型正则
|
||||||
@@ -296,7 +335,8 @@ export function isClaudeReasoningModel(model?: Model): boolean {
|
|||||||
modelId.includes('claude-3-7-sonnet') ||
|
modelId.includes('claude-3-7-sonnet') ||
|
||||||
modelId.includes('claude-3.7-sonnet') ||
|
modelId.includes('claude-3.7-sonnet') ||
|
||||||
modelId.includes('claude-sonnet-4') ||
|
modelId.includes('claude-sonnet-4') ||
|
||||||
modelId.includes('claude-opus-4')
|
modelId.includes('claude-opus-4') ||
|
||||||
|
modelId.includes('claude-haiku-4')
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,7 +365,10 @@ export const isPerplexityReasoningModel = (model?: Model): boolean => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const modelId = getLowerBaseModelName(model.id, '/')
|
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 => {
|
export const isSupportedReasoningEffortPerplexityModel = (model: Model): boolean => {
|
||||||
@@ -335,14 +378,20 @@ export const isSupportedReasoningEffortPerplexityModel = (model: Model): boolean
|
|||||||
|
|
||||||
export const isSupportedThinkingTokenZhipuModel = (model: Model): boolean => {
|
export const isSupportedThinkingTokenZhipuModel = (model: Model): boolean => {
|
||||||
const modelId = getLowerBaseModelName(model.id, '/')
|
const modelId = getLowerBaseModelName(model.id, '/')
|
||||||
return modelId.includes('glm-4.5')
|
return ['glm-4.5', 'glm-4.6'].some((id) => modelId.includes(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isDeepSeekHybridInferenceModel = (model: Model) => {
|
export const isDeepSeekHybridInferenceModel = (model: Model) => {
|
||||||
const modelId = getLowerBaseModelName(model.id)
|
const modelId = getLowerBaseModelName(model.id)
|
||||||
// deepseek官方使用chat和reasoner做推理控制,其他provider需要单独判断,id可能会有所差别
|
// deepseek官方使用chat和reasoner做推理控制,其他provider需要单独判断,id可能会有所差别
|
||||||
// openrouter: deepseek/deepseek-chat-v3.1 不知道会不会有其他provider仿照ds官方分出一个同id的作为非思考模式的模型,这里有风险
|
// openrouter: deepseek/deepseek-chat-v3.1 不知道会不会有其他provider仿照ds官方分出一个同id的作为非思考模式的模型,这里有风险
|
||||||
return /deepseek-v3(?:\.1|-1-\d+)/.test(modelId) || modelId.includes('deepseek-chat-v3.1')
|
// Matches: "deepseek-v3" followed by ".digit" or "-digit".
|
||||||
|
// Optionally, this can be followed by ".alphanumeric_sequence" or "-alphanumeric_sequence"
|
||||||
|
// until the end of the string.
|
||||||
|
// Examples: deepseek-v3.1, deepseek-v3-1, deepseek-v3.1.2, deepseek-v3.1-alpha
|
||||||
|
// Does NOT match: deepseek-v3.123 (missing separator after '1'), deepseek-v3.x (x isn't a digit)
|
||||||
|
// TODO: move to utils and add test cases
|
||||||
|
return /deepseek-v3(?:\.\d|-\d)(?:(\.|-)\w+)?$/.test(modelId) || modelId.includes('deepseek-chat-v3.1')
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isSupportedThinkingTokenDeepSeekModel = isDeepSeekHybridInferenceModel
|
export const isSupportedThinkingTokenDeepSeekModel = isDeepSeekHybridInferenceModel
|
||||||
@@ -433,6 +482,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 同步变更
|
// 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-235b-a22b-thinking-2507$': { min: 0, max: 81_920 },
|
||||||
'qwen3-30b-a3b-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-07-14$': { min: 0, max: 38_912 },
|
||||||
'qwen-plus-2025-04-28$': { min: 0, max: 38_912 },
|
'qwen-plus-2025-04-28$': { min: 0, max: 38_912 },
|
||||||
'qwen3-1\\.7b$': { min: 0, max: 30_720 },
|
'qwen3-1\\.7b$': { min: 0, max: 30_720 },
|
||||||
@@ -443,8 +494,9 @@ export const THINKING_TOKEN_MAP: Record<string, { min: number; max: number }> =
|
|||||||
'qwen3-(?!max).*$': { min: 1024, max: 38_912 },
|
'qwen3-(?!max).*$': { min: 1024, max: 38_912 },
|
||||||
|
|
||||||
// Claude models
|
// Claude models
|
||||||
'claude-3[.-]7.*sonnet.*$': { min: 1024, max: 64000 },
|
'claude-3[.-]7.*sonnet.*$': { min: 1024, max: 64_000 },
|
||||||
'claude-(:?sonnet|opus)-4.*$': { min: 1024, max: 32000 }
|
'claude-(:?haiku|sonnet)-4.*$': { min: 1024, max: 64_000 },
|
||||||
|
'claude-opus-4-1.*$': { min: 1024, max: 32_000 }
|
||||||
}
|
}
|
||||||
|
|
||||||
export const findTokenLimit = (modelId: string): { min: number; max: number } | undefined => {
|
export const findTokenLimit = (modelId: string): { min: number; max: number } | undefined => {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { isEmbeddingModel, isRerankModel } from '@renderer/config/models/embedding'
|
||||||
import { Model } from '@renderer/types'
|
import { Model } from '@renderer/types'
|
||||||
import { getLowerBaseModelName } from '@renderer/utils'
|
import { getLowerBaseModelName } from '@renderer/utils'
|
||||||
import OpenAI from 'openai'
|
import OpenAI from 'openai'
|
||||||
@@ -5,7 +6,7 @@ import OpenAI from 'openai'
|
|||||||
import { WEB_SEARCH_PROMPT_FOR_OPENROUTER } from '../prompts'
|
import { WEB_SEARCH_PROMPT_FOR_OPENROUTER } from '../prompts'
|
||||||
import { getWebSearchTools } from '../tools'
|
import { getWebSearchTools } from '../tools'
|
||||||
import { isOpenAIReasoningModel } from './reasoning'
|
import { isOpenAIReasoningModel } from './reasoning'
|
||||||
import { isGenerateImageModel, isVisionModel } from './vision'
|
import { isGenerateImageModel, isTextToImageModel, isVisionModel } from './vision'
|
||||||
import { isOpenAIWebSearchChatCompletionOnlyModel } from './websearch'
|
import { isOpenAIWebSearchChatCompletionOnlyModel } from './websearch'
|
||||||
export const NOT_SUPPORTED_REGEX = /(?:^tts|whisper|speech)/i
|
export const NOT_SUPPORTED_REGEX = /(?:^tts|whisper|speech)/i
|
||||||
|
|
||||||
@@ -246,3 +247,7 @@ export const isOpenAIOpenWeightModel = (model: Model) => {
|
|||||||
|
|
||||||
// zhipu 视觉推理模型用这组 special token 标记推理结果
|
// zhipu 视觉推理模型用这组 special token 标记推理结果
|
||||||
export const ZHIPU_RESULT_TOKENS = ['<|begin_of_box|>', '<|end_of_box|>'] as const
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,8 +12,10 @@ const visionAllowedModels = [
|
|||||||
'gemini-1\\.5',
|
'gemini-1\\.5',
|
||||||
'gemini-2\\.0',
|
'gemini-2\\.0',
|
||||||
'gemini-2\\.5',
|
'gemini-2\\.5',
|
||||||
|
'gemini-(flash|pro|flash-lite)-latest',
|
||||||
'gemini-exp',
|
'gemini-exp',
|
||||||
'claude-3',
|
'claude-3',
|
||||||
|
'claude-haiku-4',
|
||||||
'claude-sonnet-4',
|
'claude-sonnet-4',
|
||||||
'claude-opus-4',
|
'claude-opus-4',
|
||||||
'vision',
|
'vision',
|
||||||
@@ -21,7 +23,9 @@ const visionAllowedModels = [
|
|||||||
'qwen-vl',
|
'qwen-vl',
|
||||||
'qwen2-vl',
|
'qwen2-vl',
|
||||||
'qwen2.5-vl',
|
'qwen2.5-vl',
|
||||||
|
'qwen3-vl',
|
||||||
'qwen2.5-omni',
|
'qwen2.5-omni',
|
||||||
|
'qwen3-omni(?:-[\\w-]+)?',
|
||||||
'qvq',
|
'qvq',
|
||||||
'internvl2',
|
'internvl2',
|
||||||
'grok-vision-beta',
|
'grok-vision-beta',
|
||||||
@@ -79,14 +83,14 @@ export const IMAGE_ENHANCEMENT_MODELS = [
|
|||||||
'grok-2-image(?:-[\\w-]+)?',
|
'grok-2-image(?:-[\\w-]+)?',
|
||||||
'qwen-image-edit',
|
'qwen-image-edit',
|
||||||
'gpt-image-1',
|
'gpt-image-1',
|
||||||
'gemini-2.5-flash-image-preview',
|
'gemini-2.5-flash-image',
|
||||||
'gemini-2.0-flash-preview-image-generation'
|
'gemini-2.0-flash-preview-image-generation'
|
||||||
]
|
]
|
||||||
|
|
||||||
const IMAGE_ENHANCEMENT_MODELS_REGEX = new RegExp(IMAGE_ENHANCEMENT_MODELS.join('|'), 'i')
|
const IMAGE_ENHANCEMENT_MODELS_REGEX = new RegExp(IMAGE_ENHANCEMENT_MODELS.join('|'), 'i')
|
||||||
|
|
||||||
// Models that should auto-enable image generation button when selected
|
// 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 = [
|
export const OPENAI_TOOL_USE_IMAGE_GENERATION_MODELS = [
|
||||||
'o3',
|
'o3',
|
||||||
@@ -104,7 +108,7 @@ export const GENERATE_IMAGE_MODELS = [
|
|||||||
'gemini-2.0-flash-exp',
|
'gemini-2.0-flash-exp',
|
||||||
'gemini-2.0-flash-exp-image-generation',
|
'gemini-2.0-flash-exp-image-generation',
|
||||||
'gemini-2.0-flash-preview-image-generation',
|
'gemini-2.0-flash-preview-image-generation',
|
||||||
'gemini-2.5-flash-image-preview',
|
'gemini-2.5-flash-image',
|
||||||
...DEDICATED_IMAGE_MODELS
|
...DEDICATED_IMAGE_MODELS
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -7,13 +7,16 @@ import { isAnthropicModel } from './utils'
|
|||||||
import { isPureGenerateImageModel, isTextToImageModel } from './vision'
|
import { isPureGenerateImageModel, isTextToImageModel } from './vision'
|
||||||
|
|
||||||
export const CLAUDE_SUPPORTED_WEBSEARCH_REGEX = new RegExp(
|
export const CLAUDE_SUPPORTED_WEBSEARCH_REGEX = new RegExp(
|
||||||
`\\b(?:claude-3(-|\\.)(7|5)-sonnet(?:-[\\w-]+)|claude-3(-|\\.)5-haiku(?:-[\\w-]+)|claude-sonnet-4(?:-[\\w-]+)?|claude-opus-4(?:-[\\w-]+)?)\\b`,
|
`\\b(?:claude-3(-|\\.)(7|5)-sonnet(?:-[\\w-]+)|claude-3(-|\\.)5-haiku(?:-[\\w-]+)|claude-(haiku|sonnet|opus)-4(?:-[\\w-]+)?)\\b`,
|
||||||
'i'
|
'i'
|
||||||
)
|
)
|
||||||
|
|
||||||
export const GEMINI_FLASH_MODEL_REGEX = new RegExp('gemini-.*-flash.*$')
|
export const GEMINI_FLASH_MODEL_REGEX = new RegExp('gemini.*-flash.*$')
|
||||||
|
|
||||||
export const GEMINI_SEARCH_REGEX = new RegExp('gemini-2\\..*', 'i')
|
export const GEMINI_SEARCH_REGEX = new RegExp(
|
||||||
|
'gemini-(?:2.*(?:-latest)?|flash-latest|pro-latest|flash-lite-latest)(?:-[\\w-]+)*$',
|
||||||
|
'i'
|
||||||
|
)
|
||||||
|
|
||||||
export const PERPLEXITY_SEARCH_MODELS = [
|
export const PERPLEXITY_SEARCH_MODELS = [
|
||||||
'sonar-pro',
|
'sonar-pro',
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
BuiltinOcrProvider,
|
BuiltinOcrProvider,
|
||||||
BuiltinOcrProviderId,
|
BuiltinOcrProviderId,
|
||||||
|
OcrOvProvider,
|
||||||
OcrPpocrProvider,
|
OcrPpocrProvider,
|
||||||
OcrProviderCapability,
|
OcrProviderCapability,
|
||||||
OcrSystemProvider,
|
OcrSystemProvider,
|
||||||
@@ -50,10 +51,23 @@ const ppocrOcr: OcrPpocrProvider = {
|
|||||||
}
|
}
|
||||||
} as const
|
} 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 = {
|
export const BUILTIN_OCR_PROVIDERS_MAP = {
|
||||||
tesseract,
|
tesseract,
|
||||||
system: systemOcr,
|
system: systemOcr,
|
||||||
paddleocr: ppocrOcr
|
paddleocr: ppocrOcr,
|
||||||
|
ovocr: ovOcr
|
||||||
} as const satisfies Record<BuiltinOcrProviderId, BuiltinOcrProvider>
|
} as const satisfies Record<BuiltinOcrProviderId, BuiltinOcrProvider>
|
||||||
|
|
||||||
export const BUILTIN_OCR_PROVIDERS: BuiltinOcrProvider[] = Object.values(BUILTIN_OCR_PROVIDERS_MAP)
|
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 {
|
import {
|
||||||
AtLeast,
|
AtLeast,
|
||||||
isSystemProvider,
|
isSystemProvider,
|
||||||
Model,
|
|
||||||
OpenAIServiceTiers,
|
OpenAIServiceTiers,
|
||||||
Provider,
|
Provider,
|
||||||
ProviderType,
|
ProviderType,
|
||||||
@@ -82,16 +81,17 @@ export const CHERRYAI_PROVIDER: SystemProvider = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> = {
|
export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> = {
|
||||||
// cherryin: {
|
cherryin: {
|
||||||
// id: 'cherryin',
|
id: 'cherryin',
|
||||||
// name: 'CherryIN',
|
name: 'CherryIN',
|
||||||
// type: 'openai',
|
type: 'openai',
|
||||||
// apiKey: '',
|
apiKey: '',
|
||||||
// apiHost: 'https://open.cherryin.ai',
|
apiHost: 'https://open.cherryin.net',
|
||||||
// models: [],
|
anthropicApiHost: 'https://open.cherryin.net',
|
||||||
// isSystem: true,
|
models: [],
|
||||||
// enabled: true
|
isSystem: true,
|
||||||
// },
|
enabled: true
|
||||||
|
},
|
||||||
silicon: {
|
silicon: {
|
||||||
id: 'silicon',
|
id: 'silicon',
|
||||||
name: 'Silicon',
|
name: 'Silicon',
|
||||||
@@ -109,7 +109,6 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
|
|||||||
apiKey: '',
|
apiKey: '',
|
||||||
apiHost: 'https://aihubmix.com',
|
apiHost: 'https://aihubmix.com',
|
||||||
anthropicApiHost: 'https://aihubmix.com/anthropic',
|
anthropicApiHost: 'https://aihubmix.com/anthropic',
|
||||||
isAnthropicModel: (m: Model) => m.id.includes('claude'),
|
|
||||||
models: SYSTEM_MODELS.aihubmix,
|
models: SYSTEM_MODELS.aihubmix,
|
||||||
isSystem: true,
|
isSystem: true,
|
||||||
enabled: false
|
enabled: false
|
||||||
@@ -289,7 +288,7 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
|
|||||||
'new-api': {
|
'new-api': {
|
||||||
id: 'new-api',
|
id: 'new-api',
|
||||||
name: 'New API',
|
name: 'New API',
|
||||||
type: 'openai',
|
type: 'new-api',
|
||||||
apiKey: '',
|
apiKey: '',
|
||||||
apiHost: 'http://localhost:3000',
|
apiHost: 'http://localhost:3000',
|
||||||
anthropicApiHost: 'http://localhost:3000',
|
anthropicApiHost: 'http://localhost:3000',
|
||||||
@@ -742,17 +741,17 @@ type ProviderUrls = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const PROVIDER_URLS: Record<SystemProviderId, ProviderUrls> = {
|
export const PROVIDER_URLS: Record<SystemProviderId, ProviderUrls> = {
|
||||||
// cherryin: {
|
cherryin: {
|
||||||
// api: {
|
api: {
|
||||||
// url: 'https://open.cherryin.ai'
|
url: 'https://open.cherryin.net'
|
||||||
// },
|
},
|
||||||
// websites: {
|
websites: {
|
||||||
// official: 'https://open.cherryin.ai',
|
official: 'https://open.cherryin.ai',
|
||||||
// apiKey: 'https://open.cherryin.ai/console/token',
|
apiKey: 'https://open.cherryin.ai/console/token',
|
||||||
// docs: 'https://open.cherryin.ai',
|
docs: 'https://open.cherryin.ai',
|
||||||
// models: 'https://open.cherryin.ai/pricing'
|
models: 'https://open.cherryin.ai/pricing'
|
||||||
// }
|
}
|
||||||
// },
|
},
|
||||||
ph8: {
|
ph8: {
|
||||||
api: {
|
api: {
|
||||||
url: 'https://ph8.co'
|
url: 'https://ph8.co'
|
||||||
@@ -1432,5 +1431,5 @@ export const isGeminiWebSearchProvider = (provider: Provider) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const isNewApiProvider = (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'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export type UpdateAgentBaseOptions = {
|
||||||
|
/** Whether to show success toast after updating. Defaults to true. */
|
||||||
|
showSuccessToast?: boolean
|
||||||
|
}
|
||||||