feat: 优化桌面应用打包方式,改为ZIP格式以简化存储路径和代码架构

This commit is contained in:
linshen committed 2025-07-05 21:05:32 +08:00
1 parent 3c9f468b73
commit b5f3b263f7
11 files changed
+203 -61

No files matched your search

+9 -9
View File
@@ -40,7 +40,7 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: windows-build
path: packages/desktop/dist/*.exe
path: packages/desktop/dist/*.zip
retention-days: 30
# 构建 macOS 版本
@@ -72,7 +72,7 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: macos-build
path: packages/desktop/dist/*.dmg
path: packages/desktop/dist/*.zip
retention-days: 30
# 构建 Linux 版本
@@ -104,7 +104,7 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: linux-build
path: packages/desktop/dist/*.AppImage
path: packages/desktop/dist/*.zip
retention-days: 30
# 创建 GitHub Release
@@ -192,14 +192,14 @@ jobs:
## 🚀 Prompt Optimizer $CURRENT_TAG
### 📦 下载
- **Windows**: \`Prompt Optimizer Setup *.exe\`
- **macOS**: \`Prompt Optimizer-*.dmg\`
- **Linux**: \`Prompt Optimizer-*.AppImage\`
- **Windows**: \`PromptOptimizer-*-win-x64.zip\`
- **macOS**: \`PromptOptimizer-*-darwin-x64.zip\` 或 \`PromptOptimizer-*-darwin-arm64.zip\`
- **Linux**: \`PromptOptimizer-*-linux-x64.zip\`
### 🔧 安装说明
- **Windows**: 下载 \`.exe\` 文件直接安装
- **macOS**: 下载 \`.dmg\` 文件,拖拽到应用程序文件夹
- **Linux**: 下载 \`.AppImage\` 文件,添加执行权限后直接运行
- **Windows**: 下载 \`.zip\` 文件,解压到任意目录,运行 \`PromptOptimizer.exe\`
- **macOS**: 下载 \`.zip\` 文件,解压到任意目录,运行 \`PromptOptimizer.app\`
- **Linux**: 下载 \`.zip\` 文件,解压到任意目录,运行 \`PromptOptimizer\`
### 📝 更新内容
$COMMITS_LIMITED
+15 -4
View File
@@ -18,7 +18,7 @@
## 📖 项目简介
Prompt Optimizer是一个强大的AI提示词优化工具,帮助你编写更好的AI提示词,提升AI输出质量。支持Web应用和Chrome插件两种使用方式。
Prompt Optimizer是一个强大的AI提示词优化工具,帮助你编写更好的AI提示词,提升AI输出质量。支持Web应用、桌面应用和Chrome插件三种使用方式。
### 🎥 功能演示
@@ -34,7 +34,7 @@ Prompt Optimizer是一个强大的AI提示词优化工具,帮助你编写更
- ⚙️ **高级参数配置**:支持为每个模型单独配置temperature、max_tokens等LLM参数
- 🔒 **安全架构**:纯客户端处理,数据直接与AI服务商交互,不经过中间服务器
- 💾 **隐私保护**:本地加密存储历史记录和API密钥,支持数据导入导出
- 📱 **多端支持**:同时提供Web应用和Chrome插件两种使用方式
- 📱 **多端支持**:同时提供Web应用、桌面应用和Chrome插件三种使用方式
- 🎨 **用户体验**:简洁直观的界面设计,响应式布局和流畅交互动效
- 🌐 **跨域支持**:Vercel部署时支持使用Edge Runtime代理解决跨域问题
- 🔐 **访问控制**:支持密码保护功能,保障部署安全
@@ -62,11 +62,22 @@ Prompt Optimizer是一个强大的AI提示词优化工具,帮助你编写更
更多详细的部署步骤和注意事项,请查看:
- [Vercel部署指南](docs/vercel.md)
### 3. 安装Chrome插件
### 3. 下载桌面应用
1. 从 [GitHub Releases](https://github.com/linshenkx/prompt-optimizer/releases) 下载最新版本
2. **Windows**: 下载 `PromptOptimizer-*-win-x64.zip`,解压到任意目录,运行 `PromptOptimizer.exe`
3. **macOS**: 下载 `PromptOptimizer-*-darwin-x64.zip` 或 `PromptOptimizer-*-darwin-arm64.zip`,解压到任意目录,运行 `PromptOptimizer.app`
4. **Linux**: 下载 `PromptOptimizer-*-linux-x64.zip`,解压到任意目录,运行 `PromptOptimizer`
**桌面应用特点**:
- ✅ **便携模式**:数据存储在应用同级目录,便于备份和迁移
- ✅ **离线使用**:无需浏览器,独立运行
- ✅ **性能更好**:原生应用体验,响应更快
### 4. 安装Chrome插件
1. 从Chrome商店安装(由于审批较慢,可能不是最新的):[Chrome商店地址](https://chromewebstore.google.com/detail/prompt-optimizer/cakkkhboolfnadechdlgdcnjammejlna)
2. 点击图标即可打开提示词优化器
### 4. Docker部署
### 5. Docker部署
```bash
# 运行容器(默认配置)
docker run -d -p 80:80 --restart unless-stopped --name prompt-optimizer linshen/prompt-optimizer
@@ -0,0 +1,150 @@
# 116 - 桌面应用打包优化
## 概述
将桌面应用从单文件portable模式改为ZIP压缩包模式,解决了存储路径检测问题,简化了代码架构。
## 问题背景
### 原有问题
1. **存储路径问题**:
- portable模式下,`process.execPath` 指向临时解压目录
- 数据保存在临时目录,应用关闭后被清理
- 路径检测逻辑复杂,容易出错
2. **架构复杂性**:
- 需要复杂的路径检测和回退逻辑
- 大量调试代码和日志输出
- 主进程日志在生产环境难以查看
## 解决方案
### 1. 修改打包配置
**之前(不同格式)**:
```json
{
"win": { "target": "portable" },
"mac": { "target": "dmg" },
"linux": { "target": "AppImage" }
}
```
**现在(统一ZIP格式)**:
```json
{
"win": {
"target": "zip",
"artifactName": "${productName}-${version}-${os}-${arch}.${ext}"
},
"mac": {
"target": "zip",
"artifactName": "${productName}-${version}-${os}-${arch}.${ext}"
},
"linux": {
"target": "zip",
"artifactName": "${productName}-${version}-${os}-${arch}.${ext}"
}
}
```
### 2. 简化存储路径逻辑
**之前(复杂检测)**:
- 多种路径检测方法
- 临时目录检查
- 复杂的回退逻辑
- 大量调试日志
**现在(简化逻辑)**:
```javascript
if (app.isPackaged) {
// ZIP包解压后的portable模式
const exePath = app.getPath('exe');
const execDir = path.dirname(exePath);
userDataPath = path.join(execDir, 'prompt-optimizer-data');
} else {
// 开发环境
userDataPath = path.join(__dirname, '..', '..', 'prompt-optimizer-data');
}
```
### 3. 移除调试代码
- 删除 `debugLog` 函数
- 移除文件日志输出
- 删除调试API和IPC接口
- 简化错误处理
## 实施步骤
### 1. 修改打包配置
- 更新 `packages/desktop/package.json`
- 改为ZIP目标格式
### 2. 简化main.js
- 移除复杂的路径检测逻辑
- 删除调试日志函数
- 简化存储初始化代码
### 3. 清理preload.js
- 移除调试API接口
### 4. 更新文档和工作流
- 修改GitHub Actions工作流
- 更新README.md使用说明
- 创建归档文档
## 优势
### 1. 技术优势
- ✅ **路径可靠**:ZIP解压后路径确定,无临时目录问题
- ✅ **代码简洁**:移除复杂检测逻辑,维护性更好
- ✅ **性能更好**:无额外文件I/O操作
### 2. 用户体验
- ✅ **真正portable**:解压到哪里,数据就在哪里
- ✅ **便于管理**:整个文件夹包含应用+数据
- ✅ **便于备份**:复制文件夹即可完整备份
### 3. 分发优势
- ✅ **文件名清晰**:包含版本、系统、架构信息
- ✅ **便于下载**:单个ZIP文件包含所有内容
- ✅ **跨平台一致**:所有平台都使用相同的分发方式
## 使用方法
### 构建
```bash
cd packages/desktop
pnpm run build
```
### 分发
- **Windows**: `PromptOptimizer-1.2.0-win-x64.zip`
- **macOS**: `PromptOptimizer-1.2.0-darwin-x64.zip` / `PromptOptimizer-1.2.0-darwin-arm64.zip`
- **Linux**: `PromptOptimizer-1.2.0-linux-x64.zip`
所有平台:
- 用户解压到任意目录
- 运行对应的可执行文件
- 数据保存在 `prompt-optimizer-data/` 目录
### 数据管理
- **备份**:复制整个应用文件夹
- **迁移**:移动整个文件夹到新位置
- **升级**:替换exe文件,保留数据目录
## 经验总结
1. **简单即美**:复杂的路径检测不如简单的ZIP解压
2. **用户友好**:便携模式更符合用户期望
3. **维护性**:简化的代码更容易维护和调试
4. **可靠性**:减少边界情况,提高稳定性
## 后续优化
1. **自动更新**:考虑添加应用内更新功能
2. **安装包选项**:为需要的用户提供传统安装包
3. **数据迁移**:提供从旧版本迁移数据的工具
+6 -1
View File
@@ -90,12 +90,17 @@
- 完整的数据持久化解决方案
- 高性能文件I/O和错误恢复机制
- **[116-desktop-packaging-optimization](./116-desktop-packaging-optimization/)** - 桌面应用打包优化 📦
- 从portable模式改为ZIP压缩包模式
- 解决存储路径检测问题
- 简化代码架构,提升用户体验
## 🔍 快速查找指南
### 按问题类型查找
- **启动问题** → 102-web-architecture-refactor
- **显示异常** → 112-desktop-ipc-fixes
- **存储问题** → 110-desktop-indexeddb-fix, 114-desktop-file-storage
- **存储问题** → 110-desktop-indexeddb-fix, 114-desktop-file-storage, 116-desktop-packaging-optimization
- **序列化错误** → 112-desktop-ipc-fixes
- **应用退出问题** → 114-desktop-file-storage
- **语言设置问题** → 112-desktop-ipc-fixes
+7 -5
View File
@@ -181,15 +181,15 @@ async function initializeServices() {
// 根据环境确定数据存储路径
let userDataPath;
if (app.isPackaged) {
// 生产环境:使用可执行文件所在目录下的prompt-optimizer-data文件夹
const execPath = process.execPath;
const execDir = path.dirname(execPath);
// 生产环境:ZIP包解压后的portable模式
const exePath = app.getPath('exe');
const execDir = path.dirname(exePath);
userDataPath = path.join(execDir, 'prompt-optimizer-data');
console.log('[DESKTOP] Production mode - Executable path:', execPath);
console.log('[DESKTOP] Production mode - portable storage in exe directory');
} else {
// 开发环境:使用项目根目录下的prompt-optimizer-data文件夹
userDataPath = path.join(__dirname, '..', '..', 'prompt-optimizer-data');
console.log('[DESKTOP] Development mode - Project root data folder');
console.log('[DESKTOP] Development mode - project root storage');
}
console.log('[DESKTOP] Data storage path:', userDataPath);
@@ -724,6 +724,8 @@ function setupIPC() {
}
});
// 环境配置同步 - 主进程作为唯一配置源
ipcMain.handle('config-getEnvironmentVariables', async (event) => {
try {
+6 -4
View File
@@ -33,14 +33,16 @@
"node_modules/**/*"
],
"win": {
"target": "portable",
"artifactName": "${productName}-${version}.${ext}"
"target": "zip",
"artifactName": "${productName}-${version}-${os}-${arch}.${ext}"
},
"mac": {
"target": "dmg"
"target": "zip",
"artifactName": "${productName}-${version}-${os}-${arch}.${ext}"
},
"linux": {
"target": "AppImage"
"target": "zip",
"artifactName": "${productName}-${version}-${os}-${arch}.${ext}"
}
},
"keywords": [
+2
View File
@@ -406,6 +406,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
return result.data;
}
},
// Data Manager interface
data: {
+1 -2
View File
@@ -11,5 +11,4 @@ export * from './useAutoScroll'
export * from './useClipboard'
export * from './useFullscreen'
export * from 'vue-i18n'
export * from './useStorage'
export * from './useAppInitializer'
export * from './useAppInitializer'
@@ -22,7 +22,7 @@ import {
createPreferenceService,
} from '../'; // 从UI包的index导入所有核心模块
import type { AppServices } from '../types/services';
import type { IStorageProvider, IModelManager, ITemplateManager, IHistoryManager, ILLMService, IPromptService, IDataManager } from '@prompt-optimizer/core';
import type { IModelManager, ITemplateManager, IHistoryManager, ILLMService, IPromptService, IDataManager } from '@prompt-optimizer/core';
import type { IPreferenceService } from '../types/services';
/**
@@ -39,7 +39,7 @@ export function useAppInitializer() {
try {
console.log('[AppInitializer] 开始应用初始化...');
let storageProvider: IStorageProvider;
let modelManager: IModelManager;
let templateManager: ITemplateManager;
let historyManager: IHistoryManager;
@@ -58,29 +58,9 @@ export function useAppInitializer() {
}
console.log('[AppInitializer] Electron API 就绪,初始化代理服务...');
// 在Electron渲染进程中,我们需要创建一个代理存储提供器
// 这个代理会将所有存储操作转发到主进程
storageProvider = {
async getItem(key: string): Promise<string | null> {
console.warn('[ElectronStorageProxy] getItem called, but storage should be handled by main process');
return null;
},
async setItem(key: string, value: string): Promise<void> {
console.warn('[ElectronStorageProxy] setItem called, but storage should be handled by main process');
},
async removeItem(key: string): Promise<void> {
console.warn('[ElectronStorageProxy] removeItem called, but storage should be handled by main process');
},
async clearAll(): Promise<void> {
console.warn('[ElectronStorageProxy] clear called, but storage should be handled by main process');
},
async updateData<T>(key: string, modifier: (currentValue: T | null) => T): Promise<void> {
console.warn('[ElectronStorageProxy] updateData called, but storage should be handled by main process');
},
async batchUpdate(operations: Array<{ key: string; operation: 'set' | 'remove'; value?: string }>): Promise<void> {
console.warn('[ElectronStorageProxy] batchUpdate called, but storage should be handled by main process');
}
};
// 在Electron环境中,不需要storageProvider
// 所有存储操作都通过各个manager的代理完成
// 在Electron环境中,我们实例化所有轻量级的代理类
modelManager = new ElectronModelManagerProxy();
@@ -97,7 +77,6 @@ export function useAppInitializer() {
const templateLanguageService = new ElectronTemplateLanguageServiceProxy();
services.value = {
storageProvider, // 使用代理存储提供器
modelManager,
templateManager,
historyManager,
@@ -112,7 +91,7 @@ export function useAppInitializer() {
} else {
console.log('[AppInitializer] 检测到Web环境,初始化完整服务...');
// 在Web环境中,我们创建一套完整的、真实的服务
storageProvider = StorageFactory.create('dexie');
const storageProvider = StorageFactory.create('dexie');
const languageService = createTemplateLanguageService(storageProvider);
// Services with no dependencies or only storage
@@ -201,7 +180,6 @@ export function useAppInitializer() {
// 将所有服务实例赋值给 services.value
services.value = {
storageProvider,
modelManager: modelManagerAdapter, // 使用适配器
templateManager: templateManagerAdapter, // 使用适配器
historyManager: historyManagerAdapter, // 使用适配器
-2
View File
@@ -1,5 +1,4 @@
import type {
IStorageProvider,
IModelManager,
ITemplateManager,
IHistoryManager,
@@ -22,7 +21,6 @@ export interface IPreferenceService {
* 统一的应用服务接口定义
*/
export interface AppServices {
storageProvider: IStorageProvider;
modelManager: IModelManager;
templateManager: ITemplateManager;
historyManager: IHistoryManager;
@@ -10,12 +10,7 @@ vi.mock('../../src/composables/useToast', () => ({
})
}))
vi.mock('../../src/composables/useStorage', () => ({
useStorage: () => ({
getItem: vi.fn().mockResolvedValue(null),
setItem: vi.fn().mockResolvedValue(undefined)
})
}))
// useStorage已被移除,不再需要mock
vi.mock('vue-i18n', () => ({
useI18n: () => ({