diff --git a/.npmrc b/.npmrc index f668a92..2abb0d6 100644 --- a/.npmrc +++ b/.npmrc @@ -1,6 +1,6 @@ -# registry=https://registry.npmmirror.com -# electron_mirror=https://npmmirror.com/mirrors/electron/ -# electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/ +registry=https://registry.npmmirror.com +electron_mirror=https://npmmirror.com/mirrors/electron/ +electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/ package-import-method=copy shamefully-hoist=true diff --git a/Cron-Development-Plan.md b/Cron-Development-Plan.md new file mode 100644 index 0000000..6deb294 --- /dev/null +++ b/Cron-Development-Plan.md @@ -0,0 +1,449 @@ +# zn-ai 定时任务(Cron)功能开发计划 + +## 项目概述 + +基于ClawX项目的定时任务功能分析,制定zn-ai项目的功能迁移开发计划。目标是在zn-ai项目中实现类似的定时任务管理功能,保持核心功能一致性,同时适配zn-ai的技术栈(Vue 3 + Element Plus + Pinia + Tailwind CSS)。 + +## 核心目标 + +1. **实现定时任务管理**:创建、编辑、删除、启用/禁用定时任务 +2. **支持调度配置**:预设调度和自定义Cron表达式 +3. **提供基础交付模式**:至少支持"仅应用内"模式 +4. **集成现有架构**:与zn-ai现有状态管理、API通信、国际化系统集成 +5. **保持UI一致性**:使用Element Plus和现有设计模式 + +## 技术约束 + +- **前端框架**:Vue 3 Composition API +- **状态管理**:Pinia +- **UI组件库**:Element Plus + Tailwind CSS +- **国际化**:Vue I18n +- **API通信**:现有`hostApiFetch` IPC机制 +- **后端集成**:需评估现有任务调度能力,必要时实现简化版本 + +## 开发原则 + +1. **分阶段实施**:优先核心功能,逐步增强 +2. **渐进式增强**:先实现基本功能,再添加高级特性 +3. **代码复用**:尽可能复用现有组件和工具函数 +4. **类型安全**:使用TypeScript确保类型安全 +5. **测试驱动**:关键功能编写单元测试 + +## 阶段一:基础架构与类型定义 + +### 目标 +建立定时任务功能的基础架构,包括类型定义、状态管理和API接口。 + +### 任务清单 + +#### 1.1 创建类型定义文件 +- **文件**:`src/lib/cron-types.ts` +- **内容**: + - 复制并适配ClawX的`CronJob`、`CronJobCreateInput`、`CronJobDelivery`等核心类型 + - 根据zn-ai需求调整类型定义(如简化交付配置) + - 添加必要的工具类型和类型守卫 +- **依赖**:无 +- **预估工时**:2小时 + +#### 1.2 创建Pinia Store +- **文件**:`src/store/cron.ts` +- **内容**: + - 定义store状态:`jobs`、`loading`、`error` + - 实现操作:`fetchJobs`、`createJob`、`updateJob`、`deleteJob`、`toggleJob`、`triggerJob` + - 使用现有`hostApiFetch`进行API调用 + - 添加适当的错误处理和状态管理 +- **依赖**:`cron-types.ts`、`hostApiFetch` +- **预估工时**:4小时 + +#### 1.3 创建API接口占位 +- **文件**:更新现有API服务或创建`src/lib/cron-api.ts` +- **内容**: + - 定义API端点常量 + - 创建API函数包装器(如果现有`hostApiFetch`不足) + - 添加模拟数据用于开发测试 +- **依赖**:`hostApiFetch`、`cron-types.ts` +- **预估工时**:3小时 + +#### 1.4 创建国际化文件 +- **文件**:`src/i18n/locales/{zh,en,ja}/cron.json` +- **内容**: + - 翻译ClawX的cron.json文件 + - 适配zn-ai的翻译键命名约定 + - 确保所有UI文本都有翻译键 +- **依赖**:ClawX翻译文件 +- **预估工时**:2小时 + +#### 1.5 更新国际化配置 +- **文件**:`src/i18n/constants.ts`、`src/i18n/index.ts` +- **内容**: + - 添加cron命名空间到`NAMESPACES`数组 + - 确保翻译文件正确加载 +- **依赖**:国际化文件 +- **预估工时**:1小时 + +### 交付成果 +- 完整的类型定义系统 +- 可工作的Pinia store(带模拟数据) +- 国际化支持 +- 基础API接口层 + +### 阶段验收标准 +- TypeScript编译无错误 +- Store基本操作可通过模拟数据工作 +- 国际化文件正确加载 +- 代码符合项目代码规范 + +## 阶段二:核心UI组件开发 + +### 目标 +开发定时任务功能的核心用户界面组件。 + +### 任务清单 + +#### 2.1 创建主页面组件 +- **文件**:`src/pages/cron/index.vue` +- **内容**: + - 页面布局和结构 + - 集成统计卡片区域 + - 任务列表展示区域 + - 空状态处理 + - 加载状态和错误显示 + - 创建任务按钮 +- **依赖**:Cron store、国际化 +- **预估工时**:6小时 + +#### 2.2 创建任务卡片组件 +- **文件**:`src/pages/cron/components/CronJobCard.vue` +- **内容**: + - 任务信息展示(名称、调度、消息预览) + - 状态指示器(启用/禁用) + - 操作按钮(运行、编辑、删除) + - 最后运行状态显示 + - 下次运行时间显示 + - 悬停和交互效果 +- **依赖**:Cron store、国际化、Element Plus组件 +- **预估工时**:5小时 + +#### 2.3 创建任务对话框组件 +- **文件**:`src/pages/cron/components/CronTaskDialog.vue` +- **内容**: + - 表单布局和字段(名称、消息、调度、交付模式等) + - 预设调度选择器 + - 自定义Cron表达式输入 + - 表单验证和错误提示 + - 调度预览(下一次运行时间) + - 提交和取消按钮 +- **依赖**:Element Plus表单组件、Cron store、国际化 +- **预估工时**:8小时 + +#### 2.4 创建统计卡片组件 +- **文件**:`src/pages/cron/components/CronStats.vue`(或复用现有模式) +- **内容**: + - 总任务数卡片 + - 活跃任务卡片 + - 暂停任务卡片 + - 失败任务卡片 + - 响应式布局 +- **依赖**:Cron store、国际化、Tailwind CSS +- **预估工时**:3小时 + +#### 2.5 创建工具函数 +- **文件**:`src/pages/cron/utils.ts` +- **内容**: + - Cron表达式解析和格式化函数 + - 下一次运行时间预估函数 + - 时间格式化辅助函数 + - 交付模式处理函数 +- **依赖**:`cron-types.ts` +- **预估工时**:4小时 + +### 交付成果 +- 完整的用户界面组件 +- 响应式布局和设计 +- 基本的用户交互 +- 表单验证和反馈 + +### 阶段验收标准 +- 所有组件可独立渲染 +- 表单验证正常工作 +- 响应式设计适配不同屏幕尺寸 +- 与现有设计风格一致 + +## 阶段三:功能集成与数据绑定 + +### 目标 +将UI组件与store和API集成,实现完整的功能流程。 + +### 任务清单 + +#### 3.1 集成Store到主页面 +- **内容**: + - 连接主页面与Cron store + - 实现数据加载和状态管理 + - 添加刷新功能 + - 处理空状态和错误状态 +- **依赖**:阶段一和二的成果 +- **预估工时**:3小时 + +#### 3.2 实现任务操作集成 +- **内容**: + - 连接任务卡片操作按钮到store方法 + - 实现启用/禁用切换 + - 实现立即触发功能 + - 实现删除功能(带确认) +- **依赖**:Cron store、Element Plus MessageBox +- **预估工时**:4小时 + +#### 3.3 实现对话框表单提交 +- **内容**: + - 连接对话框表单到store创建/更新方法 + - 实现表单数据验证 + - 添加提交状态反馈 + - 处理成功/失败场景 +- **依赖**:Cron store、Element Plus表单验证 +- **预估工时**:5小时 + +#### 3.4 实现调度预览功能 +- **内容**: + - 实时计算并显示下一次运行时间 + - 支持预设调度和自定义Cron表达式 + - 添加时间格式化 +- **依赖**:Cron工具函数 +- **预估工时**:3小时 + +#### 3.5 添加加载状态和反馈 +- **内容**: + - 全局加载状态管理 + - 操作反馈(成功/错误toast) + - 网络错误处理 + - 数据缓存策略 +- **依赖**:Element Plus反馈组件、Cron store +- **预估工时**:3小时 + +#### 3.6 集成路由和导航 +- **文件**:`src/router/index.ts` +- **内容**: + - 添加定时任务页面路由 + - 更新侧边栏菜单(如果需要) + - 添加路由守卫(如果需要) +- **依赖**:主页面组件 +- **预估工时**:2小时 + +### 交付成果 +- 完整的功能流程 +- 实时数据绑定 +- 用户操作反馈 +- 错误处理和恢复 + +### 阶段验收标准 +- 可以完整执行创建、编辑、删除任务流程 +- 所有操作有适当的反馈 +- 数据实时同步 +- 错误场景得到妥善处理 + +## 阶段四:测试、优化与完善 + +### 目标 +确保功能质量,优化性能,添加高级特性。 + +### 任务清单 + +#### 4.1 编写单元测试 +- **文件**:`tests/unit/cron-store.test.ts`、`tests/unit/cron-utils.test.ts` +- **内容**: + - Store逻辑单元测试 + - 工具函数单元测试 + - 组件逻辑单元测试 +- **依赖**:Vue Test Utils、Vitest +- **预估工时**:6小时 + +#### 4.2 添加集成测试 +- **文件**:`tests/e2e/cron.spec.ts` +- **内容**: + - 完整用户流程测试 + - 表单验证测试 + - 错误场景测试 +- **依赖**:Playwright或Cypress +- **预估工时**:5小时 + +#### 4.3 性能优化 +- **内容**: + - 虚拟滚动(如果任务数量多) + - 数据缓存优化 + - 减少不必要的重新渲染 + - 代码分割和懒加载 +- **预估工时**:4小时 + +#### 4.4 可访问性改进 +- **内容**: + - 添加ARIA属性 + - 键盘导航支持 + - 屏幕阅读器优化 + - 颜色对比度检查 +- **预估工时**:3小时 + +#### 4.5 高级功能增强 +- **内容**: + - 任务导入/导出功能 + - 批量操作支持 + - 任务执行历史查看 + - 高级调度选项(时区支持等) +- **预估工时**:8小时(可选) + +#### 4.6 文档编写 +- **内容**: + - 组件API文档 + - 用户使用指南 + - 开发指南 +- **预估工时**:3小时 + +### 交付成果 +- 完整的测试覆盖 +- 优化后的性能 +- 可访问性支持 +- 可选的高级功能 + +### 阶段验收标准 +- 关键路径测试覆盖率达到80%以上 +- 性能指标符合要求 +- 通过可访问性检查 +- 代码质量通过审查 + +## 时间总览 + +| 阶段 | 任务数 | 预估工时 | 累计工时 | +|------|--------|----------|----------| +| 阶段一 | 5 | 12小时 | 12小时 | +| 阶段二 | 5 | 26小时 | 38小时 | +| 阶段三 | 6 | 20小时 | 58小时 | +| 阶段四 | 6 | 29小时 | 87小时 | +| **总计** | **22** | **87小时** | **87小时** | + +*注:阶段四的高级功能增强为可选,如不包括则总工时为79小时* + +## 风险与缓解措施 + +### 技术风险 + +#### 风险1:后端调度引擎缺失 +- **描述**:zn-ai可能没有现成的任务调度引擎 +- **影响**:核心功能无法实现 +- **概率**:中 +- **缓解措施**: + 1. 评估现有`electron/process/`目录中的任务执行能力 + 2. 实现简化版的Node.js调度器(使用`node-cron`或`node-schedule`) + 3. 与Gateway功能解耦,设计可插拔的后端接口 + +#### 风险2:频道系统不兼容 +- **描述**:zn-ai的频道系统与ClawX的频道系统完全不同 +- **影响**:交付功能无法直接迁移 +- **概率**:高 +- **缓解措施**: + 1. 阶段一评估现有频道系统能力 + 2. 先实现"仅应用内"模式 + 3. 设计适配器层,未来集成现有频道系统 + 4. 简化交付配置,仅支持基本功能 + +#### 风险3:Cron表达式复杂性 +- **描述**:Cron表达式解析和验证复杂 +- **影响**:用户输入错误导致任务不执行 +- **概率**:低 +- **缓解措施**: + 1. 使用成熟的Cron解析库(如`cron-parser`) + 2. 提供预设调度选项,减少用户直接输入 + 3. 添加实时验证和预览 + +### 资源风险 + +#### 风险1:开发时间不足 +- **描述**:预估工时可能低估 +- **影响**:项目延期 +- **概率**:中 +- **缓解措施**: + 1. 优先实现核心功能(阶段一至三) + 2. 阶段四作为后续迭代 + 3. 定期进度评估和调整 + +#### 风险2:技能匹配问题 +- **描述**:开发人员对Vue 3或Element Plus不熟悉 +- **影响**:开发效率低下 +- **概率**:低 +- **缓解措施**: + 1. 提供代码示例和参考实现 + 2. 复用现有组件模式 + 3. 代码审查和结对编程 + +## 成功标准 + +### 功能成功标准 +1. 用户可以创建、编辑、删除定时任务 +2. 任务可以按预定调度执行 +3. 用户界面直观易用 +4. 系统稳定可靠,错误处理得当 + +### 技术成功标准 +1. 代码质量高,符合项目规范 +2. 测试覆盖关键功能路径 +3. 性能满足要求 +4. 可维护性和可扩展性好 + +### 业务成功标准 +1. 功能满足用户需求 +2. 用户体验良好 +3. 与现有系统集成顺畅 +4. 为未来扩展奠定基础 + +## 下一步行动 + +1. **需求确认**:与相关方确认功能范围和优先级 +2. **技术评估**:详细评估后端调度能力 +3. **环境准备**:设置开发环境和测试数据 +4. **开始实施**:按照阶段一任务开始开发 + +## 附录 + +### A. 文件结构规划 + +``` +src/ +├── lib/ +│ ├── cron-types.ts # 类型定义 +│ └── cron-api.ts # API接口(可选) +├── store/ +│ └── cron.ts # Pinia store +├── pages/ +│ └── cron/ +│ ├── components/ +│ │ ├── CronJobCard.vue +│ │ ├── CronTaskDialog.vue +│ │ └── CronStats.vue +│ ├── utils.ts # 工具函数 +│ └── index.vue # 主页面 +└── i18n/ + └── locales/ + ├── zh/ + │ └── cron.json + ├── en/ + │ └── cron.json + └── ja/ + └── cron.json +``` + +### B. 依赖库评估 + +1. **Cron解析**:`cron-parser`(成熟、稳定、TypeScript支持) +2. **时间处理**:使用原生Date对象或`day.js`(如果项目已使用) +3. **UI组件**:Element Plus(项目已使用) +4. **状态管理**:Pinia(项目已使用) + +### C. 与现有系统集成点 + +1. **用户认证**:使用现有用户会话 +2. **API通信**:使用现有`hostApiFetch`机制 +3. **错误处理**:集成现有错误处理系统 +4. **主题系统**:支持现有明暗主题 +5. **路由系统**:集成现有路由配置 + +--- + +*本计划将根据实际情况进行迭代更新。建议每阶段结束后进行回顾和调整。* \ No newline at end of file diff --git a/Cron-implementation-reference.md b/Cron-implementation-reference.md new file mode 100644 index 0000000..b12aea2 --- /dev/null +++ b/Cron-implementation-reference.md @@ -0,0 +1,356 @@ +# ClawX 定时任务(Cron)功能实现分析参考 + +## 1. 功能概述 + +ClawX的定时任务(Cron)功能允许用户创建、管理、调度和自动化AI工作流。主要功能包括: + +- **任务管理**:创建、编辑、删除、启用/禁用定时任务 +- **调度配置**:支持预设调度(每分钟、每5分钟、每小时、每天9点等)和自定义Cron表达式 +- **交付模式**: + - **仅应用内**:任务结果保留在应用内部 + - **外部频道推送**:将任务结果通过已配置的频道(如飞书、Telegram、QQ机器人、企业微信、微信等)推送到指定目标 +- **频道集成**:选择已配置的频道账户,并选择目标(用户、群组、频道) +- **任务执行**:立即触发任务执行,查看最近运行记录和状态 +- **统计概览**:显示任务总数、活跃、暂停、失败的任务数量 +- **依赖Gateway**:需要Gateway服务运行才能管理任务 + +## 2. 技术架构分析 + +### 2.1 前端架构(ClawX - React) + +- **框架**:React 18 + TypeScript +- **状态管理**:Zustand(轻量级状态管理) +- **UI组件库**:shadcn/ui + Tailwind CSS +- **HTTP客户端**:自定义`hostApiFetch`封装IPC请求 +- **国际化**:react-i18next +- **路由**:基于文件系统的路由(未显示但推测使用React Router) + +### 2.2 后端架构(ClawX - Electron) + +- **API路由**:`electron/api/routes/cron.ts`处理所有定时任务相关请求 +- **数据存储**:Gateway管理的Cron作业配置,本地JSON文件(`cron.json`和`runs/*.jsonl`日志) +- **Gateway集成**:通过Gateway Manager RPC调用与OpenClaw交互 +- **错误处理**:优雅降级,当Gateway不可用时回退到直接读取本地文件 +- **数据转换**:在Gateway数据格式和UI数据格式之间进行转换 + +### 2.3 数据流 + +``` +UI组件 (React) → Zustand Store → hostApiFetch → Electron IPC → API路由 (cron.ts) + ↓ ↓ + 状态更新 Gateway Manager RPC + ↓ ↓ + 重新渲染 OpenClaw Cron引擎 +``` + +## 3. 关键组件分析 + +### 3.1 主页面组件 (`Cron/index.tsx`) + +- **功能**:展示任务列表、统计卡片、创建/编辑对话框、任务卡片 +- **子组件**: + - `TaskDialog`:创建/编辑任务的模态对话框 + - `CronJobCard`:单个任务卡片,显示任务详情和操作按钮 +- **状态管理**:使用`useCronStore`获取任务列表和操作函数 +- **依赖检查**:检查Gateway状态,显示警告信息 + +### 3.2 任务对话框组件 (`TaskDialog`) + +- **表单字段**: + - 任务名称、消息/提示词 + - 调度选择(预设或自定义Cron表达式) + - 交付模式选择(仅应用内/外部频道) + - 频道选择、账户选择、目标选择 + - 立即启用开关 +- **动态加载**:根据选择的频道动态加载可用的账户和目标 +- **验证**:前端表单验证,显示错误提示 +- **调度预览**:显示下一次运行时间的预估 + +### 3.3 任务卡片组件 (`CronJobCard`) + +- **信息展示**:任务名称、调度描述、消息预览、交付频道、最后运行状态、下次运行时间 +- **操作按钮**:运行现在、编辑、删除、启用/禁用切换 +- **状态指示器**:通过颜色圆点表示启用/禁用状态 +- **错误显示**:最后运行失败时显示错误信息 + +### 3.4 Store模块 (`stores/cron.ts`) + +- **状态**:`jobs`(任务列表)、`loading`(加载状态)、`error`(错误信息) +- **操作**: + - `fetchJobs`:获取任务列表 + - `createJob`:创建新任务 + - `updateJob`:更新任务 + - `deleteJob`:删除任务 + - `toggleJob`:启用/禁用任务 + - `triggerJob`:立即触发任务 +- **优化**:当已有数据时使用陈旧-重新验证模式,避免不必要的加载状态 + +## 4. API接口分析 + +### 4.1 接口端点 + +| 端点 | 方法 | 描述 | +|------|------|------| +| `/api/cron/jobs` | GET | 获取所有定时任务列表 | +| `/api/cron/jobs` | POST | 创建新的定时任务 | +| `/api/cron/jobs/{id}` | PUT | 更新指定定时任务 | +| `/api/cron/jobs/{id}` | DELETE | 删除指定定时任务 | +| `/api/cron/toggle` | POST | 启用/禁用定时任务 | +| `/api/cron/trigger` | POST | 立即触发定时任务 | +| `/api/cron/session-history` | GET | 获取任务运行历史记录(用于聊天会话) | + +### 4.2 数据格式 + +#### 任务对象 (`CronJob`) +```typescript +interface CronJob { + id: string; + name: string; + message: string; + schedule: string | CronSchedule; // Cron表达式或Gateway调度对象 + delivery?: CronJobDelivery; // 交付配置 + target?: CronJobTarget; // 目标信息(UI展示用) + enabled: boolean; + createdAt: string; + updatedAt: string; + lastRun?: CronJobLastRun; // 最后运行信息 + nextRun?: string; // 下次运行时间 +} +``` + +#### 创建任务输入 (`CronJobCreateInput`) +```typescript +interface CronJobCreateInput { + name: string; + message: string; + schedule: string; // Cron表达式字符串 + delivery?: CronJobDelivery; + enabled?: boolean; +} +``` + +#### 交付配置 (`CronJobDelivery`) +```typescript +interface CronJobDelivery { + mode: 'none' | 'announce'; + channel?: string; // 频道类型 + to?: string; // 目标ID + accountId?: string; // 账户ID +} +``` + +### 4.3 Gateway数据转换 + +API路由负责在Gateway数据格式和UI数据格式之间转换: + +- **Gateway格式**:包含`createdAtMs`、`updatedAtMs`等毫秒时间戳 +- **UI格式**:使用ISO字符串时间格式 +- **调度对象**:Gateway使用`{kind: 'cron', expr: '...'}`格式,UI支持字符串和对象两种格式 + +## 5. 数据类型定义 + +### 5.1 核心类型 (`types/cron.ts`) + +- `CronJobDeliveryMode`: `'none' | 'announce'` +- `CronJobDelivery`: 交付配置接口 +- `CronJobTarget`: 目标信息接口(频道类型、频道ID、频道名称、接收者) +- `CronJobLastRun`: 最后运行信息接口 +- `CronSchedule`: Gateway调度对象(`at`、`every`、`cron`三种类型) +- `CronJob`: 完整的任务对象 +- `CronJobCreateInput`: 创建任务输入 +- `CronJobUpdateInput`: 更新任务输入 +- `ScheduleType`: 调度类型(`daily`、`weekly`、`monthly`、`interval`、`custom`) + +### 5.2 辅助类型 + +- `DeliveryChannelAccount`: 交付频道账户 +- `DeliveryChannelGroup`: 交付频道分组 +- `ChannelTargetOption`: 频道目标选项 + +## 6. 错误处理机制 + +### 6.1 前端错误处理 + +- **加载错误**:显示错误提示但保留现有数据 +- **表单验证**:实时验证并显示用户友好的错误消息 +- **操作反馈**:使用toast通知显示操作结果(成功/失败) +- **降级策略**:当Gateway不可用时显示警告,但仍允许查看任务(如果存在本地缓存) + +### 6.2 后端错误处理 + +- **Gateway超时**:8秒超时,超时后回退到读取本地文件 +- **数据修复**:检测并自动修复孤立代理任务中的交付配置问题 +- **输入验证**:验证交付频道支持性,返回适当的错误消息 +- **日志记录**:记录错误但不暴露敏感信息给客户端 + +## 7. 国际化支持 + +### 7.1 多语言文件结构 + +- 支持英语、中文、日语、俄语 +- 独立`cron.json`文件,包含所有定时任务相关文本 +- 嵌套结构组织:`stats`、`empty`、`card`、`dialog`、`presets`、`toast`、`schedule` + +### 7.2 关键翻译键 + +- `title`: "Scheduled Tasks" +- `subtitle`: "Automate AI workflows with scheduled tasks" +- `stats.total`: "Total Tasks" +- `dialog.createTitle`: "Create Task" +- `presets.every5Min`: "Every 5 minutes" +- `toast.created`: "Task created" + +## 8. UI/UX设计分析 + +### 8.1 设计风格 + +- **卡片设计**:圆角大卡片,悬停效果,透明边框 +- **色彩方案**:中性背景,主色用于操作按钮,状态色(绿/黄/红)用于指示器 +- **排版**:大标题字体,清晰的层次结构 +- **交互反馈**:平滑过渡,加载状态,即时验证 + +### 8.2 用户体验特性 + +- **调度预览**:实时显示下一次运行时间 +- **智能默认值**:基于选择自动填充相关字段 +- **渐进式披露**:高级选项(如自定义Cron)默认隐藏 +- **批量操作**:支持多任务启用/禁用 +- **状态可视化**:通过颜色和图标清晰表示任务状态 + +## 9. 与zn-ai项目的技术栈映射 + +### 9.1 技术栈对比 + +| 领域 | ClawX (React) | zn-ai (Vue) | 迁移策略 | +|------|---------------|-------------|----------| +| 框架 | React 18 | Vue 3 + Composition API | 组件重写 | +| 状态管理 | Zustand | Pinia | Store转换 | +| UI组件库 | shadcn/ui + Tailwind | Element Plus + Tailwind | 组件替换 | +| 国际化 | react-i18next | Vue I18n | 翻译文件转换 | +| HTTP客户端 | 自定义hostApiFetch | 相同hostApiFetch | 复用 | +| 类型系统 | TypeScript | TypeScript | 类型定义复用 | + +### 9.2 组件映射建议 + +| ClawX组件 | Vue等价实现 | 说明 | +|-----------|-------------|------| +| `TaskDialog` | `CronTaskDialog.vue` | 使用Element Plus的Dialog、Form、Select等组件 | +| `CronJobCard` | `CronJobCard.vue` | 自定义卡片组件,使用Tailwind样式 | +| 统计卡片 | 使用Element Plus Card组件 | 可复用现有统计组件模式 | +| 确认对话框 | Element Plus MessageBox | 替换ConfirmDialog | +| 开关组件 | Element Plus Switch | 替换shadcn/ui Switch | +| 选择器 | Element Plus Select | 替换自定义SelectField | + +### 9.3 Store迁移策略 + +- **保持相同状态结构**:`jobs`、`loading`、`error` +- **保持相同操作**:`fetchJobs`、`createJob`等 +- **Pinia语法适配**:将Zustand的`create`转换为Pinia的`defineStore` +- **组合式函数**:可将部分逻辑提取到组合式函数中 + +## 10. 实现优先级建议 + +### 10.1 阶段一:基础架构与类型定义 +1. 创建类型定义文件 (`src/lib/cron-types.ts`) +2. 创建Pinia store (`src/store/cron.ts`) +3. 创建API接口层(扩展host-api或创建专用服务) +4. 创建国际化文件 (`src/i18n/locales/*/cron.json`) + +### 10.2 阶段二:核心UI组件开发 +1. 主页面组件 (`src/pages/cron/index.vue`) +2. 任务卡片组件 (`src/pages/cron/components/CronJobCard.vue`) +3. 任务对话框组件 (`src/pages/cron/components/CronTaskDialog.vue`) +4. 统计卡片组件(可复用现有模式) + +### 10.3 阶段三:功能集成与数据绑定 +1. 集成store到组件 +2. 实现表单验证和提交逻辑 +3. 添加交付频道集成(依赖现有频道系统) +4. 实现调度预览功能 + +### 10.4 阶段四:测试、优化与完善 +1. 添加加载状态和错误处理 +2. 实现Gateway状态检查 +3. 添加键盘快捷键和可访问性支持 +4. 性能优化和代码清理 + +## 11. 风险评估与缓解措施 + +### 11.1 技术风险 +- **Gateway依赖**:zn-ai可能没有相同的Gateway架构 + - *缓解*:评估现有任务调度能力,或实现简化版本 +- **频道集成复杂性**:交付功能依赖现有的频道配置系统 + - *缓解*:先实现"仅应用内"模式,交付功能作为后续增强 +- **Cron表达式解析**:需要可靠的Cron解析和预览 + - *缓解*:使用成熟的Cron解析库(如`cron-parser`) + +### 11.2 兼容性风险 +- **数据格式差异**:与现有任务/调度系统的数据模型可能不兼容 + - *缓解*:设计适配器层,确保向后兼容 +- **UI一致性**:与zn-ai现有设计风格保持一致 + - *缓解*:复用现有UI模式和组件库 + +### 11.3 开发资源风险 +- **功能范围**:完整功能可能较复杂 + - *缓解*:采用分阶段实施,优先核心功能 + +## 12. 依赖项分析 + +### 12.1 前端依赖 +- **必需**:Vue 3, Pinia, Element Plus, Vue I18n, Tailwind CSS +- **可选**:Cron解析库(用于高级调度功能) + +### 12.2 后端依赖 +- **必需**:任务调度引擎(现有或需要实现) +- **必需**:频道账户管理系统(用于交付功能) +- **必需**:数据持久化(JSON文件或数据库) + +### 12.3 外部服务依赖 +- **可选**:外部频道服务(飞书、Telegram等API) + +## 13. 测试策略 + +### 13.1 单元测试 +- Store逻辑测试 +- 工具函数测试(Cron解析、时间格式化等) +- 组件逻辑测试(使用Vue Test Utils) + +### 13.2 集成测试 +- API端点测试 +- 组件集成测试 +- 跨页面交互测试 + +### 13.3 端到端测试 +- 完整用户流程测试(创建、编辑、删除任务) +- 调度执行测试 +- 交付功能测试(如果实现) + +## 14. 部署与维护 + +### 14.1 配置管理 +- 调度配置持久化策略 +- 日志文件管理和轮转 +- 错误报告和监控 + +### 14.2 性能考虑 +- 任务列表虚拟化(如果任务数量大) +- 调度检查优化(避免频繁轮询) +- 内存使用优化(大型历史记录处理) + +### 14.3 可扩展性 +- 插件架构支持(自定义调度类型) +- Webhook集成支持 +- 多租户支持(如果未来需要) + +--- + +## 总结 + +ClawX的定时任务功能是一个完整、用户友好的调度系统,具有清晰的架构和良好的代码组织。迁移到zn-ai项目需要: + +1. **技术栈转换**:从React/Zustand到Vue/Pinia的转换 +2. **组件重写**:使用Element Plus替代shadcn/ui组件 +3. **架构适配**:根据zn-ai现有架构调整后端集成 +4. **分阶段实施**:优先核心功能,逐步增强 + +建议从基础的任务管理开始,先实现"仅应用内"模式,再逐步添加频道交付等高级功能。这样可以快速验证核心流程,降低初始风险。 \ No newline at end of file diff --git a/dist-electron/main/main.js b/dist-electron/main/main.js index a8c04dd..5639b8d 100644 --- a/dist-electron/main/main.js +++ b/dist-electron/main/main.js @@ -1,2 +1,1644 @@ -require('bytenode') -require('./main.jsc') \ No newline at end of file +"use strict"; +const electron = require("electron"); +const OpenAI = require("openai"); +const util = require("util"); +const log = require("electron-log"); +const path = require("path"); +const fs = require("fs"); +const jsBase64 = require("js-base64"); +const path$1 = require("node:path"); +const crypto = require("crypto"); +const started = require("electron-squirrel-startup"); +const net = require("net"); +const http = require("http"); +const child_process = require("child_process"); +const events = require("events"); +require("bytenode"); +const electronUpdater = require("electron-updater"); +function _interopNamespaceDefault(e) { + const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } }); + if (e) { + for (const k in e) { + if (k !== "default") { + const d = Object.getOwnPropertyDescriptor(e, k); + Object.defineProperty(n, k, d.get ? d : { + enumerable: true, + get: () => e[k] + }); + } + } + } + n.default = e; + return Object.freeze(n); +} +const path__namespace = /* @__PURE__ */ _interopNamespaceDefault(path); +const fs__namespace = /* @__PURE__ */ _interopNamespaceDefault(fs); +var IPC_EVENTS = /* @__PURE__ */ ((IPC_EVENTS2) => { + IPC_EVENTS2["EXTERNAL_OPEN"] = "external-open"; + IPC_EVENTS2["WINDOW_MINIMIZE"] = "window-minimize"; + IPC_EVENTS2["WINDOW_MAXIMIZE"] = "window-maximize"; + IPC_EVENTS2["WINDOW_CLOSE"] = "window-close"; + IPC_EVENTS2["IS_WINDOW_MAXIMIZED"] = "is-window-maximized"; + IPC_EVENTS2["APP_SET_FRAMELESS"] = "app:set-frameless"; + IPC_EVENTS2["APP_LOAD_PAGE"] = "app:load-page"; + IPC_EVENTS2["TAB_CREATE"] = "tab:create"; + IPC_EVENTS2["TAB_LIST"] = "tab:list"; + IPC_EVENTS2["TAB_NAVIGATE"] = "tab:navigate"; + IPC_EVENTS2["TAB_RELOAD"] = "tab:reload"; + IPC_EVENTS2["TAB_BACK"] = "tab:back"; + IPC_EVENTS2["TAB_FORWARD"] = "tab:forward"; + IPC_EVENTS2["TAB_SWITCH"] = "tab:switch"; + IPC_EVENTS2["TAB_CLOSE"] = "tab:close"; + IPC_EVENTS2["LOG_TO_MAIN"] = "log-to-main"; + IPC_EVENTS2["READ_FILE"] = "read-file"; + IPC_EVENTS2["INVOKE"] = "ipc:invoke"; + IPC_EVENTS2["INVOKE_ASYNC"] = "ipc:invokeAsync"; + IPC_EVENTS2["APP_MINIMIZE"] = "app:minimize"; + IPC_EVENTS2["APP_MAXIMIZE"] = "app:maximize"; + IPC_EVENTS2["APP_QUIT"] = "app:quit"; + IPC_EVENTS2["FILE_READ"] = "file:read"; + IPC_EVENTS2["FILE_WRITE"] = "file:write"; + IPC_EVENTS2["GET_WINDOW_ID"] = "get-window-id"; + IPC_EVENTS2["CUSTOM_EVENT"] = "custom:event"; + IPC_EVENTS2["TIME_UPDATE"] = "time:update"; + IPC_EVENTS2["RENDERER_IS_READY"] = "renderer-ready"; + IPC_EVENTS2["SHOW_CONTEXT_MENU"] = "show-context-menu"; + IPC_EVENTS2["START_A_DIALOGUE"] = "start-a-dialogue"; + IPC_EVENTS2["OPEN_WINDOW"] = "open-window"; + IPC_EVENTS2["LOG_DEBUG"] = "log-debug"; + IPC_EVENTS2["LOG_INFO"] = "log-info"; + IPC_EVENTS2["LOG_WARN"] = "log-warn"; + IPC_EVENTS2["LOG_ERROR"] = "log-error"; + IPC_EVENTS2["CONFIG_UPDATED"] = "config-updated"; + IPC_EVENTS2["SET_CONFIG"] = "set-config"; + IPC_EVENTS2["GET_CONFIG"] = "get-config"; + IPC_EVENTS2["UPDATE_CONFIG"] = "update-config"; + IPC_EVENTS2["SET_THEME_MODE"] = "set-theme-mode"; + IPC_EVENTS2["GET_THEME_MODE"] = "get-theme-mode"; + IPC_EVENTS2["IS_DARK_THEME"] = "is-dark-theme"; + IPC_EVENTS2["THEME_MODE_UPDATED"] = "theme-mode-updated"; + IPC_EVENTS2["EXECUTE_SCRIPT"] = "execute-script"; + IPC_EVENTS2["OPEN_CHANNEL"] = "open-channel"; + IPC_EVENTS2["UPDATE_CHECK"] = "update:check"; + IPC_EVENTS2["UPDATE_DOWNLOAD"] = "update:download"; + IPC_EVENTS2["UPDATE_INSTALL"] = "update:install"; + IPC_EVENTS2["UPDATE_VERSION"] = "update:version"; + IPC_EVENTS2["UPDATE_STATUS_CHANGED"] = "update:status-changed"; + return IPC_EVENTS2; +})(IPC_EVENTS || {}); +const MAIN_WIN_SIZE = { + width: 1440, + height: 900, + minWidth: 1440, + minHeight: 900 +}; +var WINDOW_NAMES = /* @__PURE__ */ ((WINDOW_NAMES2) => { + WINDOW_NAMES2["MAIN"] = "main"; + WINDOW_NAMES2["SETTING"] = "setting"; + WINDOW_NAMES2["DIALOG"] = "dialog"; + WINDOW_NAMES2["LOADING"] = "loading"; + return WINDOW_NAMES2; +})(WINDOW_NAMES || {}); +var CONFIG_KEYS = /* @__PURE__ */ ((CONFIG_KEYS2) => { + CONFIG_KEYS2["THEME_MODE"] = "themeMode"; + CONFIG_KEYS2["PRIMARY_COLOR"] = "primaryColor"; + CONFIG_KEYS2["LANGUAGE"] = "language"; + CONFIG_KEYS2["FONT_SIZE"] = "fontSize"; + CONFIG_KEYS2["MINIMIZE_TO_TRAY"] = "minimizeToTray"; + CONFIG_KEYS2["PROVIDER"] = "provider"; + CONFIG_KEYS2["DEFAULT_MODEL"] = "defaultModel"; + CONFIG_KEYS2["AUTO_CHECK_UPDATE"] = "autoCheckUpdate"; + CONFIG_KEYS2["AUTO_DOWNLOAD_UPDATE"] = "autoDownloadUpdate"; + return CONFIG_KEYS2; +})(CONFIG_KEYS || {}); +var MENU_IDS = /* @__PURE__ */ ((MENU_IDS2) => { + MENU_IDS2["CONVERSATION_ITEM"] = "conversation-item"; + MENU_IDS2["CONVERSATION_LIST"] = "conversation-list"; + MENU_IDS2["MESSAGE_ITEM"] = "message-item"; + return MENU_IDS2; +})(MENU_IDS || {}); +var CONVERSATION_ITEM_MENU_IDS = /* @__PURE__ */ ((CONVERSATION_ITEM_MENU_IDS2) => { + CONVERSATION_ITEM_MENU_IDS2["PIN"] = "pin"; + CONVERSATION_ITEM_MENU_IDS2["RENAME"] = "rename"; + CONVERSATION_ITEM_MENU_IDS2["DEL"] = "del"; + return CONVERSATION_ITEM_MENU_IDS2; +})(CONVERSATION_ITEM_MENU_IDS || {}); +var CONVERSATION_LIST_MENU_IDS = /* @__PURE__ */ ((CONVERSATION_LIST_MENU_IDS2) => { + CONVERSATION_LIST_MENU_IDS2["NEW_CONVERSATION"] = "newConversation"; + CONVERSATION_LIST_MENU_IDS2["SORT_BY"] = "sortBy"; + CONVERSATION_LIST_MENU_IDS2["SORT_BY_CREATE_TIME"] = "sortByCreateTime"; + CONVERSATION_LIST_MENU_IDS2["SORT_BY_UPDATE_TIME"] = "sortByUpdateTime"; + CONVERSATION_LIST_MENU_IDS2["SORT_BY_NAME"] = "sortByName"; + CONVERSATION_LIST_MENU_IDS2["SORT_BY_MODEL"] = "sortByModel"; + CONVERSATION_LIST_MENU_IDS2["SORT_ASCENDING"] = "sortAscending"; + CONVERSATION_LIST_MENU_IDS2["SORT_DESCENDING"] = "sortDescending"; + CONVERSATION_LIST_MENU_IDS2["BATCH_OPERATIONS"] = "batchOperations"; + return CONVERSATION_LIST_MENU_IDS2; +})(CONVERSATION_LIST_MENU_IDS || {}); +var MESSAGE_ITEM_MENU_IDS = /* @__PURE__ */ ((MESSAGE_ITEM_MENU_IDS2) => { + MESSAGE_ITEM_MENU_IDS2["COPY"] = "copy"; + MESSAGE_ITEM_MENU_IDS2["DELETE"] = "delete"; + MESSAGE_ITEM_MENU_IDS2["SELECT"] = "select"; + return MESSAGE_ITEM_MENU_IDS2; +})(MESSAGE_ITEM_MENU_IDS || {}); +class BaseProvider { +} +const readdirAsync = util.promisify(fs__namespace.readdir); +const statAsync = util.promisify(fs__namespace.stat); +const unlinkAsync = util.promisify(fs__namespace.unlink); +class LogService { + static _instance; + // 日志保留天数,默认7天 + LOG_RETENTION_DAYS = 7; + // 清理间隔,默认24小时(毫秒) + CLEANUP_INTERVAL_MS = 24 * 60 * 60 * 1e3; + constructor() { + const logPath = path__namespace.join(electron.app.getPath("userData"), "logs"); + try { + if (!fs__namespace.existsSync(logPath)) { + fs__namespace.mkdirSync(logPath, { recursive: true }); + } + } catch (err) { + this.error("Failed to create log directory:", err); + } + log.transports.file.resolvePathFn = () => { + const today = /* @__PURE__ */ new Date(); + const formattedDate = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, "0")}-${String(today.getDate()).padStart(2, "0")}`; + return path__namespace.join(logPath, `${formattedDate}.log`); + }; + log.transports.file.format = "[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}] {text}"; + log.transports.file.maxSize = 10 * 1024 * 1024; + log.transports.console.level = process.env.NODE_ENV === "development" ? "debug" : "info"; + log.transports.file.level = "debug"; + this._setupIpcEvents(); + this._rewriteConsole(); + this.info("LogService initialized successfully."); + this._cleanupOldLogs(); + setInterval(() => this._cleanupOldLogs(), this.CLEANUP_INTERVAL_MS); + } + _setupIpcEvents() { + electron.ipcMain.on(IPC_EVENTS.LOG_DEBUG, (_e, message, ...meta) => this.debug(message, ...meta)); + electron.ipcMain.on(IPC_EVENTS.LOG_INFO, (_e, message, ...meta) => this.info(message, ...meta)); + electron.ipcMain.on(IPC_EVENTS.LOG_WARN, (_e, message, ...meta) => this.warn(message, ...meta)); + electron.ipcMain.on(IPC_EVENTS.LOG_ERROR, (_e, message, ...meta) => this.error(message, ...meta)); + } + _rewriteConsole() { + console.debug = log.debug; + console.log = log.info; + console.info = log.info; + console.warn = log.warn; + console.error = log.error; + } + async _cleanupOldLogs() { + try { + const logPath = path__namespace.join(electron.app.getPath("userData"), "logs"); + if (!fs__namespace.existsSync(logPath)) return; + const now = /* @__PURE__ */ new Date(); + const expirationDate = new Date(now.getTime() - this.LOG_RETENTION_DAYS * 24 * 60 * 60 * 1e3); + const files = await readdirAsync(logPath); + let deletedCount = 0; + for (const file of files) { + if (!file.endsWith(".log")) continue; + const filePath = path__namespace.join(logPath, file); + try { + const stats = await statAsync(filePath); + if (stats.isFile() && stats.birthtime < expirationDate) { + await unlinkAsync(filePath); + deletedCount++; + } + } catch (error) { + this.error(`Failed to delete old log file ${filePath}:`, error); + } + } + if (deletedCount > 0) { + this.info(`Successfully cleaned up ${deletedCount} old log files.`); + } + } catch (err) { + this.error("Failed to cleanup old logs:", err); + } + } + static getInstance() { + if (!this._instance) { + this._instance = new LogService(); + } + return this._instance; + } + /** + * 记录调试信息 + * @param {string} message - 日志消息 + * @param {any[]} meta - 附加的元数据 + */ + debug(message, ...meta) { + log.debug(message, ...meta); + } + /** + * 记录一般信息 + * @param {string} message - 日志消息 + * @param {any[]} meta - 附加的元数据 + */ + info(message, ...meta) { + log.info(message, ...meta); + } + /** + * 记录警告信息 + * @param {string} message - 日志消息 + * @param {any[]} meta - 附加的元数据 + */ + warn(message, ...meta) { + log.warn(message, ...meta); + } + /** + * 记录错误信息 + * @param {string} message - 日志消息 + * @param {any[]} meta - 附加的元数据,通常是错误对象 + */ + error(message, ...meta) { + log.error(message, ...meta); + } + logApiRequest(endpoint, data = {}, method = "POST") { + this.info(`API Request: ${endpoint}, Method: ${method}, Request: ${JSON.stringify(data)}`); + } + logApiResponse(endpoint, response = {}, statusCode = 200, responseTime = 0) { + if (statusCode >= 400) { + this.error(`API Error Response: ${endpoint}, Status: ${statusCode}, Response Time: ${responseTime}ms, Response: ${JSON.stringify(response)}`); + } else { + this.debug(`API Response: ${endpoint}, Status: ${statusCode}, Response Time: ${responseTime}ms, Response: ${JSON.stringify(response)}`); + } + } + logUserOperation(operation, userId = "unknown", details = {}) { + this.info(`User Operation: ${operation} by ${userId}, Details: ${JSON.stringify(details)}`); + } +} +const logManager = LogService.getInstance(); +function _transformChunk(chunk) { + const choice = chunk.choices[0]; + return { + isEnd: choice?.finish_reason === "stop", + result: choice?.delta?.content ?? "" + }; +} +class OpenAIProvider extends BaseProvider { + client; + constructor(apiKey, baseURL) { + super(); + this.client = new OpenAI({ apiKey, baseURL }); + } + async chat(messages2, model) { + const startTime = Date.now(); + const lastMessage = messages2[messages2.length - 1]; + logManager.logApiRequest("chat.completions.create", { + model, + lastMessage: lastMessage?.content?.substring(0, 100) + (lastMessage?.content?.length > 100 ? "..." : ""), + messageCount: messages2.length + }, "POST"); + try { + const chunks = await this.client.chat.completions.create({ + model, + messages: messages2, + stream: true + }); + const responseTime = Date.now() - startTime; + logManager.logApiResponse("chat.completions.create", { success: true }, 200, responseTime); + return { + async *[Symbol.asyncIterator]() { + for await (const chunk of chunks) { + yield _transformChunk(chunk); + } + } + }; + } catch (error) { + const responseTime = Date.now() - startTime; + logManager.logApiResponse("chat.completions.create", { error: error instanceof Error ? error.message : String(error) }, 500, responseTime); + throw error; + } + } +} +function debounce(fn, delay) { + let timer = null; + return function(...args) { + if (timer) { + clearTimeout(timer); + } + timer = setTimeout(() => { + fn.apply(this, args); + }, delay); + }; +} +function cloneDeep(obj) { + if (obj === null || typeof obj !== "object") { + return obj; + } + if (Array.isArray(obj)) { + return obj.map((item) => cloneDeep(item)); + } + const clone = Object.assign({}, obj); + for (const key in clone) { + if (Object.prototype.hasOwnProperty.call(clone, key)) { + clone[key] = cloneDeep(clone[key]); + } + } + return clone; +} +function simpleCloneDeep(obj) { + try { + return JSON.parse(JSON.stringify(obj)); + } catch (error) { + console.error("simpleCloneDeep failed:", error); + return obj; + } +} +function parseOpenAISetting(setting) { + try { + return JSON.parse(jsBase64.decode(setting)); + } catch (error) { + console.error("parseOpenAISetting failed:", error); + return {}; + } +} +const DEFAULT_CONFIG = { + [CONFIG_KEYS.THEME_MODE]: "system", + [CONFIG_KEYS.PRIMARY_COLOR]: "#BB5BE7", + [CONFIG_KEYS.LANGUAGE]: "zh", + [CONFIG_KEYS.FONT_SIZE]: 14, + [CONFIG_KEYS.MINIMIZE_TO_TRAY]: false, + [CONFIG_KEYS.PROVIDER]: "", + [CONFIG_KEYS.DEFAULT_MODEL]: null +}; +class ConfigService { + static _instance; + _config; + _configPath; + _defaultConfig = DEFAULT_CONFIG; + _listeners = []; + constructor() { + this._configPath = path__namespace.join(electron.app.getPath("userData"), "config.json"); + this._config = this._loadConfig(); + this._setupIpcEvents(); + logManager.info("ConfigService initialized successfully."); + } + _setupIpcEvents() { + const duration = 200; + const handelUpdate = debounce((val) => this.update(val), duration); + electron.ipcMain.handle(IPC_EVENTS.GET_CONFIG, (_, key) => this.get(key)); + electron.ipcMain.on(IPC_EVENTS.SET_CONFIG, (_, key, val) => this.set(key, val)); + electron.ipcMain.on(IPC_EVENTS.UPDATE_CONFIG, (_, updates) => handelUpdate(updates)); + } + static getInstance() { + if (!this._instance) { + this._instance = new ConfigService(); + } + return this._instance; + } + _loadConfig() { + try { + if (fs__namespace.existsSync(this._configPath)) { + const configContent = fs__namespace.readFileSync(this._configPath, "utf-8"); + const config = { ...this._defaultConfig, ...JSON.parse(configContent) }; + logManager.info("Config loaded successfully from:", this._configPath); + return config; + } + } catch (error) { + logManager.error("Failed to load config:", error); + } + return { ...this._defaultConfig }; + } + _saveConfig() { + try { + fs__namespace.mkdirSync(path__namespace.dirname(this._configPath), { recursive: true }); + fs__namespace.writeFileSync(this._configPath, JSON.stringify(this._config, null, 2), "utf-8"); + this._notifyListeners(); + logManager.info("Config saved successfully to:", this._configPath); + } catch (error) { + logManager.error("Failed to save config:", error); + } + } + _notifyListeners() { + electron.BrowserWindow.getAllWindows().forEach((win) => win.webContents.send(IPC_EVENTS.CONFIG_UPDATED, this._config)); + this._listeners.forEach((listener) => listener({ ...this._config })); + } + getConfig() { + return simpleCloneDeep(this._config); + } + get(key) { + return this._config[key]; + } + set(key, value, autoSave = true) { + if (!(key in this._config)) return; + const oldValue = this._config[key]; + if (oldValue === value) return; + this._config[key] = value; + logManager.debug(`Config set: ${key} = ${value}`); + autoSave && this._saveConfig(); + } + update(updates, autoSave = true) { + this._config = { ...this._config, ...updates }; + autoSave && this._saveConfig(); + } + resetToDefault() { + this._config = { ...this._defaultConfig }; + logManager.info("Config reset to default."); + this._saveConfig(); + } + onConfigChange(listener) { + this._listeners.push(listener); + return () => this._listeners = this._listeners.filter((l) => l !== listener); + } +} +const configManager = ConfigService.getInstance(); +[ + { + id: 1, + name: "bigmodel", + title: "智谱AI", + models: ["glm-4.5-flash"], + openAISetting: { + baseURL: "https://open.bigmodel.cn/api/paas/v4", + apiKey: process.env.BIGMODEL_API_KEY || "" + }, + createdAt: (/* @__PURE__ */ new Date()).getTime(), + updatedAt: (/* @__PURE__ */ new Date()).getTime() + }, + { + id: 2, + name: "deepseek", + title: "深度求索 (DeepSeek)", + models: ["deepseek-chat"], + openAISetting: { + baseURL: "https://api.deepseek.com/v1", + apiKey: process.env.DEEPSEEK_API_KEY || "" + }, + createdAt: (/* @__PURE__ */ new Date()).getTime(), + updatedAt: (/* @__PURE__ */ new Date()).getTime() + }, + { + id: 3, + name: "siliconflow", + title: "硅基流动", + models: ["Qwen/Qwen3-8B", "deepseek-ai/DeepSeek-R1-0528-Qwen3-8B"], + openAISetting: { + baseURL: "https://api.siliconflow.cn/v1", + apiKey: process.env.SILICONFLOW_API_KEY || "" + }, + createdAt: (/* @__PURE__ */ new Date()).getTime(), + updatedAt: (/* @__PURE__ */ new Date()).getTime() + }, + { + id: 4, + name: "qianfan", + title: "百度千帆", + models: ["ernie-speed-128k", "ernie-4.0-8k", "ernie-3.5-8k"], + openAISetting: { + baseURL: "https://qianfan.baidubce.com/v2", + apiKey: process.env.QIANFAN_API_KEY || "" + }, + createdAt: (/* @__PURE__ */ new Date()).getTime(), + updatedAt: (/* @__PURE__ */ new Date()).getTime() + } +]; +const _parseProvider = () => { + let result = []; + let isBase64Parsed = false; + const providerConfig = configManager.get(CONFIG_KEYS.PROVIDER); + const mapCallback = (provider) => ({ + ...provider, + openAISetting: typeof provider.openAISetting === "string" ? parseOpenAISetting(provider.openAISetting ?? "") : provider.openAISetting + }); + try { + result = JSON.parse(jsBase64.decode(providerConfig)); + isBase64Parsed = true; + } catch (error) { + logManager.error(`parse base64 provider failed: ${error}`); + } + if (!isBase64Parsed) try { + result = JSON.parse(providerConfig); + } catch (error) { + logManager.error(`parse provider failed: ${error}`); + } + if (!result.length) return; + return result.map(mapCallback); +}; +const getProviderConfig = () => { + try { + return _parseProvider(); + } catch (error) { + logManager.error(`get provider config failed: ${error}`); + return null; + } +}; +function createProvider(name) { + const providers2 = getProviderConfig(); + if (!providers2) { + throw new Error("provider config not found"); + } + for (const provider of providers2) { + if (provider.name === name) { + if (!provider.openAISetting?.apiKey || !provider.openAISetting?.baseURL) { + throw new Error("apiKey or baseURL not found"); + } + return new OpenAIProvider(provider.openAISetting.apiKey, provider.openAISetting.baseURL); + } + } +} +const window$1 = { "minimize": "Minimize", "maximize": "Maximize", "restore": "Restore", "close": "Close" }; +const main$1 = { "welcome": { "helloMessage": "Hello, I'm Diona" }, "conversation": { "placeholder": "Type a message...", "newConversation": "New Conversation", "selectModel": "Please select model", "createConversation": "Create Conversation", "searchPlaceholder": "Search conversations...", "goSettings": "Go to", "settings": "Settings Window", "addModel": "to add a model", "dialog": { "title": "Confirm Deletion", "content": "Are you sure you want to delete this conversation?", "content_1": "Are you sure you want to delete the selected conversations? This action cannot be undone." }, "operations": { "pin": "Pin Selected", "del": "Delete Selected", "selectAll": "Select All", "cancel": "Cancel" } }, "sidebar": { "conversations": "Conversations", "settings": "Settings", "help": "Help" }, "message": { "dialog": { "title": "Confirm Deletion", "messageDelete": "Are you sure you want to delete this message?", "batchDelete": "Are you sure you want to delete the selected messages?", "copySuccess": "Copied successfully" }, "batchActions": { "deleteSelected": "Delete Selected" }, "rendering": "Thinking...", "stoppedGeneration": "(Stopped generating)", "sending": "Sending", "stopGeneration": "Stop generating", "send": "Send" } }; +const dialog$1 = { "cancel": "Cancel", "confirm": "Confirm" }; +const settings$1 = { "title": "Settings", "base": "Basic Settings", "provider": { "modelConfig": "Model Configuration" }, "theme": { "label": "Theme Settings", "dark": "Dark Theme", "light": "Light Theme", "system": "System Theme", "primaryColor": "Primary Color" }, "appearance": { "fontSize": "Font Size", "fontSizeOptions": { "10": "Tiny (10px)", "12": "Small (12px)", "14": "Normal (14px)", "16": "Medium (16px)", "18": "Large (18px)", "20": "Larger (20px)", "24": "Extra Large (24px)" } }, "behavior": { "minimizeToTray": "Minimize to tray when closed" }, "language": { "label": "Language" }, "providers": { "defaultModel": "Default Model", "apiKey": "API Key", "apiUrl": "API URL" } }; +const menu$1 = { "conversation": { "newConversation": "New Conversation", "sortBy": "Sort By", "sortByCreateTime": "Sort by Creation Time", "sortByUpdateTime": "Sort by Update Time", "sortByName": "Sort by Name", "sortByModel": "Sort by Model", "sortAscending": "Ascending", "sortDescending": "Descending", "pinConversation": "Pin Conversation", "unpinConversation": "Unpin Conversation", "renameConversation": "Rename Conversation", "delConversation": "Delete Conversation", "batchOperations": "Batch Operations" }, "message": { "copyMessage": "Copy Message", "deleteMessage": "Delete Message", "selectMessage": "Select Message" } }; +const tray$1 = { "tooltip": "Diona Application", "showWindow": "Show Window", "exit": "Exit" }; +const timeAgo$1 = { "justNow": "Just now", "minutes": "{count} minutes ago", "hours": "{count} hours ago", "days": "{count} days ago", "months": "{count} months ago", "years": "{count} years ago", "weekday": { "sun": "Sunday", "mon": "Monday", "tue": "Tuesday", "wed": "Wednesday", "thu": "Thursday", "fri": "Friday", "sat": "Saturday" } }; +const app$1 = { "title": "Diona Application" }; +const en = { + window: window$1, + main: main$1, + dialog: dialog$1, + settings: settings$1, + menu: menu$1, + tray: tray$1, + timeAgo: timeAgo$1, + app: app$1 +}; +const window = { "minimize": "最小化", "maximize": "最大化", "restore": "还原", "close": "关闭" }; +const main = { "welcome": { "helloMessage": "你好,我是迪奥娜" }, "conversation": { "placeholder": "输入消息...", "newConversation": "新对话", "selectModel": "请选择模型", "createConversation": "创建对话", "searchPlaceholder": "搜索对话...", "goSettings": "快去", "settings": "设置窗口", "addModel": "添加模型", "dialog": { "title": "确认删除", "content": "确定要删除这个对话吗?", "content_1": "确定要删除选中的对话吗?此操作不可撤销。" }, "operations": { "pin": "置顶所选", "del": "删除所选", "selectAll": "全选", "cancel": "取消" } }, "sidebar": { "conversations": "对话", "settings": "设置", "help": "帮助" }, "message": { "dialog": { "title": "确认删除", "messageDelete": "确认删除该条消息?", "batchDelete": "确认删除选中的消息?", "copySuccess": "复制成功" }, "batchActions": { "deleteSelected": "删除选中项" }, "rendering": "思考中...", "stoppedGeneration": "(已停止生成)", "sending": "发送中", "stopGeneration": "停止生成", "send": "发送" } }; +const dialog = { "cancel": "取消", "confirm": "确认" }; +const settings = { "title": "设置", "base": "基础设置", "provider": { "modelConfig": "模型配置" }, "providers": { "defaultModel": "默认模型", "apiKey": "API密钥", "apiUrl": "API地址" }, "theme": { "label": "主题设置", "dark": "深色主题", "light": "浅色主题", "system": "跟随系统", "primaryColor": "主题颜色" }, "appearance": { "fontSize": "字体大小", "fontSizeOptions": { "10": "极小 (10px)", "12": "小 (12px)", "14": "正常 (14px)", "16": "中 (16px)", "18": "大 (18px)", "20": "较大 (20px)", "24": "超大 (24px)" } }, "behavior": { "minimizeToTray": "关闭时最小化到托盘" }, "language": { "label": "语言设置" } }; +const menu = { "conversation": { "newConversation": "新建对话", "sortBy": "排序方式", "sortByCreateTime": "按创建时间排序", "sortByUpdateTime": "按更新时间排序", "sortByName": "按名称排序", "sortByModel": "按模型排序", "sortAscending": "递增", "sortDescending": "递减", "pinConversation": "置顶对话", "unpinConversation": "取消置顶", "renameConversation": "重命名对话", "delConversation": "删除对话", "batchOperations": "批量操作" }, "message": { "copyMessage": "复制消息", "deleteMessage": "删除消息", "selectMessage": "选择消息" } }; +const tray = { "tooltip": "迪奥娜", "showWindow": "显示窗口", "exit": "退出" }; +const timeAgo = { "justNow": "刚刚", "minutes": "{count}分钟前", "hours": "{count}小时前", "days": "{count}天前", "months": "{count}个月前", "years": "{count}年前", "weekday": { "sun": "星期日", "mon": "星期一", "tue": "星期二", "wed": "星期三", "thu": "星期四", "fri": "星期五", "sat": "星期六" } }; +const app = { "title": "迪奥娜" }; +const zh = { + window, + main, + dialog, + settings, + menu, + tray, + timeAgo, + app +}; +const messages = { en, zh }; +function createTranslator() { + return (key) => { + if (!key) return void 0; + try { + const keys = key?.split("."); + let result = messages[configManager.get(CONFIG_KEYS.LANGUAGE)]; + for (const _key of keys) { + result = result[_key]; + } + return result; + } catch (e) { + logManager.error("failed to translate key:", key, e); + return key; + } + }; +} +let logo = void 0; +function createLogo() { + if (logo != null) { + return logo; + } + const appPath = electron.app.getAppPath(); + const iconPath = path$1.join(appPath, "resources", "icons", "icon.ico"); + logo = iconPath; + return logo; +} +class ThemeService { + static _instance; + _isDark = electron.nativeTheme.shouldUseDarkColors; + constructor() { + const themeMode = configManager.get(CONFIG_KEYS.THEME_MODE); + if (themeMode) { + electron.nativeTheme.themeSource = themeMode; + this._isDark = electron.nativeTheme.shouldUseDarkColors; + } + this._setupIpcEvent(); + logManager.info("ThemeService initialized successfully."); + } + _setupIpcEvent() { + electron.ipcMain.handle(IPC_EVENTS.SET_THEME_MODE, (_e, mode) => { + electron.nativeTheme.themeSource = mode; + configManager.set(CONFIG_KEYS.THEME_MODE, mode); + return electron.nativeTheme.shouldUseDarkColors; + }); + electron.ipcMain.handle(IPC_EVENTS.GET_THEME_MODE, () => { + return electron.nativeTheme.themeSource; + }); + electron.ipcMain.handle(IPC_EVENTS.IS_DARK_THEME, () => { + return electron.nativeTheme.shouldUseDarkColors; + }); + electron.nativeTheme.on("updated", () => { + this._isDark = electron.nativeTheme.shouldUseDarkColors; + electron.BrowserWindow.getAllWindows().forEach( + (win) => win.webContents.send(IPC_EVENTS.THEME_MODE_UPDATED, this._isDark) + ); + }); + } + static getInstance() { + if (!this._instance) { + this._instance = new ThemeService(); + } + return this._instance; + } + get isDark() { + return this._isDark; + } + get themeMode() { + return electron.nativeTheme.themeSource; + } +} +const themeManager = ThemeService.getInstance(); +const SHARED_WINDOW_OPTIONS = { + frame: false, + titleBarStyle: "hidden", + trafficLightPosition: { x: -100, y: -100 }, + show: false, + title: "NIANXX", + darkTheme: themeManager.isDark, + backgroundColor: themeManager.isDark ? "#2C2C2C" : "#FFFFFF", + webPreferences: { + nodeIntegration: false, + // 禁用 Node.js 集成,提高安全性 + contextIsolation: true, + // 启用上下文隔离,防止渲染进程访问主进程 API + sandbox: true, + // 启用沙箱模式,进一步增强安全性 + backgroundThrottling: false, + preload: path$1.join(process.cwd(), "dist-electron/preload/preload.js") + } +}; +class WindowService { + static _instance; + _logo = createLogo(); + isDev = true; + _winStates = { + main: { instance: void 0, isHidden: false, onCreate: [], onClosed: [] }, + setting: { instance: void 0, isHidden: false, onCreate: [], onClosed: [] }, + dialog: { instance: void 0, isHidden: false, onCreate: [], onClosed: [] }, + login: { instance: void 0, isHidden: false, onCreate: [], onClosed: [] }, + loading: { instance: void 0, isHidden: false, onCreate: [], onClosed: [] } + }; + constructor() { + this._setupIpcEvents(); + logManager.info("WindowService initialized successfully."); + } + _isReallyClose(windowName) { + if (windowName === WINDOW_NAMES.MAIN) return configManager.get(CONFIG_KEYS.MINIMIZE_TO_TRAY) === false; + if (windowName === WINDOW_NAMES.SETTING) return false; + return true; + } + _setupIpcEvents() { + const handleCloseWindow = (e) => { + const target = electron.BrowserWindow.fromWebContents(e.sender); + const winName = this.getName(target); + this.close(target, this._isReallyClose(winName)); + }; + const handleMinimizeWindow = (e) => { + electron.BrowserWindow.fromWebContents(e.sender)?.minimize(); + }; + const handleMaximizeWindow = (e) => { + this.toggleMax(electron.BrowserWindow.fromWebContents(e.sender)); + }; + const handleIsWindowMaximized = (e) => { + return electron.BrowserWindow.fromWebContents(e.sender)?.isMaximized() ?? false; + }; + electron.ipcMain.on(IPC_EVENTS.WINDOW_CLOSE, handleCloseWindow); + electron.ipcMain.on(IPC_EVENTS.WINDOW_MINIMIZE, handleMinimizeWindow); + electron.ipcMain.on(IPC_EVENTS.WINDOW_MAXIMIZE, handleMaximizeWindow); + electron.ipcMain.handle(IPC_EVENTS.IS_WINDOW_MAXIMIZED, handleIsWindowMaximized); + electron.ipcMain.handle(IPC_EVENTS.APP_LOAD_PAGE, (e, page) => { + const win = electron.BrowserWindow.fromWebContents(e.sender); + if (win) this._loadPage(win, page); + }); + } + static getInstance() { + if (!this._instance) { + this._instance = new WindowService(); + } + return this._instance; + } + create(name, size, moreOpts) { + if (this.get(name)) return; + const isHiddenWin = this._isHiddenWin(name); + let window2 = this._createWinInstance(name, { ...size, ...moreOpts }); + if (this.isDev) window2.webContents.openDevTools(); + !isHiddenWin && this._setupWinLifecycle(window2, name)._loadWindowTemplate(window2, name); + this._listenWinReady({ + win: window2, + isHiddenWin, + size + }); + if (!isHiddenWin) { + this._winStates[name].instance = window2; + this._winStates[name].onCreate.forEach((callback) => callback(window2)); + } + if (isHiddenWin) { + this._winStates[name].isHidden = false; + logManager.info(`Hidden window show: ${name}`); + } + return window2; + } + _setupWinLifecycle(window2, name) { + const updateWinStatus = debounce(() => !window2?.isDestroyed() && window2?.webContents?.send(IPC_EVENTS.WINDOW_MAXIMIZE + "back", window2?.isMaximized()), 80); + window2.once("closed", () => { + this._winStates[name].onClosed.forEach((callback) => callback(window2)); + window2?.destroy(); + window2?.removeListener("resize", updateWinStatus); + this._winStates[name].instance = void 0; + this._winStates[name].isHidden = false; + logManager.info(`Window closed: ${name}`); + }); + window2.on("resize", updateWinStatus); + return this; + } + _listenWinReady(params) { + const onReady = () => { + params.win?.once("show", () => setTimeout(() => this._applySizeConstraints(params.win, params.size), 2)); + params.win?.show(); + }; + if (!params.isHiddenWin) { + const loadingHandler = this._addLoadingView(params.win, params.size); + loadingHandler?.(onReady); + } else { + onReady(); + } + } + _addLoadingView(window2, size) { + let rendererIsReady = false; + const onRendererIsReady = (e) => { + if (e.sender !== window2?.webContents || rendererIsReady) return; + rendererIsReady = true; + electron.ipcMain.removeListener(IPC_EVENTS.RENDERER_IS_READY, onRendererIsReady); + }; + electron.ipcMain.on(IPC_EVENTS.RENDERER_IS_READY, onRendererIsReady); + return (cb) => { + cb(); + }; + } + _applySizeConstraints(win, size) { + if (size.maxHeight && size.maxWidth) { + win.setMaximumSize(size.maxWidth, size.maxHeight); + } + if (size.minHeight && size.minWidth) { + win.setMinimumSize(size.minWidth, size.minHeight); + } + } + _loadPage(window2, pageName) { + { + return window2.loadURL(`${"http://localhost:5173"}/${pageName}.html`); + } + } + _loadWindowTemplate(window2, name) { + const page = "index"; + this._loadPage(window2, page); + } + _handleCloseWindowState(target, really) { + const name = this.getName(target); + if (name) { + if (!really) this._winStates[name].isHidden = true; + else this._winStates[name].instance = void 0; + } + setTimeout(() => { + target[really ? "close" : "hide"]?.(); + this._checkAndCloseAllWinodws(); + }, 210); + } + _checkAndCloseAllWinodws() { + if (!this._winStates[WINDOW_NAMES.MAIN].instance || this._winStates[WINDOW_NAMES.MAIN].instance?.isDestroyed()) + return Object.values(this._winStates).forEach((win) => win?.instance?.close()); + const minimizeToTray = configManager.get(CONFIG_KEYS.MINIMIZE_TO_TRAY); + if (!minimizeToTray && !this.get(WINDOW_NAMES.MAIN)?.isVisible()) + return Object.values(this._winStates).forEach((win) => !win?.instance?.isVisible() && win?.instance?.close()); + } + _isHiddenWin(name) { + return this._winStates[name] && this._winStates[name].isHidden; + } + _createWinInstance(name, opts) { + return this._isHiddenWin(name) ? this._winStates[name].instance : new electron.BrowserWindow({ + ...SHARED_WINDOW_OPTIONS, + icon: this._logo, + ...opts + }); + } + focus(target) { + if (!target) return; + const name = this.getName(target); + if (target?.isMaximized()) { + target?.restore(); + logManager.debug(`Window ${name} restored and focused`); + } else { + logManager.debug(`Window ${name} focused`); + } + target?.focus(); + } + close(target, really = true) { + if (!target) return; + const name = this.getName(target); + logManager.info(`Close window: ${name}, really: ${really}`); + this._handleCloseWindowState(target, really); + } + toggleMax(target) { + if (!target) return; + target.isMaximized() ? target.unmaximize() : target.maximize(); + } + getName(target) { + if (!target) return; + for (const [name, win] of Object.entries(this._winStates)) { + if (win?.instance === target) return name; + } + } + get(name) { + if (this._winStates[name].isHidden) return void 0; + return this._winStates[name].instance; + } + onWindowCreate(name, callback) { + this._winStates[name].onCreate.push(callback); + } + onWindowClosed(name, callback) { + this._winStates[name].onClosed.push(callback); + } +} +const windowManager = WindowService.getInstance(); +let t$1 = createTranslator(); +class MenuService { + static _instance; + _menuTemplates = /* @__PURE__ */ new Map(); + _currentMenu = void 0; + constructor() { + this._setupIpcListener(); + this._setupLanguageChangeListener(); + logManager.info("MenuService initialized successfully."); + } + _setupIpcListener() { + electron.ipcMain.handle(IPC_EVENTS.SHOW_CONTEXT_MENU, (_, menuId, dynamicOptions) => new Promise((resolve) => this.showMenu(menuId, () => resolve(true), dynamicOptions))); + } + _setupLanguageChangeListener() { + configManager.onConfigChange((config) => { + if (!config[CONFIG_KEYS.LANGUAGE]) return; + t$1 = createTranslator(); + }); + } + static getInstance() { + if (!this._instance) + this._instance = new MenuService(); + return this._instance; + } + register(menuId, template) { + this._menuTemplates.set(menuId, template); + return menuId; + } + showMenu(menuId, onClose, dynamicOptions) { + if (this._currentMenu) return; + const template = cloneDeep(this._menuTemplates.get(menuId)); + if (!template) { + logManager.warn(`Menu ${menuId} not found.`); + onClose?.(); + return; + } + let _dynamicOptions = []; + try { + _dynamicOptions = Array.isArray(dynamicOptions) ? dynamicOptions : JSON.parse(dynamicOptions ?? "[]"); + } catch (error) { + logManager.error(`Failed to parse dynamicOptions for menu ${menuId}: ${error}`); + } + const translationItem = (item) => { + if (item.submenu) { + return { + ...item, + label: t$1(item?.label) ?? void 0, + submenu: item.submenu?.map((item2) => translationItem(item2)) + }; + } + return { + ...item, + label: t$1(item?.label) ?? void 0 + }; + }; + const localizedTemplate = template.map((item) => { + if (!Array.isArray(_dynamicOptions) || !_dynamicOptions.length) { + return translationItem(item); + } + const dynamicItem = _dynamicOptions.find((_item) => _item.id === item.id); + if (dynamicItem) { + const mergedItem = { ...item, ...dynamicItem }; + return translationItem(mergedItem); + } + if (item.submenu) { + return translationItem({ + ...item, + submenu: item.submenu?.map((__item) => { + const dynamicItem2 = _dynamicOptions.find((_item) => _item.id === __item.id); + return { ...__item, ...dynamicItem2 }; + }) + }); + } + return translationItem(item); + }); + const menu2 = electron.Menu.buildFromTemplate(localizedTemplate); + this._currentMenu = menu2; + menu2.popup({ + callback: () => { + this._currentMenu = void 0; + onClose?.(); + } + }); + } + destroyMenu(menuId) { + this._menuTemplates.delete(menuId); + } + destroyed() { + this._menuTemplates.clear(); + this._currentMenu = void 0; + } +} +const menuManager = MenuService.getInstance(); +let t = createTranslator(); +class TrayService { + static _instance; + _tray = null; + _removeLanguageListener; + _setupLanguageChangeListener() { + this._removeLanguageListener = configManager.onConfigChange((config) => { + if (!config[CONFIG_KEYS.LANGUAGE]) return; + t = createTranslator(); + if (this._tray) { + this._updateTray(); + } + }); + } + _updateTray() { + if (!this._tray) { + this._tray = new electron.Tray(createLogo()); + } + const showWindow = () => { + const mainWindow = windowManager.get(WINDOW_NAMES.MAIN); + if (mainWindow && !mainWindow?.isDestroyed() && mainWindow?.isVisible() && !mainWindow?.isFocused()) { + return mainWindow.focus(); + } + if (mainWindow?.isMinimized()) { + return mainWindow?.restore(); + } + if (mainWindow?.isVisible() && mainWindow?.isFocused()) return; + windowManager.create(WINDOW_NAMES.MAIN, MAIN_WIN_SIZE); + }; + this._tray.setToolTip(t("tray.tooltip") ?? "Diona Application"); + this._tray.setContextMenu(electron.Menu.buildFromTemplate([ + { label: t("tray.showWindow"), accelerator: "CmdOrCtrl+N", click: showWindow }, + { type: "separator" }, + { label: t("settings.title"), click: () => electron.ipcMain.emit(`${IPC_EVENTS.OPEN_WINDOW}:${WINDOW_NAMES.SETTING}`) }, + { role: "quit", label: t("tray.exit") } + ])); + this._tray.removeAllListeners("click"); + this._tray.on("click", showWindow); + } + constructor() { + this._setupLanguageChangeListener(); + logManager.info("TrayService initialized successfully."); + } + static getInstance() { + if (!this._instance) { + this._instance = new TrayService(); + } + return this._instance; + } + create() { + if (this._tray) return; + this._updateTray(); + electron.app.on("quit", () => { + this.destroy(); + }); + } + destroy() { + this._tray?.destroy(); + this._tray = null; + if (this._removeLanguageListener) { + this._removeLanguageListener(); + this._removeLanguageListener = void 0; + } + } +} +const trayManager = TrayService.getInstance(); +class TabManager { + win; + views = /* @__PURE__ */ new Map(); + activeId = null; + skipNextNavigate = /* @__PURE__ */ new Map(); + enabled = false; + constructor(win) { + this.win = win; + this.win.on("resize", () => this.updateActiveBounds()); + this._setupIpcEvents(); + } + _setupIpcEvents() { + electron.ipcMain.handle(IPC_EVENTS.TAB_CREATE, (_e, url) => { + const info = this.create(url); + return info; + }); + electron.ipcMain.handle(IPC_EVENTS.TAB_LIST, () => { + return this.list(); + }); + electron.ipcMain.handle(IPC_EVENTS.TAB_NAVIGATE, (_e, { tabId, url }) => { + this.navigate(tabId, url); + }); + electron.ipcMain.handle(IPC_EVENTS.TAB_RELOAD, (_e, tabId) => { + this.reload(tabId); + }); + electron.ipcMain.handle(IPC_EVENTS.TAB_BACK, (_e, tabId) => { + this.goBack(tabId); + }); + electron.ipcMain.handle(IPC_EVENTS.TAB_FORWARD, (_e, tabId) => { + this.goForward(tabId); + }); + electron.ipcMain.handle(IPC_EVENTS.TAB_SWITCH, (_e, tabId) => { + this.switch(tabId); + }); + electron.ipcMain.handle(IPC_EVENTS.TAB_CLOSE, (_e, tabId) => { + this.close(tabId); + }); + } + enable() { + this.enabled = true; + this.updateActiveBounds(); + if (this.activeId) this.attach(this.activeId); + } + disable() { + this.enabled = false; + const view = this.activeId ? this.views.get(this.activeId) : null; + if (view) this.win.removeBrowserView(view); + } + destroy() { + this.disable(); + this.views.forEach((view) => { + view.webContents.destroy(); + }); + this.views.clear(); + electron.ipcMain.removeHandler(IPC_EVENTS.TAB_CREATE); + electron.ipcMain.removeHandler(IPC_EVENTS.TAB_LIST); + electron.ipcMain.removeHandler(IPC_EVENTS.TAB_NAVIGATE); + electron.ipcMain.removeHandler(IPC_EVENTS.TAB_RELOAD); + electron.ipcMain.removeHandler(IPC_EVENTS.TAB_BACK); + electron.ipcMain.removeHandler(IPC_EVENTS.TAB_FORWARD); + electron.ipcMain.removeHandler(IPC_EVENTS.TAB_SWITCH); + electron.ipcMain.removeHandler(IPC_EVENTS.TAB_CLOSE); + } + list() { + return Array.from(this.views.entries()).map(([id, view]) => this.info(id, view)); + } + create(url, active = true) { + const id = crypto.randomUUID(); + const view = new electron.BrowserView({ + webPreferences: { + nodeIntegration: false, + contextIsolation: true, + sandbox: true, + preload: path$1.join(process.cwd(), "dist-electron/preload/preload.js") + } + }); + this.views.set(id, view); + if (this.enabled && active) this.attach(id); + const target = url && url.length > 0 ? url : "about:blank"; + view.webContents.loadURL(target); + this.bindEvents(id, view); + const info = this.info(id, view); + this.win.webContents.send("tab-created", info); + return info; + } + switch(tabId) { + if (!this.views.has(tabId)) return; + if (this.enabled) this.attach(tabId); + this.win.webContents.send("tab-switched", { tabId }); + } + close(tabId) { + const view = this.views.get(tabId); + if (!view) return; + if (this.activeId === tabId) { + this.win.removeBrowserView(view); + this.activeId = null; + } + view.webContents.destroy(); + this.views.delete(tabId); + this.win.webContents.send("tab-closed", { tabId }); + const next = this.views.keys().next().value; + if (next) this.switch(next); + } + navigate(tabId, url) { + const view = this.views.get(tabId); + if (!view) return; + this.skipNextNavigate.set(tabId, true); + view.webContents.loadURL(url); + } + reload(tabId) { + const view = this.views.get(tabId); + if (!view) return; + view.webContents.reload(); + } + goBack(tabId) { + const view = this.views.get(tabId); + if (!view) return; + if (view.webContents.canGoBack()) view.webContents.goBack(); + } + goForward(tabId) { + const view = this.views.get(tabId); + if (!view) return; + if (view.webContents.canGoForward()) view.webContents.goForward(); + } + attach(tabId) { + if (!this.enabled) return; + const view = this.views.get(tabId); + if (!view) return; + if (this.activeId && this.views.get(this.activeId)) { + const prev = this.views.get(this.activeId); + this.win.removeBrowserView(prev); + } + this.activeId = tabId; + this.win.addBrowserView(view); + this.updateActiveBounds(); + } + updateActiveBounds() { + if (!this.enabled || !this.activeId) return; + const view = this.views.get(this.activeId); + if (!view) return; + const [winWidth, winHeight] = this.win.getContentSize(); + const HEADER_HEIGHT = 88; + const PADDING = 8; + const RIGHT_PANEL_WIDTH = 392 + 80 + 8 + 8; + const x = PADDING; + const y = HEADER_HEIGHT + PADDING; + const width = winWidth - RIGHT_PANEL_WIDTH - PADDING; + const height = winHeight - HEADER_HEIGHT - PADDING * 2; + view.setBounds({ + x, + y, + width: Math.max(0, width), + height: Math.max(0, height) + }); + } + bindEvents(id, view) { + const send = () => this.win.webContents.send("tab-updated", this.info(id, view)); + view.webContents.on("did-start-loading", send); + view.webContents.on("did-stop-loading", send); + view.webContents.on("did-finish-load", send); + view.webContents.on("page-title-updated", send); + view.webContents.on("did-navigate", send); + view.webContents.on("did-navigate-in-page", send); + view.webContents.on("will-navigate", (event, url) => { + if (this.skipNextNavigate.get(id)) { + this.skipNextNavigate.set(id, false); + return; + } + event.preventDefault(); + this.create(url); + }); + view.webContents.setWindowOpenHandler(({ url }) => { + this.create(url); + return { action: "deny" }; + }); + } + info(id, view) { + const wc = view.webContents; + return { + id, + url: wc.getURL(), + title: wc.getTitle(), + isLoading: wc.isLoading(), + canGoBack: wc.canGoBack(), + canGoForward: wc.canGoForward() + }; + } +} +const handleTray = (minimizeToTray) => { + if (minimizeToTray) { + trayManager.create(); + return; + } + trayManager.destroy(); +}; +const registerMenus = (window2) => { + const conversationItemMenuItemClick = (id) => { + logManager.logUserOperation(`${IPC_EVENTS.SHOW_CONTEXT_MENU}:${MENU_IDS.CONVERSATION_ITEM}-${id}`); + window2.webContents.send(`${IPC_EVENTS.SHOW_CONTEXT_MENU}:${MENU_IDS.CONVERSATION_ITEM}`, id); + }; + menuManager.register(MENU_IDS.CONVERSATION_ITEM, [ + { + id: CONVERSATION_ITEM_MENU_IDS.PIN, + label: "menu.conversation.pinConversation", + click: () => conversationItemMenuItemClick(CONVERSATION_ITEM_MENU_IDS.PIN) + }, + { + id: CONVERSATION_ITEM_MENU_IDS.RENAME, + label: "menu.conversation.renameConversation", + click: () => conversationItemMenuItemClick(CONVERSATION_ITEM_MENU_IDS.RENAME) + }, + { + id: CONVERSATION_ITEM_MENU_IDS.DEL, + label: "menu.conversation.delConversation", + click: () => conversationItemMenuItemClick(CONVERSATION_ITEM_MENU_IDS.DEL) + } + ]); + const conversationListMenuItemClick = (id) => { + logManager.logUserOperation(`${IPC_EVENTS.SHOW_CONTEXT_MENU}:${MENU_IDS.CONVERSATION_LIST}-${id}`); + window2.webContents.send(`${IPC_EVENTS.SHOW_CONTEXT_MENU}:${MENU_IDS.CONVERSATION_LIST}`, id); + }; + menuManager.register(MENU_IDS.CONVERSATION_LIST, [ + { + id: CONVERSATION_LIST_MENU_IDS.NEW_CONVERSATION, + label: "menu.conversation.newConversation", + click: () => conversationListMenuItemClick(CONVERSATION_LIST_MENU_IDS.NEW_CONVERSATION) + }, + { type: "separator" }, + { + id: CONVERSATION_LIST_MENU_IDS.SORT_BY, + label: "menu.conversation.sortBy", + submenu: [ + { id: CONVERSATION_LIST_MENU_IDS.SORT_BY_CREATE_TIME, label: "menu.conversation.sortByCreateTime", type: "radio", checked: false, click: () => conversationListMenuItemClick(CONVERSATION_LIST_MENU_IDS.SORT_BY_CREATE_TIME) }, + { id: CONVERSATION_LIST_MENU_IDS.SORT_BY_UPDATE_TIME, label: "menu.conversation.sortByUpdateTime", type: "radio", checked: false, click: () => conversationListMenuItemClick(CONVERSATION_LIST_MENU_IDS.SORT_BY_UPDATE_TIME) }, + { id: CONVERSATION_LIST_MENU_IDS.SORT_BY_NAME, label: "menu.conversation.sortByName", type: "radio", checked: false, click: () => conversationListMenuItemClick(CONVERSATION_LIST_MENU_IDS.SORT_BY_NAME) }, + { id: CONVERSATION_LIST_MENU_IDS.SORT_BY_MODEL, label: "menu.conversation.sortByModel", type: "radio", checked: false, click: () => conversationListMenuItemClick(CONVERSATION_LIST_MENU_IDS.SORT_BY_MODEL) }, + { type: "separator" }, + { id: CONVERSATION_LIST_MENU_IDS.SORT_ASCENDING, label: "menu.conversation.sortAscending", type: "radio", checked: false, click: () => conversationListMenuItemClick(CONVERSATION_LIST_MENU_IDS.SORT_ASCENDING) }, + { id: CONVERSATION_LIST_MENU_IDS.SORT_DESCENDING, label: "menu.conversation.sortDescending", type: "radio", checked: false, click: () => conversationListMenuItemClick(CONVERSATION_LIST_MENU_IDS.SORT_DESCENDING) } + ] + }, + { + id: CONVERSATION_LIST_MENU_IDS.BATCH_OPERATIONS, + label: "menu.conversation.batchOperations", + click: () => conversationListMenuItemClick(CONVERSATION_LIST_MENU_IDS.BATCH_OPERATIONS) + } + ]); + const messageItemMenuItemClick = (id) => { + logManager.logUserOperation(`${IPC_EVENTS.SHOW_CONTEXT_MENU}:${MENU_IDS.MESSAGE_ITEM}-${id}`); + window2.webContents.send(`${IPC_EVENTS.SHOW_CONTEXT_MENU}:${MENU_IDS.MESSAGE_ITEM}`, id); + }; + menuManager.register(MENU_IDS.MESSAGE_ITEM, [ + { + id: MESSAGE_ITEM_MENU_IDS.COPY, + label: "menu.message.copyMessage", + click: () => messageItemMenuItemClick(MESSAGE_ITEM_MENU_IDS.COPY) + }, + { + id: MESSAGE_ITEM_MENU_IDS.SELECT, + label: "menu.message.selectMessage", + click: () => messageItemMenuItemClick(MESSAGE_ITEM_MENU_IDS.SELECT) + }, + { type: "separator" }, + { + id: MESSAGE_ITEM_MENU_IDS.DELETE, + label: "menu.message.deleteMessage", + click: () => messageItemMenuItemClick(MESSAGE_ITEM_MENU_IDS.DELETE) + } + ]); +}; +function setupMainWindow() { + windowManager.onWindowCreate(WINDOW_NAMES.MAIN, (mainWindow) => { + let minimizeToTray = configManager.get(CONFIG_KEYS.MINIMIZE_TO_TRAY); + configManager.onConfigChange((config) => { + if (minimizeToTray === config[CONFIG_KEYS.MINIMIZE_TO_TRAY]) return; + minimizeToTray = config[CONFIG_KEYS.MINIMIZE_TO_TRAY]; + handleTray(minimizeToTray); + }); + handleTray(minimizeToTray); + registerMenus(mainWindow); + const tabManager = new TabManager(mainWindow); + tabManager.enable(); + mainWindow.on("closed", () => { + tabManager.destroy(); + }); + }); + windowManager.create(WINDOW_NAMES.MAIN, MAIN_WIN_SIZE); + electron.ipcMain.on(IPC_EVENTS.START_A_DIALOGUE, async (_event, props) => { + const { providerName, messages: messages2, messageId, selectedModel } = props; + const mainWindow = windowManager.get(WINDOW_NAMES.MAIN); + if (!mainWindow) { + throw new Error("mainWindow not found"); + } + try { + const provider = createProvider(providerName); + const chunks = await provider?.chat(messages2, selectedModel); + if (!chunks) { + throw new Error("chunks or stream not found"); + } + for await (const chunk of chunks) { + const chunkContent = { + messageId, + data: chunk + }; + mainWindow.webContents.send(IPC_EVENTS.START_A_DIALOGUE + "back" + messageId, chunkContent); + } + } catch (error) { + const errorContent = { + messageId, + data: { + isEnd: true, + isError: true, + result: error instanceof Error ? error.message : String(error) + } + }; + mainWindow.webContents.send(IPC_EVENTS.START_A_DIALOGUE + "back" + messageId, errorContent); + } + }); +} +function getChromePath() { + if (process.platform === "win32") { + return "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"; + } + if (process.platform === "darwin") { + return "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"; + } + if (process.platform === "linux") { + return "google-chrome"; + } +} +function getProfileDir(accountId) { + return path$1.join(electron.app.getPath("userData"), `profiles`, accountId); +} +function isPortInUse(port) { + return new Promise((resolve) => { + const server = net.createServer(); + server.once("error", (err) => resolve(true)); + server.once("listening", () => { + server.close(); + resolve(false); + }); + server.listen(port); + }); +} +async function isChromeRunning() { + try { + return new Promise((resolve) => { + const req = http.get("http://127.0.0.1:9222/json/version", (res) => { + resolve(res.statusCode === 200); + }); + req.on("error", () => resolve(false)); + req.setTimeout(1e3, () => { + req.destroy(); + resolve(false); + }); + }); + } catch (error) { + return false; + } +} +async function launchLocalChrome() { + const chromePath = getChromePath(); + const profileDir = getProfileDir("default"); + log.info(`Launching Chrome with user data dir: ${profileDir}`); + const portInUse = await isPortInUse(9222); + if (portInUse) { + log.info("Chrome already running on port 9222, skip launching."); + return; + } + if (await isChromeRunning()) { + log.info("Chrome already running, skip launching."); + return; + } + return new Promise((resolve, reject) => { + const chromeProcess = child_process.spawn(chromePath, [ + "--remote-debugging-port=9222", + "--window-size=1920,1080", + "--window-position=0,0", + "--no-first-run", + `--user-data-dir=${profileDir}`, + "--no-default-browser-check", + "about:blank" + // '--window-maximized', + ], { + detached: true, + stdio: "ignore" + }); + chromeProcess.on("error", reject); + setTimeout(() => { + resolve(0); + }, 1e3); + }); +} +class executeScriptService extends events.EventEmitter { + // 执行脚本 + async executeScript(scriptPath, options) { + const MAX_TAIL = 32 * 1024; + const appendTail = (current, chunk) => { + const next = current + chunk; + return next.length > MAX_TAIL ? next.slice(next.length - MAX_TAIL) : next; + }; + return await new Promise((resolve) => { + try { + const roomType = options?.roomType ?? ""; + const startTime = options?.startTime ?? ""; + const endTime = options?.endTime ?? ""; + const operation = options?.operation ?? ""; + const tabIndex = options?.tabIndex ?? ""; + const channels = options?.channels ?? ""; + const startTabIndex = options?.startTabIndex ?? ""; + const child = electron.utilityProcess.fork(scriptPath, [], { + env: { + ...process.env, + ROOM_TYPE: String(roomType), + START_DATE: String(startTime), + END_DATE: String(endTime), + OPERATION: String(operation), + TAB_INDEX: String(tabIndex), + CHANNELS: typeof channels === "string" ? channels : JSON.stringify(channels), + START_TAB_INDEX: String(startTabIndex) + }, + stdio: "pipe" + }); + let stdoutTail = ""; + let stderrTail = ""; + if (child.stdout) { + child.stdout.on("data", (data) => { + const text = data.toString(); + stdoutTail = appendTail(stdoutTail, text); + log.info(`stdout: ${text}`); + }); + } + if (child.stderr) { + child.stderr.on("data", (data) => { + const text = data.toString(); + stderrTail = appendTail(stderrTail, text); + log.info(`stderr: ${text}`); + }); + } + child.on("exit", (code) => { + log.info(`子进程退出,退出码 ${code}`); + resolve({ + success: code === 0, + exitCode: code, + stdoutTail, + stderrTail, + ...code === 0 ? {} : { error: `Script exited with code ${code}` } + }); + }); + } catch (error) { + resolve({ + success: false, + exitCode: null, + stdoutTail: "", + stderrTail: "", + error: error?.message || "运行 Node 脚本时出错" + }); + } + }); + } +} +const openedTabIndexByChannelName = /* @__PURE__ */ new Map(); +function getScriptsDir() { + return electron.app.isPackaged ? path.join(__dirname, "scripts") : path.join(process.cwd(), "electron/scripts"); +} +function runTaskOperationService() { + const executeScriptServiceInstance = new executeScriptService(); + electron.ipcMain.handle(IPC_EVENTS.OPEN_CHANNEL, async (_event, channels) => { + try { + await launchLocalChrome(); + const scriptsDir = getScriptsDir(); + const scriptPath = path.join(scriptsDir, "open_all_channel.js"); + openedTabIndexByChannelName.clear(); + if (Array.isArray(channels)) { + for (let i = 0; i < channels.length; i++) { + const name = channels[i]?.channelName; + if (name) openedTabIndexByChannelName.set(String(name), i); + } + } + const result = await executeScriptServiceInstance.executeScript(scriptPath, { channels }); + return { success: true, result }; + } catch (error) { + return { success: false, error: error?.message || "open channel failed" }; + } + }); + electron.ipcMain.handle(IPC_EVENTS.EXECUTE_SCRIPT, async (_event, options) => { + try { + const roomType = options.roomList.find((item) => item.id === options.roomType); + const pairs = [ + ["fzName", "fg_trace.js"], + ["mtName", "mt_trace.js"], + ["dyHotelName", "dy_hotel_trace.js"], + ["dyHotSpringName", "dy_hot_spring_trace.js"] + ]; + const scriptEntries = pairs.filter(([prop]) => roomType?.[prop]); + const scriptsDir = getScriptsDir(); + const scriptPaths = scriptEntries.map(([channel, fileName]) => { + const p = path.join(scriptsDir, fileName); + if (!fs.existsSync(p)) { + throw new Error(`Script not found for channel ${channel}: ${p}`); + } + return { channel, scriptPath: p }; + }); + const results = []; + for (let i = 0; i < scriptPaths.length; i++) { + const item = scriptPaths[i]; + const channelNameMap = { + fzName: "fliggy", + mtName: "meituan", + dyHotelName: "douyin", + dyHotSpringName: "douyin" + }; + const defaultTabIndexMap = { + fliggy: 0, + meituan: 1, + douyin: 2 + }; + const mappedName = channelNameMap[item.channel]; + const tabIndex = mappedName ? openedTabIndexByChannelName.get(mappedName) ?? defaultTabIndexMap[mappedName] ?? i : i; + log.info(`Launching script for channel ${item.channel}: ${item.scriptPath} (tabIndex: ${tabIndex})`); + const result = await executeScriptServiceInstance.executeScript(item.scriptPath, { + roomType: roomType[item.channel], + startTime: options.startTime, + endTime: options.endTime, + operation: options.operation, + tabIndex + }); + results.push({ + channel: item.channel, + scriptPath: item.scriptPath, + ...result + }); + } + return { success: true, result: results }; + } catch (error) { + return { success: false, error: error.message }; + } + }); +} +class AppUpdater { + mainWindow = null; + static _instance; + _initialized = false; + constructor() { + electronUpdater.autoUpdater.autoDownload = false; + } + init() { + if (this._initialized) return; + this._initialized = true; + this.setupListeners(); + this.registerHandlers(); + } + static getInstance() { + if (!this._instance) { + this._instance = new AppUpdater(); + } + return this._instance; + } + setMainWindow(window2) { + this.mainWindow = window2; + } + setupListeners() { + electronUpdater.autoUpdater.on("checking-for-update", () => this.sendToRenderer(IPC_EVENTS.UPDATE_STATUS_CHANGED, { status: "checking" })); + electronUpdater.autoUpdater.on("update-available", (info) => this.sendToRenderer(IPC_EVENTS.UPDATE_STATUS_CHANGED, { status: "available", info })); + electronUpdater.autoUpdater.on("update-not-available", () => this.sendToRenderer(IPC_EVENTS.UPDATE_STATUS_CHANGED, { status: "not-available" })); + electronUpdater.autoUpdater.on("download-progress", (progress) => this.sendToRenderer(IPC_EVENTS.UPDATE_STATUS_CHANGED, { status: "downloading", progress })); + electronUpdater.autoUpdater.on("update-downloaded", (info) => this.sendToRenderer(IPC_EVENTS.UPDATE_STATUS_CHANGED, { status: "downloaded", info })); + electronUpdater.autoUpdater.on("error", (error) => this.sendToRenderer(IPC_EVENTS.UPDATE_STATUS_CHANGED, { status: "error", error: error.message })); + } + sendToRenderer(channel, data) { + electron.BrowserWindow.getAllWindows().forEach((win) => { + if (!win.isDestroyed()) { + win.webContents.send(channel, data); + } + }); + } + registerHandlers() { + electron.ipcMain.handle(IPC_EVENTS.UPDATE_CHECK, () => { + if (electron.app.isPackaged) { + return electronUpdater.autoUpdater.checkForUpdates(); + } else { + this.sendToRenderer(IPC_EVENTS.UPDATE_STATUS_CHANGED, { status: "checking" }); + setTimeout(() => { + this.sendToRenderer(IPC_EVENTS.UPDATE_STATUS_CHANGED, { status: "not-available" }); + }, 1500); + return null; + } + }); + electron.ipcMain.handle(IPC_EVENTS.UPDATE_DOWNLOAD, () => { + if (electron.app.isPackaged) { + return electronUpdater.autoUpdater.downloadUpdate(); + } + return null; + }); + electron.ipcMain.handle(IPC_EVENTS.UPDATE_INSTALL, () => { + if (electron.app.isPackaged) { + return electronUpdater.autoUpdater.quitAndInstall(); + } + return null; + }); + electron.ipcMain.handle(IPC_EVENTS.UPDATE_VERSION, () => { + return electron.app.getVersion(); + }); + } +} +const appUpdater = AppUpdater.getInstance(); +appUpdater.init(); +if (started) { + electron.app.quit(); +} +electron.app.whenReady().then(() => { + setupMainWindow(); + runTaskOperationService(); +}); +electron.app.on("window-all-closed", () => { + if (process.platform !== "darwin" && !configManager.get(CONFIG_KEYS.MINIMIZE_TO_TRAY)) { + log.info("app closing due to all windows being closed"); + electron.app.quit(); + } +}); +electron.app.on("activate", () => { + if (electron.BrowserWindow.getAllWindows().length === 0) { + setupMainWindow(); + } +}); diff --git a/dist-electron/main/main.jsc b/dist-electron/main/main.jsc index 32107a2..b0b98f4 100644 Binary files a/dist-electron/main/main.jsc and b/dist-electron/main/main.jsc differ diff --git a/dist-electron/preload/preload.js b/dist-electron/preload/preload.js index 59832c9..0164736 100644 --- a/dist-electron/preload/preload.js +++ b/dist-electron/preload/preload.js @@ -1 +1,99 @@ -"use strict";const n=require("electron");var i=(e=>(e.EXTERNAL_OPEN="external-open",e.WINDOW_MINIMIZE="window-minimize",e.WINDOW_MAXIMIZE="window-maximize",e.WINDOW_CLOSE="window-close",e.IS_WINDOW_MAXIMIZED="is-window-maximized",e.APP_SET_FRAMELESS="app:set-frameless",e.APP_LOAD_PAGE="app:load-page",e.TAB_CREATE="tab:create",e.TAB_LIST="tab:list",e.TAB_NAVIGATE="tab:navigate",e.TAB_RELOAD="tab:reload",e.TAB_BACK="tab:back",e.TAB_FORWARD="tab:forward",e.TAB_SWITCH="tab:switch",e.TAB_CLOSE="tab:close",e.LOG_TO_MAIN="log-to-main",e.READ_FILE="read-file",e.INVOKE="ipc:invoke",e.INVOKE_ASYNC="ipc:invokeAsync",e.APP_MINIMIZE="app:minimize",e.APP_MAXIMIZE="app:maximize",e.APP_QUIT="app:quit",e.FILE_READ="file:read",e.FILE_WRITE="file:write",e.GET_WINDOW_ID="get-window-id",e.CUSTOM_EVENT="custom:event",e.TIME_UPDATE="time:update",e.RENDERER_IS_READY="renderer-ready",e.SHOW_CONTEXT_MENU="show-context-menu",e.START_A_DIALOGUE="start-a-dialogue",e.OPEN_WINDOW="open-window",e.LOG_DEBUG="log-debug",e.LOG_INFO="log-info",e.LOG_WARN="log-warn",e.LOG_ERROR="log-error",e.CONFIG_UPDATED="config-updated",e.SET_CONFIG="set-config",e.GET_CONFIG="get-config",e.UPDATE_CONFIG="update-config",e.SET_THEME_MODE="set-theme-mode",e.GET_THEME_MODE="get-theme-mode",e.IS_DARK_THEME="is-dark-theme",e.THEME_MODE_UPDATED="theme-mode-updated",e.EXECUTE_SCRIPT="execute-script",e.OPEN_CHANNEL="open-channel",e.UPDATE_CHECK="update:check",e.UPDATE_DOWNLOAD="update:download",e.UPDATE_INSTALL="update:install",e.UPDATE_VERSION="update:version",e.UPDATE_STATUS_CHANGED="update:status-changed",e))(i||{});const a={versions:process.versions,external:{open:e=>n.ipcRenderer.invoke("external-open",e)},closeWindow:()=>n.ipcRenderer.send(i.WINDOW_CLOSE),minimizeWindow:()=>n.ipcRenderer.send(i.WINDOW_MINIMIZE),maximizeWindow:()=>n.ipcRenderer.send(i.WINDOW_MAXIMIZE),onWindowMaximized:e=>n.ipcRenderer.on(i.WINDOW_MAXIMIZE+"back",(r,d)=>e(d)),isWindowMaximized:()=>n.ipcRenderer.invoke(i.IS_WINDOW_MAXIMIZED),viewIsReady:()=>n.ipcRenderer.send(i.RENDERER_IS_READY),app:{setFrameless:e=>n.ipcRenderer.invoke(i.APP_SET_FRAMELESS,e),loadPage:e=>n.ipcRenderer.invoke(i.APP_LOAD_PAGE,e)},readFile:e=>n.ipcRenderer.invoke(i.READ_FILE,e),invoke:(e,...r)=>n.ipcRenderer.invoke(e,...r),invokeAsync:(e,...r)=>n.ipcRenderer.invoke(e,...r),on:(e,r)=>{const d=(t,...o)=>r(...o);return n.ipcRenderer.on(e,d),()=>n.ipcRenderer.removeListener(e,d)},send:(e,...r)=>n.ipcRenderer.send(e,...r),getCurrentWindowId:()=>n.ipcRenderer.sendSync(i.GET_WINDOW_ID),logger:{debug:(e,...r)=>n.ipcRenderer.send(i.LOG_DEBUG,e,...r),info:(e,...r)=>n.ipcRenderer.send(i.LOG_INFO,e,...r),warn:(e,...r)=>n.ipcRenderer.send(i.LOG_WARN,e,...r),error:(e,...r)=>n.ipcRenderer.send(i.LOG_ERROR,e,...r)},executeScript:e=>n.ipcRenderer.invoke(i.EXECUTE_SCRIPT,e),openChannel:e=>n.ipcRenderer.invoke(i.OPEN_CHANNEL,e)};n.contextBridge.exposeInMainWorld("api",a); +"use strict"; +const electron = require("electron"); +var IPC_EVENTS = /* @__PURE__ */ ((IPC_EVENTS2) => { + IPC_EVENTS2["EXTERNAL_OPEN"] = "external-open"; + IPC_EVENTS2["WINDOW_MINIMIZE"] = "window-minimize"; + IPC_EVENTS2["WINDOW_MAXIMIZE"] = "window-maximize"; + IPC_EVENTS2["WINDOW_CLOSE"] = "window-close"; + IPC_EVENTS2["IS_WINDOW_MAXIMIZED"] = "is-window-maximized"; + IPC_EVENTS2["APP_SET_FRAMELESS"] = "app:set-frameless"; + IPC_EVENTS2["APP_LOAD_PAGE"] = "app:load-page"; + IPC_EVENTS2["TAB_CREATE"] = "tab:create"; + IPC_EVENTS2["TAB_LIST"] = "tab:list"; + IPC_EVENTS2["TAB_NAVIGATE"] = "tab:navigate"; + IPC_EVENTS2["TAB_RELOAD"] = "tab:reload"; + IPC_EVENTS2["TAB_BACK"] = "tab:back"; + IPC_EVENTS2["TAB_FORWARD"] = "tab:forward"; + IPC_EVENTS2["TAB_SWITCH"] = "tab:switch"; + IPC_EVENTS2["TAB_CLOSE"] = "tab:close"; + IPC_EVENTS2["LOG_TO_MAIN"] = "log-to-main"; + IPC_EVENTS2["READ_FILE"] = "read-file"; + IPC_EVENTS2["INVOKE"] = "ipc:invoke"; + IPC_EVENTS2["INVOKE_ASYNC"] = "ipc:invokeAsync"; + IPC_EVENTS2["APP_MINIMIZE"] = "app:minimize"; + IPC_EVENTS2["APP_MAXIMIZE"] = "app:maximize"; + IPC_EVENTS2["APP_QUIT"] = "app:quit"; + IPC_EVENTS2["FILE_READ"] = "file:read"; + IPC_EVENTS2["FILE_WRITE"] = "file:write"; + IPC_EVENTS2["GET_WINDOW_ID"] = "get-window-id"; + IPC_EVENTS2["CUSTOM_EVENT"] = "custom:event"; + IPC_EVENTS2["TIME_UPDATE"] = "time:update"; + IPC_EVENTS2["RENDERER_IS_READY"] = "renderer-ready"; + IPC_EVENTS2["SHOW_CONTEXT_MENU"] = "show-context-menu"; + IPC_EVENTS2["START_A_DIALOGUE"] = "start-a-dialogue"; + IPC_EVENTS2["OPEN_WINDOW"] = "open-window"; + IPC_EVENTS2["LOG_DEBUG"] = "log-debug"; + IPC_EVENTS2["LOG_INFO"] = "log-info"; + IPC_EVENTS2["LOG_WARN"] = "log-warn"; + IPC_EVENTS2["LOG_ERROR"] = "log-error"; + IPC_EVENTS2["CONFIG_UPDATED"] = "config-updated"; + IPC_EVENTS2["SET_CONFIG"] = "set-config"; + IPC_EVENTS2["GET_CONFIG"] = "get-config"; + IPC_EVENTS2["UPDATE_CONFIG"] = "update-config"; + IPC_EVENTS2["SET_THEME_MODE"] = "set-theme-mode"; + IPC_EVENTS2["GET_THEME_MODE"] = "get-theme-mode"; + IPC_EVENTS2["IS_DARK_THEME"] = "is-dark-theme"; + IPC_EVENTS2["THEME_MODE_UPDATED"] = "theme-mode-updated"; + IPC_EVENTS2["EXECUTE_SCRIPT"] = "execute-script"; + IPC_EVENTS2["OPEN_CHANNEL"] = "open-channel"; + IPC_EVENTS2["UPDATE_CHECK"] = "update:check"; + IPC_EVENTS2["UPDATE_DOWNLOAD"] = "update:download"; + IPC_EVENTS2["UPDATE_INSTALL"] = "update:install"; + IPC_EVENTS2["UPDATE_VERSION"] = "update:version"; + IPC_EVENTS2["UPDATE_STATUS_CHANGED"] = "update:status-changed"; + return IPC_EVENTS2; +})(IPC_EVENTS || {}); +const api = { + versions: process.versions, + external: { + open: (url) => electron.ipcRenderer.invoke("external-open", url) + }, + closeWindow: () => electron.ipcRenderer.send(IPC_EVENTS.WINDOW_CLOSE), + minimizeWindow: () => electron.ipcRenderer.send(IPC_EVENTS.WINDOW_MINIMIZE), + maximizeWindow: () => electron.ipcRenderer.send(IPC_EVENTS.WINDOW_MAXIMIZE), + onWindowMaximized: (callback) => electron.ipcRenderer.on(IPC_EVENTS.WINDOW_MAXIMIZE + "back", (_, isMaximized) => callback(isMaximized)), + isWindowMaximized: () => electron.ipcRenderer.invoke(IPC_EVENTS.IS_WINDOW_MAXIMIZED), + viewIsReady: () => electron.ipcRenderer.send(IPC_EVENTS.RENDERER_IS_READY), + app: { + setFrameless: (route) => electron.ipcRenderer.invoke(IPC_EVENTS.APP_SET_FRAMELESS, route), + loadPage: (page) => electron.ipcRenderer.invoke(IPC_EVENTS.APP_LOAD_PAGE, page) + }, + // 通过 IPC 调用主进程 + readFile: (filePath) => electron.ipcRenderer.invoke(IPC_EVENTS.READ_FILE, filePath), + // 异步调用(映射为 electron 的 invoke) + invoke: (channel, ...args) => electron.ipcRenderer.invoke(channel, ...args), + // 异步调用(为了兼容老代码) + invokeAsync: (channel, ...args) => electron.ipcRenderer.invoke(channel, ...args), + // 监听主进程消息 + on: (event, callback) => { + const subscription = (_event, ...args) => callback(...args); + electron.ipcRenderer.on(event, subscription); + return () => electron.ipcRenderer.removeListener(event, subscription); + }, + // 发送消息到主进程 + send: (channel, ...args) => electron.ipcRenderer.send(channel, ...args), + // 获取窗口ID + getCurrentWindowId: () => electron.ipcRenderer.sendSync(IPC_EVENTS.GET_WINDOW_ID), + // 发送日志 + logger: { + debug: (message, ...meta) => electron.ipcRenderer.send(IPC_EVENTS.LOG_DEBUG, message, ...meta), + info: (message, ...meta) => electron.ipcRenderer.send(IPC_EVENTS.LOG_INFO, message, ...meta), + warn: (message, ...meta) => electron.ipcRenderer.send(IPC_EVENTS.LOG_WARN, message, ...meta), + error: (message, ...meta) => electron.ipcRenderer.send(IPC_EVENTS.LOG_ERROR, message, ...meta) + }, + // 执行脚本 + executeScript: (params) => electron.ipcRenderer.invoke(IPC_EVENTS.EXECUTE_SCRIPT, params), + // 打开渠道 + openChannel: (channels) => electron.ipcRenderer.invoke(IPC_EVENTS.OPEN_CHANNEL, channels) +}; +electron.contextBridge.exposeInMainWorld("api", api); diff --git a/dist/index.html b/dist/index.html index 54eda75..94a9cd0 100644 --- a/dist/index.html +++ b/dist/index.html @@ -8,8 +8,8 @@ http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' http://8.138.234.141 https://one-feel-bucket.oss-cn-guangzhou.aliyuncs.com; connect-src 'self' http://8.138.234.141 https://api.iconify.design wss://onefeel.brother7.cn" /> - - + +
diff --git a/package-lock.json b/package-lock.json index cd8d099..71533e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,7 @@ "electron-squirrel-startup": "^1.0.1", "electron-store": "^11.0.2", "electron-updater": "^6.8.3", - "element-plus": "^2.12.0", + "element-plus": "^2.13.7", "highlight.js": "^11.11.1", "js-base64": "^3.7.8", "js-cookie": "^3.0.5", @@ -4963,7 +4963,7 @@ }, "node_modules/electron": { "version": "40.8.5", - "resolved": "https://registry.npmjs.org/electron/-/electron-40.8.5.tgz", + "resolved": "https://registry.npmmirror.com/electron/-/electron-40.8.5.tgz", "integrity": "sha512-pgTY/VPQKaiU4sTjfU96iyxCXrFm4htVPCMRT4b7q9ijNTRgtLmLvcmzp2G4e7xDrq9p7OLHSmu1rBKFf6Y1/A==", "dev": true, "hasInstallScript": true, @@ -5290,9 +5290,9 @@ } }, "node_modules/element-plus": { - "version": "2.13.5", - "resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.13.5.tgz", - "integrity": "sha512-dmY24fhSREfZN/PuUt0YZigMso7wWzl+B5o+YKNN15kQIn/0hzamsPU+ebj9SES0IbUqsLX1wkrzYmzU8VrVOQ==", + "version": "2.13.7", + "resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.13.7.tgz", + "integrity": "sha512-XdHATFZOyzVFL1DaHQ90IOJQSg9UnSAV+bhDW+YB5UoZ0Hxs50mwqjqfwXkuwpSag+VXXizVcErBR6Movo5daw==", "license": "MIT", "dependencies": { "@ctrl/tinycolor": "^4.2.0", @@ -5308,7 +5308,8 @@ "lodash-es": "^4.17.23", "lodash-unified": "^1.0.3", "memoize-one": "^6.0.0", - "normalize-wheel-es": "^1.2.0" + "normalize-wheel-es": "^1.2.0", + "vue-component-type-helpers": "^3.2.4" }, "peerDependencies": { "vue": "^3.3.0" @@ -10795,6 +10796,12 @@ } } }, + "node_modules/vue-component-type-helpers": { + "version": "3.2.6", + "resolved": "https://registry.npmmirror.com/vue-component-type-helpers/-/vue-component-type-helpers-3.2.6.tgz", + "integrity": "sha512-O02tnvIfOQVmnvoWwuSydwRoHjZVt8UEBR+2p4rT35p8GAy5VTlWP8o5qXfJR/GWCN0nVZoYWsVUvx2jwgdBmQ==", + "license": "MIT" + }, "node_modules/vue-demi": { "version": "0.14.10", "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz", diff --git a/package.json b/package.json index 6d8b195..06ac291 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "description": "智念AI - 一款应用智能的多功能桌面应用", "main": "dist-electron/main/main.js", "scripts": { + "init": "pnpm install && pnpm run uv:download", "dev": "vite", "start": "vite", "build": "vite build && electron-builder", @@ -82,7 +83,7 @@ "electron-squirrel-startup": "^1.0.1", "electron-store": "^11.0.2", "electron-updater": "^6.8.3", - "element-plus": "^2.12.0", + "element-plus": "^2.13.7", "highlight.js": "^11.11.1", "js-base64": "^3.7.8", "js-cookie": "^3.0.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9287267..a16486f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -63,8 +63,8 @@ importers: specifier: ^6.8.3 version: 6.8.3 element-plus: - specifier: ^2.12.0 - version: 2.13.6(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)) + specifier: ^2.13.7 + version: 2.13.7(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)) highlight.js: specifier: ^11.11.1 version: 11.11.1 @@ -525,67 +525,79 @@ packages: resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-arm@1.0.5': resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-s390x@1.0.4': resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-x64@1.0.4': resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.0.4': resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.0.4': resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-linux-arm64@0.33.5': resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-linux-arm@0.33.5': resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-linux-s390x@0.33.5': resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-linux-x64@0.33.5': resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-linuxmusl-arm64@0.33.5': resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-linuxmusl-x64@0.33.5': resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-wasm32@0.33.5': resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} @@ -726,66 +738,79 @@ packages: resolution: {integrity: sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.60.1': resolution: {integrity: sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.60.1': resolution: {integrity: sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.60.1': resolution: {integrity: sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.60.1': resolution: {integrity: sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.60.1': resolution: {integrity: sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.60.1': resolution: {integrity: sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.60.1': resolution: {integrity: sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.60.1': resolution: {integrity: sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.60.1': resolution: {integrity: sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.60.1': resolution: {integrity: sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.60.1': resolution: {integrity: sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.60.1': resolution: {integrity: sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.60.1': resolution: {integrity: sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==} @@ -866,24 +891,28 @@ packages: engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.2.2': resolution: {integrity: sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.2.2': resolution: {integrity: sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.2.2': resolution: {integrity: sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.2.2': resolution: {integrity: sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==} @@ -1621,8 +1650,8 @@ packages: engines: {node: '>= 12.20.55'} hasBin: true - element-plus@2.13.6: - resolution: {integrity: sha512-XHgwXr8Fjz6i+6BaqFhAbae/dJbG7bBAAlHrY3pWL7dpj+JcqcOyKYt4Oy5KP86FQwS1k4uIZDjCx2FyUR5lDg==} + element-plus@2.13.7: + resolution: {integrity: sha512-XdHATFZOyzVFL1DaHQ90IOJQSg9UnSAV+bhDW+YB5UoZ0Hxs50mwqjqfwXkuwpSag+VXXizVcErBR6Movo5daw==} peerDependencies: vue: ^3.3.0 @@ -2204,24 +2233,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} @@ -4829,7 +4862,7 @@ snapshots: transitivePeerDependencies: - supports-color - element-plus@2.13.6(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)): + element-plus@2.13.7(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)): dependencies: '@ctrl/tinycolor': 4.2.0 '@element-plus/icons-vue': 2.3.2(vue@3.5.32(typescript@5.9.3)) diff --git a/resources/bin/darwin-x64/uv b/resources/bin/darwin-x64/uv new file mode 100755 index 0000000..9693eb3 Binary files /dev/null and b/resources/bin/darwin-x64/uv differ diff --git a/src/components/SideMenus/index.vue b/src/components/SideMenus/index.vue index 6bb21b8..6d77551 100644 --- a/src/components/SideMenus/index.vue +++ b/src/components/SideMenus/index.vue @@ -1,6 +1,6 @@