diff --git a/electron/main/common/fs-utils.ts b/electron/main/common/fs-utils.ts index 859aa2950147678de60a0efa8c967ca4778a2780..27b80ad9081b3b4653075ab6a73e18cb922144d5 100644 --- a/electron/main/common/fs-utils.ts +++ b/electron/main/common/fs-utils.ts @@ -8,6 +8,7 @@ // PURPOSE. // See the Mulan PSL v2 for more details. import fs from 'node:fs'; +import path from 'node:path'; /** * 创建目录(如果不存在) @@ -25,7 +26,7 @@ export async function mkdirpIgnoreError( await fs.promises.mkdir(dir, { recursive: true }); return dir; - } catch (error) { + } catch { // ignore } } @@ -38,15 +39,34 @@ export async function mkdirpIgnoreError( * @param dir 配置文件路径 * @returns 配置对象 */ -export function getUserDefinedConf(dir: string): any { +export function getUserDefinedConf(dir: string): Record { try { if (!fs.existsSync(dir)) { fs.writeFileSync(dir, JSON.stringify({})); } return JSON.parse(fs.readFileSync(dir, 'utf-8')); - } catch (error) { + } catch { // Ignore error return {}; } } + +/** + * 检查配置文件是否存在,不存在则创建默认内容 + * @param filePath 配置文件路径 + * @param defaultContent 默认内容对象 + */ +export function ensureConfigFile(filePath: string, defaultContent: object) { + try { + const dir = path.dirname(filePath); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + if (!fs.existsSync(filePath)) { + fs.writeFileSync(filePath, JSON.stringify(defaultContent, null, 4)); + } + } catch { + // ignore + } +} diff --git a/electron/main/index.ts b/electron/main/index.ts index 380c6e0b33afcafd4b176372e54a973d43b842bf..926bff1779c860a4b6474ccb19dff5c7194b8f81 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -10,7 +10,11 @@ import { app, session, ipcMain, Menu, BrowserWindow } from 'electron'; import { createDefaultWindow, createChatWindow, createTray } from './window'; import { cachePath, commonCacheConfPath } from './common/conf'; -import { mkdirpIgnoreError, getUserDefinedConf } from './common/fs-utils'; +import { + mkdirpIgnoreError, + getUserDefinedConf, + ensureConfigFile, +} from './common/fs-utils'; import { getOsLocale, resolveNlsConfiguration } from './common/locale'; import { resolveThemeConfiguration, setApplicationTheme } from './common/theme'; import { productObj } from './common/product'; @@ -21,6 +25,8 @@ import { } from './common/shortcuts'; import { buildAppMenu } from './common/menu'; import { registerIpcListeners } from './common/ipc'; +import * as path from 'node:path'; +import { homedir } from 'node:os'; // 允许本地部署时使用无效证书,仅在 Electron 主进程下生效 if (process.versions.electron) { @@ -36,6 +42,30 @@ let isQuitting = false; // 获取系统语言环境 const osLocale = getOsLocale(); +// 启动时确保 smart-shell.json 存在 +const configDir = path.join( + process.platform === 'linux' + ? path.join( + process.env['XDG_CONFIG_HOME'] || path.join(homedir(), '.config'), + ) + : path.join(homedir(), '.config'), + 'eulercopilot', +); +const configPath = path.join(configDir, 'smart-shell.json'); +const defaultConfig = { + backend: 'openai', + openai: { + base_url: '', + model: '', + api_key: '', + }, + eulercopilot: { + base_url: 'https://www.eulercopilot.local', + api_key: '', + }, +}; +ensureConfigFile(configPath, defaultConfig); + // 应用初始化 app.once('ready', () => { onReady(); @@ -137,7 +167,7 @@ async function startup() { registerIpcListeners(); // 创建系统托盘 - const tray = createTray(); + createTray(); // 创建应用窗口 let win = createDefaultWindow(); @@ -180,3 +210,45 @@ async function startup() { chatWindow.hide(); }); } + +// 定义配置类型,避免使用 any +interface Config { + backend: string; + openai: { + base_url: string; + model: string; + api_key: string; + }; + eulercopilot: { + base_url: string; + api_key: string; + }; +} + +// 注册获取代理URL的IPC +ipcMain.handle('copilot:get-proxy-url', async () => { + try { + // 兼容Linux/macOS,配置文件路径 + const configDir = path.join( + process.platform === 'linux' + ? path.join( + process.env['XDG_CONFIG_HOME'] || path.join(homedir(), '.config'), + ) + : path.join(homedir(), '.config'), + 'eulercopilot', + ); + const configPath = path.join(configDir, 'smart-shell.json'); + const confRaw = getUserDefinedConf(configPath); + const conf = confRaw as unknown as Config | undefined; + if ( + conf && + conf.eulercopilot && + typeof conf.eulercopilot.base_url === 'string' + ) { + return conf.eulercopilot.base_url || ''; + } + return ''; + } catch { + return ''; + } +}); diff --git a/electron/preload/index.ts b/electron/preload/index.ts index 2700a192cc1a5447f015ce29bb8b0ea283227a6b..c6165756904d4dc1037f5be671b542f88e643218 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -54,6 +54,11 @@ const globals = { validateIPC(channel); ipcRenderer.removeAllListeners(channel); }, + + // 动态获取代理URL + getProxyUrl: async (): Promise => { + return await ipcRenderer.invoke('copilot:get-proxy-url'); + }, }, process: { diff --git a/src/apis/server.ts b/src/apis/server.ts index a65ff8d1cae41c499913a260f5479503d919c114..71dbdb6e4107d81c9522b29832d43d379ed959f2 100644 --- a/src/apis/server.ts +++ b/src/apis/server.ts @@ -18,6 +18,7 @@ import type { } from 'axios'; import { ElMessage } from 'element-plus'; import { successMsg } from 'src/components/Message'; +import { getBaseProxyUrl } from 'src/utils/tools'; export interface FcResponse { error: string; @@ -35,10 +36,12 @@ export interface IAnyObj { export type Fn = (data: FcResponse) => unknown; -const baseURL = - import.meta.env.MODE === 'electron-production' - ? import.meta.env.VITE_BASE_PROXY_URL - : './'; +const baseURL: string = './'; +if (import.meta.env.MODE === 'electron-production') { + getBaseProxyUrl().then((url) => { + server.defaults.baseURL = url; + }); +} // 创建 axios 实例 export const server = axios.create({ diff --git a/src/apis/tools.ts b/src/apis/tools.ts index 80b8b05389d2e7aa2645a8ef8152a5de02c3d7cf..7c75fc66ed7876af13e9bd747a9184ceee34c99b 100644 --- a/src/apis/tools.ts +++ b/src/apis/tools.ts @@ -10,6 +10,7 @@ import { ElNotification, ElMessageBox } from 'element-plus'; import { LOGOUT_CALLBACK_URL } from 'src/views/dialogue/constants'; import { useAccountStore } from 'src/store'; +import { getBaseProxyUrl } from 'src/utils/tools'; import type { AxiosError, @@ -22,9 +23,7 @@ import i18n from 'src/i18n'; function getCookie(name: string) { const matches = document.cookie.match( new RegExp( - '(?:^|; )' + - name.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, '\\$1') + - '=([^;]*)', + '(?:^|; )' + name.replace(/([.$?*|{}()[]\\\/\+^])/g, '\\$1') + '=([^;]*)', ), ); return matches ? decodeURIComponent(matches[1]) : undefined; @@ -64,8 +63,8 @@ async function toAuthorization() { `width=${w},height=${h},resizable=yes,scrollbars=yes,top=${top},left=${left}`, ); - const postMessageListener = (event: MessageEvent) => { - const AUTH_SERVER_URL = import.meta.env.VITE_BASE_PROXY_URL; + const postMessageListener = async (event: MessageEvent) => { + const AUTH_SERVER_URL = await getBaseProxyUrl(); // 期望 event.data = { type: 'auth_success', sessionId: 'xxxx' } const { sessionId, type } = event.data || {}; // 校验域名,防止攻击,兼容 Electron 没有域名的情况 diff --git a/src/i18n/lang/en.ts b/src/i18n/lang/en.ts index c0774861ad272864ec43d805d3639b283350c956..5e9b2b2b3d764d5a6494728d1cc6618c2602c01b 100644 --- a/src/i18n/lang/en.ts +++ b/src/i18n/lang/en.ts @@ -122,7 +122,7 @@ export default { service_agreement: 'Service Agreement', privacy_policy: 'Privacy Policy', contact_us: 'Contact Us', - version: 'Version 0.9.5', + version: 'Version 0.9.6-Beta', }, history: { new_chat: 'New Chat', diff --git a/src/i18n/lang/zh-cn.ts b/src/i18n/lang/zh-cn.ts index c0159ec0148c180a0a4160287b6fc69c4d65f651..a1ef030473806d183c249c923a638c5106c0cf06 100644 --- a/src/i18n/lang/zh-cn.ts +++ b/src/i18n/lang/zh-cn.ts @@ -25,7 +25,7 @@ export default { url: '请输入 URL', api_key: '请输入 API KEY', model_name: '请选择模型', - max_token: '请输入最大Token', + max_token: '请输入最大 Token 数', }, }, home: { @@ -116,7 +116,7 @@ export default { refresh: '换一换', query_interpretation: '请选择识别方式', Automatic: '自动识别', - ask_me_anything: '在此输入您想了解的内容,输入Shift+Enter换行', + ask_me_anything: '在此输入您想了解的内容,输入 Shift+Enter 换行', you_might_want_to_know: '你可能想问', close: '关闭', email1: '联系邮箱', @@ -125,7 +125,7 @@ export default { service_agreement: '服务协议', privacy_policy: '隐私政策', contact_us: '联系我们', - version: '版本号0.9.5-内测版', + version: '版本号0.9.6-内测版', }, history: { new_chat: '新建对话', @@ -163,7 +163,7 @@ export default { feedbackSuccesful: '反馈成功', regenerate: '重新生成', try_ask_me: '你可以继续问我:', - eulercopilot_is_thinking: 'openEuler Intelligence正在生成回答...', + eulercopilot_is_thinking: 'openEuler Intelligence 正在生成回答...', generation_stopped: '回答已停止生成', stop: '停止回答', stopSuccessful: '暂停成功', @@ -213,55 +213,56 @@ export default { password: '密码', enter_password: '请输入密码', incorrect_password: '密码输入有误', - api_key_management: 'API Key管理', - no_api_key_available: '暂无可用的API Key', - create_api_key: '新建API Key', - api_key_display_once: '此API Key只展示一次,请复制后妥善保存。', + api_key_management: 'API Key 管理', + no_api_key_available: '暂无可用的 API Key', + create_api_key: '新建 API Key', + api_key_display_once: '此 API Key 只展示一次,请复制后妥善保存。', revoke: '撤销', refresh: '刷新', unauthorized: '页面未授权,请先登录', }, question: { - open_euler_community_edition_categories: 'openEuler社区版本有哪些分类?', + open_euler_community_edition_categories: 'openEuler 社区版本有哪些分类?', lts_release_cycle_and_support: - 'openEuler长期支持版本的发布间隔周期和社区支持各是多久?', + 'openEuler 长期支持版本的发布间隔周期和社区支持各是多久?', innovation_release_cycle_and_support: - 'openEuler社区创新版本的发布间隔周期和社区支持各是多久?', + 'openEuler 社区创新版本的发布间隔周期和社区支持各是多久?', container_cloud_platform_solution: - 'openEuler社区的容器云管理平台解决方案(CCPS)是什么?', - sec_gear_main_functions: 'secGear主要提供哪三大能力?', - dde_description: 'DDE是一款什么组件?', - lustre_description: 'Lustre是什么?', + 'openEuler 社区的容器云管理平台解决方案(CCPS)是什么?', + sec_gear_main_functions: 'secGear 主要提供哪三大能力?', + dde_description: 'DDE 是一款什么组件?', + lustre_description: 'Lustre 是什么?', open_euler_testing_management_platform: - 'openEuler社区的测试管理平台是什么?', - open_euler_pkgship: 'openEuler的pkgship是什么?', + 'openEuler 社区的测试管理平台是什么?', + open_euler_pkgship: 'openEuler 的 pkgship 是什么?', open_euler_software_package_introduction_principles: - 'openEuler软件包引入原则是什么?', + 'openEuler 软件包引入原则是什么?', download_rpm_without_installing: - 'openEuler系统如何将一个RPM包下载到本地而不安装?', + 'openEuler 系统如何将一个RPM包下载到本地而不安装?', count_the_occurrences_of_the_hello: - '请给我一个shell命令,实现以下功能:计算test.txt文件中hello字符串的出现次数', + '请给我一个 shell 命令,实现以下功能:计算 test.txt 文件中 hello 字符串的出现次数', convert_uppercase_to_lowercase: - '给我一个shell命令,实现以下功能:linux命令将本目录及子目录文本文件中的大写字母修改成小写字母', + '给我一个 shell 命令,实现以下功能:linux 命令将本目录及子目录文本文件中的大写字母修改成小写字母', list_files_with_specific_permissions: - '给我一个shell命令,实现以下功能:shell命令查找当前目录下权限符合的文件并列出', + '给我一个 shell 命令,实现以下功能:shell 命令查找当前目录下权限符合的文件并列出', search_error_keyword_with_context: - '给我一个shell命令,实现以下功能:在/home目录及其子目录中查找关键字“error”的文本文件,并将匹配行以及它们前后的3行内容输出到名为“result.txt”的文件中', + '给我一个 shell 命令,实现以下功能:在 /home 目录及其子目录中查找关键字“error”的文本文件,并将匹配行以及它们前后的3行内容输出到名为“result.txt”的文件中', clear_dependencies_for_software_package: - 'openEuler系统如何清除软件源的依赖?', - gpgcheck_purpose_in_dnf: 'openEuler系统DNF中的gpgcheck参数是用来做什么的?', + 'openEuler 系统如何清除软件源的依赖?', + gpgcheck_purpose_in_dnf: + 'openEuler 系统 DNF 中的 gpgcheck 参数是用来做什么的?', installonly_limit_function_in_dnf: - 'openEuler系统DNF中的installonly_limit参数的作用是?', + 'openEuler 系统 DNF 中的 installonly_limit 参数的作用是?', clean_requirement_on_remove_function_in_dnf: - 'openEuler系统DNF中的clean_requirement_on_remove参数具有什么功能?', + 'openEuler 系统 DNF 中的 clean_requirement_on_remove 参数具有什么功能?', hunan_tobacco_monopoly_applications_on_openeuler: - '湖南省烟草专卖局基于openeuler系统有哪些应用?', + '湖南省烟草专卖局基于 openeuler 系统有哪些应用?', xsky_applications_on_openeuler: - 'XSKY星辰天合公司基于openeuler系统有哪些应用?', + 'XSKY星辰天合公司基于 openeuler 系统有哪些应用?', }, upload: { upload_tip_text: - '支持上传文件(最多上传10个,总大小限制为64MB)接受pdf、docx、doc、txt、md、xlsx', + '支持上传文件(最多上传10个,总大小限制为64MB)接受 pdf、docx、doc、txt、md、xlsx', uploading: '正在上传...', upload_fail: '上传失败', resolving: '正在解析...', @@ -273,14 +274,14 @@ export default { aside_session_file_count_back: '个文档', }, apikey: { - save_apikey: '此API KEY只展示一次,请复制后妥善保存', - no_apikey: '暂无可用的apikey', - create_apikey: '新建apikey', + save_apikey: '此 API KEY 只展示一次,请复制后妥善保存', + no_apikey: '暂无可用的 API Key', + create_apikey: '新建 API Key', cancel: '取消', }, witChainD: { witChainD: '资产库配置', - witChainD_id: '资产库id', + witChainD_id: '资产库 ID', describe_the_witChainD: '请输入', find_witChainD: '请输入知识库名称/ID', }, diff --git a/src/store/conversation.ts b/src/store/conversation.ts index 8a057e81b5af487b50b35bdfe05c88618240b247..9d1960c1f26410c8b78177b401fb36b6843761f9 100644 --- a/src/store/conversation.ts +++ b/src/store/conversation.ts @@ -25,13 +25,12 @@ import { Application } from 'src/apis/paths/type'; import { handleAuthorize } from 'src/apis/tools'; import $bus from 'src/bus/index'; import { fetchEventSource } from '@microsoft/fetch-event-source'; +import { getBaseProxyUrl } from 'src/utils/tools'; -const STREAM_URL = '/api/chat'; -const newStreamUrl = 'api/chat'; let controller = new AbortController(); -export var txt2imgPath = ref(''); -export var echartsObj = ref({}); -export var echartsHas = ref(false); +export const txt2imgPath = ref(''); +export const echartsObj = ref({}); +export const echartsHas = ref(false); const excelPath = ref(''); const resp = ref(); const features = { @@ -315,10 +314,12 @@ export const useSessionStore = defineStore('conversation', () => { $bus.emit('getNodesStatue', { data: message }); } }; + const baseProxyUrl = await getBaseProxyUrl(); + const streamUrl = baseProxyUrl + '/api/chat'; if (params.user_selected_flow) { // 之前的对话历史记录 if (!params.type) { - await fetchEventSource(STREAM_URL, { + await fetchEventSource(streamUrl, { signal: controller.signal, keepalive: true, method: 'POST', @@ -346,7 +347,7 @@ export const useSessionStore = defineStore('conversation', () => { }); } else { // 新的工作流调试记录 - await fetchEventSource(newStreamUrl, { + await fetchEventSource(streamUrl, { signal: controller.signal, keepalive: true, method: 'POST', @@ -371,7 +372,7 @@ export const useSessionStore = defineStore('conversation', () => { } } else if (params.user_selected_app) { // 新的工作流调试记录 - await fetchEventSource(STREAM_URL, { + await fetchEventSource(streamUrl, { signal: controller.signal, keepalive: true, method: 'POST', @@ -401,7 +402,7 @@ export const useSessionStore = defineStore('conversation', () => { } else if (false) { //写传参数情况 } else { - await fetchEventSource(STREAM_URL, { + await fetchEventSource(streamUrl, { signal: controller.signal, keepalive: true, method: 'POST', diff --git a/src/utils/tools.ts b/src/utils/tools.ts index 9a4474dc9e723eb45a159ab7395d01bba7033d16..8ca89147cefa14dc09d3cc338fbaa8826551c6a4 100644 --- a/src/utils/tools.ts +++ b/src/utils/tools.ts @@ -30,11 +30,11 @@ type HtmlEvent = 'copyPreCode'; * @param data 自定义属性 */ export const onHtmlEventDispatch = ( - _t: any, - _ty: any, - event: any, + _t: EventTarget | null, + _ty: string, + _event: Event, type: HtmlEvent, - data: any, + data: string, ): void => { if (type === 'copyPreCode') { const code = document.getElementById(data); @@ -69,8 +69,32 @@ export const writeText = (text: string): void => { textArea.focus(); textArea.select(); new Promise((res, rej) => { - document.execCommand('copy') ? res() : rej(new Error('复制失败')); + if (document.execCommand('copy')) { + res(); + } else { + rej(new Error('复制失败')); + } textArea.remove(); }); } }; + +/** + * 获取后端代理URL,兼容web、electron开发和electron生产 + */ +export async function getBaseProxyUrl(): Promise { + // Electron 生产环境(file:协议)读取配置 + if ( + window.eulercopilot && + typeof window.eulercopilot.ipcRenderer?.getProxyUrl === 'function' && + window.location.protocol === 'file:' + ) { + const url = await window.eulercopilot.ipcRenderer.getProxyUrl(); + if (url) return url; + } + // 本地开发环境(localhost:3000),直接返回空字符串,确保 axios 只拼接 path + if (window.location.hostname === 'localhost') { + return ''; + } + return import.meta.env.VITE_BASE_PROXY_URL; +}