mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-04 11:49:02 +08:00
feat: support Github Copilot (#2432)
* feat: support Github Copilot * feat: finish i18n translate * fix: add safeStorage * clean code * chore: remove vision model * ✨ feat: add Model Context Protocol (MCP) support (#2809) * ✨ feat: add Model Context Protocol (MCP) server configuration (main) - Added `@modelcontextprotocol/sdk` dependency for MCP integration. - Introduced MCP server configuration UI in settings with add, edit, delete, and activation functionalities. - Created `useMCPServers` hook to manage MCP server state and actions. - Added i18n support for MCP settings with translation keys. - Integrated MCP settings into the application's settings navigation and routing. - Implemented Redux state management for MCP servers. - Updated `yarn.lock` with new dependencies and their resolutions. * 🌟 feat: implement mcp service and integrate with ipc handlers - Added `MCPService` class to manage Model Context Protocol servers. - Implemented various handlers in `ipc.ts` for managing MCP servers including listing, adding, updating, deleting, and activating/deactivating servers. - Integrated MCP related types into existing type declarations for consistency across the application. - Updated `preload` to expose new MCP related APIs to the renderer process. - Enhanced `MCPSettings` component to interact directly with the new MCP service for adding, updating, deleting servers and setting their active states. - Introduced selectors in the MCP Redux slice for fetching active and all servers from the store. - Moved MCP types to a centralized location in `@renderer/types` for reuse across different parts of the application. * feat: enhance MCPService initialization to prevent recursive calls and improve error handling * feat: enhance MCP integration by adding MCPTool type and updating related methods * feat: implement streaming support for tool calls in OpenAIProvider and enhance message processing * fix: finish_reason undefined * fix migrate * feat: add rate limit and warning * feat: add delete copilot token file feat: add login message feat: add default headers and change getCopilotToken algorithm * fix * feat: add rate limit * chore: change apihost * fix: remove duplicate apikey * fix: change api host * chore: add vertify first tooltip --------- Co-authored-by: 亢奋猫 <kangfenmao@qq.com> Co-authored-by: LiuVaayne <10231735+vaayne@users.noreply.github.com>
This commit is contained in:
parent
f9f2586dc4
commit
0ddcecabdf
@ -9,6 +9,7 @@ import { titleBarOverlayDark, titleBarOverlayLight } from './config'
|
|||||||
import AppUpdater from './services/AppUpdater'
|
import AppUpdater from './services/AppUpdater'
|
||||||
import BackupManager from './services/BackupManager'
|
import BackupManager from './services/BackupManager'
|
||||||
import { configManager } from './services/ConfigManager'
|
import { configManager } from './services/ConfigManager'
|
||||||
|
import CopilotService from './services/CopilotService'
|
||||||
import { ExportService } from './services/ExportService'
|
import { ExportService } from './services/ExportService'
|
||||||
import FileService from './services/FileService'
|
import FileService from './services/FileService'
|
||||||
import FileStorage from './services/FileStorage'
|
import FileStorage from './services/FileStorage'
|
||||||
@ -252,6 +253,13 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
|||||||
mainWindow?.webContents.send('mcp:servers-updated', servers)
|
mainWindow?.webContents.send('mcp:servers-updated', servers)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Clean up MCP services when app quits
|
|
||||||
app.on('before-quit', () => mcpService.cleanup())
|
app.on('before-quit', () => mcpService.cleanup())
|
||||||
|
|
||||||
|
//copilot
|
||||||
|
ipcMain.handle('copilot:get-auth-message', CopilotService.getAuthMessage)
|
||||||
|
ipcMain.handle('copilot:get-copilot-token', CopilotService.getCopilotToken)
|
||||||
|
ipcMain.handle('copilot:save-copilot-token', CopilotService.saveCopilotToken)
|
||||||
|
ipcMain.handle('copilot:get-token', CopilotService.getToken)
|
||||||
|
ipcMain.handle('copilot:logout', CopilotService.logout)
|
||||||
|
ipcMain.handle('copilot:get-user', CopilotService.getUser)
|
||||||
}
|
}
|
||||||
|
|||||||
247
src/main/services/CopilotService.ts
Normal file
247
src/main/services/CopilotService.ts
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
import axios, { AxiosRequestConfig } from 'axios'
|
||||||
|
import { app, safeStorage } from 'electron'
|
||||||
|
import fs from 'fs/promises'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
// 配置常量,集中管理
|
||||||
|
const CONFIG = {
|
||||||
|
GITHUB_CLIENT_ID: 'Iv1.b507a08c87ecfe98',
|
||||||
|
POLLING: {
|
||||||
|
MAX_ATTEMPTS: 8,
|
||||||
|
INITIAL_DELAY_MS: 1000,
|
||||||
|
MAX_DELAY_MS: 16000 // 最大延迟16秒
|
||||||
|
},
|
||||||
|
DEFAULT_HEADERS: {
|
||||||
|
accept: 'application/json',
|
||||||
|
'editor-version': 'Neovim/0.6.1',
|
||||||
|
'editor-plugin-version': 'copilot.vim/1.16.0',
|
||||||
|
'content-type': 'application/json',
|
||||||
|
'user-agent': 'GithubCopilot/1.155.0',
|
||||||
|
'accept-encoding': 'gzip,deflate,br'
|
||||||
|
},
|
||||||
|
// API端点集中管理
|
||||||
|
API_URLS: {
|
||||||
|
GITHUB_USER: 'https://api.github.com/user',
|
||||||
|
GITHUB_DEVICE_CODE: 'https://github.com/login/device/code',
|
||||||
|
GITHUB_ACCESS_TOKEN: 'https://github.com/login/oauth/access_token',
|
||||||
|
COPILOT_TOKEN: 'https://api.github.com/copilot_internal/v2/token'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 接口定义移到顶部,便于查阅
|
||||||
|
interface UserResponse {
|
||||||
|
login: string
|
||||||
|
avatar: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AuthResponse {
|
||||||
|
device_code: string
|
||||||
|
user_code: string
|
||||||
|
verification_uri: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TokenResponse {
|
||||||
|
access_token: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CopilotTokenResponse {
|
||||||
|
token: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自定义错误类,统一错误处理
|
||||||
|
class CopilotServiceError extends Error {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
public readonly cause?: unknown
|
||||||
|
) {
|
||||||
|
super(message)
|
||||||
|
this.name = 'CopilotServiceError'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CopilotService {
|
||||||
|
private readonly tokenFilePath: string
|
||||||
|
private headers: Record<string, string>
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.tokenFilePath = path.join(app.getPath('userData'), '.copilot_token')
|
||||||
|
this.headers = { ...CONFIG.DEFAULT_HEADERS }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置自定义请求头
|
||||||
|
*/
|
||||||
|
private updateHeaders = (headers?: Record<string, string>): void => {
|
||||||
|
if (headers && Object.keys(headers).length > 0) {
|
||||||
|
this.headers = { ...headers }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取GitHub登录信息
|
||||||
|
*/
|
||||||
|
public getUser = async (_: Electron.IpcMainInvokeEvent, token: string): Promise<UserResponse> => {
|
||||||
|
try {
|
||||||
|
const config: AxiosRequestConfig = {
|
||||||
|
headers: {
|
||||||
|
Connection: 'keep-alive',
|
||||||
|
'user-agent': 'Visual Studio Code (desktop)',
|
||||||
|
'Sec-Fetch-Site': 'none',
|
||||||
|
'Sec-Fetch-Mode': 'no-cors',
|
||||||
|
'Sec-Fetch-Dest': 'empty',
|
||||||
|
authorization: `token ${token}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await axios.get(CONFIG.API_URLS.GITHUB_USER, config)
|
||||||
|
return {
|
||||||
|
login: response.data.login,
|
||||||
|
avatar: response.data.avatar_url
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get user information:', error)
|
||||||
|
throw new CopilotServiceError('无法获取GitHub用户信息', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取GitHub设备授权信息
|
||||||
|
*/
|
||||||
|
public getAuthMessage = async (
|
||||||
|
_: Electron.IpcMainInvokeEvent,
|
||||||
|
headers?: Record<string, string>
|
||||||
|
): Promise<AuthResponse> => {
|
||||||
|
try {
|
||||||
|
this.updateHeaders(headers)
|
||||||
|
|
||||||
|
const response = await axios.post<AuthResponse>(
|
||||||
|
CONFIG.API_URLS.GITHUB_DEVICE_CODE,
|
||||||
|
{
|
||||||
|
client_id: CONFIG.GITHUB_CLIENT_ID,
|
||||||
|
scope: 'read:user'
|
||||||
|
},
|
||||||
|
{ headers: this.headers }
|
||||||
|
)
|
||||||
|
|
||||||
|
return response.data
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get auth message:', error)
|
||||||
|
throw new CopilotServiceError('无法获取GitHub授权信息', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用设备码获取访问令牌 - 优化轮询逻辑
|
||||||
|
*/
|
||||||
|
public getCopilotToken = async (
|
||||||
|
_: Electron.IpcMainInvokeEvent,
|
||||||
|
device_code: string,
|
||||||
|
headers?: Record<string, string>
|
||||||
|
): Promise<TokenResponse> => {
|
||||||
|
this.updateHeaders(headers)
|
||||||
|
|
||||||
|
let currentDelay = CONFIG.POLLING.INITIAL_DELAY_MS
|
||||||
|
|
||||||
|
for (let attempt = 0; attempt < CONFIG.POLLING.MAX_ATTEMPTS; attempt++) {
|
||||||
|
await this.delay(currentDelay)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post<TokenResponse>(
|
||||||
|
CONFIG.API_URLS.GITHUB_ACCESS_TOKEN,
|
||||||
|
{
|
||||||
|
client_id: CONFIG.GITHUB_CLIENT_ID,
|
||||||
|
device_code,
|
||||||
|
grant_type: 'urn:ietf:params:oauth:grant-type:device_code'
|
||||||
|
},
|
||||||
|
{ headers: this.headers }
|
||||||
|
)
|
||||||
|
|
||||||
|
const { access_token } = response.data
|
||||||
|
if (access_token) {
|
||||||
|
return { access_token }
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 指数退避策略
|
||||||
|
currentDelay = Math.min(currentDelay * 2, CONFIG.POLLING.MAX_DELAY_MS)
|
||||||
|
|
||||||
|
// 仅在最后一次尝试失败时记录详细错误
|
||||||
|
const isLastAttempt = attempt === CONFIG.POLLING.MAX_ATTEMPTS - 1
|
||||||
|
if (isLastAttempt) {
|
||||||
|
console.error(`Token polling failed after ${CONFIG.POLLING.MAX_ATTEMPTS} attempts:`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new CopilotServiceError('获取访问令牌超时,请重试')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存Copilot令牌到本地文件
|
||||||
|
*/
|
||||||
|
public saveCopilotToken = async (_: Electron.IpcMainInvokeEvent, token: string): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const encryptedToken = safeStorage.encryptString(token)
|
||||||
|
await fs.writeFile(this.tokenFilePath, encryptedToken)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save token:', error)
|
||||||
|
throw new CopilotServiceError('无法保存访问令牌', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从本地文件读取令牌并获取Copilot令牌
|
||||||
|
*/
|
||||||
|
public getToken = async (
|
||||||
|
_: Electron.IpcMainInvokeEvent,
|
||||||
|
headers?: Record<string, string>
|
||||||
|
): Promise<CopilotTokenResponse> => {
|
||||||
|
try {
|
||||||
|
this.updateHeaders(headers)
|
||||||
|
|
||||||
|
const encryptedToken = await fs.readFile(this.tokenFilePath)
|
||||||
|
const access_token = safeStorage.decryptString(Buffer.from(encryptedToken))
|
||||||
|
|
||||||
|
const config: AxiosRequestConfig = {
|
||||||
|
headers: {
|
||||||
|
...this.headers,
|
||||||
|
authorization: `token ${access_token}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await axios.get<CopilotTokenResponse>(CONFIG.API_URLS.COPILOT_TOKEN, config)
|
||||||
|
|
||||||
|
return response.data
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get Copilot token:', error)
|
||||||
|
throw new CopilotServiceError('无法获取Copilot令牌,请重新授权', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退出登录,删除本地token文件
|
||||||
|
*/
|
||||||
|
public logout = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
try {
|
||||||
|
await fs.access(this.tokenFilePath)
|
||||||
|
await fs.unlink(this.tokenFilePath)
|
||||||
|
console.log('Successfully logged out from Copilot')
|
||||||
|
} catch (error) {
|
||||||
|
// 文件不存在不是错误,只是记录一下
|
||||||
|
console.log('Token file not found, nothing to delete')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to logout:', error)
|
||||||
|
throw new CopilotServiceError('无法完成退出登录操作', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 辅助方法:延迟执行
|
||||||
|
*/
|
||||||
|
private delay = (ms: number): Promise<void> => {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new CopilotService()
|
||||||
10
src/preload/index.d.ts
vendored
10
src/preload/index.d.ts
vendored
@ -144,6 +144,16 @@ declare global {
|
|||||||
// status
|
// status
|
||||||
cleanup: () => Promise<void>
|
cleanup: () => Promise<void>
|
||||||
}
|
}
|
||||||
|
copilot: {
|
||||||
|
getAuthMessage: (
|
||||||
|
headers?: Record<string, string>
|
||||||
|
) => Promise<{ device_code: string; user_code: string; verification_uri: string }>
|
||||||
|
getCopilotToken: (device_code: string, headers?: Record<string, string>) => Promise<{ access_token: string }>
|
||||||
|
saveCopilotToken: (access_token: string) => Promise<void>
|
||||||
|
getToken: (headers?: Record<string, string>) => Promise<{ token: string }>
|
||||||
|
logout: () => Promise<void>
|
||||||
|
getUser: (token: string) => Promise<{ login: string; avatar: string }>
|
||||||
|
}
|
||||||
isBinaryExist: (name: string) => Promise<boolean>
|
isBinaryExist: (name: string) => Promise<boolean>
|
||||||
getBinaryPath: (name: string) => Promise<string>
|
getBinaryPath: (name: string) => Promise<string>
|
||||||
installUVBinary: () => Promise<void>
|
installUVBinary: () => Promise<void>
|
||||||
|
|||||||
@ -121,7 +121,16 @@ const api = {
|
|||||||
cleanup: () => ipcRenderer.invoke('mcp:cleanup')
|
cleanup: () => ipcRenderer.invoke('mcp:cleanup')
|
||||||
},
|
},
|
||||||
shell: {
|
shell: {
|
||||||
openExternal: shell?.openExternal
|
openExternal: shell.openExternal
|
||||||
|
},
|
||||||
|
copilot: {
|
||||||
|
getAuthMessage: (headers?: Record<string, string>) => ipcRenderer.invoke('copilot:get-auth-message', headers),
|
||||||
|
getCopilotToken: (device_code: string, headers?: Record<string, string>) =>
|
||||||
|
ipcRenderer.invoke('copilot:get-copilot-token', device_code, headers),
|
||||||
|
saveCopilotToken: (access_token: string) => ipcRenderer.invoke('copilot:save-copilot-token', access_token),
|
||||||
|
getToken: (headers?: Record<string, string>) => ipcRenderer.invoke('copilot:get-token', headers),
|
||||||
|
logout: () => ipcRenderer.invoke('copilot:logout'),
|
||||||
|
getUser: (token: string) => ipcRenderer.invoke('copilot:get-user', token)
|
||||||
},
|
},
|
||||||
|
|
||||||
// Binary related APIs
|
// Binary related APIs
|
||||||
|
|||||||
@ -1035,6 +1035,14 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
|
|||||||
group: 'OpenAI'
|
group: 'OpenAI'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
copilot: [
|
||||||
|
{
|
||||||
|
id: 'gpt-4o-mini',
|
||||||
|
provider: 'copilot',
|
||||||
|
name: 'OpenAI GPT-4o-mini',
|
||||||
|
group: 'OpenAI'
|
||||||
|
}
|
||||||
|
],
|
||||||
yi: [
|
yi: [
|
||||||
{ id: 'yi-lightning', name: 'Yi Lightning', provider: 'yi', group: 'yi-lightning', owned_by: '01.ai' },
|
{ id: 'yi-lightning', name: 'Yi Lightning', provider: 'yi', group: 'yi-lightning', owned_by: '01.ai' },
|
||||||
{ id: 'yi-vision-v2', name: 'Yi Vision v2', provider: 'yi', group: 'yi-vision', owned_by: '01.ai' }
|
{ id: 'yi-vision-v2', name: 'Yi Vision v2', provider: 'yi', group: 'yi-vision', owned_by: '01.ai' }
|
||||||
@ -1895,6 +1903,9 @@ export function isVisionModel(model: Model): boolean {
|
|||||||
if (!model) {
|
if (!model) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if (model.provider === 'copilot') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
if (model.provider === 'doubao') {
|
if (model.provider === 'doubao') {
|
||||||
return VISION_REGEX.test(model.name) || model.type?.includes('vision') || false
|
return VISION_REGEX.test(model.name) || model.type?.includes('vision') || false
|
||||||
|
|||||||
@ -66,6 +66,7 @@ const PROVIDER_LOGO_MAP = {
|
|||||||
'graphrag-kylin-mountain': GraphRagProviderLogo,
|
'graphrag-kylin-mountain': GraphRagProviderLogo,
|
||||||
minimax: MinimaxProviderLogo,
|
minimax: MinimaxProviderLogo,
|
||||||
github: GithubProviderLogo,
|
github: GithubProviderLogo,
|
||||||
|
copilot: GithubProviderLogo,
|
||||||
ocoolai: OcoolAiProviderLogo,
|
ocoolai: OcoolAiProviderLogo,
|
||||||
together: TogetherProviderLogo,
|
together: TogetherProviderLogo,
|
||||||
fireworks: FireworksProviderLogo,
|
fireworks: FireworksProviderLogo,
|
||||||
@ -238,6 +239,11 @@ export const PROVIDER_CONFIG = {
|
|||||||
models: 'https://github.com/marketplace/models'
|
models: 'https://github.com/marketplace/models'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
copilot: {
|
||||||
|
api: {
|
||||||
|
url: 'https://api.githubcopilot.com/'
|
||||||
|
}
|
||||||
|
},
|
||||||
yi: {
|
yi: {
|
||||||
api: {
|
api: {
|
||||||
url: 'https://api.lingyiwanwu.com'
|
url: 'https://api.lingyiwanwu.com'
|
||||||
|
|||||||
52
src/renderer/src/hooks/useCopilot.ts
Normal file
52
src/renderer/src/hooks/useCopilot.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
|
|
||||||
|
import type { RootState } from '../store'
|
||||||
|
import {
|
||||||
|
type CopilotState,
|
||||||
|
resetCopilotState,
|
||||||
|
setAvatar,
|
||||||
|
setDefaultHeaders,
|
||||||
|
setUsername,
|
||||||
|
updateCopilotState
|
||||||
|
} from '../store/copilot'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用于访问和操作Copilot相关状态的钩子函数
|
||||||
|
* @returns Copilot状态和操作方法
|
||||||
|
*/
|
||||||
|
export function useCopilot() {
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
const copilotState = useSelector((state: RootState) => state.copilot)
|
||||||
|
|
||||||
|
const updateUsername = (username: string) => {
|
||||||
|
dispatch(setUsername(username))
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateAvatar = (avatar: string) => {
|
||||||
|
dispatch(setAvatar(avatar))
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateDefaultHeaders = (headers: Record<string, string>) => {
|
||||||
|
dispatch(setDefaultHeaders(headers))
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateState = (state: Partial<CopilotState>) => {
|
||||||
|
dispatch(updateCopilotState(state))
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetState = () => {
|
||||||
|
dispatch(resetCopilotState())
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// 当前状态
|
||||||
|
...copilotState,
|
||||||
|
|
||||||
|
// 状态更新方法
|
||||||
|
updateUsername,
|
||||||
|
updateAvatar,
|
||||||
|
updateDefaultHeaders,
|
||||||
|
updateState,
|
||||||
|
resetState
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -233,6 +233,8 @@
|
|||||||
"topics": "Topics",
|
"topics": "Topics",
|
||||||
"warning": "Warning",
|
"warning": "Warning",
|
||||||
"you": "You",
|
"you": "You",
|
||||||
|
"copied": "Copied",
|
||||||
|
"confirm": "Confirm",
|
||||||
"more": "More"
|
"more": "More"
|
||||||
},
|
},
|
||||||
"docs": {
|
"docs": {
|
||||||
@ -475,7 +477,8 @@
|
|||||||
"upgrade.success.button": "Restart",
|
"upgrade.success.button": "Restart",
|
||||||
"upgrade.success.content": "Please restart the application to complete the upgrade",
|
"upgrade.success.content": "Please restart the application to complete the upgrade",
|
||||||
"upgrade.success.title": "Upgrade successfully",
|
"upgrade.success.title": "Upgrade successfully",
|
||||||
"warn.notion.exporting": "Exporting to Notion, please do not request export repeatedly!"
|
"warn.notion.exporting": "Exporting to Notion, please do not request export repeatedly!",
|
||||||
|
"warning.rate.limit": "Too many requests. Please wait {{seconds}} seconds before trying again."
|
||||||
},
|
},
|
||||||
"minapp": {
|
"minapp": {
|
||||||
"sidebar.add.title": "Add to sidebar",
|
"sidebar.add.title": "Add to sidebar",
|
||||||
@ -639,6 +642,7 @@
|
|||||||
"yi": "Yi",
|
"yi": "Yi",
|
||||||
"zhinao": "360AI",
|
"zhinao": "360AI",
|
||||||
"zhipu": "ZHIPU AI",
|
"zhipu": "ZHIPU AI",
|
||||||
|
"copilot": "GitHub Copilot",
|
||||||
"gpustack": "GPUStack",
|
"gpustack": "GPUStack",
|
||||||
"alayanew": "Alaya NeW"
|
"alayanew": "Alaya NeW"
|
||||||
},
|
},
|
||||||
@ -976,7 +980,31 @@
|
|||||||
"remove_invalid_keys": "Remove Invalid Keys",
|
"remove_invalid_keys": "Remove Invalid Keys",
|
||||||
"search": "Search Providers...",
|
"search": "Search Providers...",
|
||||||
"search_placeholder": "Search model id or name",
|
"search_placeholder": "Search model id or name",
|
||||||
"title": "Model Provider"
|
"title": "Model Provider",
|
||||||
|
"copilot": {
|
||||||
|
"tooltip": "You need to log in to Github before using Github Copilot in Cherry Studio.",
|
||||||
|
"description": "Your GitHub account needs to subscribe to Copilot.",
|
||||||
|
"login": "Log in to Github",
|
||||||
|
"connect": "Connect to Github",
|
||||||
|
"logout": "Exit GitHub",
|
||||||
|
"auth_success_title": "Certification successful.",
|
||||||
|
"code_generated_title": "Obtain Device Code",
|
||||||
|
"code_generated_desc": "Please copy the device code into the browser link below.",
|
||||||
|
"code_failed": "Failed to obtain Device Code, please try again.",
|
||||||
|
"auth_success": "GitHub Copilot authentication successful.",
|
||||||
|
"auth_failed": "Github Copilot authentication failed.",
|
||||||
|
"logout_success": "Successfully logged out.",
|
||||||
|
"logout_failed": "Exit failed, please try again.",
|
||||||
|
"confirm_title": "Risk Warning",
|
||||||
|
"confirm_login": "Excessive use may lead to your Github account being banned, please use it cautiously!!!!",
|
||||||
|
"rate_limit": "Rate limiting",
|
||||||
|
"custom_headers": "Custom request header",
|
||||||
|
"headers_description": "Custom request headers (JSON format)",
|
||||||
|
"expand": "Expand",
|
||||||
|
"model_setting": "Model settings",
|
||||||
|
"invalid_json": "JSON format error",
|
||||||
|
"open_verification_first": "Please click the link above to access the verification page."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"proxy": {
|
"proxy": {
|
||||||
"mode": {
|
"mode": {
|
||||||
|
|||||||
@ -233,6 +233,8 @@
|
|||||||
"topics": "トピック",
|
"topics": "トピック",
|
||||||
"warning": "警告",
|
"warning": "警告",
|
||||||
"you": "あなた",
|
"you": "あなた",
|
||||||
|
"copied": "コピーされました",
|
||||||
|
"confirm": "確認",
|
||||||
"more": "もっと"
|
"more": "もっと"
|
||||||
},
|
},
|
||||||
"docs": {
|
"docs": {
|
||||||
@ -475,7 +477,8 @@
|
|||||||
"upgrade.success.button": "再起動",
|
"upgrade.success.button": "再起動",
|
||||||
"upgrade.success.content": "アップグレードを完了するためにアプリケーションを再起動してください",
|
"upgrade.success.content": "アップグレードを完了するためにアプリケーションを再起動してください",
|
||||||
"upgrade.success.title": "アップグレードに成功しました",
|
"upgrade.success.title": "アップグレードに成功しました",
|
||||||
"warn.notion.exporting": "Notionにエクスポート中です。重複してエクスポートしないでください! "
|
"warn.notion.exporting": "Notionにエクスポート中です。重複してエクスポートしないでください! ",
|
||||||
|
"warning.rate.limit": "送信が頻繁すぎます。{{seconds}} 秒待ってから再試行してください。"
|
||||||
},
|
},
|
||||||
"minapp": {
|
"minapp": {
|
||||||
"sidebar.add.title": "サイドバーに追加",
|
"sidebar.add.title": "サイドバーに追加",
|
||||||
@ -639,6 +642,7 @@
|
|||||||
"yi": "零一万物",
|
"yi": "零一万物",
|
||||||
"zhinao": "360智脳",
|
"zhinao": "360智脳",
|
||||||
"zhipu": "智譜AI",
|
"zhipu": "智譜AI",
|
||||||
|
"copilot": "GitHub Copilot",
|
||||||
"gpustack": "GPUStack",
|
"gpustack": "GPUStack",
|
||||||
"alayanew": "Alaya NeW"
|
"alayanew": "Alaya NeW"
|
||||||
},
|
},
|
||||||
@ -976,7 +980,31 @@
|
|||||||
"remove_invalid_keys": "無効なキーを削除",
|
"remove_invalid_keys": "無効なキーを削除",
|
||||||
"search": "プロバイダーを検索...",
|
"search": "プロバイダーを検索...",
|
||||||
"search_placeholder": "モデルIDまたは名前を検索",
|
"search_placeholder": "モデルIDまたは名前を検索",
|
||||||
"title": "モデルプロバイダー"
|
"title": "モデルプロバイダー",
|
||||||
|
"copilot": {
|
||||||
|
"tooltip": "Cherry StudioでGithub Copilotを使用するには、まずGithubにログインする必要があります。",
|
||||||
|
"description": "あなたのGithubアカウントはCopilotを購読する必要があります。",
|
||||||
|
"login": "GitHubにログインする",
|
||||||
|
"connect": "GitHubに接続する",
|
||||||
|
"logout": "GitHubから退出する",
|
||||||
|
"auth_success_title": "認証成功",
|
||||||
|
"code_generated_title": "デバイスコードを取得する",
|
||||||
|
"code_generated_desc": "デバイスコードを下記のブラウザリンクにコピーしてください。",
|
||||||
|
"code_failed": "デバイスコードの取得に失敗しました。再試行してください。",
|
||||||
|
"auth_success": "Github Copilotの認証が成功しました",
|
||||||
|
"auth_failed": "Github Copilotの認証に失敗しました。",
|
||||||
|
"logout_success": "正常にログアウトしました。",
|
||||||
|
"logout_failed": "ログアウトに失敗しました。もう一度お試しください。",
|
||||||
|
"confirm_title": "リスク警告",
|
||||||
|
"confirm_login": "過度使用すると、あなたのGithubアカウントが停止される可能性があるため、慎重に使用してください!!!!",
|
||||||
|
"rate_limit": "レート制限",
|
||||||
|
"custom_headers": "カスタムリクエストヘッダー",
|
||||||
|
"headers_description": "カスタムリクエストヘッダー(JSONフォーマット)",
|
||||||
|
"expand": "展開",
|
||||||
|
"model_setting": "モデル設定",
|
||||||
|
"invalid_json": "JSONフォーマットエラー",
|
||||||
|
"open_verification_first": "上のリンクをクリックして、確認ページにアクセスしてください。"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"proxy": {
|
"proxy": {
|
||||||
"mode": {
|
"mode": {
|
||||||
|
|||||||
@ -233,6 +233,8 @@
|
|||||||
"topics": "Топики",
|
"topics": "Топики",
|
||||||
"warning": "Предупреждение",
|
"warning": "Предупреждение",
|
||||||
"you": "Вы",
|
"you": "Вы",
|
||||||
|
"confirm": "确认的翻译是: Подтверждение",
|
||||||
|
"copied": "Скопировано",
|
||||||
"more": "Ещё"
|
"more": "Ещё"
|
||||||
},
|
},
|
||||||
"docs": {
|
"docs": {
|
||||||
@ -481,7 +483,8 @@
|
|||||||
"upgrade.success.button": "Перезапустить",
|
"upgrade.success.button": "Перезапустить",
|
||||||
"upgrade.success.content": "Пожалуйста, перезапустите приложение для завершения обновления",
|
"upgrade.success.content": "Пожалуйста, перезапустите приложение для завершения обновления",
|
||||||
"upgrade.success.title": "Обновление успешно",
|
"upgrade.success.title": "Обновление успешно",
|
||||||
"warn.notion.exporting": "Экспортируется в Notion, пожалуйста, не отправляйте повторные запросы!"
|
"warn.notion.exporting": "Экспортируется в Notion, пожалуйста, не отправляйте повторные запросы!",
|
||||||
|
"warning.rate.limit": "Отправка слишком частая, пожалуйста, подождите {{seconds}} секунд, прежде чем попробовать снова."
|
||||||
},
|
},
|
||||||
"minapp": {
|
"minapp": {
|
||||||
"sidebar.add.title": "Добавить в боковую панель",
|
"sidebar.add.title": "Добавить в боковую панель",
|
||||||
@ -639,6 +642,7 @@
|
|||||||
"yi": "Yi",
|
"yi": "Yi",
|
||||||
"zhinao": "360AI",
|
"zhinao": "360AI",
|
||||||
"zhipu": "ZHIPU AI",
|
"zhipu": "ZHIPU AI",
|
||||||
|
"copilot": "GitHub Copilot",
|
||||||
"gpustack": "GPUStack",
|
"gpustack": "GPUStack",
|
||||||
"alayanew": "Alaya NeW"
|
"alayanew": "Alaya NeW"
|
||||||
},
|
},
|
||||||
@ -976,7 +980,31 @@
|
|||||||
"remove_invalid_keys": "Удалить недействительные ключи",
|
"remove_invalid_keys": "Удалить недействительные ключи",
|
||||||
"search": "Поиск поставщиков...",
|
"search": "Поиск поставщиков...",
|
||||||
"search_placeholder": "Поиск по ID или имени модели",
|
"search_placeholder": "Поиск по ID или имени модели",
|
||||||
"title": "Провайдеры моделей"
|
"title": "Провайдеры моделей",
|
||||||
|
"copilot": {
|
||||||
|
"tooltip": "В Cherry Studio для использования Github Copilot необходимо сначала войти в Github.",
|
||||||
|
"description": "Ваша учетная запись Github должна подписаться на Copilot.",
|
||||||
|
"login": "Войти в Github",
|
||||||
|
"connect": "Подключить Github",
|
||||||
|
"logout": "Выйти из Github",
|
||||||
|
"auth_success_title": "Аутентификация успешна",
|
||||||
|
"code_generated_title": "Получить код устройства",
|
||||||
|
"code_generated_desc": "Пожалуйста, скопируйте код устройства в приведенную ниже ссылку браузера.",
|
||||||
|
"code_failed": "Получение кода устройства не удалось, пожалуйста, попробуйте еще раз.",
|
||||||
|
"auth_success": "Github Copilot认证成功",
|
||||||
|
"auth_failed": "Github Copilot认证失败",
|
||||||
|
"logout_success": "Успешно вышел",
|
||||||
|
"logout_failed": "Не удалось выйти, пожалуйста, повторите попытку.",
|
||||||
|
"confirm_title": "Предупреждение о рисках",
|
||||||
|
"confirm_login": "Чрезмерное использование может привести к блокировке вашего Github, будьте осторожны!!!!",
|
||||||
|
"rate_limit": "Ограничение скорости",
|
||||||
|
"custom_headers": "Пользовательские заголовки запроса",
|
||||||
|
"headers_description": "Пользовательские заголовки запроса (формат json)",
|
||||||
|
"expand": "развернуть",
|
||||||
|
"model_setting": "Настройки модели",
|
||||||
|
"invalid_json": "Ошибка формата JSON",
|
||||||
|
"open_verification_first": "Пожалуйста, сначала щелкните по ссылке выше, чтобы перейти на страницу проверки."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"proxy": {
|
"proxy": {
|
||||||
"mode": {
|
"mode": {
|
||||||
|
|||||||
@ -204,7 +204,9 @@
|
|||||||
"chat": "聊天",
|
"chat": "聊天",
|
||||||
"clear": "清除",
|
"clear": "清除",
|
||||||
"close": "关闭",
|
"close": "关闭",
|
||||||
|
"confirm": "确认",
|
||||||
"copy": "复制",
|
"copy": "复制",
|
||||||
|
"copied": "已复制",
|
||||||
"cut": "剪切",
|
"cut": "剪切",
|
||||||
"default": "默认",
|
"default": "默认",
|
||||||
"delete": "删除",
|
"delete": "删除",
|
||||||
@ -475,7 +477,8 @@
|
|||||||
"upgrade.success.button": "重启",
|
"upgrade.success.button": "重启",
|
||||||
"upgrade.success.content": "重启用以完成升级",
|
"upgrade.success.content": "重启用以完成升级",
|
||||||
"upgrade.success.title": "升级成功",
|
"upgrade.success.title": "升级成功",
|
||||||
"warn.notion.exporting": "正在导出到 Notion, 请勿重复请求导出!"
|
"warn.notion.exporting": "正在导出到 Notion, 请勿重复请求导出!",
|
||||||
|
"warning.rate.limit": "发送过于频繁,请等待 {{seconds}} 秒后再尝试"
|
||||||
},
|
},
|
||||||
"minapp": {
|
"minapp": {
|
||||||
"sidebar.add.title": "添加到侧边栏",
|
"sidebar.add.title": "添加到侧边栏",
|
||||||
@ -633,6 +636,7 @@
|
|||||||
"yi": "零一万物",
|
"yi": "零一万物",
|
||||||
"zhinao": "360智脑",
|
"zhinao": "360智脑",
|
||||||
"zhipu": "智谱AI",
|
"zhipu": "智谱AI",
|
||||||
|
"copilot": "GitHub Copilot",
|
||||||
"gpustack": "GPUStack",
|
"gpustack": "GPUStack",
|
||||||
"alayanew": "Alaya NeW"
|
"alayanew": "Alaya NeW"
|
||||||
},
|
},
|
||||||
@ -976,7 +980,31 @@
|
|||||||
"remove_invalid_keys": "删除无效密钥",
|
"remove_invalid_keys": "删除无效密钥",
|
||||||
"search": "搜索模型平台...",
|
"search": "搜索模型平台...",
|
||||||
"search_placeholder": "搜索模型 ID 或名称",
|
"search_placeholder": "搜索模型 ID 或名称",
|
||||||
"title": "模型服务"
|
"title": "模型服务",
|
||||||
|
"copilot": {
|
||||||
|
"tooltip": "在Cherry Studio中使用Github Copilot需要先登录Github",
|
||||||
|
"description": "您的Github账号需要订阅Copilot",
|
||||||
|
"login": "登录Github",
|
||||||
|
"connect": "连接Github",
|
||||||
|
"logout": "退出Github",
|
||||||
|
"auth_success_title": "认证成功",
|
||||||
|
"code_generated_title": "获取Device Code",
|
||||||
|
"code_generated_desc": "请将device code复制到下面的浏览器链接中",
|
||||||
|
"code_failed": "获取Device Code失败,请重试",
|
||||||
|
"auth_success": "Github Copilot认证成功",
|
||||||
|
"auth_failed": "Github Copilot认证失败",
|
||||||
|
"logout_success": "已成功退出",
|
||||||
|
"logout_failed": "退出失败,请重试",
|
||||||
|
"confirm_title": "风险警告",
|
||||||
|
"confirm_login": "过度使用可能会导致您的Github遭到封号,请谨慎使用!!!!",
|
||||||
|
"rate_limit": "速率限制",
|
||||||
|
"custom_headers": "自定义请求头",
|
||||||
|
"headers_description": "自定义请求头(json格式)",
|
||||||
|
"expand": "展开",
|
||||||
|
"model_setting": "模型设置",
|
||||||
|
"invalid_json": "JSON格式错误",
|
||||||
|
"open_verification_first": "请先点击上方链接访问验证页面"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"proxy": {
|
"proxy": {
|
||||||
"mode": {
|
"mode": {
|
||||||
|
|||||||
@ -233,6 +233,8 @@
|
|||||||
"topics": "話題",
|
"topics": "話題",
|
||||||
"warning": "警告",
|
"warning": "警告",
|
||||||
"you": "您",
|
"you": "您",
|
||||||
|
"copied": "已複製",
|
||||||
|
"confirm": "確認",
|
||||||
"more": "更多"
|
"more": "更多"
|
||||||
},
|
},
|
||||||
"docs": {
|
"docs": {
|
||||||
@ -475,7 +477,8 @@
|
|||||||
"upgrade.success.button": "重新啟動",
|
"upgrade.success.button": "重新啟動",
|
||||||
"upgrade.success.content": "請重新啟動程式以完成升級",
|
"upgrade.success.content": "請重新啟動程式以完成升級",
|
||||||
"upgrade.success.title": "升級成功",
|
"upgrade.success.title": "升級成功",
|
||||||
"warn.notion.exporting": "正在匯出到 Notion,請勿重複請求匯出!"
|
"warn.notion.exporting": "正在匯出到 Notion,請勿重複請求匯出!",
|
||||||
|
"warning.rate.limit": "發送過於頻繁,請在 {{seconds}} 秒後再嘗試"
|
||||||
},
|
},
|
||||||
"minapp": {
|
"minapp": {
|
||||||
"sidebar.add.title": "新增到側邊欄",
|
"sidebar.add.title": "新增到側邊欄",
|
||||||
@ -633,6 +636,7 @@
|
|||||||
"yi": "零一萬物",
|
"yi": "零一萬物",
|
||||||
"zhinao": "360 智腦",
|
"zhinao": "360 智腦",
|
||||||
"zhipu": "智譜 AI",
|
"zhipu": "智譜 AI",
|
||||||
|
"copilot": "GitHub Copilot",
|
||||||
"gpustack": "GPUStack",
|
"gpustack": "GPUStack",
|
||||||
"alayanew": "Alaya NeW"
|
"alayanew": "Alaya NeW"
|
||||||
},
|
},
|
||||||
@ -976,7 +980,31 @@
|
|||||||
"remove_invalid_keys": "刪除無效金鑰",
|
"remove_invalid_keys": "刪除無效金鑰",
|
||||||
"search": "搜尋模型平臺...",
|
"search": "搜尋模型平臺...",
|
||||||
"search_placeholder": "搜尋模型 ID 或名稱",
|
"search_placeholder": "搜尋模型 ID 或名稱",
|
||||||
"title": "模型提供者"
|
"title": "模型提供者",
|
||||||
|
"copilot": {
|
||||||
|
"tooltip": "在Cherry Studio中使用Github Copilot需要先登入Github",
|
||||||
|
"description": "您的Github帳號需要訂閱Copilot",
|
||||||
|
"login": "登入Github",
|
||||||
|
"connect": "連接Github",
|
||||||
|
"logout": "退出Github",
|
||||||
|
"auth_success_title": "認證成功",
|
||||||
|
"code_generated_title": "獲取設備代碼",
|
||||||
|
"code_generated_desc": "請將設備代碼複製到下面的瀏覽器連結中。",
|
||||||
|
"code_failed": "獲取Device Code失敗,請重試",
|
||||||
|
"auth_success": "Github Copilot 認證成功",
|
||||||
|
"auth_failed": "Github Copilot認證失敗",
|
||||||
|
"logout_success": "已成功登出",
|
||||||
|
"logout_failed": "退出失敗,請重試",
|
||||||
|
"confirm_title": "風險警告",
|
||||||
|
"confirm_login": "過度使用可能會導致您的Github帳號被封,請謹慎使用!!!!",
|
||||||
|
"rate_limit": "速率限制",
|
||||||
|
"custom_headers": "自訂請求標頭",
|
||||||
|
"headers_description": "自訂請求標頭(json格式)",
|
||||||
|
"expand": "展開",
|
||||||
|
"model_setting": "模型設定",
|
||||||
|
"invalid_json": "JSON格式錯誤",
|
||||||
|
"open_verification_first": "請先點擊上方連結訪問驗證頁面"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"proxy": {
|
"proxy": {
|
||||||
"mode": {
|
"mode": {
|
||||||
|
|||||||
@ -21,7 +21,7 @@ import { useSidebarIconShow } from '@renderer/hooks/useSidebarIcon'
|
|||||||
import { addAssistantMessagesToTopic, getDefaultTopic } from '@renderer/services/AssistantService'
|
import { addAssistantMessagesToTopic, getDefaultTopic } from '@renderer/services/AssistantService'
|
||||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||||
import FileManager from '@renderer/services/FileManager'
|
import FileManager from '@renderer/services/FileManager'
|
||||||
import { getUserMessage } from '@renderer/services/MessagesService'
|
import { checkRateLimit, getUserMessage } from '@renderer/services/MessagesService'
|
||||||
import { estimateMessageUsage, estimateTextTokens as estimateTxtTokens } from '@renderer/services/TokenService'
|
import { estimateMessageUsage, estimateTextTokens as estimateTxtTokens } from '@renderer/services/TokenService'
|
||||||
import { translateText } from '@renderer/services/TranslateService'
|
import { translateText } from '@renderer/services/TranslateService'
|
||||||
import WebSearchService from '@renderer/services/WebSearchService'
|
import WebSearchService from '@renderer/services/WebSearchService'
|
||||||
@ -147,6 +147,9 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
|||||||
if (inputEmpty || loading) {
|
if (inputEmpty || loading) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (checkRateLimit(assistant)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE)
|
EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE)
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,291 @@
|
|||||||
|
import { CheckCircleOutlined, CopyOutlined, ExclamationCircleOutlined } from '@ant-design/icons'
|
||||||
|
import { useCopilot } from '@renderer/hooks/useCopilot'
|
||||||
|
import { useProvider } from '@renderer/hooks/useProvider'
|
||||||
|
import { Provider } from '@renderer/types'
|
||||||
|
import { Alert, Button, Input, message, Popconfirm, Slider, Space, Tooltip, Typography } from 'antd'
|
||||||
|
import { FC, useCallback, useEffect, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
import { SettingDivider, SettingGroup, SettingHelpText, SettingRow, SettingTitle } from '..'
|
||||||
|
|
||||||
|
interface GithubCopilotSettingsProps {
|
||||||
|
provider: Provider
|
||||||
|
setApiKey: (apiKey: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AuthStatus {
|
||||||
|
NOT_STARTED,
|
||||||
|
CODE_GENERATED,
|
||||||
|
AUTHENTICATED
|
||||||
|
}
|
||||||
|
|
||||||
|
const GithubCopilotSettings: FC<GithubCopilotSettingsProps> = ({ provider: initialProvider, setApiKey }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { provider, updateProvider } = useProvider(initialProvider.id)
|
||||||
|
const { username, avatar, defaultHeaders, updateState, updateDefaultHeaders } = useCopilot()
|
||||||
|
// 状态管理
|
||||||
|
const [authStatus, setAuthStatus] = useState<AuthStatus>(AuthStatus.NOT_STARTED)
|
||||||
|
const [deviceCode, setDeviceCode] = useState<string>('')
|
||||||
|
const [userCode, setUserCode] = useState<string>('')
|
||||||
|
const [verificationUri, setVerificationUri] = useState<string>('')
|
||||||
|
const [loading, setLoading] = useState<boolean>(false)
|
||||||
|
const [showHeadersForm, setShowHeadersForm] = useState<boolean>(false)
|
||||||
|
const [headerText, setHeaderText] = useState<string>(JSON.stringify(defaultHeaders || {}, null, 2))
|
||||||
|
const [verificationPageOpened, setVerificationPageOpened] = useState<boolean>(false)
|
||||||
|
|
||||||
|
// 初始化及同步状态
|
||||||
|
useEffect(() => {
|
||||||
|
if (provider.isAuthed) {
|
||||||
|
setAuthStatus(AuthStatus.AUTHENTICATED)
|
||||||
|
} else {
|
||||||
|
setAuthStatus(AuthStatus.NOT_STARTED)
|
||||||
|
// 重置其他状态
|
||||||
|
setDeviceCode('')
|
||||||
|
setUserCode('')
|
||||||
|
setVerificationUri('')
|
||||||
|
}
|
||||||
|
}, [provider])
|
||||||
|
|
||||||
|
// 获取设备代码
|
||||||
|
const handleGetDeviceCode = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true)
|
||||||
|
const { device_code, user_code, verification_uri } = await window.api.copilot.getAuthMessage(defaultHeaders)
|
||||||
|
|
||||||
|
setDeviceCode(device_code)
|
||||||
|
setUserCode(user_code)
|
||||||
|
setVerificationUri(verification_uri)
|
||||||
|
setAuthStatus(AuthStatus.CODE_GENERATED)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get device code:', error)
|
||||||
|
message.error(t('settings.provider.copilot.code_failed'))
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}, [t, defaultHeaders])
|
||||||
|
|
||||||
|
// 使用设备代码获取访问令牌
|
||||||
|
const handleGetToken = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true)
|
||||||
|
const { access_token } = await window.api.copilot.getCopilotToken(deviceCode, defaultHeaders)
|
||||||
|
|
||||||
|
await window.api.copilot.saveCopilotToken(access_token)
|
||||||
|
const { token } = await window.api.copilot.getToken(defaultHeaders)
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
const { login, avatar } = await window.api.copilot.getUser(access_token)
|
||||||
|
setAuthStatus(AuthStatus.AUTHENTICATED)
|
||||||
|
updateState({ username: login, avatar: avatar })
|
||||||
|
updateProvider({ ...provider, apiKey: token, isAuthed: true })
|
||||||
|
setApiKey(token)
|
||||||
|
message.success(t('settings.provider.copilot.auth_success'))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get token:', error)
|
||||||
|
message.error(t('settings.provider.copilot.auth_failed'))
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}, [deviceCode, t, updateProvider, provider, setApiKey, updateState, defaultHeaders])
|
||||||
|
|
||||||
|
// 登出
|
||||||
|
const handleLogout = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
// 1. 保存登出状态到本地
|
||||||
|
updateProvider({ ...provider, apiKey: '', isAuthed: false })
|
||||||
|
setApiKey('')
|
||||||
|
|
||||||
|
// 3. 清除本地存储的token
|
||||||
|
await window.api.copilot.logout()
|
||||||
|
|
||||||
|
// 4. 更新UI状态
|
||||||
|
setAuthStatus(AuthStatus.NOT_STARTED)
|
||||||
|
setDeviceCode('')
|
||||||
|
setUserCode('')
|
||||||
|
setVerificationUri('')
|
||||||
|
|
||||||
|
message.success(t('settings.provider.copilot.logout_success'))
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to logout:', error)
|
||||||
|
message.error(t('settings.provider.copilot.logout_failed'))
|
||||||
|
// 如果登出失败,重置登出状态
|
||||||
|
updateProvider({ ...provider, apiKey: '', isAuthed: false })
|
||||||
|
setApiKey('')
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}, [t, updateProvider, provider, setApiKey])
|
||||||
|
|
||||||
|
// 复制用户代码
|
||||||
|
const handleCopyUserCode = useCallback(() => {
|
||||||
|
navigator.clipboard.writeText(userCode)
|
||||||
|
message.success(t('common.copied'))
|
||||||
|
}, [userCode, t])
|
||||||
|
|
||||||
|
// 打开验证页面
|
||||||
|
const handleOpenVerificationPage = useCallback(() => {
|
||||||
|
if (verificationUri) {
|
||||||
|
window.open(verificationUri, '_blank')
|
||||||
|
setVerificationPageOpened(true)
|
||||||
|
}
|
||||||
|
}, [verificationUri])
|
||||||
|
|
||||||
|
// 处理更新请求头
|
||||||
|
const handleUpdateHeaders = useCallback(() => {
|
||||||
|
try {
|
||||||
|
// 处理headerText可能为空的情况
|
||||||
|
const headers = headerText.trim() ? JSON.parse(headerText) : {}
|
||||||
|
updateDefaultHeaders(headers)
|
||||||
|
message.success(t('message.save.success.title'))
|
||||||
|
} catch (error) {
|
||||||
|
message.error(t('settings.provider.copilot.invalid_json'))
|
||||||
|
}
|
||||||
|
}, [headerText, updateDefaultHeaders, t])
|
||||||
|
|
||||||
|
// 根据认证状态渲染不同的UI
|
||||||
|
const renderAuthContent = () => {
|
||||||
|
switch (authStatus) {
|
||||||
|
case AuthStatus.AUTHENTICATED:
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Alert
|
||||||
|
type="success"
|
||||||
|
message={
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%' }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
{avatar && (
|
||||||
|
<img
|
||||||
|
src={avatar}
|
||||||
|
alt="Avatar"
|
||||||
|
style={{ width: 20, height: 20, borderRadius: '50%', marginRight: 8 }}
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<span>{username || t('settings.provider.copilot.auth_success_title')}</span>
|
||||||
|
</div>
|
||||||
|
<Button type="primary" danger size="small" loading={loading} onClick={handleLogout}>
|
||||||
|
{t('settings.provider.copilot.logout')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
icon={<CheckCircleOutlined />}
|
||||||
|
showIcon
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
|
case AuthStatus.CODE_GENERATED:
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Alert
|
||||||
|
type="info"
|
||||||
|
message={t('settings.provider.copilot.code_generated_title')}
|
||||||
|
description={
|
||||||
|
<>
|
||||||
|
<p>{t('settings.provider.copilot.code_generated_desc')}</p>
|
||||||
|
<Typography.Link onClick={handleOpenVerificationPage}>{verificationUri}</Typography.Link>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
showIcon
|
||||||
|
/>
|
||||||
|
<SettingRow>
|
||||||
|
<Input value={userCode} readOnly />
|
||||||
|
<Button icon={<CopyOutlined />} onClick={handleCopyUserCode}>
|
||||||
|
{t('common.copy')}
|
||||||
|
</Button>
|
||||||
|
</SettingRow>
|
||||||
|
<SettingRow>
|
||||||
|
<Tooltip title={!verificationPageOpened ? t('settings.provider.copilot.open_verification_first') : ''}>
|
||||||
|
<Button type="primary" loading={loading} disabled={!verificationPageOpened} onClick={handleGetToken}>
|
||||||
|
{t('settings.provider.copilot.connect')}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</SettingRow>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
|
default: // AuthStatus.NOT_STARTED
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Alert
|
||||||
|
type="warning"
|
||||||
|
message={t('settings.provider.copilot.tooltip')}
|
||||||
|
description={t('settings.provider.copilot.description')}
|
||||||
|
showIcon
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Popconfirm
|
||||||
|
title={t('settings.provider.copilot.confirm_title')}
|
||||||
|
description={t('settings.provider.copilot.confirm_login')}
|
||||||
|
okText={t('common.confirm')}
|
||||||
|
cancelText={t('common.cancel')}
|
||||||
|
onConfirm={handleGetDeviceCode}
|
||||||
|
icon={<ExclamationCircleOutlined style={{ color: 'red' }} />}>
|
||||||
|
<Button type="primary" loading={loading}>
|
||||||
|
{t('settings.provider.copilot.login')}
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }}>
|
||||||
|
{renderAuthContent()}
|
||||||
|
<SettingDivider />
|
||||||
|
<SettingGroup>
|
||||||
|
<SettingTitle> {t('settings.provider.copilot.model_setting')}</SettingTitle>
|
||||||
|
<SettingDivider />
|
||||||
|
<SettingRow>
|
||||||
|
{t('settings.provider.copilot.rate_limit')}
|
||||||
|
<Slider
|
||||||
|
defaultValue={provider.rateLimit ?? 10}
|
||||||
|
style={{ width: 200 }}
|
||||||
|
min={1}
|
||||||
|
max={60}
|
||||||
|
step={1}
|
||||||
|
marks={{ 1: '1', 10: t('settings.websearch.search_result_default'), 60: '60' }}
|
||||||
|
onChangeComplete={(value) => updateProvider({ ...provider, rateLimit: value })}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
<SettingRow>
|
||||||
|
{t('settings.provider.copilot.custom_headers')}
|
||||||
|
<Button onClick={() => setShowHeadersForm((prev) => !prev)} style={{ width: 200 }}>
|
||||||
|
{t('settings.provider.copilot.expand')}
|
||||||
|
</Button>
|
||||||
|
</SettingRow>
|
||||||
|
{showHeadersForm && (
|
||||||
|
<SettingRow>
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }}>
|
||||||
|
<SettingHelpText>{t('settings.provider.copilot.headers_description')}</SettingHelpText>
|
||||||
|
<Input.TextArea
|
||||||
|
rows={5}
|
||||||
|
autoSize={{ minRows: 2, maxRows: 8 }}
|
||||||
|
value={headerText}
|
||||||
|
onChange={(e) => setHeaderText(e.target.value)}
|
||||||
|
placeholder={`{\n "Header-Name": "Header-Value"\n}`}
|
||||||
|
/>
|
||||||
|
<Space>
|
||||||
|
<Button onClick={handleUpdateHeaders} type="primary">
|
||||||
|
{t('common.save')}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => setHeaderText(JSON.stringify({}, null, 2))}>{t('common.reset')}</Button>
|
||||||
|
</Space>
|
||||||
|
</Space>
|
||||||
|
</SettingRow>
|
||||||
|
)}
|
||||||
|
</SettingGroup>
|
||||||
|
</Space>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Container = styled.div``
|
||||||
|
|
||||||
|
export default GithubCopilotSettings
|
||||||
@ -28,6 +28,7 @@ import {
|
|||||||
SettingTitle
|
SettingTitle
|
||||||
} from '..'
|
} from '..'
|
||||||
import ApiCheckPopup from './ApiCheckPopup'
|
import ApiCheckPopup from './ApiCheckPopup'
|
||||||
|
import GithubCopilotSettings from './GithubCopilotSettings'
|
||||||
import GPUStackSettings from './GPUStackSettings'
|
import GPUStackSettings from './GPUStackSettings'
|
||||||
import GraphRAGSettings from './GraphRAGSettings'
|
import GraphRAGSettings from './GraphRAGSettings'
|
||||||
import HealthCheckPopup from './HealthCheckPopup'
|
import HealthCheckPopup from './HealthCheckPopup'
|
||||||
@ -242,9 +243,12 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (provider.id === 'copilot') {
|
||||||
|
return
|
||||||
|
}
|
||||||
setApiKey(provider.apiKey)
|
setApiKey(provider.apiKey)
|
||||||
setApiHost(provider.apiHost)
|
setApiHost(provider.apiHost)
|
||||||
}, [provider.apiKey, provider.apiHost])
|
}, [provider.apiKey, provider.apiHost, provider.id])
|
||||||
|
|
||||||
// Save apiKey to provider when unmount
|
// Save apiKey to provider when unmount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -283,6 +287,7 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
|||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
type="password"
|
type="password"
|
||||||
autoFocus={provider.enabled && apiKey === ''}
|
autoFocus={provider.enabled && apiKey === ''}
|
||||||
|
disabled={provider.id === 'copilot'}
|
||||||
/>
|
/>
|
||||||
{isProviderSupportAuth(provider) && <OAuthButton provider={provider} onSuccess={setApiKey} />}
|
{isProviderSupportAuth(provider) && <OAuthButton provider={provider} onSuccess={setApiKey} />}
|
||||||
<Button
|
<Button
|
||||||
@ -350,6 +355,7 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
|||||||
{provider.id === 'graphrag-kylin-mountain' && provider.models.length > 0 && (
|
{provider.id === 'graphrag-kylin-mountain' && provider.models.length > 0 && (
|
||||||
<GraphRAGSettings provider={provider} />
|
<GraphRAGSettings provider={provider} />
|
||||||
)}
|
)}
|
||||||
|
{provider.id === 'copilot' && <GithubCopilotSettings provider={provider} setApiKey={setApiKey} />}
|
||||||
<SettingSubtitle style={{ marginBottom: 5 }}>
|
<SettingSubtitle style={{ marginBottom: 5 }}>
|
||||||
<Flex align="center" justify="space-between" style={{ width: '100%' }}>
|
<Flex align="center" justify="space-between" style={{ width: '100%' }}>
|
||||||
<span>{t('common.models')}</span>
|
<span>{t('common.models')}</span>
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import {
|
|||||||
filterEmptyMessages,
|
filterEmptyMessages,
|
||||||
filterUserRoleStartMessages
|
filterUserRoleStartMessages
|
||||||
} from '@renderer/services/MessagesService'
|
} from '@renderer/services/MessagesService'
|
||||||
|
import store from '@renderer/store'
|
||||||
import {
|
import {
|
||||||
Assistant,
|
Assistant,
|
||||||
FileTypes,
|
FileTypes,
|
||||||
@ -69,7 +70,10 @@ export default class OpenAIProvider extends BaseProvider {
|
|||||||
dangerouslyAllowBrowser: true,
|
dangerouslyAllowBrowser: true,
|
||||||
apiKey: this.apiKey,
|
apiKey: this.apiKey,
|
||||||
baseURL: this.getBaseURL(),
|
baseURL: this.getBaseURL(),
|
||||||
defaultHeaders: this.defaultHeaders()
|
defaultHeaders: {
|
||||||
|
...this.defaultHeaders(),
|
||||||
|
...(this.provider.id === 'copilot' ? { 'editor-version': 'vscode/1.97.2' } : {})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -415,6 +419,7 @@ export default class OpenAIProvider extends BaseProvider {
|
|||||||
const lastUserMessage = _messages.findLast((m) => m.role === 'user')
|
const lastUserMessage = _messages.findLast((m) => m.role === 'user')
|
||||||
const { abortController, cleanup, signalPromise } = this.createAbortController(lastUserMessage?.id, true)
|
const { abortController, cleanup, signalPromise } = this.createAbortController(lastUserMessage?.id, true)
|
||||||
const { signal } = abortController
|
const { signal } = abortController
|
||||||
|
await this.checkIsCopilot()
|
||||||
|
|
||||||
mcpTools = filterMCPTools(mcpTools, lastUserMessage?.enabledMCPs)
|
mcpTools = filterMCPTools(mcpTools, lastUserMessage?.enabledMCPs)
|
||||||
const tools = mcpTools && mcpTools.length > 0 ? mcpToolsToOpenAITools(mcpTools) : undefined
|
const tools = mcpTools && mcpTools.length > 0 ? mcpToolsToOpenAITools(mcpTools) : undefined
|
||||||
@ -598,7 +603,6 @@ export default class OpenAIProvider extends BaseProvider {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const stream = await this.sdk.chat.completions
|
const stream = await this.sdk.chat.completions
|
||||||
// @ts-ignore key is not typed
|
// @ts-ignore key is not typed
|
||||||
.create(
|
.create(
|
||||||
@ -659,6 +663,8 @@ export default class OpenAIProvider extends BaseProvider {
|
|||||||
|
|
||||||
const stream = isSupportedStreamOutput()
|
const stream = isSupportedStreamOutput()
|
||||||
|
|
||||||
|
await this.checkIsCopilot()
|
||||||
|
|
||||||
// @ts-ignore key is not typed
|
// @ts-ignore key is not typed
|
||||||
const response = await this.sdk.chat.completions.create({
|
const response = await this.sdk.chat.completions.create({
|
||||||
model: model.id,
|
model: model.id,
|
||||||
@ -732,6 +738,8 @@ export default class OpenAIProvider extends BaseProvider {
|
|||||||
content: userMessageContent
|
content: userMessageContent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.checkIsCopilot()
|
||||||
|
|
||||||
// @ts-ignore key is not typed
|
// @ts-ignore key is not typed
|
||||||
const response = await this.sdk.chat.completions.create({
|
const response = await this.sdk.chat.completions.create({
|
||||||
model: model.id,
|
model: model.id,
|
||||||
@ -791,6 +799,8 @@ export default class OpenAIProvider extends BaseProvider {
|
|||||||
public async generateText({ prompt, content }: { prompt: string; content: string }): Promise<string> {
|
public async generateText({ prompt, content }: { prompt: string; content: string }): Promise<string> {
|
||||||
const model = getDefaultModel()
|
const model = getDefaultModel()
|
||||||
|
|
||||||
|
await this.checkIsCopilot()
|
||||||
|
|
||||||
const response = await this.sdk.chat.completions.create({
|
const response = await this.sdk.chat.completions.create({
|
||||||
model: model.id,
|
model: model.id,
|
||||||
stream: false,
|
stream: false,
|
||||||
@ -816,6 +826,8 @@ export default class OpenAIProvider extends BaseProvider {
|
|||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.checkIsCopilot()
|
||||||
|
|
||||||
const response: any = await this.sdk.request({
|
const response: any = await this.sdk.request({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
path: '/advice_questions',
|
path: '/advice_questions',
|
||||||
@ -840,7 +852,6 @@ export default class OpenAIProvider extends BaseProvider {
|
|||||||
if (!model) {
|
if (!model) {
|
||||||
return { valid: false, error: new Error('No model found') }
|
return { valid: false, error: new Error('No model found') }
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = {
|
const body = {
|
||||||
model: model.id,
|
model: model.id,
|
||||||
messages: [{ role: 'user', content: 'hi' }],
|
messages: [{ role: 'user', content: 'hi' }],
|
||||||
@ -848,6 +859,7 @@ export default class OpenAIProvider extends BaseProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
await this.checkIsCopilot()
|
||||||
const response = await this.sdk.chat.completions.create(body as ChatCompletionCreateParamsNonStreaming)
|
const response = await this.sdk.chat.completions.create(body as ChatCompletionCreateParamsNonStreaming)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -868,6 +880,8 @@ export default class OpenAIProvider extends BaseProvider {
|
|||||||
*/
|
*/
|
||||||
public async models(): Promise<OpenAI.Models.Model[]> {
|
public async models(): Promise<OpenAI.Models.Model[]> {
|
||||||
try {
|
try {
|
||||||
|
await this.checkIsCopilot()
|
||||||
|
|
||||||
const response = await this.sdk.models.list()
|
const response = await this.sdk.models.list()
|
||||||
|
|
||||||
if (this.provider.id === 'github') {
|
if (this.provider.id === 'github') {
|
||||||
@ -945,10 +959,20 @@ export default class OpenAIProvider extends BaseProvider {
|
|||||||
* @returns The embedding dimensions
|
* @returns The embedding dimensions
|
||||||
*/
|
*/
|
||||||
public async getEmbeddingDimensions(model: Model): Promise<number> {
|
public async getEmbeddingDimensions(model: Model): Promise<number> {
|
||||||
|
await this.checkIsCopilot()
|
||||||
|
|
||||||
const data = await this.sdk.embeddings.create({
|
const data = await this.sdk.embeddings.create({
|
||||||
model: model.id,
|
model: model.id,
|
||||||
input: model?.provider === 'baidu-cloud' ? ['hi'] : 'hi'
|
input: model?.provider === 'baidu-cloud' ? ['hi'] : 'hi'
|
||||||
})
|
})
|
||||||
return data.data[0].embedding.length
|
return data.data[0].embedding.length
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async checkIsCopilot() {
|
||||||
|
if (this.provider.id !== 'copilot') return
|
||||||
|
const defaultHeaders = store.getState().copilot.defaultHeaders
|
||||||
|
// copilot每次请求前需要重新获取token,因为token中附带时间戳
|
||||||
|
const { token } = await window.api.copilot.getToken(defaultHeaders)
|
||||||
|
this.sdk.apiKey = token
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,10 +6,11 @@ import store from '@renderer/store'
|
|||||||
import { Assistant, Message, Model, Topic } from '@renderer/types'
|
import { Assistant, Message, Model, Topic } from '@renderer/types'
|
||||||
import { getTitleFromString, uuid } from '@renderer/utils'
|
import { getTitleFromString, uuid } from '@renderer/utils'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
|
import { t } from 'i18next'
|
||||||
import { isEmpty, remove, takeRight } from 'lodash'
|
import { isEmpty, remove, takeRight } from 'lodash'
|
||||||
import { NavigateFunction } from 'react-router'
|
import { NavigateFunction } from 'react-router'
|
||||||
|
|
||||||
import { getAssistantById, getDefaultModel } from './AssistantService'
|
import { getAssistantById, getAssistantProvider, getDefaultModel } from './AssistantService'
|
||||||
import { EVENT_NAMES, EventEmitter } from './EventService'
|
import { EVENT_NAMES, EventEmitter } from './EventService'
|
||||||
import FileManager from './FileManager'
|
import FileManager from './FileManager'
|
||||||
|
|
||||||
@ -212,3 +213,36 @@ export function getMessageTitle(message: Message, length = 30) {
|
|||||||
|
|
||||||
return title
|
return title
|
||||||
}
|
}
|
||||||
|
export function checkRateLimit(assistant: Assistant): boolean {
|
||||||
|
const provider = getAssistantProvider(assistant)
|
||||||
|
|
||||||
|
if (!provider.rateLimit) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const topicId = assistant.topics[0].id
|
||||||
|
const messages = store.getState().messages.messagesByTopic[topicId]
|
||||||
|
|
||||||
|
if (!messages || messages.length <= 1) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Date.now()
|
||||||
|
const lastMessage = messages[messages.length - 1]
|
||||||
|
const lastMessageTime = new Date(lastMessage.createdAt).getTime()
|
||||||
|
const timeDiff = now - lastMessageTime
|
||||||
|
const rateLimitMs = provider.rateLimit * 1000
|
||||||
|
|
||||||
|
if (timeDiff < rateLimitMs) {
|
||||||
|
const waitTimeSeconds = Math.ceil((rateLimitMs - timeDiff) / 1000)
|
||||||
|
|
||||||
|
window.message.warning({
|
||||||
|
content: t('message.warning.rate.limit', { seconds: waitTimeSeconds }),
|
||||||
|
duration: 5,
|
||||||
|
key: 'rate-limit-message'
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
36
src/renderer/src/store/copilot.ts
Normal file
36
src/renderer/src/store/copilot.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||||
|
|
||||||
|
export interface CopilotState {
|
||||||
|
username?: string
|
||||||
|
avatar?: string
|
||||||
|
defaultHeaders?: Record<string, string>
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: CopilotState = {
|
||||||
|
username: '',
|
||||||
|
avatar: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export const copilotSlice = createSlice({
|
||||||
|
name: 'copilot',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setUsername: (state, action: PayloadAction<string>) => {
|
||||||
|
state.username = action.payload
|
||||||
|
},
|
||||||
|
setAvatar: (state, action: PayloadAction<string>) => {
|
||||||
|
state.avatar = action.payload
|
||||||
|
},
|
||||||
|
setDefaultHeaders: (state, action: PayloadAction<Record<string, string>>) => {
|
||||||
|
state.defaultHeaders = action.payload
|
||||||
|
},
|
||||||
|
updateCopilotState: (state, action: PayloadAction<Partial<CopilotState>>) => {
|
||||||
|
return { ...state, ...action.payload }
|
||||||
|
},
|
||||||
|
resetCopilotState: () => initialState
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const { setUsername, setAvatar, setDefaultHeaders, updateCopilotState, resetCopilotState } = copilotSlice.actions
|
||||||
|
|
||||||
|
export default copilotSlice.reducer
|
||||||
@ -5,6 +5,7 @@ import storage from 'redux-persist/lib/storage'
|
|||||||
|
|
||||||
import agents from './agents'
|
import agents from './agents'
|
||||||
import assistants from './assistants'
|
import assistants from './assistants'
|
||||||
|
import copilot from './copilot'
|
||||||
import knowledge from './knowledge'
|
import knowledge from './knowledge'
|
||||||
import llm from './llm'
|
import llm from './llm'
|
||||||
import mcp from './mcp'
|
import mcp from './mcp'
|
||||||
@ -28,8 +29,9 @@ const rootReducer = combineReducers({
|
|||||||
knowledge,
|
knowledge,
|
||||||
minapps,
|
minapps,
|
||||||
websearch,
|
websearch,
|
||||||
messages: messagesReducer,
|
mcp,
|
||||||
mcp
|
copilot,
|
||||||
|
messages: messagesReducer
|
||||||
})
|
})
|
||||||
|
|
||||||
const persistedReducer = persistReducer(
|
const persistedReducer = persistReducer(
|
||||||
|
|||||||
@ -200,6 +200,17 @@ const initialState: LlmState = {
|
|||||||
isSystem: true,
|
isSystem: true,
|
||||||
enabled: false
|
enabled: false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'copilot',
|
||||||
|
name: 'Github Copilot',
|
||||||
|
type: 'openai',
|
||||||
|
apiKey: '',
|
||||||
|
apiHost: 'https://api.githubcopilot.com/',
|
||||||
|
models: SYSTEM_MODELS.copilot,
|
||||||
|
isSystem: true,
|
||||||
|
enabled: false,
|
||||||
|
isAuthed: false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'dmxapi',
|
id: 'dmxapi',
|
||||||
name: 'DMXAPI',
|
name: 'DMXAPI',
|
||||||
|
|||||||
@ -1178,16 +1178,18 @@ const migrateConfig = {
|
|||||||
return state
|
return state
|
||||||
},
|
},
|
||||||
'74': (state: RootState) => {
|
'74': (state: RootState) => {
|
||||||
state.llm.providers.push({
|
if (!state.llm.providers.find((provider) => provider.id === 'xirang')) {
|
||||||
id: 'xirang',
|
state.llm.providers.push({
|
||||||
name: 'Xirang',
|
id: 'xirang',
|
||||||
type: 'openai',
|
name: 'Xirang',
|
||||||
apiKey: '',
|
type: 'openai',
|
||||||
apiHost: 'https://wishub-x1.ctyun.cn',
|
apiKey: '',
|
||||||
models: SYSTEM_MODELS.xirang,
|
apiHost: 'https://wishub-x1.ctyun.cn',
|
||||||
isSystem: true,
|
models: SYSTEM_MODELS.xirang,
|
||||||
enabled: false
|
isSystem: true,
|
||||||
})
|
enabled: false
|
||||||
|
})
|
||||||
|
}
|
||||||
return state
|
return state
|
||||||
},
|
},
|
||||||
'75': (state: RootState) => {
|
'75': (state: RootState) => {
|
||||||
@ -1235,10 +1237,21 @@ const migrateConfig = {
|
|||||||
delete p.enabled
|
delete p.enabled
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return state
|
return state
|
||||||
},
|
},
|
||||||
'78': (state: RootState) => {
|
'78': (state: RootState) => {
|
||||||
|
if (!state.llm.providers.find((p) => p.id === 'copilot')) {
|
||||||
|
state.llm.providers.push({
|
||||||
|
id: 'copilot',
|
||||||
|
name: 'Github Copilot',
|
||||||
|
type: 'openai',
|
||||||
|
apiKey: '',
|
||||||
|
apiHost: 'https://api.githubcopilot.com/',
|
||||||
|
models: SYSTEM_MODELS.copilot,
|
||||||
|
isSystem: true,
|
||||||
|
enabled: false
|
||||||
|
})
|
||||||
|
}
|
||||||
state.llm.providers = moveProvider(state.llm.providers, 'ppio', 9)
|
state.llm.providers = moveProvider(state.llm.providers, 'ppio', 9)
|
||||||
state.llm.providers = moveProvider(state.llm.providers, 'infini', 10)
|
state.llm.providers = moveProvider(state.llm.providers, 'infini', 10)
|
||||||
removeMiniAppIconsFromState(state)
|
removeMiniAppIconsFromState(state)
|
||||||
|
|||||||
@ -116,6 +116,8 @@ export type Provider = {
|
|||||||
models: Model[]
|
models: Model[]
|
||||||
enabled?: boolean
|
enabled?: boolean
|
||||||
isSystem?: boolean
|
isSystem?: boolean
|
||||||
|
isAuthed?: boolean
|
||||||
|
rateLimit?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ProviderType = 'openai' | 'anthropic' | 'gemini' | 'qwenlm' | 'azure-openai'
|
export type ProviderType = 'openai' | 'anthropic' | 'gemini' | 'qwenlm' | 'azure-openai'
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user