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 BackupManager from './services/BackupManager'
|
||||
import { configManager } from './services/ConfigManager'
|
||||
import CopilotService from './services/CopilotService'
|
||||
import { ExportService } from './services/ExportService'
|
||||
import FileService from './services/FileService'
|
||||
import FileStorage from './services/FileStorage'
|
||||
@ -252,6 +253,13 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
mainWindow?.webContents.send('mcp:servers-updated', servers)
|
||||
})
|
||||
|
||||
// Clean up MCP services when app quits
|
||||
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
|
||||
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>
|
||||
getBinaryPath: (name: string) => Promise<string>
|
||||
installUVBinary: () => Promise<void>
|
||||
|
||||
@ -121,7 +121,16 @@ const api = {
|
||||
cleanup: () => ipcRenderer.invoke('mcp:cleanup')
|
||||
},
|
||||
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
|
||||
|
||||
@ -1035,6 +1035,14 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
|
||||
group: 'OpenAI'
|
||||
}
|
||||
],
|
||||
copilot: [
|
||||
{
|
||||
id: 'gpt-4o-mini',
|
||||
provider: 'copilot',
|
||||
name: 'OpenAI GPT-4o-mini',
|
||||
group: 'OpenAI'
|
||||
}
|
||||
],
|
||||
yi: [
|
||||
{ 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' }
|
||||
@ -1895,6 +1903,9 @@ export function isVisionModel(model: Model): boolean {
|
||||
if (!model) {
|
||||
return false
|
||||
}
|
||||
if (model.provider === 'copilot') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (model.provider === 'doubao') {
|
||||
return VISION_REGEX.test(model.name) || model.type?.includes('vision') || false
|
||||
|
||||
@ -66,6 +66,7 @@ const PROVIDER_LOGO_MAP = {
|
||||
'graphrag-kylin-mountain': GraphRagProviderLogo,
|
||||
minimax: MinimaxProviderLogo,
|
||||
github: GithubProviderLogo,
|
||||
copilot: GithubProviderLogo,
|
||||
ocoolai: OcoolAiProviderLogo,
|
||||
together: TogetherProviderLogo,
|
||||
fireworks: FireworksProviderLogo,
|
||||
@ -238,6 +239,11 @@ export const PROVIDER_CONFIG = {
|
||||
models: 'https://github.com/marketplace/models'
|
||||
}
|
||||
},
|
||||
copilot: {
|
||||
api: {
|
||||
url: 'https://api.githubcopilot.com/'
|
||||
}
|
||||
},
|
||||
yi: {
|
||||
api: {
|
||||
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",
|
||||
"warning": "Warning",
|
||||
"you": "You",
|
||||
"copied": "Copied",
|
||||
"confirm": "Confirm",
|
||||
"more": "More"
|
||||
},
|
||||
"docs": {
|
||||
@ -475,7 +477,8 @@
|
||||
"upgrade.success.button": "Restart",
|
||||
"upgrade.success.content": "Please restart the application to complete the upgrade",
|
||||
"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": {
|
||||
"sidebar.add.title": "Add to sidebar",
|
||||
@ -639,6 +642,7 @@
|
||||
"yi": "Yi",
|
||||
"zhinao": "360AI",
|
||||
"zhipu": "ZHIPU AI",
|
||||
"copilot": "GitHub Copilot",
|
||||
"gpustack": "GPUStack",
|
||||
"alayanew": "Alaya NeW"
|
||||
},
|
||||
@ -976,7 +980,31 @@
|
||||
"remove_invalid_keys": "Remove Invalid Keys",
|
||||
"search": "Search Providers...",
|
||||
"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": {
|
||||
"mode": {
|
||||
|
||||
@ -233,6 +233,8 @@
|
||||
"topics": "トピック",
|
||||
"warning": "警告",
|
||||
"you": "あなた",
|
||||
"copied": "コピーされました",
|
||||
"confirm": "確認",
|
||||
"more": "もっと"
|
||||
},
|
||||
"docs": {
|
||||
@ -475,7 +477,8 @@
|
||||
"upgrade.success.button": "再起動",
|
||||
"upgrade.success.content": "アップグレードを完了するためにアプリケーションを再起動してください",
|
||||
"upgrade.success.title": "アップグレードに成功しました",
|
||||
"warn.notion.exporting": "Notionにエクスポート中です。重複してエクスポートしないでください! "
|
||||
"warn.notion.exporting": "Notionにエクスポート中です。重複してエクスポートしないでください! ",
|
||||
"warning.rate.limit": "送信が頻繁すぎます。{{seconds}} 秒待ってから再試行してください。"
|
||||
},
|
||||
"minapp": {
|
||||
"sidebar.add.title": "サイドバーに追加",
|
||||
@ -639,6 +642,7 @@
|
||||
"yi": "零一万物",
|
||||
"zhinao": "360智脳",
|
||||
"zhipu": "智譜AI",
|
||||
"copilot": "GitHub Copilot",
|
||||
"gpustack": "GPUStack",
|
||||
"alayanew": "Alaya NeW"
|
||||
},
|
||||
@ -976,7 +980,31 @@
|
||||
"remove_invalid_keys": "無効なキーを削除",
|
||||
"search": "プロバイダーを検索...",
|
||||
"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": {
|
||||
"mode": {
|
||||
|
||||
@ -233,6 +233,8 @@
|
||||
"topics": "Топики",
|
||||
"warning": "Предупреждение",
|
||||
"you": "Вы",
|
||||
"confirm": "确认的翻译是: Подтверждение",
|
||||
"copied": "Скопировано",
|
||||
"more": "Ещё"
|
||||
},
|
||||
"docs": {
|
||||
@ -481,7 +483,8 @@
|
||||
"upgrade.success.button": "Перезапустить",
|
||||
"upgrade.success.content": "Пожалуйста, перезапустите приложение для завершения обновления",
|
||||
"upgrade.success.title": "Обновление успешно",
|
||||
"warn.notion.exporting": "Экспортируется в Notion, пожалуйста, не отправляйте повторные запросы!"
|
||||
"warn.notion.exporting": "Экспортируется в Notion, пожалуйста, не отправляйте повторные запросы!",
|
||||
"warning.rate.limit": "Отправка слишком частая, пожалуйста, подождите {{seconds}} секунд, прежде чем попробовать снова."
|
||||
},
|
||||
"minapp": {
|
||||
"sidebar.add.title": "Добавить в боковую панель",
|
||||
@ -639,6 +642,7 @@
|
||||
"yi": "Yi",
|
||||
"zhinao": "360AI",
|
||||
"zhipu": "ZHIPU AI",
|
||||
"copilot": "GitHub Copilot",
|
||||
"gpustack": "GPUStack",
|
||||
"alayanew": "Alaya NeW"
|
||||
},
|
||||
@ -976,7 +980,31 @@
|
||||
"remove_invalid_keys": "Удалить недействительные ключи",
|
||||
"search": "Поиск поставщиков...",
|
||||
"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": {
|
||||
"mode": {
|
||||
|
||||
@ -204,7 +204,9 @@
|
||||
"chat": "聊天",
|
||||
"clear": "清除",
|
||||
"close": "关闭",
|
||||
"confirm": "确认",
|
||||
"copy": "复制",
|
||||
"copied": "已复制",
|
||||
"cut": "剪切",
|
||||
"default": "默认",
|
||||
"delete": "删除",
|
||||
@ -475,7 +477,8 @@
|
||||
"upgrade.success.button": "重启",
|
||||
"upgrade.success.content": "重启用以完成升级",
|
||||
"upgrade.success.title": "升级成功",
|
||||
"warn.notion.exporting": "正在导出到 Notion, 请勿重复请求导出!"
|
||||
"warn.notion.exporting": "正在导出到 Notion, 请勿重复请求导出!",
|
||||
"warning.rate.limit": "发送过于频繁,请等待 {{seconds}} 秒后再尝试"
|
||||
},
|
||||
"minapp": {
|
||||
"sidebar.add.title": "添加到侧边栏",
|
||||
@ -633,6 +636,7 @@
|
||||
"yi": "零一万物",
|
||||
"zhinao": "360智脑",
|
||||
"zhipu": "智谱AI",
|
||||
"copilot": "GitHub Copilot",
|
||||
"gpustack": "GPUStack",
|
||||
"alayanew": "Alaya NeW"
|
||||
},
|
||||
@ -976,7 +980,31 @@
|
||||
"remove_invalid_keys": "删除无效密钥",
|
||||
"search": "搜索模型平台...",
|
||||
"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": {
|
||||
"mode": {
|
||||
|
||||
@ -233,6 +233,8 @@
|
||||
"topics": "話題",
|
||||
"warning": "警告",
|
||||
"you": "您",
|
||||
"copied": "已複製",
|
||||
"confirm": "確認",
|
||||
"more": "更多"
|
||||
},
|
||||
"docs": {
|
||||
@ -475,7 +477,8 @@
|
||||
"upgrade.success.button": "重新啟動",
|
||||
"upgrade.success.content": "請重新啟動程式以完成升級",
|
||||
"upgrade.success.title": "升級成功",
|
||||
"warn.notion.exporting": "正在匯出到 Notion,請勿重複請求匯出!"
|
||||
"warn.notion.exporting": "正在匯出到 Notion,請勿重複請求匯出!",
|
||||
"warning.rate.limit": "發送過於頻繁,請在 {{seconds}} 秒後再嘗試"
|
||||
},
|
||||
"minapp": {
|
||||
"sidebar.add.title": "新增到側邊欄",
|
||||
@ -633,6 +636,7 @@
|
||||
"yi": "零一萬物",
|
||||
"zhinao": "360 智腦",
|
||||
"zhipu": "智譜 AI",
|
||||
"copilot": "GitHub Copilot",
|
||||
"gpustack": "GPUStack",
|
||||
"alayanew": "Alaya NeW"
|
||||
},
|
||||
@ -976,7 +980,31 @@
|
||||
"remove_invalid_keys": "刪除無效金鑰",
|
||||
"search": "搜尋模型平臺...",
|
||||
"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": {
|
||||
"mode": {
|
||||
|
||||
@ -21,7 +21,7 @@ import { useSidebarIconShow } from '@renderer/hooks/useSidebarIcon'
|
||||
import { addAssistantMessagesToTopic, getDefaultTopic } from '@renderer/services/AssistantService'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
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 { translateText } from '@renderer/services/TranslateService'
|
||||
import WebSearchService from '@renderer/services/WebSearchService'
|
||||
@ -147,6 +147,9 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
if (inputEmpty || loading) {
|
||||
return
|
||||
}
|
||||
if (checkRateLimit(assistant)) {
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
} from '..'
|
||||
import ApiCheckPopup from './ApiCheckPopup'
|
||||
import GithubCopilotSettings from './GithubCopilotSettings'
|
||||
import GPUStackSettings from './GPUStackSettings'
|
||||
import GraphRAGSettings from './GraphRAGSettings'
|
||||
import HealthCheckPopup from './HealthCheckPopup'
|
||||
@ -242,9 +243,12 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (provider.id === 'copilot') {
|
||||
return
|
||||
}
|
||||
setApiKey(provider.apiKey)
|
||||
setApiHost(provider.apiHost)
|
||||
}, [provider.apiKey, provider.apiHost])
|
||||
}, [provider.apiKey, provider.apiHost, provider.id])
|
||||
|
||||
// Save apiKey to provider when unmount
|
||||
useEffect(() => {
|
||||
@ -283,6 +287,7 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
||||
spellCheck={false}
|
||||
type="password"
|
||||
autoFocus={provider.enabled && apiKey === ''}
|
||||
disabled={provider.id === 'copilot'}
|
||||
/>
|
||||
{isProviderSupportAuth(provider) && <OAuthButton provider={provider} onSuccess={setApiKey} />}
|
||||
<Button
|
||||
@ -350,6 +355,7 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
||||
{provider.id === 'graphrag-kylin-mountain' && provider.models.length > 0 && (
|
||||
<GraphRAGSettings provider={provider} />
|
||||
)}
|
||||
{provider.id === 'copilot' && <GithubCopilotSettings provider={provider} setApiKey={setApiKey} />}
|
||||
<SettingSubtitle style={{ marginBottom: 5 }}>
|
||||
<Flex align="center" justify="space-between" style={{ width: '100%' }}>
|
||||
<span>{t('common.models')}</span>
|
||||
|
||||
@ -15,6 +15,7 @@ import {
|
||||
filterEmptyMessages,
|
||||
filterUserRoleStartMessages
|
||||
} from '@renderer/services/MessagesService'
|
||||
import store from '@renderer/store'
|
||||
import {
|
||||
Assistant,
|
||||
FileTypes,
|
||||
@ -69,7 +70,10 @@ export default class OpenAIProvider extends BaseProvider {
|
||||
dangerouslyAllowBrowser: true,
|
||||
apiKey: this.apiKey,
|
||||
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 { abortController, cleanup, signalPromise } = this.createAbortController(lastUserMessage?.id, true)
|
||||
const { signal } = abortController
|
||||
await this.checkIsCopilot()
|
||||
|
||||
mcpTools = filterMCPTools(mcpTools, lastUserMessage?.enabledMCPs)
|
||||
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
|
||||
// @ts-ignore key is not typed
|
||||
.create(
|
||||
@ -659,6 +663,8 @@ export default class OpenAIProvider extends BaseProvider {
|
||||
|
||||
const stream = isSupportedStreamOutput()
|
||||
|
||||
await this.checkIsCopilot()
|
||||
|
||||
// @ts-ignore key is not typed
|
||||
const response = await this.sdk.chat.completions.create({
|
||||
model: model.id,
|
||||
@ -732,6 +738,8 @@ export default class OpenAIProvider extends BaseProvider {
|
||||
content: userMessageContent
|
||||
}
|
||||
|
||||
await this.checkIsCopilot()
|
||||
|
||||
// @ts-ignore key is not typed
|
||||
const response = await this.sdk.chat.completions.create({
|
||||
model: model.id,
|
||||
@ -791,6 +799,8 @@ export default class OpenAIProvider extends BaseProvider {
|
||||
public async generateText({ prompt, content }: { prompt: string; content: string }): Promise<string> {
|
||||
const model = getDefaultModel()
|
||||
|
||||
await this.checkIsCopilot()
|
||||
|
||||
const response = await this.sdk.chat.completions.create({
|
||||
model: model.id,
|
||||
stream: false,
|
||||
@ -816,6 +826,8 @@ export default class OpenAIProvider extends BaseProvider {
|
||||
return []
|
||||
}
|
||||
|
||||
await this.checkIsCopilot()
|
||||
|
||||
const response: any = await this.sdk.request({
|
||||
method: 'post',
|
||||
path: '/advice_questions',
|
||||
@ -840,7 +852,6 @@ export default class OpenAIProvider extends BaseProvider {
|
||||
if (!model) {
|
||||
return { valid: false, error: new Error('No model found') }
|
||||
}
|
||||
|
||||
const body = {
|
||||
model: model.id,
|
||||
messages: [{ role: 'user', content: 'hi' }],
|
||||
@ -848,6 +859,7 @@ export default class OpenAIProvider extends BaseProvider {
|
||||
}
|
||||
|
||||
try {
|
||||
await this.checkIsCopilot()
|
||||
const response = await this.sdk.chat.completions.create(body as ChatCompletionCreateParamsNonStreaming)
|
||||
|
||||
return {
|
||||
@ -868,6 +880,8 @@ export default class OpenAIProvider extends BaseProvider {
|
||||
*/
|
||||
public async models(): Promise<OpenAI.Models.Model[]> {
|
||||
try {
|
||||
await this.checkIsCopilot()
|
||||
|
||||
const response = await this.sdk.models.list()
|
||||
|
||||
if (this.provider.id === 'github') {
|
||||
@ -945,10 +959,20 @@ export default class OpenAIProvider extends BaseProvider {
|
||||
* @returns The embedding dimensions
|
||||
*/
|
||||
public async getEmbeddingDimensions(model: Model): Promise<number> {
|
||||
await this.checkIsCopilot()
|
||||
|
||||
const data = await this.sdk.embeddings.create({
|
||||
model: model.id,
|
||||
input: model?.provider === 'baidu-cloud' ? ['hi'] : 'hi'
|
||||
})
|
||||
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 { getTitleFromString, uuid } from '@renderer/utils'
|
||||
import dayjs from 'dayjs'
|
||||
import { t } from 'i18next'
|
||||
import { isEmpty, remove, takeRight } from 'lodash'
|
||||
import { NavigateFunction } from 'react-router'
|
||||
|
||||
import { getAssistantById, getDefaultModel } from './AssistantService'
|
||||
import { getAssistantById, getAssistantProvider, getDefaultModel } from './AssistantService'
|
||||
import { EVENT_NAMES, EventEmitter } from './EventService'
|
||||
import FileManager from './FileManager'
|
||||
|
||||
@ -212,3 +213,36 @@ export function getMessageTitle(message: Message, length = 30) {
|
||||
|
||||
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 assistants from './assistants'
|
||||
import copilot from './copilot'
|
||||
import knowledge from './knowledge'
|
||||
import llm from './llm'
|
||||
import mcp from './mcp'
|
||||
@ -28,8 +29,9 @@ const rootReducer = combineReducers({
|
||||
knowledge,
|
||||
minapps,
|
||||
websearch,
|
||||
messages: messagesReducer,
|
||||
mcp
|
||||
mcp,
|
||||
copilot,
|
||||
messages: messagesReducer
|
||||
})
|
||||
|
||||
const persistedReducer = persistReducer(
|
||||
|
||||
@ -200,6 +200,17 @@ const initialState: LlmState = {
|
||||
isSystem: true,
|
||||
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',
|
||||
name: 'DMXAPI',
|
||||
|
||||
@ -1178,6 +1178,7 @@ const migrateConfig = {
|
||||
return state
|
||||
},
|
||||
'74': (state: RootState) => {
|
||||
if (!state.llm.providers.find((provider) => provider.id === 'xirang')) {
|
||||
state.llm.providers.push({
|
||||
id: 'xirang',
|
||||
name: 'Xirang',
|
||||
@ -1188,6 +1189,7 @@ const migrateConfig = {
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
})
|
||||
}
|
||||
return state
|
||||
},
|
||||
'75': (state: RootState) => {
|
||||
@ -1235,10 +1237,21 @@ const migrateConfig = {
|
||||
delete p.enabled
|
||||
})
|
||||
}
|
||||
|
||||
return state
|
||||
},
|
||||
'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, 'infini', 10)
|
||||
removeMiniAppIconsFromState(state)
|
||||
|
||||
@ -116,6 +116,8 @@ export type Provider = {
|
||||
models: Model[]
|
||||
enabled?: boolean
|
||||
isSystem?: boolean
|
||||
isAuthed?: boolean
|
||||
rateLimit?: number
|
||||
}
|
||||
|
||||
export type ProviderType = 'openai' | 'anthropic' | 'gemini' | 'qwenlm' | 'azure-openai'
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user