feat: 优化桌面应用打包方式,改为ZIP格式以简化存储路径和代码架构
This commit is contained in:
1 parent
3c9f468b73
commit
b5f3b263f7
11 files changed
+203
-61
No files matched your search
@@ -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
|
||||
|
||||
@@ -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. **数据迁移**:提供从旧版本迁移数据的工具
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -406,6 +406,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
return result.data;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
|
||||
// Data Manager interface
|
||||
data: {
|
||||
|
||||
@@ -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, // 使用适配器
|
||||
|
||||
@@ -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: () => ({
|
||||
|
||||
Reference in new issue
Block a user