mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-19 06:30:10 +08:00
Merge branch 'main' of github.com:CherryHQ/cherry-studio into refactor/heroui-antd
# Conflicts: # src/renderer/src/pages/home/Tabs/components/AddButton.tsx # src/renderer/src/pages/home/Tabs/components/SessionItem.tsx # src/renderer/src/pages/home/Tabs/components/Sessions.tsx # src/renderer/src/pages/home/Tabs/components/Topics.tsx # src/renderer/src/pages/paintings/NewApiPage.tsx
This commit is contained in:
commit
21ea8ccf37
2
.github/workflows/auto-i18n.yml
vendored
2
.github/workflows/auto-i18n.yml
vendored
@ -29,7 +29,7 @@ jobs:
|
||||
- name: 📦 Setting Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 22
|
||||
package-manager-cache: false
|
||||
|
||||
- name: 📦 Install dependencies in isolated directory
|
||||
|
||||
6
.github/workflows/github-issue-tracker.yml
vendored
6
.github/workflows/github-issue-tracker.yml
vendored
@ -5,7 +5,7 @@ on:
|
||||
types: [opened]
|
||||
schedule:
|
||||
# Run every day at 8:30 Beijing Time (00:30 UTC)
|
||||
- cron: '30 0 * * *'
|
||||
- cron: "30 0 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@ -56,7 +56,7 @@ jobs:
|
||||
if: steps.check_time.outputs.should_delay == 'false'
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '20'
|
||||
node-version: 22
|
||||
|
||||
- name: Process issue with Claude
|
||||
if: steps.check_time.outputs.should_delay == 'false'
|
||||
@ -123,7 +123,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '20'
|
||||
node-version: 22
|
||||
|
||||
- name: Process pending issues with Claude
|
||||
uses: anthropics/claude-code-action@main
|
||||
|
||||
6
.github/workflows/nightly-build.yml
vendored
6
.github/workflows/nightly-build.yml
vendored
@ -3,7 +3,7 @@ name: Nightly Build
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 17 * * *' # 1:00 BJ Time
|
||||
- cron: "0 17 * * *" # 1:00 BJ Time
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@ -58,7 +58,7 @@ jobs:
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 22
|
||||
|
||||
- name: macos-latest dependencies fix
|
||||
if: matrix.os == 'macos-latest'
|
||||
@ -66,7 +66,7 @@ jobs:
|
||||
brew install python-setuptools
|
||||
|
||||
- name: Install corepack
|
||||
run: corepack enable && corepack prepare yarn@4.6.0 --activate
|
||||
run: corepack enable && corepack prepare yarn@4.9.1 --activate
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
|
||||
4
.github/workflows/pr-ci.yml
vendored
4
.github/workflows/pr-ci.yml
vendored
@ -26,10 +26,10 @@ jobs:
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 22
|
||||
|
||||
- name: Install corepack
|
||||
run: corepack enable && corepack prepare yarn@4.6.0 --activate
|
||||
run: corepack enable && corepack prepare yarn@4.9.1 --activate
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
|
||||
10
.github/workflows/release.yml
vendored
10
.github/workflows/release.yml
vendored
@ -4,9 +4,9 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Release tag (e.g. v1.0.0)'
|
||||
description: "Release tag (e.g. v1.0.0)"
|
||||
required: true
|
||||
default: 'v1.0.0'
|
||||
default: "v1.0.0"
|
||||
push:
|
||||
tags:
|
||||
- v*.*.*
|
||||
@ -49,7 +49,7 @@ jobs:
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 22
|
||||
|
||||
- name: macos-latest dependencies fix
|
||||
if: matrix.os == 'macos-latest'
|
||||
@ -57,7 +57,7 @@ jobs:
|
||||
brew install python-setuptools
|
||||
|
||||
- name: Install corepack
|
||||
run: corepack enable && corepack prepare yarn@4.6.0 --activate
|
||||
run: corepack enable && corepack prepare yarn@4.9.1 --activate
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
@ -127,5 +127,5 @@ jobs:
|
||||
allowUpdates: true
|
||||
makeLatest: false
|
||||
tag: ${{ steps.get-tag.outputs.tag }}
|
||||
artifacts: 'dist/*.exe,dist/*.zip,dist/*.dmg,dist/*.AppImage,dist/*.snap,dist/*.deb,dist/*.rpm,dist/*.tar.gz,dist/latest*.yml,dist/rc*.yml,dist/beta*.yml,dist/*.blockmap'
|
||||
artifacts: "dist/*.exe,dist/*.zip,dist/*.dmg,dist/*.AppImage,dist/*.snap,dist/*.deb,dist/*.rpm,dist/*.tar.gz,dist/latest*.yml,dist/rc*.yml,dist/beta*.yml,dist/*.blockmap"
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@ -18,13 +18,13 @@ yarn
|
||||
|
||||
### Setup Node.js
|
||||
|
||||
Download and install [Node.js v20.x.x](https://nodejs.org/en/download)
|
||||
Download and install [Node.js v22.x.x](https://nodejs.org/en/download)
|
||||
|
||||
### Setup Yarn
|
||||
|
||||
```bash
|
||||
corepack enable
|
||||
corepack prepare yarn@4.6.0 --activate
|
||||
corepack prepare yarn@4.9.1 --activate
|
||||
```
|
||||
|
||||
### Install Dependencies
|
||||
|
||||
@ -21,6 +21,8 @@ files:
|
||||
- "**/*"
|
||||
- "!**/{.vscode,.yarn,.yarn-lock,.github,.cursorrules,.prettierrc}"
|
||||
- "!electron.vite.config.{js,ts,mjs,cjs}}"
|
||||
- "!.*"
|
||||
- "!components.json"
|
||||
- "!**/{.eslintignore,.eslintrc.js,.eslintrc.json,.eslintcache,root.eslint.config.js,eslint.config.js,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,eslint.config.mjs,dev-app-update.yml,CHANGELOG.md,README.md,biome.jsonc}"
|
||||
- "!**/{.env,.env.*,.npmrc,pnpm-lock.yaml}"
|
||||
- "!**/{tsconfig.json,tsconfig.tsbuildinfo,tsconfig.node.json,tsconfig.web.json}"
|
||||
|
||||
@ -82,6 +82,7 @@
|
||||
"@libsql/client": "0.14.0",
|
||||
"@libsql/win32-x64-msvc": "^0.4.7",
|
||||
"@napi-rs/system-ocr": "patch:@napi-rs/system-ocr@npm%3A1.0.2#~/.yarn/patches/@napi-rs-system-ocr-npm-1.0.2-59e7a78e8b.patch",
|
||||
"@paymoapp/electron-shutdown-handler": "^1.1.2",
|
||||
"@strongtz/win32-arm64-msvc": "^0.4.7",
|
||||
"express": "^5.1.0",
|
||||
"font-list": "^2.0.0",
|
||||
@ -113,9 +114,9 @@
|
||||
"@ant-design/v5-patch-for-react-19": "^1.0.3",
|
||||
"@anthropic-ai/sdk": "^0.41.0",
|
||||
"@anthropic-ai/vertex-sdk": "patch:@anthropic-ai/vertex-sdk@npm%3A0.11.4#~/.yarn/patches/@anthropic-ai-vertex-sdk-npm-0.11.4-c19cb41edb.patch",
|
||||
"@aws-sdk/client-bedrock": "^3.840.0",
|
||||
"@aws-sdk/client-bedrock-runtime": "^3.840.0",
|
||||
"@aws-sdk/client-s3": "^3.840.0",
|
||||
"@aws-sdk/client-bedrock": "^3.910.0",
|
||||
"@aws-sdk/client-bedrock-runtime": "^3.910.0",
|
||||
"@aws-sdk/client-s3": "^3.910.0",
|
||||
"@biomejs/biome": "2.2.4",
|
||||
"@cherrystudio/ai-core": "workspace:^1.0.0-alpha.18",
|
||||
"@cherrystudio/embedjs": "^0.1.31",
|
||||
@ -373,6 +374,7 @@
|
||||
"zod": "^4.1.5"
|
||||
},
|
||||
"resolutions": {
|
||||
"@smithy/types": "4.7.1",
|
||||
"@codemirror/language": "6.11.3",
|
||||
"@codemirror/lint": "6.8.5",
|
||||
"@codemirror/view": "6.38.1",
|
||||
|
||||
@ -21,6 +21,7 @@ import { appMenuService } from './services/AppMenuService'
|
||||
import { configManager } from './services/ConfigManager'
|
||||
import mcpService from './services/MCPService'
|
||||
import { nodeTraceService } from './services/NodeTraceService'
|
||||
import powerMonitorService from './services/PowerMonitorService'
|
||||
import {
|
||||
CHERRY_STUDIO_PROTOCOL,
|
||||
handleProtocolUrl,
|
||||
@ -30,6 +31,7 @@ import {
|
||||
import selectionService, { initSelectionService } from './services/SelectionService'
|
||||
import { registerShortcuts } from './services/ShortcutService'
|
||||
import { TrayService } from './services/TrayService'
|
||||
import { versionService } from './services/VersionService'
|
||||
import { windowService } from './services/WindowService'
|
||||
import { initWebviewHotkeys } from './services/WebviewService'
|
||||
|
||||
@ -110,6 +112,10 @@ if (!app.requestSingleInstanceLock()) {
|
||||
// Some APIs can only be used after this event occurs.
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
// Record current version for tracking
|
||||
// A preparation for v2 data refactoring
|
||||
versionService.recordCurrentVersion()
|
||||
|
||||
initWebviewHotkeys()
|
||||
// Set app user model id for windows
|
||||
electronApp.setAppUserModelId(import.meta.env.VITE_MAIN_BUNDLE_ID || 'com.kangfenmao.CherryStudio')
|
||||
@ -127,6 +133,7 @@ if (!app.requestSingleInstanceLock()) {
|
||||
appMenuService?.setupApplicationMenu()
|
||||
|
||||
nodeTraceService.init()
|
||||
powerMonitorService.init()
|
||||
|
||||
app.on('activate', function () {
|
||||
const mainWindow = windowService.getMainWindow()
|
||||
|
||||
@ -50,6 +50,7 @@ import * as NutstoreService from './services/NutstoreService'
|
||||
import ObsidianVaultService from './services/ObsidianVaultService'
|
||||
import { ocrService } from './services/ocr/OcrService'
|
||||
import OvmsManager from './services/OvmsManager'
|
||||
import powerMonitorService from './services/PowerMonitorService'
|
||||
import { proxyManager } from './services/ProxyManager'
|
||||
import { pythonService } from './services/PythonService'
|
||||
import { FileServiceManager } from './services/remotefile/FileServiceManager'
|
||||
@ -115,6 +116,18 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
const appUpdater = new AppUpdater()
|
||||
const notificationService = new NotificationService()
|
||||
|
||||
// Register shutdown handlers
|
||||
powerMonitorService.registerShutdownHandler(() => {
|
||||
appUpdater.setAutoUpdate(false)
|
||||
})
|
||||
|
||||
powerMonitorService.registerShutdownHandler(() => {
|
||||
const mw = windowService.getMainWindow()
|
||||
if (mw && !mw.isDestroyed()) {
|
||||
mw.webContents.send(IpcChannel.App_SaveData)
|
||||
}
|
||||
})
|
||||
|
||||
const checkMainWindow = () => {
|
||||
if (!mainWindow || mainWindow.isDestroyed()) {
|
||||
throw new Error('Main window does not exist or has been destroyed')
|
||||
|
||||
@ -7,16 +7,33 @@ import { app, Menu, shell } from 'electron'
|
||||
|
||||
import { configManager } from './ConfigManager'
|
||||
export class AppMenuService {
|
||||
private languageChangeCallback?: (newLanguage: string) => void
|
||||
|
||||
constructor() {
|
||||
// Subscribe to language change events
|
||||
this.languageChangeCallback = () => {
|
||||
this.setupApplicationMenu()
|
||||
}
|
||||
configManager.subscribe('language', this.languageChangeCallback)
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
// Clean up subscription to prevent memory leaks
|
||||
if (this.languageChangeCallback) {
|
||||
configManager.unsubscribe('language', this.languageChangeCallback)
|
||||
}
|
||||
}
|
||||
|
||||
public setupApplicationMenu(): void {
|
||||
const locale = locales[configManager.getLanguage()]
|
||||
const { common } = locale.translation
|
||||
const { appMenu } = locale.translation
|
||||
|
||||
const template: MenuItemConstructorOptions[] = [
|
||||
{
|
||||
label: app.name,
|
||||
submenu: [
|
||||
{
|
||||
label: common.about + ' ' + app.name,
|
||||
label: appMenu.about + ' ' + app.name,
|
||||
click: () => {
|
||||
// Emit event to navigate to About page
|
||||
const mainWindow = windowService.getMainWindow()
|
||||
@ -27,50 +44,78 @@ export class AppMenuService {
|
||||
}
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{ role: 'services' },
|
||||
{ role: 'services', label: appMenu.services },
|
||||
{ type: 'separator' },
|
||||
{ role: 'hide' },
|
||||
{ role: 'hideOthers' },
|
||||
{ role: 'unhide' },
|
||||
{ role: 'hide', label: `${appMenu.hide} ${app.name}` },
|
||||
{ role: 'hideOthers', label: appMenu.hideOthers },
|
||||
{ role: 'unhide', label: appMenu.unhide },
|
||||
{ type: 'separator' },
|
||||
{ role: 'quit' }
|
||||
{ role: 'quit', label: `${appMenu.quit} ${app.name}` }
|
||||
]
|
||||
},
|
||||
{
|
||||
role: 'fileMenu'
|
||||
label: appMenu.file,
|
||||
submenu: [{ role: 'close', label: appMenu.close }]
|
||||
},
|
||||
{
|
||||
role: 'editMenu'
|
||||
label: appMenu.edit,
|
||||
submenu: [
|
||||
{ role: 'undo', label: appMenu.undo },
|
||||
{ role: 'redo', label: appMenu.redo },
|
||||
{ type: 'separator' },
|
||||
{ role: 'cut', label: appMenu.cut },
|
||||
{ role: 'copy', label: appMenu.copy },
|
||||
{ role: 'paste', label: appMenu.paste },
|
||||
{ role: 'delete', label: appMenu.delete },
|
||||
{ role: 'selectAll', label: appMenu.selectAll }
|
||||
]
|
||||
},
|
||||
{
|
||||
role: 'viewMenu'
|
||||
label: appMenu.view,
|
||||
submenu: [
|
||||
{ role: 'reload', label: appMenu.reload },
|
||||
{ role: 'forceReload', label: appMenu.forceReload },
|
||||
{ role: 'toggleDevTools', label: appMenu.toggleDevTools },
|
||||
{ type: 'separator' },
|
||||
{ role: 'resetZoom', label: appMenu.resetZoom },
|
||||
{ role: 'zoomIn', label: appMenu.zoomIn },
|
||||
{ role: 'zoomOut', label: appMenu.zoomOut },
|
||||
{ type: 'separator' },
|
||||
{ role: 'togglefullscreen', label: appMenu.toggleFullscreen }
|
||||
]
|
||||
},
|
||||
{
|
||||
role: 'windowMenu'
|
||||
label: appMenu.window,
|
||||
submenu: [
|
||||
{ role: 'minimize', label: appMenu.minimize },
|
||||
{ role: 'zoom', label: appMenu.zoom },
|
||||
{ type: 'separator' },
|
||||
{ role: 'front', label: appMenu.front }
|
||||
]
|
||||
},
|
||||
{
|
||||
role: 'help',
|
||||
label: appMenu.help,
|
||||
submenu: [
|
||||
{
|
||||
label: 'Website',
|
||||
label: appMenu.website,
|
||||
click: () => {
|
||||
shell.openExternal('https://cherry-ai.com')
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Documentation',
|
||||
label: appMenu.documentation,
|
||||
click: () => {
|
||||
shell.openExternal('https://cherry-ai.com/docs')
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Feedback',
|
||||
label: appMenu.feedback,
|
||||
click: () => {
|
||||
shell.openExternal('https://github.com/CherryHQ/cherry-studio/issues/new/choose')
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Releases',
|
||||
label: appMenu.releases,
|
||||
click: () => {
|
||||
shell.openExternal('https://github.com/CherryHQ/cherry-studio/releases')
|
||||
}
|
||||
|
||||
112
src/main/services/PowerMonitorService.ts
Normal file
112
src/main/services/PowerMonitorService.ts
Normal file
@ -0,0 +1,112 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { isLinux, isMac, isWin } from '@main/constant'
|
||||
import ElectronShutdownHandler from '@paymoapp/electron-shutdown-handler'
|
||||
import { BrowserWindow } from 'electron'
|
||||
import { powerMonitor } from 'electron'
|
||||
|
||||
const logger = loggerService.withContext('PowerMonitorService')
|
||||
|
||||
type ShutdownHandler = () => void | Promise<void>
|
||||
|
||||
export class PowerMonitorService {
|
||||
private static instance: PowerMonitorService
|
||||
private initialized = false
|
||||
private shutdownHandlers: ShutdownHandler[] = []
|
||||
|
||||
private constructor() {
|
||||
// Private constructor to prevent direct instantiation
|
||||
}
|
||||
|
||||
public static getInstance(): PowerMonitorService {
|
||||
if (!PowerMonitorService.instance) {
|
||||
PowerMonitorService.instance = new PowerMonitorService()
|
||||
}
|
||||
return PowerMonitorService.instance
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a shutdown handler to be called when system shutdown is detected
|
||||
* @param handler - The handler function to be called on shutdown
|
||||
*/
|
||||
public registerShutdownHandler(handler: ShutdownHandler): void {
|
||||
this.shutdownHandlers.push(handler)
|
||||
logger.info('Shutdown handler registered', { totalHandlers: this.shutdownHandlers.length })
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize power monitor to listen for shutdown events
|
||||
*/
|
||||
public init(): void {
|
||||
if (this.initialized) {
|
||||
logger.warn('PowerMonitorService already initialized')
|
||||
return
|
||||
}
|
||||
|
||||
if (isWin) {
|
||||
this.initWindowsShutdownHandler()
|
||||
} else if (isMac || isLinux) {
|
||||
this.initElectronPowerMonitor()
|
||||
}
|
||||
|
||||
this.initialized = true
|
||||
logger.info('PowerMonitorService initialized', { platform: process.platform })
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute all registered shutdown handlers
|
||||
*/
|
||||
private async executeShutdownHandlers(): Promise<void> {
|
||||
logger.info('Executing shutdown handlers', { count: this.shutdownHandlers.length })
|
||||
for (const handler of this.shutdownHandlers) {
|
||||
try {
|
||||
await handler()
|
||||
} catch (error) {
|
||||
logger.error('Error executing shutdown handler', error as Error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize shutdown handler for Windows using @paymoapp/electron-shutdown-handler
|
||||
*/
|
||||
private initWindowsShutdownHandler(): void {
|
||||
try {
|
||||
const zeroMemoryWindow = new BrowserWindow({ show: false })
|
||||
// Set the window handle for the shutdown handler
|
||||
ElectronShutdownHandler.setWindowHandle(zeroMemoryWindow.getNativeWindowHandle())
|
||||
|
||||
// Listen for shutdown event
|
||||
ElectronShutdownHandler.on('shutdown', async () => {
|
||||
logger.info('System shutdown event detected (Windows)')
|
||||
// Execute all registered shutdown handlers
|
||||
await this.executeShutdownHandlers()
|
||||
// Release the shutdown block to allow the system to shut down
|
||||
ElectronShutdownHandler.releaseShutdown()
|
||||
})
|
||||
|
||||
logger.info('Windows shutdown handler registered')
|
||||
} catch (error) {
|
||||
logger.error('Failed to initialize Windows shutdown handler', error as Error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize power monitor for macOS and Linux using Electron's powerMonitor
|
||||
*/
|
||||
private initElectronPowerMonitor(): void {
|
||||
try {
|
||||
powerMonitor.on('shutdown', async () => {
|
||||
logger.info('System shutdown event detected', { platform: process.platform })
|
||||
// Execute all registered shutdown handlers
|
||||
await this.executeShutdownHandlers()
|
||||
})
|
||||
|
||||
logger.info('Electron powerMonitor shutdown listener registered')
|
||||
} catch (error) {
|
||||
logger.error('Failed to initialize Electron powerMonitor', error as Error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default export as singleton instance
|
||||
export default PowerMonitorService.getInstance()
|
||||
285
src/main/services/VersionService.ts
Normal file
285
src/main/services/VersionService.ts
Normal file
@ -0,0 +1,285 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { app } from 'electron'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
const logger = loggerService.withContext('VersionService')
|
||||
|
||||
type OS = 'win' | 'mac' | 'linux' | 'unknown'
|
||||
type Environment = 'prod' | 'dev'
|
||||
type Packaged = 'packaged' | 'unpackaged'
|
||||
type Mode = 'install' | 'portable'
|
||||
|
||||
/**
|
||||
* Version record stored in version.log
|
||||
*/
|
||||
interface VersionRecord {
|
||||
version: string
|
||||
os: OS
|
||||
environment: Environment
|
||||
packaged: Packaged
|
||||
mode: Mode
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Service for tracking application version history
|
||||
* Stores version information in userData/version.log for data migration and diagnostics
|
||||
*/
|
||||
class VersionService {
|
||||
private readonly VERSION_LOG_FILE = 'version.log'
|
||||
private versionLogPath: string | null = null
|
||||
|
||||
constructor() {
|
||||
// Lazy initialization of path since app.getPath may not be available during construction
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the full path to version.log file
|
||||
* @returns {string} Full path to version log file
|
||||
*/
|
||||
private getVersionLogPath(): string {
|
||||
if (!this.versionLogPath) {
|
||||
this.versionLogPath = path.join(app.getPath('userData'), this.VERSION_LOG_FILE)
|
||||
}
|
||||
return this.versionLogPath
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets current operating system identifier
|
||||
* @returns {OS} OS identifier
|
||||
*/
|
||||
private getCurrentOS(): OS {
|
||||
switch (process.platform) {
|
||||
case 'win32':
|
||||
return 'win'
|
||||
case 'darwin':
|
||||
return 'mac'
|
||||
case 'linux':
|
||||
return 'linux'
|
||||
default:
|
||||
return 'unknown'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets current environment (production or development)
|
||||
* @returns {Environment} Environment identifier
|
||||
*/
|
||||
private getCurrentEnvironment(): Environment {
|
||||
return import.meta.env.MODE === 'production' ? 'prod' : 'dev'
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets packaging status
|
||||
* @returns {Packaged} Packaging status
|
||||
*/
|
||||
private getPackagedStatus(): Packaged {
|
||||
return app.isPackaged ? 'packaged' : 'unpackaged'
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets installation mode (install or portable)
|
||||
* @returns {Mode} Installation mode
|
||||
*/
|
||||
private getInstallMode(): Mode {
|
||||
return process.env.PORTABLE_EXECUTABLE_DIR !== undefined ? 'portable' : 'install'
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates version log line for current application state
|
||||
* @returns {string} Pipe-separated version record line
|
||||
*/
|
||||
private generateCurrentVersionLine(): string {
|
||||
const version = app.getVersion()
|
||||
const os = this.getCurrentOS()
|
||||
const environment = this.getCurrentEnvironment()
|
||||
const packaged = this.getPackagedStatus()
|
||||
const mode = this.getInstallMode()
|
||||
const timestamp = new Date().toISOString()
|
||||
|
||||
return `${version}|${os}|${environment}|${packaged}|${mode}|${timestamp}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a version log line into a VersionRecord object
|
||||
* @param {string} line - Pipe-separated version record line
|
||||
* @returns {VersionRecord | null} Parsed version record or null if invalid
|
||||
*/
|
||||
private parseVersionLine(line: string): VersionRecord | null {
|
||||
try {
|
||||
const parts = line.trim().split('|')
|
||||
if (parts.length !== 6) {
|
||||
return null
|
||||
}
|
||||
|
||||
const [version, os, environment, packaged, mode, timestamp] = parts
|
||||
|
||||
// Validate data
|
||||
if (
|
||||
!version ||
|
||||
!['win', 'mac', 'linux', 'unknown'].includes(os) ||
|
||||
!['prod', 'dev'].includes(environment) ||
|
||||
!['packaged', 'unpackaged'].includes(packaged) ||
|
||||
!['install', 'portable'].includes(mode) ||
|
||||
!timestamp
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
version,
|
||||
os: os as OS,
|
||||
environment: environment as Environment,
|
||||
packaged: packaged as Packaged,
|
||||
mode: mode as Mode,
|
||||
timestamp
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to parse version line: ${line}`, error as Error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the last 1KB from version.log and returns all lines
|
||||
* Uses reverse reading from file end to avoid reading the entire file
|
||||
* @returns {string[]} Array of version lines from the last 1KB
|
||||
*/
|
||||
private readLastVersionLines(): string[] {
|
||||
const logPath = this.getVersionLogPath()
|
||||
|
||||
try {
|
||||
if (!fs.existsSync(logPath)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const stats = fs.statSync(logPath)
|
||||
const fileSize = stats.size
|
||||
|
||||
if (fileSize === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
// Read from the end of the file, 1KB is enough to find previous version
|
||||
// Typical line: "1.7.0-beta.3|win|prod|packaged|install|2025-01-15T08:30:00.000Z\n" (~70 bytes)
|
||||
// 1KB can store ~14 lines, which is more than enough
|
||||
const bufferSize = Math.min(1024, fileSize)
|
||||
const buffer = Buffer.alloc(bufferSize)
|
||||
|
||||
const fd = fs.openSync(logPath, 'r')
|
||||
try {
|
||||
const startPosition = Math.max(0, fileSize - bufferSize)
|
||||
fs.readSync(fd, buffer, 0, bufferSize, startPosition)
|
||||
|
||||
const content = buffer.toString('utf-8')
|
||||
const lines = content
|
||||
.trim()
|
||||
.split('\n')
|
||||
.filter((line) => line.trim())
|
||||
|
||||
return lines
|
||||
} finally {
|
||||
fs.closeSync(fd)
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to read version log:', error as Error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a version record line to version.log
|
||||
* @param {string} line - Version record line to append
|
||||
*/
|
||||
private appendVersionLine(line: string): void {
|
||||
const logPath = this.getVersionLogPath()
|
||||
|
||||
try {
|
||||
fs.appendFileSync(logPath, line + '\n', 'utf-8')
|
||||
logger.debug(`Version recorded: ${line}`)
|
||||
} catch (error) {
|
||||
logger.error('Failed to append version log:', error as Error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Records the current version on application startup
|
||||
* Only adds a new record if the version has changed since the last run
|
||||
*/
|
||||
recordCurrentVersion(): void {
|
||||
try {
|
||||
const currentLine = this.generateCurrentVersionLine()
|
||||
const lines = this.readLastVersionLines()
|
||||
|
||||
// Add new record if this is the first run or version has changed
|
||||
if (lines.length === 0) {
|
||||
logger.info('First run detected, creating version log')
|
||||
this.appendVersionLine(currentLine)
|
||||
return
|
||||
}
|
||||
|
||||
const lastLine = lines[lines.length - 1]
|
||||
const lastRecord = this.parseVersionLine(lastLine)
|
||||
const currentVersion = app.getVersion()
|
||||
|
||||
// Check if any meaningful field has changed (version, os, environment, packaged, mode)
|
||||
const currentOS = this.getCurrentOS()
|
||||
const currentEnvironment = this.getCurrentEnvironment()
|
||||
const currentPackaged = this.getPackagedStatus()
|
||||
const currentMode = this.getInstallMode()
|
||||
|
||||
const hasMeaningfulChange =
|
||||
!lastRecord ||
|
||||
lastRecord.version !== currentVersion ||
|
||||
lastRecord.os !== currentOS ||
|
||||
lastRecord.environment !== currentEnvironment ||
|
||||
lastRecord.packaged !== currentPackaged ||
|
||||
lastRecord.mode !== currentMode
|
||||
|
||||
if (hasMeaningfulChange) {
|
||||
logger.info(`Version information changed, recording new entry`)
|
||||
this.appendVersionLine(currentLine)
|
||||
} else {
|
||||
logger.debug(`Version information not changed, skip recording`)
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to record current version:', error as Error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the previous version record (last record with different version than current)
|
||||
* Reads from the last 1KB of version.log to find the most recent different version
|
||||
* Useful for detecting version upgrades and running migrations
|
||||
* @returns {VersionRecord | null} Previous version record or null if not available
|
||||
*/
|
||||
getPreviousVersion(): VersionRecord | null {
|
||||
try {
|
||||
const lines = this.readLastVersionLines()
|
||||
if (lines.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const currentVersion = app.getVersion()
|
||||
|
||||
// Read from the end backwards to find the first different version
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
const record = this.parseVersionLine(lines[i])
|
||||
if (record && record.version !== currentVersion) {
|
||||
return record
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
} catch (error) {
|
||||
logger.error('Failed to get previous version:', error as Error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Singleton instance of VersionService
|
||||
*/
|
||||
export const versionService = new VersionService()
|
||||
@ -1,6 +1,7 @@
|
||||
import { BedrockClient, ListFoundationModelsCommand, ListInferenceProfilesCommand } from '@aws-sdk/client-bedrock'
|
||||
import {
|
||||
BedrockRuntimeClient,
|
||||
type BedrockRuntimeClientConfig,
|
||||
ConverseCommand,
|
||||
InvokeModelCommand,
|
||||
InvokeModelWithResponseStreamCommand
|
||||
@ -11,6 +12,8 @@ import { DEFAULT_MAX_TOKENS } from '@renderer/config/constant'
|
||||
import { findTokenLimit, isReasoningModel } from '@renderer/config/models'
|
||||
import {
|
||||
getAwsBedrockAccessKeyId,
|
||||
getAwsBedrockApiKey,
|
||||
getAwsBedrockAuthType,
|
||||
getAwsBedrockRegion,
|
||||
getAwsBedrockSecretAccessKey
|
||||
} from '@renderer/hooks/useAwsBedrock'
|
||||
@ -75,32 +78,48 @@ export class AwsBedrockAPIClient extends BaseApiClient<
|
||||
}
|
||||
|
||||
const region = getAwsBedrockRegion()
|
||||
const accessKeyId = getAwsBedrockAccessKeyId()
|
||||
const secretAccessKey = getAwsBedrockSecretAccessKey()
|
||||
const authType = getAwsBedrockAuthType()
|
||||
|
||||
if (!region) {
|
||||
throw new Error('AWS region is required. Please configure AWS-Region in extra headers.')
|
||||
throw new Error('AWS region is required. Please configure AWS region in settings.')
|
||||
}
|
||||
|
||||
if (!accessKeyId || !secretAccessKey) {
|
||||
throw new Error('AWS credentials are required. Please configure AWS-Access-Key-ID and AWS-Secret-Access-Key.')
|
||||
// Build client configuration based on auth type
|
||||
let clientConfig: BedrockRuntimeClientConfig
|
||||
|
||||
if (authType === 'iam') {
|
||||
// IAM credentials authentication
|
||||
const accessKeyId = getAwsBedrockAccessKeyId()
|
||||
const secretAccessKey = getAwsBedrockSecretAccessKey()
|
||||
|
||||
if (!accessKeyId || !secretAccessKey) {
|
||||
throw new Error('AWS credentials are required. Please configure Access Key ID and Secret Access Key.')
|
||||
}
|
||||
|
||||
clientConfig = {
|
||||
region,
|
||||
credentials: {
|
||||
accessKeyId,
|
||||
secretAccessKey
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// API Key authentication
|
||||
const awsBedrockApiKey = getAwsBedrockApiKey()
|
||||
|
||||
if (!awsBedrockApiKey) {
|
||||
throw new Error('AWS Bedrock API Key is required. Please configure API Key in settings.')
|
||||
}
|
||||
|
||||
clientConfig = {
|
||||
region,
|
||||
token: { token: awsBedrockApiKey },
|
||||
authSchemePreference: ['httpBearerAuth']
|
||||
}
|
||||
}
|
||||
|
||||
const client = new BedrockRuntimeClient({
|
||||
region,
|
||||
credentials: {
|
||||
accessKeyId,
|
||||
secretAccessKey
|
||||
}
|
||||
})
|
||||
|
||||
const bedrockClient = new BedrockClient({
|
||||
region,
|
||||
credentials: {
|
||||
accessKeyId,
|
||||
secretAccessKey
|
||||
}
|
||||
})
|
||||
const client = new BedrockRuntimeClient(clientConfig)
|
||||
const bedrockClient = new BedrockClient(clientConfig)
|
||||
|
||||
this.sdkInstance = { client, bedrockClient, region }
|
||||
return this.sdkInstance
|
||||
|
||||
@ -21,10 +21,45 @@ vi.mock('@renderer/store', () => ({
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@renderer/utils/api', () => ({
|
||||
formatApiHost: vi.fn((host, isSupportedAPIVersion = true) => {
|
||||
if (isSupportedAPIVersion === false) {
|
||||
return host // Return host as-is when isSupportedAPIVersion is false
|
||||
}
|
||||
return `${host}/v1` // Default behavior when isSupportedAPIVersion is true
|
||||
}),
|
||||
routeToEndpoint: vi.fn((host) => ({
|
||||
baseURL: host,
|
||||
endpoint: '/chat/completions'
|
||||
}))
|
||||
}))
|
||||
|
||||
vi.mock('@renderer/config/providers', async (importOriginal) => {
|
||||
const actual = (await importOriginal()) as any
|
||||
return {
|
||||
...actual,
|
||||
isCherryAIProvider: vi.fn(),
|
||||
isPerplexityProvider: vi.fn(),
|
||||
isAnthropicProvider: vi.fn(() => false),
|
||||
isAzureOpenAIProvider: vi.fn(() => false),
|
||||
isGeminiProvider: vi.fn(() => false),
|
||||
isNewApiProvider: vi.fn(() => false)
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@renderer/hooks/useVertexAI', () => ({
|
||||
isVertexProvider: vi.fn(() => false),
|
||||
isVertexAIConfigured: vi.fn(() => false),
|
||||
createVertexProvider: vi.fn()
|
||||
}))
|
||||
|
||||
import { isCherryAIProvider, isPerplexityProvider } from '@renderer/config/providers'
|
||||
import { getProviderByModel } from '@renderer/services/AssistantService'
|
||||
import type { Model, Provider } from '@renderer/types'
|
||||
import { formatApiHost } from '@renderer/utils/api'
|
||||
|
||||
import { COPILOT_DEFAULT_HEADERS, COPILOT_EDITOR_VERSION, isCopilotResponsesModel } from '../constants'
|
||||
import { providerToAiSdkConfig } from '../providerConfig'
|
||||
import { getActualProvider, providerToAiSdkConfig } from '../providerConfig'
|
||||
|
||||
const createWindowKeyv = () => {
|
||||
const store = new Map<string, string>()
|
||||
@ -46,11 +81,31 @@ const createCopilotProvider = (): Provider => ({
|
||||
isSystem: true
|
||||
})
|
||||
|
||||
const createModel = (id: string, name = id): Model => ({
|
||||
const createModel = (id: string, name = id, provider = 'copilot'): Model => ({
|
||||
id,
|
||||
name,
|
||||
provider: 'copilot',
|
||||
group: 'copilot'
|
||||
provider,
|
||||
group: provider
|
||||
})
|
||||
|
||||
const createCherryAIProvider = (): Provider => ({
|
||||
id: 'cherryai',
|
||||
type: 'openai',
|
||||
name: 'CherryAI',
|
||||
apiKey: 'test-key',
|
||||
apiHost: 'https://api.cherryai.com',
|
||||
models: [],
|
||||
isSystem: false
|
||||
})
|
||||
|
||||
const createPerplexityProvider = (): Provider => ({
|
||||
id: 'perplexity',
|
||||
type: 'openai',
|
||||
name: 'Perplexity',
|
||||
apiKey: 'test-key',
|
||||
apiHost: 'https://api.perplexity.ai',
|
||||
models: [],
|
||||
isSystem: false
|
||||
})
|
||||
|
||||
describe('Copilot responses routing', () => {
|
||||
@ -87,3 +142,134 @@ describe('Copilot responses routing', () => {
|
||||
expect(config.options.headers?.['Copilot-Integration-Id']).toBe(COPILOT_DEFAULT_HEADERS['Copilot-Integration-Id'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('CherryAI provider configuration', () => {
|
||||
beforeEach(() => {
|
||||
;(globalThis as any).window = {
|
||||
...(globalThis as any).window,
|
||||
keyv: createWindowKeyv()
|
||||
}
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('formats CherryAI provider apiHost with false parameter', () => {
|
||||
const provider = createCherryAIProvider()
|
||||
const model = createModel('gpt-4', 'GPT-4', 'cherryai')
|
||||
|
||||
// Mock the functions to simulate CherryAI provider detection
|
||||
vi.mocked(isCherryAIProvider).mockReturnValue(true)
|
||||
vi.mocked(getProviderByModel).mockReturnValue(provider)
|
||||
|
||||
// Call getActualProvider which should trigger formatProviderApiHost
|
||||
const actualProvider = getActualProvider(model)
|
||||
|
||||
// Verify that formatApiHost was called with false as the second parameter
|
||||
expect(formatApiHost).toHaveBeenCalledWith('https://api.cherryai.com', false)
|
||||
expect(actualProvider.apiHost).toBe('https://api.cherryai.com')
|
||||
})
|
||||
|
||||
it('does not format non-CherryAI provider with false parameter', () => {
|
||||
const provider = {
|
||||
id: 'openai',
|
||||
type: 'openai',
|
||||
name: 'OpenAI',
|
||||
apiKey: 'test-key',
|
||||
apiHost: 'https://api.openai.com',
|
||||
models: [],
|
||||
isSystem: false
|
||||
} as Provider
|
||||
const model = createModel('gpt-4', 'GPT-4', 'openai')
|
||||
|
||||
// Mock the functions to simulate non-CherryAI provider
|
||||
vi.mocked(isCherryAIProvider).mockReturnValue(false)
|
||||
vi.mocked(getProviderByModel).mockReturnValue(provider)
|
||||
|
||||
// Call getActualProvider
|
||||
const actualProvider = getActualProvider(model)
|
||||
|
||||
// Verify that formatApiHost was called with default parameters (true)
|
||||
expect(formatApiHost).toHaveBeenCalledWith('https://api.openai.com')
|
||||
expect(actualProvider.apiHost).toBe('https://api.openai.com/v1')
|
||||
})
|
||||
|
||||
it('handles CherryAI provider with empty apiHost', () => {
|
||||
const provider = createCherryAIProvider()
|
||||
provider.apiHost = ''
|
||||
const model = createModel('gpt-4', 'GPT-4', 'cherryai')
|
||||
|
||||
vi.mocked(isCherryAIProvider).mockReturnValue(true)
|
||||
vi.mocked(getProviderByModel).mockReturnValue(provider)
|
||||
|
||||
const actualProvider = getActualProvider(model)
|
||||
|
||||
expect(formatApiHost).toHaveBeenCalledWith('', false)
|
||||
expect(actualProvider.apiHost).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Perplexity provider configuration', () => {
|
||||
beforeEach(() => {
|
||||
;(globalThis as any).window = {
|
||||
...(globalThis as any).window,
|
||||
keyv: createWindowKeyv()
|
||||
}
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('formats Perplexity provider apiHost with false parameter', () => {
|
||||
const provider = createPerplexityProvider()
|
||||
const model = createModel('sonar', 'Sonar', 'perplexity')
|
||||
|
||||
// Mock the functions to simulate Perplexity provider detection
|
||||
vi.mocked(isCherryAIProvider).mockReturnValue(false)
|
||||
vi.mocked(isPerplexityProvider).mockReturnValue(true)
|
||||
vi.mocked(getProviderByModel).mockReturnValue(provider)
|
||||
|
||||
// Call getActualProvider which should trigger formatProviderApiHost
|
||||
const actualProvider = getActualProvider(model)
|
||||
|
||||
// Verify that formatApiHost was called with false as the second parameter
|
||||
expect(formatApiHost).toHaveBeenCalledWith('https://api.perplexity.ai', false)
|
||||
expect(actualProvider.apiHost).toBe('https://api.perplexity.ai')
|
||||
})
|
||||
|
||||
it('does not format non-Perplexity provider with false parameter', () => {
|
||||
const provider = {
|
||||
id: 'openai',
|
||||
type: 'openai',
|
||||
name: 'OpenAI',
|
||||
apiKey: 'test-key',
|
||||
apiHost: 'https://api.openai.com',
|
||||
models: [],
|
||||
isSystem: false
|
||||
} as Provider
|
||||
const model = createModel('gpt-4', 'GPT-4', 'openai')
|
||||
|
||||
// Mock the functions to simulate non-Perplexity provider
|
||||
vi.mocked(isCherryAIProvider).mockReturnValue(false)
|
||||
vi.mocked(isPerplexityProvider).mockReturnValue(false)
|
||||
vi.mocked(getProviderByModel).mockReturnValue(provider)
|
||||
|
||||
// Call getActualProvider
|
||||
const actualProvider = getActualProvider(model)
|
||||
|
||||
// Verify that formatApiHost was called with default parameters (true)
|
||||
expect(formatApiHost).toHaveBeenCalledWith('https://api.openai.com')
|
||||
expect(actualProvider.apiHost).toBe('https://api.openai.com/v1')
|
||||
})
|
||||
|
||||
it('handles Perplexity provider with empty apiHost', () => {
|
||||
const provider = createPerplexityProvider()
|
||||
provider.apiHost = ''
|
||||
const model = createModel('sonar', 'Sonar', 'perplexity')
|
||||
|
||||
vi.mocked(isCherryAIProvider).mockReturnValue(false)
|
||||
vi.mocked(isPerplexityProvider).mockReturnValue(true)
|
||||
vi.mocked(getProviderByModel).mockReturnValue(provider)
|
||||
|
||||
const actualProvider = getActualProvider(model)
|
||||
|
||||
expect(formatApiHost).toHaveBeenCalledWith('', false)
|
||||
expect(actualProvider.apiHost).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
@ -9,11 +9,15 @@ import { isOpenAIChatCompletionOnlyModel } from '@renderer/config/models'
|
||||
import {
|
||||
isAnthropicProvider,
|
||||
isAzureOpenAIProvider,
|
||||
isCherryAIProvider,
|
||||
isGeminiProvider,
|
||||
isNewApiProvider
|
||||
isNewApiProvider,
|
||||
isPerplexityProvider
|
||||
} from '@renderer/config/providers'
|
||||
import {
|
||||
getAwsBedrockAccessKeyId,
|
||||
getAwsBedrockApiKey,
|
||||
getAwsBedrockAuthType,
|
||||
getAwsBedrockRegion,
|
||||
getAwsBedrockSecretAccessKey
|
||||
} from '@renderer/hooks/useAwsBedrock'
|
||||
@ -98,6 +102,10 @@ function formatProviderApiHost(provider: Provider): Provider {
|
||||
formatted.apiHost = formatAzureOpenAIApiHost(formatted.apiHost)
|
||||
} else if (isVertexProvider(formatted)) {
|
||||
formatted.apiHost = formatVertexApiHost(formatted)
|
||||
} else if (isCherryAIProvider(formatted)) {
|
||||
formatted.apiHost = formatApiHost(formatted.apiHost, false)
|
||||
} else if (isPerplexityProvider(formatted)) {
|
||||
formatted.apiHost = formatApiHost(formatted.apiHost, false)
|
||||
} else {
|
||||
formatted.apiHost = formatApiHost(formatted.apiHost)
|
||||
}
|
||||
@ -192,9 +200,15 @@ export function providerToAiSdkConfig(
|
||||
|
||||
// bedrock
|
||||
if (aiSdkProviderId === 'bedrock') {
|
||||
const authType = getAwsBedrockAuthType()
|
||||
extraOptions.region = getAwsBedrockRegion()
|
||||
extraOptions.accessKeyId = getAwsBedrockAccessKeyId()
|
||||
extraOptions.secretAccessKey = getAwsBedrockSecretAccessKey()
|
||||
|
||||
if (authType === 'apiKey') {
|
||||
extraOptions.apiKey = getAwsBedrockApiKey()
|
||||
} else {
|
||||
extraOptions.accessKeyId = getAwsBedrockAccessKeyId()
|
||||
extraOptions.secretAccessKey = getAwsBedrockSecretAccessKey()
|
||||
}
|
||||
}
|
||||
// google-vertex
|
||||
if (aiSdkProviderId === 'google-vertex' || aiSdkProviderId === 'google-vertex-anthropic') {
|
||||
|
||||
@ -17,6 +17,7 @@ import { getAiSdkProviderId } from '../provider/factory'
|
||||
import { buildGeminiGenerateImageParams } from './image'
|
||||
import {
|
||||
getAnthropicReasoningParams,
|
||||
getBedrockReasoningParams,
|
||||
getCustomParameters,
|
||||
getGeminiReasoningParams,
|
||||
getOpenAIReasoningParams,
|
||||
@ -127,6 +128,9 @@ export function buildProviderOptions(
|
||||
case 'google-vertex-anthropic':
|
||||
providerSpecificOptions = buildAnthropicProviderOptions(assistant, model, capabilities)
|
||||
break
|
||||
case 'bedrock':
|
||||
providerSpecificOptions = buildBedrockProviderOptions(assistant, model, capabilities)
|
||||
break
|
||||
default:
|
||||
// 对于其他 provider,使用通用的构建逻辑
|
||||
providerSpecificOptions = {
|
||||
@ -266,6 +270,32 @@ function buildXAIProviderOptions(
|
||||
return providerOptions
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Bedrock providerOptions
|
||||
*/
|
||||
function buildBedrockProviderOptions(
|
||||
assistant: Assistant,
|
||||
model: Model,
|
||||
capabilities: {
|
||||
enableReasoning: boolean
|
||||
enableWebSearch: boolean
|
||||
enableGenerateImage: boolean
|
||||
}
|
||||
): Record<string, any> {
|
||||
const { enableReasoning } = capabilities
|
||||
let providerOptions: Record<string, any> = {}
|
||||
|
||||
if (enableReasoning) {
|
||||
const reasoningParams = getBedrockReasoningParams(assistant, model)
|
||||
providerOptions = {
|
||||
...providerOptions,
|
||||
...reasoningParams
|
||||
}
|
||||
}
|
||||
|
||||
return providerOptions
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建通用的 providerOptions(用于其他 provider)
|
||||
*/
|
||||
|
||||
@ -485,6 +485,34 @@ export function getXAIReasoningParams(assistant: Assistant, model: Model): Recor
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Bedrock reasoning parameters
|
||||
*/
|
||||
export function getBedrockReasoningParams(assistant: Assistant, model: Model): Record<string, any> {
|
||||
if (!isReasoningModel(model)) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const reasoningEffort = assistant?.settings?.reasoning_effort
|
||||
|
||||
if (reasoningEffort === undefined) {
|
||||
return {}
|
||||
}
|
||||
|
||||
// Only apply thinking budget for Claude reasoning models
|
||||
if (!isSupportedThinkingTokenClaudeModel(model)) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const budgetTokens = getAnthropicThinkingBudget(assistant, model)
|
||||
return {
|
||||
reasoningConfig: {
|
||||
type: 'enabled',
|
||||
budgetTokens: budgetTokens
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取自定义参数
|
||||
* 从 assistant 设置中提取自定义参数
|
||||
|
||||
@ -64,6 +64,7 @@ export type QuickPanelListItem = {
|
||||
isSelected?: boolean
|
||||
isMenu?: boolean
|
||||
disabled?: boolean
|
||||
hidden?: boolean
|
||||
/**
|
||||
* 固定显示项:不参与过滤,始终出现在列表顶部。
|
||||
* 例如“清除”按钮可设置为 alwaysVisible,从而在有匹配项时始终可见;
|
||||
|
||||
@ -143,7 +143,8 @@ export const QuickPanelView: React.FC<Props> = ({ setInputText }) => {
|
||||
prevSymbolRef.current = ctx.symbol
|
||||
|
||||
// 固定项置顶 + 过滤后的普通项
|
||||
return [...pinnedItems, ...filteredNormalItems]
|
||||
const pinnedFiltered = [...pinnedItems, ...filteredNormalItems]
|
||||
return pinnedFiltered.filter((item) => !item.hidden)
|
||||
}, [ctx.isVisible, ctx.symbol, ctx.list, searchText])
|
||||
|
||||
const canForwardAndBackward = useMemo(() => {
|
||||
|
||||
@ -1486,6 +1486,14 @@ export const isNewApiProvider = (provider: Provider) => {
|
||||
return ['new-api', 'cherryin'].includes(provider.id) || provider.type === 'new-api'
|
||||
}
|
||||
|
||||
export function isCherryAIProvider(provider: Provider): boolean {
|
||||
return provider.id === 'cherryai'
|
||||
}
|
||||
|
||||
export function isPerplexityProvider(provider: Provider): boolean {
|
||||
return provider.id === 'perplexity'
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为 OpenAI 兼容的提供商
|
||||
* @param {Provider} provider 提供商对象
|
||||
@ -1511,7 +1519,7 @@ export function isGeminiProvider(provider: Provider): boolean {
|
||||
return provider.type === 'gemini'
|
||||
}
|
||||
|
||||
const NOT_SUPPORT_API_VERSION_PROVIDERS = ['github', 'copilot'] as const satisfies SystemProviderId[]
|
||||
const NOT_SUPPORT_API_VERSION_PROVIDERS = ['github', 'copilot', 'perplexity'] as const satisfies SystemProviderId[]
|
||||
|
||||
export const isSupportAPIVersionProvider = (provider: Provider) => {
|
||||
if (isSystemProvider(provider)) {
|
||||
|
||||
@ -1,5 +1,12 @@
|
||||
import store, { useAppSelector } from '@renderer/store'
|
||||
import { setAwsBedrockAccessKeyId, setAwsBedrockRegion, setAwsBedrockSecretAccessKey } from '@renderer/store/llm'
|
||||
import {
|
||||
setAwsBedrockAccessKeyId,
|
||||
setAwsBedrockApiKey,
|
||||
setAwsBedrockAuthType,
|
||||
setAwsBedrockRegion,
|
||||
setAwsBedrockSecretAccessKey
|
||||
} from '@renderer/store/llm'
|
||||
import type { AwsBedrockAuthType } from '@renderer/types'
|
||||
import { useDispatch } from 'react-redux'
|
||||
|
||||
export function useAwsBedrockSettings() {
|
||||
@ -8,8 +15,10 @@ export function useAwsBedrockSettings() {
|
||||
|
||||
return {
|
||||
...settings,
|
||||
setAuthType: (authType: AwsBedrockAuthType) => dispatch(setAwsBedrockAuthType(authType)),
|
||||
setAccessKeyId: (accessKeyId: string) => dispatch(setAwsBedrockAccessKeyId(accessKeyId)),
|
||||
setSecretAccessKey: (secretAccessKey: string) => dispatch(setAwsBedrockSecretAccessKey(secretAccessKey)),
|
||||
setApiKey: (apiKey: string) => dispatch(setAwsBedrockApiKey(apiKey)),
|
||||
setRegion: (region: string) => dispatch(setAwsBedrockRegion(region))
|
||||
}
|
||||
}
|
||||
@ -18,6 +27,10 @@ export function getAwsBedrockSettings() {
|
||||
return store.getState().llm.settings.awsBedrock
|
||||
}
|
||||
|
||||
export function getAwsBedrockAuthType() {
|
||||
return store.getState().llm.settings.awsBedrock.authType
|
||||
}
|
||||
|
||||
export function getAwsBedrockAccessKeyId() {
|
||||
return store.getState().llm.settings.awsBedrock.accessKeyId
|
||||
}
|
||||
@ -26,6 +39,10 @@ export function getAwsBedrockSecretAccessKey() {
|
||||
return store.getState().llm.settings.awsBedrock.secretAccessKey
|
||||
}
|
||||
|
||||
export function getAwsBedrockApiKey() {
|
||||
return store.getState().llm.settings.awsBedrock.apiKey
|
||||
}
|
||||
|
||||
export function getAwsBedrockRegion() {
|
||||
return store.getState().llm.settings.awsBedrock.region
|
||||
}
|
||||
|
||||
@ -339,6 +339,41 @@
|
||||
},
|
||||
"title": "API Server"
|
||||
},
|
||||
"appMenu": {
|
||||
"about": "About",
|
||||
"close": "Close Window",
|
||||
"copy": "Copy",
|
||||
"cut": "Cut",
|
||||
"delete": "Delete",
|
||||
"documentation": "Documentation",
|
||||
"edit": "Edit",
|
||||
"feedback": "Feedback",
|
||||
"file": "File",
|
||||
"forceReload": "Force Reload",
|
||||
"front": "Bring All to Front",
|
||||
"help": "Help",
|
||||
"hide": "Hide",
|
||||
"hideOthers": "Hide Others",
|
||||
"minimize": "Minimize",
|
||||
"paste": "Paste",
|
||||
"quit": "Quit",
|
||||
"redo": "Redo",
|
||||
"releases": "Releases",
|
||||
"reload": "Reload",
|
||||
"resetZoom": "Actual Size",
|
||||
"selectAll": "Select All",
|
||||
"services": "Services",
|
||||
"toggleDevTools": "Toggle Developer Tools",
|
||||
"toggleFullscreen": "Toggle Fullscreen",
|
||||
"undo": "Undo",
|
||||
"unhide": "Show All",
|
||||
"view": "View",
|
||||
"website": "Website",
|
||||
"window": "Window",
|
||||
"zoom": "Zoom",
|
||||
"zoomIn": "Zoom In",
|
||||
"zoomOut": "Zoom Out"
|
||||
},
|
||||
"assistants": {
|
||||
"abbr": "Assistants",
|
||||
"clear": {
|
||||
@ -4260,6 +4295,12 @@
|
||||
"aws-bedrock": {
|
||||
"access_key_id": "AWS Access Key ID",
|
||||
"access_key_id_help": "Your AWS Access Key ID for accessing AWS Bedrock services",
|
||||
"api_key": "Bedrock API Key",
|
||||
"api_key_help": "Your AWS Bedrock API Key for authentication",
|
||||
"auth_type": "Authentication Type",
|
||||
"auth_type_api_key": "Bedrock API Key",
|
||||
"auth_type_help": "Choose between IAM credentials or Bedrock API Key authentication",
|
||||
"auth_type_iam": "IAM Credentials",
|
||||
"description": "AWS Bedrock is Amazon's fully managed foundation model service that supports various advanced large language models",
|
||||
"region": "AWS Region",
|
||||
"region_help": "Your AWS service region, e.g., us-east-1",
|
||||
|
||||
@ -339,6 +339,41 @@
|
||||
},
|
||||
"title": "API 服务器"
|
||||
},
|
||||
"appMenu": {
|
||||
"about": "关于",
|
||||
"close": "关闭窗口",
|
||||
"copy": "复制",
|
||||
"cut": "剪切",
|
||||
"delete": "删除",
|
||||
"documentation": "文档",
|
||||
"edit": "编辑",
|
||||
"feedback": "反馈",
|
||||
"file": "文件",
|
||||
"forceReload": "强制重新加载",
|
||||
"front": "全部置于顶层",
|
||||
"help": "帮助",
|
||||
"hide": "隐藏",
|
||||
"hideOthers": "隐藏其他",
|
||||
"minimize": "最小化",
|
||||
"paste": "粘贴",
|
||||
"quit": "退出",
|
||||
"redo": "重做",
|
||||
"releases": "版本发布",
|
||||
"reload": "重新加载",
|
||||
"resetZoom": "实际大小",
|
||||
"selectAll": "全选",
|
||||
"services": "服务",
|
||||
"toggleDevTools": "切换开发者工具",
|
||||
"toggleFullscreen": "切换全屏",
|
||||
"undo": "撤销",
|
||||
"unhide": "全部显示",
|
||||
"view": "视图",
|
||||
"website": "网站",
|
||||
"window": "窗口",
|
||||
"zoom": "缩放",
|
||||
"zoomIn": "放大",
|
||||
"zoomOut": "缩小"
|
||||
},
|
||||
"assistants": {
|
||||
"abbr": "助手",
|
||||
"clear": {
|
||||
@ -4260,6 +4295,12 @@
|
||||
"aws-bedrock": {
|
||||
"access_key_id": "AWS 访问密钥 ID",
|
||||
"access_key_id_help": "您的 AWS 访问密钥 ID,用于访问 AWS Bedrock 服务",
|
||||
"api_key": "Bedrock API 密钥",
|
||||
"api_key_help": "您的 AWS Bedrock API 密钥,用于身份验证",
|
||||
"auth_type": "认证方式",
|
||||
"auth_type_api_key": "Bedrock API 密钥",
|
||||
"auth_type_help": "选择使用 IAM 凭证或 Bedrock API 密钥进行身份验证",
|
||||
"auth_type_iam": "IAM 凭证",
|
||||
"description": "AWS Bedrock 是亚马逊提供的全托管基础模型服务,支持多种先进的大语言模型",
|
||||
"region": "AWS 区域",
|
||||
"region_help": "您的 AWS 服务区域,例如 us-east-1",
|
||||
|
||||
@ -339,6 +339,41 @@
|
||||
},
|
||||
"title": "API 伺服器"
|
||||
},
|
||||
"appMenu": {
|
||||
"about": "關於",
|
||||
"close": "關閉視窗",
|
||||
"copy": "複製",
|
||||
"cut": "剪下",
|
||||
"delete": "刪除",
|
||||
"documentation": "文件",
|
||||
"edit": "編輯",
|
||||
"feedback": "回饋",
|
||||
"file": "檔案",
|
||||
"forceReload": "強制重新載入",
|
||||
"front": "全部置於頂層",
|
||||
"help": "幫助",
|
||||
"hide": "隱藏",
|
||||
"hideOthers": "隱藏其他",
|
||||
"minimize": "最小化",
|
||||
"paste": "貼上",
|
||||
"quit": "結束",
|
||||
"redo": "重做",
|
||||
"releases": "版本發布",
|
||||
"reload": "重新載入",
|
||||
"resetZoom": "實際大小",
|
||||
"selectAll": "全選",
|
||||
"services": "服務",
|
||||
"toggleDevTools": "切換開發者工具",
|
||||
"toggleFullscreen": "切換全螢幕",
|
||||
"undo": "復原",
|
||||
"unhide": "全部顯示",
|
||||
"view": "檢視",
|
||||
"website": "網站",
|
||||
"window": "視窗",
|
||||
"zoom": "縮放",
|
||||
"zoomIn": "放大",
|
||||
"zoomOut": "縮小"
|
||||
},
|
||||
"assistants": {
|
||||
"abbr": "助手",
|
||||
"clear": {
|
||||
@ -4260,6 +4295,12 @@
|
||||
"aws-bedrock": {
|
||||
"access_key_id": "AWS 存取密鑰 ID",
|
||||
"access_key_id_help": "您的 AWS 存取密鑰 ID,用於存取 AWS Bedrock 服務",
|
||||
"api_key": "Bedrock API 金鑰",
|
||||
"api_key_help": "您的 AWS Bedrock API 金鑰,用於身份驗證",
|
||||
"auth_type": "認證方式",
|
||||
"auth_type_api_key": "Bedrock API 金鑰",
|
||||
"auth_type_help": "選擇使用 IAM 憑證或 Bedrock API 金鑰進行身份驗證",
|
||||
"auth_type_iam": "IAM 憑證",
|
||||
"description": "AWS Bedrock 是亞馬遜提供的全托管基础模型服務,支持多種先進的大語言模型",
|
||||
"region": "AWS 區域",
|
||||
"region_help": "您的 AWS 服務區域,例如 us-east-1",
|
||||
|
||||
@ -4260,6 +4260,12 @@
|
||||
"aws-bedrock": {
|
||||
"access_key_id": "AWS-Zugriffsschlüssel-ID",
|
||||
"access_key_id_help": "Ihre AWS-Zugriffsschlüssel-ID, um auf AWS Bedrock-Dienste zuzugreifen",
|
||||
"api_key": "Bedrock-API-Schlüssel",
|
||||
"api_key_help": "Ihr AWS Bedrock-API-Schlüssel für die Authentifizierung",
|
||||
"auth_type": "Authentifizierungstyp",
|
||||
"auth_type_api_key": "Bedrock-API-Schlüssel",
|
||||
"auth_type_help": "Wählen Sie zwischen IAM-Anmeldeinformationen oder Bedrock-API-Schlüssel-Authentifizierung",
|
||||
"auth_type_iam": "IAM-Anmeldeinformationen",
|
||||
"description": "AWS Bedrock ist ein vollständig verwalteter Basismodell-Dienst von Amazon, der eine Vielzahl moderner großer Sprachmodelle unterstützt",
|
||||
"region": "AWS-Region",
|
||||
"region_help": "Ihre AWS-Serviceregion, z.B. us-east-1",
|
||||
|
||||
@ -4260,6 +4260,12 @@
|
||||
"aws-bedrock": {
|
||||
"access_key_id": "Αναγνωριστικό κλειδιού πρόσβασης AWS",
|
||||
"access_key_id_help": "Το ID του κλειδιού πρόσβασης AWS που χρησιμοποιείται για την πρόσβαση στην υπηρεσία AWS Bedrock",
|
||||
"api_key": "Κλειδί API Bedrock",
|
||||
"api_key_help": "Το κλειδί API του AWS Bedrock για έλεγχο ταυτότητας",
|
||||
"auth_type": "Τύπος Πιστοποίησης",
|
||||
"auth_type_api_key": "Κλειδί API Bedrock",
|
||||
"auth_type_help": "Επιλέξτε μεταξύ πιστοποιητικών IAM ή πιστοποίησης με κλειδί API Bedrock",
|
||||
"auth_type_iam": "Διαπιστευτήρια IAM",
|
||||
"description": "Η AWS Bedrock είναι μια πλήρως διαχειριζόμενη υπηρεσία βασικών μοντέλων που παρέχεται από την Amazon και υποστηρίζει διάφορα προηγμένα μεγάλα γλωσσικά μοντέλα.",
|
||||
"region": "Περιοχές AWS",
|
||||
"region_help": "Η περιοχή υπηρεσίας AWS σας, για παράδειγμα us-east-1",
|
||||
|
||||
@ -4260,6 +4260,12 @@
|
||||
"aws-bedrock": {
|
||||
"access_key_id": "ID de clave de acceso de AWS",
|
||||
"access_key_id_help": "Su ID de clave de acceso de AWS, utilizado para acceder al servicio AWS Bedrock",
|
||||
"api_key": "Clave de API de Bedrock",
|
||||
"api_key_help": "Tu clave de API de AWS Bedrock para autenticación",
|
||||
"auth_type": "Tipo de autenticación",
|
||||
"auth_type_api_key": "Clave de API de Bedrock",
|
||||
"auth_type_help": "Elige entre credenciales IAM o autenticación con clave API de Bedrock",
|
||||
"auth_type_iam": "Credenciales de IAM",
|
||||
"description": "AWS Bedrock es un servicio de modelos fundamentales completamente gestionado proporcionado por Amazon, que admite diversos modelos avanzados de lenguaje de gran tamaño.",
|
||||
"region": "Región de AWS",
|
||||
"region_help": "Su región de servicio AWS, por ejemplo us-east-1",
|
||||
|
||||
@ -4260,6 +4260,12 @@
|
||||
"aws-bedrock": {
|
||||
"access_key_id": "Identifiant de clé d'accès AWS",
|
||||
"access_key_id_help": "Votre identifiant de clé d'accès AWS, utilisé pour accéder au service AWS Bedrock",
|
||||
"api_key": "Clé API Bedrock",
|
||||
"api_key_help": "Votre clé API AWS Bedrock pour l'authentification",
|
||||
"auth_type": "Type d'authentification",
|
||||
"auth_type_api_key": "Clé API Bedrock",
|
||||
"auth_type_help": "Choisissez entre l'authentification par identifiants IAM ou par clé API Bedrock",
|
||||
"auth_type_iam": "Identifiants IAM",
|
||||
"description": "AWS Bedrock est un service de modèles de base entièrement géré proposé par Amazon, prenant en charge divers grands modèles linguistiques avancés.",
|
||||
"region": "Région AWS",
|
||||
"region_help": "Votre région de service AWS, par exemple us-east-1",
|
||||
|
||||
@ -4260,6 +4260,12 @@
|
||||
"aws-bedrock": {
|
||||
"access_key_id": "AWS アクセスキー ID",
|
||||
"access_key_id_help": "あなたの AWS アクセスキー ID は、AWS Bedrock サービスへのアクセスに使用されます",
|
||||
"api_key": "Bedrock APIキー",
|
||||
"api_key_help": "認証用のAWS Bedrock APIキー",
|
||||
"auth_type": "認証タイプ",
|
||||
"auth_type_api_key": "Bedrock APIキー",
|
||||
"auth_type_help": "IAM認証情報とBedrock APIキー認証のどちらかを選択してください",
|
||||
"auth_type_iam": "IAM認証情報",
|
||||
"description": "AWS Bedrock は、Amazon が提供する完全に管理されたベースモデルサービスで、さまざまな最先端の大言語モデルをサポートしています",
|
||||
"region": "AWS リージョン",
|
||||
"region_help": "あなたの AWS サービスリージョン、例:us-east-1",
|
||||
|
||||
@ -4260,6 +4260,12 @@
|
||||
"aws-bedrock": {
|
||||
"access_key_id": "ID da chave de acesso da AWS",
|
||||
"access_key_id_help": "O seu ID da chave de acesso AWS, utilizado para aceder ao serviço AWS Bedrock",
|
||||
"api_key": "Chave de API do Bedrock",
|
||||
"api_key_help": "Sua Chave de API AWS Bedrock para autenticação",
|
||||
"auth_type": "Tipo de Autenticação",
|
||||
"auth_type_api_key": "Chave de API do Bedrock",
|
||||
"auth_type_help": "Escolha entre credenciais IAM ou autenticação por chave de API do Bedrock",
|
||||
"auth_type_iam": "Credenciais IAM",
|
||||
"description": "A AWS Bedrock é um serviço de modelos fundamentais totalmente gerido fornecido pela Amazon, que suporta diversos modelos avançados de linguagem.",
|
||||
"region": "Região da AWS",
|
||||
"region_help": "A sua região de serviço da AWS, por exemplo, us-east-1",
|
||||
|
||||
@ -4260,6 +4260,12 @@
|
||||
"aws-bedrock": {
|
||||
"access_key_id": "AWS Ключ доступа ID",
|
||||
"access_key_id_help": "Ваш AWS Ключ доступа ID для доступа к AWS Bedrock",
|
||||
"api_key": "Ключ API Bedrock",
|
||||
"api_key_help": "Ваш ключ API AWS Bedrock для аутентификации",
|
||||
"auth_type": "Тип аутентификации",
|
||||
"auth_type_api_key": "Ключ API Bedrock",
|
||||
"auth_type_help": "Выберите между аутентификацией с помощью учетных данных IAM или ключа API Bedrock",
|
||||
"auth_type_iam": "Учетные данные IAM",
|
||||
"description": "AWS Bedrock — это полное управляемое сервисное предложение для моделей, поддерживающее различные современные модели языка",
|
||||
"region": "AWS регион",
|
||||
"region_help": "Ваш регион AWS, например us-east-1",
|
||||
|
||||
@ -2,6 +2,7 @@ import type { DropResult } from '@hello-pangea/dnd'
|
||||
import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd'
|
||||
import { loggerService } from '@logger'
|
||||
import { ActionIconButton } from '@renderer/components/Buttons'
|
||||
import { MdiLightbulbOn } from '@renderer/components/Icons'
|
||||
import type { QuickPanelListItem } from '@renderer/components/QuickPanel'
|
||||
import {
|
||||
isAnthropicModel,
|
||||
@ -230,6 +231,15 @@ const InputbarTools = ({
|
||||
quickPhrasesButtonRef.current?.openQuickPanel()
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('assistants.settings.reasoning_effort.label'),
|
||||
description: '',
|
||||
icon: <MdiLightbulbOn />,
|
||||
isMenu: true,
|
||||
action: () => {
|
||||
thinkingButtonRef.current?.openQuickPanel()
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('assistants.presets.edit.model.select.title'),
|
||||
description: '',
|
||||
@ -245,6 +255,7 @@ const InputbarTools = ({
|
||||
icon: <FileSearch />,
|
||||
isMenu: true,
|
||||
disabled: files.length > 0,
|
||||
hidden: !showKnowledgeBaseButton,
|
||||
action: () => {
|
||||
knowledgeBaseButtonRef.current?.openQuickPanel()
|
||||
}
|
||||
@ -312,7 +323,7 @@ const InputbarTools = ({
|
||||
translate()
|
||||
}
|
||||
}
|
||||
]
|
||||
] satisfies QuickPanelListItem[]
|
||||
}
|
||||
|
||||
const handleDragEnd = (result: DropResult) => {
|
||||
|
||||
131
src/renderer/src/pages/home/Tabs/components/shared.tsx
Normal file
131
src/renderer/src/pages/home/Tabs/components/shared.tsx
Normal file
@ -0,0 +1,131 @@
|
||||
import { cn } from '@heroui/react'
|
||||
import type { ComponentPropsWithoutRef, ComponentPropsWithRef } from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
export const ListItem = ({ children, className, ...props }: ComponentPropsWithoutRef<'div'>) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'mb-2 flex w-[calc(var(--assistants-width)-20px)] cursor-pointer flex-col justify-between rounded-lg px-3 py-2 text-sm',
|
||||
'transition-colors duration-100',
|
||||
'hover:bg-[var(--color-list-item-hover)]',
|
||||
'[.active]:bg-[var(--color-list-item)] [.active]:shadow-[0_1px_2px_0_rgba(0,0,0,0.05)]',
|
||||
'[&_.menu]:text-[var(--color-text-3)] [&_.menu]:opacity-0',
|
||||
'hover:[&_.menu]:opacity-100',
|
||||
'[.active]:[&_.menu]:opacity-100 [.active]:[&_.menu]:hover:text-[var(--color-text-2)]',
|
||||
'[.singlealone.active]:border-[var(--color-primary)] [.singlealone.active]:shadow-none [.singlealone]:rounded-none [.singlealone]:border-transparent [.singlealone]:border-l-2 [.singlealone]:hover:bg-[var(--color-background-soft)]',
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export const ListItemNameContainer = ({ children, className, ...props }: ComponentPropsWithoutRef<'div'>) => {
|
||||
return (
|
||||
<div className={cn('flex h-5 flex-row items-center justify-between gap-1', className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// This component involves complex animations and will not be migrated for now.
|
||||
export const ListItemName = styled.div`
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
position: relative;
|
||||
will-change: background-position, width;
|
||||
|
||||
--color-shimmer-mid: var(--color-text-1);
|
||||
--color-shimmer-end: color-mix(in srgb, var(--color-text-1) 25%, transparent);
|
||||
|
||||
&.shimmer {
|
||||
background: linear-gradient(to left, var(--color-shimmer-end), var(--color-shimmer-mid), var(--color-shimmer-end));
|
||||
background-size: 200% 100%;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
animation: shimmer 3s linear infinite;
|
||||
}
|
||||
|
||||
&.typing {
|
||||
display: block;
|
||||
-webkit-line-clamp: unset;
|
||||
-webkit-box-orient: unset;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
animation: typewriter 0.5s steps(40, end);
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes typewriter {
|
||||
from {
|
||||
width: 0;
|
||||
}
|
||||
to {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const ListItemEditInput = ({ className, ...props }: ComponentPropsWithRef<'input'>) => {
|
||||
return (
|
||||
<input
|
||||
className={cn(
|
||||
'w-full border-none bg-[var(--color-background)] p-0 font-inherit text-[var(--color-text-1)] text-sm outline-none',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const ListContainer = ({ children, className, ...props }: ComponentPropsWithoutRef<'div'>) => {
|
||||
return (
|
||||
<div className={cn('flex h-full w-full flex-col p-2', className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const MenuButton = ({ children, className, ...props }: ComponentPropsWithoutRef<'div'>) => {
|
||||
return (
|
||||
<div className={cn('menu', 'flex min-h-5 min-w-5 flex-row items-center justify-center', className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const StatusIndicator = ({ variant }: { variant: 'pending' | 'fulfilled' }) => {
|
||||
const colors = useMemo(() => {
|
||||
switch (variant) {
|
||||
case 'pending':
|
||||
return {
|
||||
wave: 'bg-warning-400',
|
||||
back: 'bg-warning-500'
|
||||
}
|
||||
case 'fulfilled':
|
||||
return {
|
||||
wave: 'bg-success-400',
|
||||
back: 'bg-success-500'
|
||||
}
|
||||
}
|
||||
}, [variant])
|
||||
return (
|
||||
<div className="absolute top-4 left-1 flex size-1">
|
||||
<span className={cn('absolute inline-flex h-full w-full animate-ping rounded-full opacity-75', colors.wave)} />
|
||||
<span className={cn('relative inline-flex size-1 rounded-full bg-warning-500', colors.back)} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -102,7 +102,7 @@ const NewApiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
setPainting(updatedPainting)
|
||||
updatePainting(mode, updatedPainting)
|
||||
},
|
||||
[mode, newApiProvider.id, painting, updatePainting]
|
||||
[painting, newApiProvider.id, mode, updatePainting]
|
||||
)
|
||||
|
||||
// ---------------- Model Related Configurations ----------------
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import { PROVIDER_URLS } from '@renderer/config/providers'
|
||||
import { useAwsBedrockSettings } from '@renderer/hooks/useAwsBedrock'
|
||||
import { Alert, Input } from 'antd'
|
||||
import { Alert, Input, Radio } from 'antd'
|
||||
import type { FC } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -10,14 +10,25 @@ import { SettingHelpLink, SettingHelpText, SettingHelpTextRow, SettingSubtitle }
|
||||
|
||||
const AwsBedrockSettings: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { accessKeyId, secretAccessKey, region, setAccessKeyId, setSecretAccessKey, setRegion } =
|
||||
useAwsBedrockSettings()
|
||||
const {
|
||||
authType,
|
||||
accessKeyId,
|
||||
secretAccessKey,
|
||||
apiKey,
|
||||
region,
|
||||
setAuthType,
|
||||
setAccessKeyId,
|
||||
setSecretAccessKey,
|
||||
setApiKey,
|
||||
setRegion
|
||||
} = useAwsBedrockSettings()
|
||||
|
||||
const providerConfig = PROVIDER_URLS['aws-bedrock']
|
||||
const apiKeyWebsite = providerConfig?.websites?.apiKey
|
||||
|
||||
const [localAccessKeyId, setLocalAccessKeyId] = useState(accessKeyId)
|
||||
const [localSecretAccessKey, setLocalSecretAccessKey] = useState(secretAccessKey)
|
||||
const [localApiKey, setLocalApiKey] = useState(apiKey)
|
||||
const [localRegion, setLocalRegion] = useState(region)
|
||||
|
||||
return (
|
||||
@ -25,39 +36,75 @@ const AwsBedrockSettings: FC = () => {
|
||||
<SettingSubtitle style={{ marginTop: 5 }}>{t('settings.provider.aws-bedrock.title')}</SettingSubtitle>
|
||||
<Alert type="info" style={{ marginTop: 5 }} message={t('settings.provider.aws-bedrock.description')} showIcon />
|
||||
|
||||
<SettingSubtitle style={{ marginTop: 5 }}>{t('settings.provider.aws-bedrock.access_key_id')}</SettingSubtitle>
|
||||
<Input
|
||||
value={localAccessKeyId}
|
||||
placeholder="Access Key ID"
|
||||
onChange={(e) => setLocalAccessKeyId(e.target.value)}
|
||||
onBlur={() => setAccessKeyId(localAccessKeyId)}
|
||||
style={{ marginTop: 5 }}
|
||||
/>
|
||||
{/* Authentication Type Selector */}
|
||||
<SettingSubtitle style={{ marginTop: 15 }}>{t('settings.provider.aws-bedrock.auth_type')}</SettingSubtitle>
|
||||
<Radio.Group value={authType} onChange={(e) => setAuthType(e.target.value)} style={{ marginTop: 5 }}>
|
||||
<Radio value="iam">{t('settings.provider.aws-bedrock.auth_type_iam')}</Radio>
|
||||
<Radio value="apiKey">{t('settings.provider.aws-bedrock.auth_type_api_key')}</Radio>
|
||||
</Radio.Group>
|
||||
<SettingHelpTextRow>
|
||||
<SettingHelpText>{t('settings.provider.aws-bedrock.access_key_id_help')}</SettingHelpText>
|
||||
<SettingHelpText>{t('settings.provider.aws-bedrock.auth_type_help')}</SettingHelpText>
|
||||
</SettingHelpTextRow>
|
||||
|
||||
<SettingSubtitle style={{ marginTop: 5 }}>{t('settings.provider.aws-bedrock.secret_access_key')}</SettingSubtitle>
|
||||
<Input.Password
|
||||
value={localSecretAccessKey}
|
||||
placeholder="Secret Access Key"
|
||||
onChange={(e) => setLocalSecretAccessKey(e.target.value)}
|
||||
onBlur={() => setSecretAccessKey(localSecretAccessKey)}
|
||||
style={{ marginTop: 5 }}
|
||||
spellCheck={false}
|
||||
/>
|
||||
{apiKeyWebsite && (
|
||||
<SettingHelpTextRow style={{ justifyContent: 'space-between' }}>
|
||||
<HStack>
|
||||
<SettingHelpLink target="_blank" href={apiKeyWebsite}>
|
||||
{t('settings.provider.get_api_key')}
|
||||
</SettingHelpLink>
|
||||
</HStack>
|
||||
<SettingHelpText>{t('settings.provider.aws-bedrock.secret_access_key_help')}</SettingHelpText>
|
||||
</SettingHelpTextRow>
|
||||
{/* IAM Credentials Fields */}
|
||||
{authType === 'iam' && (
|
||||
<>
|
||||
<SettingSubtitle style={{ marginTop: 15 }}>
|
||||
{t('settings.provider.aws-bedrock.access_key_id')}
|
||||
</SettingSubtitle>
|
||||
<Input
|
||||
value={localAccessKeyId}
|
||||
placeholder="Access Key ID"
|
||||
onChange={(e) => setLocalAccessKeyId(e.target.value)}
|
||||
onBlur={() => setAccessKeyId(localAccessKeyId)}
|
||||
style={{ marginTop: 5 }}
|
||||
/>
|
||||
<SettingHelpTextRow>
|
||||
<SettingHelpText>{t('settings.provider.aws-bedrock.access_key_id_help')}</SettingHelpText>
|
||||
</SettingHelpTextRow>
|
||||
|
||||
<SettingSubtitle style={{ marginTop: 15 }}>
|
||||
{t('settings.provider.aws-bedrock.secret_access_key')}
|
||||
</SettingSubtitle>
|
||||
<Input.Password
|
||||
value={localSecretAccessKey}
|
||||
placeholder="Secret Access Key"
|
||||
onChange={(e) => setLocalSecretAccessKey(e.target.value)}
|
||||
onBlur={() => setSecretAccessKey(localSecretAccessKey)}
|
||||
style={{ marginTop: 5 }}
|
||||
spellCheck={false}
|
||||
/>
|
||||
{apiKeyWebsite && (
|
||||
<SettingHelpTextRow style={{ justifyContent: 'space-between' }}>
|
||||
<HStack>
|
||||
<SettingHelpLink target="_blank" href={apiKeyWebsite}>
|
||||
{t('settings.provider.get_api_key')}
|
||||
</SettingHelpLink>
|
||||
</HStack>
|
||||
<SettingHelpText>{t('settings.provider.aws-bedrock.secret_access_key_help')}</SettingHelpText>
|
||||
</SettingHelpTextRow>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<SettingSubtitle style={{ marginTop: 5 }}>{t('settings.provider.aws-bedrock.region')}</SettingSubtitle>
|
||||
{authType === 'apiKey' && (
|
||||
<>
|
||||
<SettingSubtitle style={{ marginTop: 15 }}>{t('settings.provider.aws-bedrock.api_key')}</SettingSubtitle>
|
||||
<Input.Password
|
||||
value={localApiKey}
|
||||
placeholder="Bedrock API Key"
|
||||
onChange={(e) => setLocalApiKey(e.target.value)}
|
||||
onBlur={() => setApiKey(localApiKey)}
|
||||
style={{ marginTop: 5 }}
|
||||
spellCheck={false}
|
||||
/>
|
||||
<SettingHelpTextRow>
|
||||
<SettingHelpText>{t('settings.provider.aws-bedrock.api_key_help')}</SettingHelpText>
|
||||
</SettingHelpTextRow>
|
||||
</>
|
||||
)}
|
||||
|
||||
<SettingSubtitle style={{ marginTop: 15 }}>{t('settings.provider.aws-bedrock.region')}</SettingSubtitle>
|
||||
<Input
|
||||
value={localRegion}
|
||||
placeholder="us-east-1"
|
||||
|
||||
@ -230,8 +230,10 @@ vi.mock('@renderer/store/llm.ts', () => {
|
||||
location: ''
|
||||
},
|
||||
awsBedrock: {
|
||||
authType: 'iam',
|
||||
accessKeyId: '',
|
||||
secretAccessKey: '',
|
||||
apiKey: '',
|
||||
region: ''
|
||||
}
|
||||
}
|
||||
|
||||
@ -67,7 +67,7 @@ const persistedReducer = persistReducer(
|
||||
{
|
||||
key: 'cherry-studio',
|
||||
storage,
|
||||
version: 170,
|
||||
version: 171,
|
||||
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs', 'toolPermissions'],
|
||||
migrate
|
||||
},
|
||||
|
||||
@ -3,7 +3,7 @@ import { createSlice } from '@reduxjs/toolkit'
|
||||
import { isLocalAi } from '@renderer/config/env'
|
||||
import { SYSTEM_MODELS } from '@renderer/config/models'
|
||||
import { SYSTEM_PROVIDERS } from '@renderer/config/providers'
|
||||
import type { Model, Provider } from '@renderer/types'
|
||||
import type { AwsBedrockAuthType, Model, Provider } from '@renderer/types'
|
||||
import { uniqBy } from 'lodash'
|
||||
|
||||
type LlmSettings = {
|
||||
@ -25,8 +25,10 @@ type LlmSettings = {
|
||||
location: string
|
||||
}
|
||||
awsBedrock: {
|
||||
authType: AwsBedrockAuthType
|
||||
accessKeyId: string
|
||||
secretAccessKey: string
|
||||
apiKey: string
|
||||
region: string
|
||||
}
|
||||
}
|
||||
@ -68,8 +70,10 @@ export const initialState: LlmState = {
|
||||
location: ''
|
||||
},
|
||||
awsBedrock: {
|
||||
authType: 'iam',
|
||||
accessKeyId: '',
|
||||
secretAccessKey: '',
|
||||
apiKey: '',
|
||||
region: ''
|
||||
}
|
||||
}
|
||||
@ -197,12 +201,18 @@ const llmSlice = createSlice({
|
||||
setVertexAIServiceAccountClientEmail: (state, action: PayloadAction<string>) => {
|
||||
state.settings.vertexai.serviceAccount.clientEmail = action.payload
|
||||
},
|
||||
setAwsBedrockAuthType: (state, action: PayloadAction<AwsBedrockAuthType>) => {
|
||||
state.settings.awsBedrock.authType = action.payload
|
||||
},
|
||||
setAwsBedrockAccessKeyId: (state, action: PayloadAction<string>) => {
|
||||
state.settings.awsBedrock.accessKeyId = action.payload
|
||||
},
|
||||
setAwsBedrockSecretAccessKey: (state, action: PayloadAction<string>) => {
|
||||
state.settings.awsBedrock.secretAccessKey = action.payload
|
||||
},
|
||||
setAwsBedrockApiKey: (state, action: PayloadAction<string>) => {
|
||||
state.settings.awsBedrock.apiKey = action.payload
|
||||
},
|
||||
setAwsBedrockRegion: (state, action: PayloadAction<string>) => {
|
||||
state.settings.awsBedrock.region = action.payload
|
||||
},
|
||||
@ -242,8 +252,10 @@ export const {
|
||||
setVertexAILocation,
|
||||
setVertexAIServiceAccountPrivateKey,
|
||||
setVertexAIServiceAccountClientEmail,
|
||||
setAwsBedrockAuthType,
|
||||
setAwsBedrockAccessKeyId,
|
||||
setAwsBedrockSecretAccessKey,
|
||||
setAwsBedrockApiKey,
|
||||
setAwsBedrockRegion,
|
||||
updateModel
|
||||
} = llmSlice.actions
|
||||
|
||||
@ -2794,6 +2794,29 @@ const migrateConfig = {
|
||||
logger.error('migrate 170 error', error as Error)
|
||||
return state
|
||||
}
|
||||
},
|
||||
'171': (state: RootState) => {
|
||||
try {
|
||||
// Ensure aws-bedrock provider exists
|
||||
addProvider(state, 'aws-bedrock')
|
||||
|
||||
// Ensure awsBedrock settings exist and have all required fields
|
||||
if (!state.llm.settings.awsBedrock) {
|
||||
state.llm.settings.awsBedrock = llmInitialState.settings.awsBedrock
|
||||
} else {
|
||||
// For users who have awsBedrock but missing new fields (authType and apiKey)
|
||||
if (!state.llm.settings.awsBedrock.authType) {
|
||||
state.llm.settings.awsBedrock.authType = 'iam'
|
||||
}
|
||||
if (state.llm.settings.awsBedrock.apiKey === undefined) {
|
||||
state.llm.settings.awsBedrock.apiKey = ''
|
||||
}
|
||||
}
|
||||
return state
|
||||
} catch (error) {
|
||||
logger.error('migrate 171 error', error as Error)
|
||||
return state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -36,6 +36,7 @@ export type Assistant = {
|
||||
description?: string
|
||||
model?: Model
|
||||
defaultModel?: Model
|
||||
// This field should be considered as not Partial and not optional in v2
|
||||
settings?: Partial<AssistantSettings>
|
||||
messages?: AssistantMessage[]
|
||||
/** enableWebSearch 代表使用模型内置网络搜索功能 */
|
||||
|
||||
@ -73,6 +73,17 @@ export function isServiceTier(tier: string): tier is ServiceTier {
|
||||
return isGroqServiceTier(tier) || isOpenAIServiceTier(tier)
|
||||
}
|
||||
|
||||
export const AwsBedrockAuthTypes = {
|
||||
iam: 'iam',
|
||||
apiKey: 'apiKey'
|
||||
} as const
|
||||
|
||||
export type AwsBedrockAuthType = keyof typeof AwsBedrockAuthTypes
|
||||
|
||||
export function isAwsBedrockAuthType(type: string): type is AwsBedrockAuthType {
|
||||
return Object.hasOwn(AwsBedrockAuthTypes, type)
|
||||
}
|
||||
|
||||
export type Provider = {
|
||||
id: string
|
||||
type: ProviderType
|
||||
|
||||
Loading…
Reference in New Issue
Block a user