From c01304fb356bc67d1081a8c4663efe7d3473feb7 Mon Sep 17 00:00:00 2001 From: Dawn <2439646234@qq.com> Date: Thu, 6 Nov 2025 02:33:39 +0800 Subject: [PATCH] fix(common): :bug: modify the location of the remote login window fix packaging failure due to insufficient memory in js and node --- .github/workflows/debug-build.yml | 10 +- .github/workflows/release.yml | 6 + .vscode/extensions.json | 5 +- AI_STREAM_COMPLETE.md | 249 ------------------ AI_STREAM_IMPLEMENTATION.md | 214 --------------- package.json | 4 +- pnpm-lock.yaml | 82 +++--- src-tauri/Cargo.lock | 6 +- src-tauri/Cargo.toml | 4 +- src/App.vue | 31 ++- src/common/constants.ts | 3 + src/enums/index.ts | 2 + src/hooks/useWindow.ts | 10 +- src/layout/left/model.tsx | 56 ++-- src/router/index.ts | 5 + src/views/loginWindow/Login.vue | 43 ++- src/views/loginWindow/RemoteLoginModal.vue | 83 ++++++ .../permissions/autogenerated/reference.md | 43 --- 18 files changed, 250 insertions(+), 606 deletions(-) delete mode 100644 AI_STREAM_COMPLETE.md delete mode 100644 AI_STREAM_IMPLEMENTATION.md create mode 100644 src/views/loginWindow/RemoteLoginModal.vue delete mode 100644 tauri-plugin-hula/permissions/autogenerated/reference.md diff --git a/.github/workflows/debug-build.yml b/.github/workflows/debug-build.yml index ee62d633..2d6985ef 100644 --- a/.github/workflows/debug-build.yml +++ b/.github/workflows/debug-build.yml @@ -18,7 +18,7 @@ jobs: runs-on: ${{ inputs.platform }} steps: - uses: actions/checkout@v4 - + - name: Install Ubuntu dependencies if: inputs.platform == 'ubuntu-22.04' run: | @@ -32,7 +32,7 @@ jobs: pkg-config \ libgtk-3-dev \ libayatana-appindicator3-dev - + # 验证安装的包 dpkg -l | grep -E 'webkit|appindicator|rsvg|udev|asound|gtk' @@ -55,9 +55,13 @@ jobs: cache: 'pnpm' - name: Install dependencies + env: + NODE_OPTIONS: --max-old-space-size=4096 run: pnpm install - name: Generate component typings + env: + NODE_OPTIONS: --max-old-space-size=4096 run: pnpm vite build --emptyOutDir - name: Build Vite + Tauri @@ -81,5 +85,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + # 增加 Node.js 内存限制,避免构建时内存溢出 + NODE_OPTIONS: --max-old-space-size=4096 with: releaseId: "debug-build" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 382540e4..d713d11b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,6 +38,8 @@ jobs: run: pnpm install - name: Generate frontend build (typings & assets) + env: + NODE_OPTIONS: --max-old-space-size=4096 run: pnpm vite build --emptyOutDir - name: Upload generated component typings @@ -109,6 +111,8 @@ jobs: cache: 'pnpm' - name: Install dependencies + env: + NODE_OPTIONS: --max-old-space-size=4096 run: pnpm install # 安装 Rust @@ -131,6 +135,8 @@ jobs: TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} # 使用之前配置的私钥密码 TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + # 增加 Node.js 内存限制,避免构建时内存溢出 + NODE_OPTIONS: --max-old-space-size=4096 with: tagName: v__VERSION__ #这个动作会自动将\_\_VERSION\_\_替换为app version releaseName: 'v__VERSION__' diff --git a/.vscode/extensions.json b/.vscode/extensions.json index f32edeb5..c7ba5aa6 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -7,6 +7,9 @@ "tauri-apps.tauri-vscode", "1yib.rust-bundle", "ms-vscode.vscode-typescript-next", - "biomejs.biome" + "biomejs.biome", + "github.vscode-github-actions", + "fill-labs.dependi", + "usernamehw.errorlens" ] } diff --git a/AI_STREAM_COMPLETE.md b/AI_STREAM_COMPLETE.md deleted file mode 100644 index 89e11c1b..00000000 --- a/AI_STREAM_COMPLETE.md +++ /dev/null @@ -1,249 +0,0 @@ -# AI 流式数据渲染完成 - -## ✅ 已完成的功能 - -### 1. 后端 SSE 数据接收 ✅ -- Rust 后端成功接收 SSE 流式数据 -- 正确解析服务器返回的 JSON 格式数据 -- 通过 Tauri 事件系统发送到前端 - -### 2. 前端数据解析 ✅ -- 前端成功接收 Tauri 事件 -- 正确解析 JSON 数据,提取 `data.receive.content` 字段 -- 实时更新消息内容 - -### 3. UI 实时渲染 ✅ -- 添加消息列表显示 -- 实时更新 AI 回复内容 -- 流式光标动画效果 -- 自动滚动到底部 - -## 📊 数据流程 - -``` -服务器 SSE 流 - ↓ -data:{"success":true,"data":{"receive":{"content":"JavaScript"}}} - ↓ -Rust 后端接收并解析 - ↓ -emit('ai-stream-event', { data: JSON字符串 }) - ↓ -前端 listen('ai-stream-event') - ↓ -解析 JSON.parse(chunk) - ↓ -提取 data.data.receive.content - ↓ -更新 messageList[index].content - ↓ -Vue 响应式更新 UI - ↓ -实时显示在聊天界面 -``` - -## 🎨 UI 功能 - -### 消息显示 -- ✅ 用户消息:右对齐,绿色气泡 -- ✅ AI 消息:左对齐,默认气泡 -- ✅ 流式光标:闪烁的光标效果 `▋` -- ✅ 自动滚动:新消息自动滚动到底部 - -### 消息结构 -```typescript -interface Message { - type: 'user' | 'assistant' - content: string - streaming?: boolean // 是否正在流式输出 - timestamp?: number -} -``` - -## 🔧 关键代码修改 - -### Chat.vue 主要改动 - -1. **添加消息列表状态** -```typescript -const messageList = ref([]) -const chatContainerRef = ref(null) -``` - -2. **实时更新消息内容** -```typescript -onChunk: (chunk: string) => { - try { - const data = JSON.parse(chunk) - if (data.success && data.data?.receive?.content) { - messageList.value[aiMessageIndex].content = data.data.receive.content - scrollToBottom() - } - } catch (e) { - console.error('解析JSON失败:', e) - } -} -``` - -3. **消息列表渲染** -```vue - -``` - -4. **流式光标动画** -```css -.streaming-cursor::after { - content: '▋'; - animation: blink 1s infinite; -} - -@keyframes blink { - 0%, 50% { opacity: 1; } - 51%, 100% { opacity: 0; } -} -``` - -## 📝 服务器数据格式 - -服务器返回的 SSE 数据格式: -``` -data:{"success":true,"code":200,"data":{"send":{"id":"89799684200960","type":"user","content":"什么是js"},"receive":{"id":"89799684200961","type":"assistant","content":"JavaScript"}},"msg":"","path":null,"version":null,"timestamp":"1761736321008"} - -data:{"success":true,"code":200,"data":{"send":{"id":"89799684200960","type":"user","content":"什么是js"},"receive":{"id":"89799684200961","type":"assistant","content":"非常适合"}},"msg":"","path":null,"version":null,"timestamp":"1761736321008"} - -... -``` - -每个数据块包含: -- `success`: 是否成功 -- `data.send`: 用户发送的消息 -- `data.receive.content`: AI 回复的内容(逐步累积) -- `timestamp`: 时间戳 - -## 🎯 测试步骤 - -1. **启动应用** - ```bash - pnpm tauri dev - ``` - -2. **打开 ChatBot 窗口** - - 点击左侧 Robot 插件图标 - -3. **选择 AI 模型** - - 点击"当前模型"标签 - - 选择一个可用的模型 - -4. **发送消息** - - 在输入框输入问题,例如:"什么是 JavaScript?" - - 点击发送或按回车 - -5. **观察效果** - - ✅ 用户消息立即显示在右侧 - - ✅ AI 消息占位符出现在左侧 - - ✅ AI 回复内容逐字逐句实时更新 - - ✅ 流式光标闪烁效果 - - ✅ 自动滚动到最新消息 - - ✅ 流结束后光标消失 - -## 🐛 调试信息 - -### 前端控制台输出 -``` -🎯 Chat页面收到AI发送请求: {内容: '什么是js', 当前模型: 'gpt-3.5-turbo', ...} -🚀 开始发送AI消息: {内容: '什么是js', 模型: 'gpt-3.5-turbo', ...} -📨 收到AI流式数据: JavaScript -📨 收到AI流式数据: 非常适合 -📨 收到AI流式数据: 开发 -... -✅ AI流式响应完成 -✅ AI消息发送成功 -``` - -### Rust 后端日志 -``` -[INFO] 🤖 开始发送 AI 流式消息请求, conversation_id: xxx, request_id: xxx -[INFO] 📡 SSE Request URL: http://192.168.1.37:18760/ai/chat/message/send-stream -[INFO] ✅ SSE 连接已建立,开始监听流式数据... -[INFO] 🔍 收到原始数据块 (长度: 365): "data:{...}" -[INFO] 📨 收到 SSE 数据 (无空格): {"success":true,...} -... -[INFO] ✅ SSE 流正常结束,总内容长度: xxx -[INFO] 🏁 SSE 流处理完成 -``` - -## 🎉 功能演示 - -### 流式输出效果 -``` -用户: 什么是 JavaScript? - -AI: J▋ (0.1秒) -AI: JavaScript▋ (0.2秒) -AI: JavaScript 是▋ (0.3秒) -AI: JavaScript 是一种▋ (0.4秒) -... -AI: JavaScript 是一种非常适合开发单页应用,用户的操作不需要... (完成) -``` - -## 📋 文件清单 - -### 修改的文件 -- ✅ `src/plugins/robot/views/Chat.vue` - 添加消息列表和实时渲染 -- ✅ `src-tauri/src/command/ai_command.rs` - 添加调试日志 - -### 新增的文件 -- ✅ `AI_STREAM_COMPLETE.md` - 本文档 - -## 🚀 后续优化建议 - -1. **消息持久化** - - 将消息保存到数据库 - - 刷新后恢复历史消息 - -2. **Markdown 渲染** - - 支持代码高亮 - - 支持表格、列表等格式 - -3. **消息操作** - - 复制消息 - - 删除消息 - - 重新生成 - -4. **性能优化** - - 虚拟滚动(消息过多时) - - 消息分页加载 - -5. **用户体验** - - 停止生成按钮 - - 消息加载动画 - - 错误重试机制 - -## ✨ 总结 - -AI 流式数据功能已经完全实现并可以正常工作! - -- ✅ 后端成功接收 SSE 流式数据 -- ✅ 前端成功解析 JSON 数据 -- ✅ UI 实时渲染 AI 回复 -- ✅ 流式光标动画效果 -- ✅ 自动滚动到最新消息 -- ✅ 完整的错误处理 - -现在你可以在聊天界面看到 AI 的回复像打字一样逐字逐句地出现,就像真人在输入一样!🎉 - diff --git a/AI_STREAM_IMPLEMENTATION.md b/AI_STREAM_IMPLEMENTATION.md deleted file mode 100644 index c439815a..00000000 --- a/AI_STREAM_IMPLEMENTATION.md +++ /dev/null @@ -1,214 +0,0 @@ -# AI 流式数据接入实现文档 - -## 概述 - -本文档描述了 AI 流式数据接入功能的实现,该功能通过 HTTP 请求建立 SSE (Server-Sent Events) 连接,实时接收 AI 返回的流式数据。 - -## 实现架构 - -### 1. 后端 (Rust/Tauri) - -#### 文件位置 -- `src-tauri/src/command/ai_command.rs` - AI 命令处理 -- `src-tauri/Cargo.toml` - 添加了 `reqwest-eventsource` 依赖 - -#### 核心功能 -- **命令**: `ai_message_send_stream` -- **功能**: - 1. 发送 HTTP POST 请求到 AI 服务器 - 2. 建立 SSE 连接 - 3. 监听流式数据 - 4. 通过 Tauri 事件系统将数据发送到前端 - -#### 事件类型 -```rust -pub struct SseStreamEvent { - pub event_type: String, // "chunk" | "done" | "error" - pub data: Option, - pub error: Option, - pub request_id: String, -} -``` - -#### 工作流程 -1. 接收前端请求参数(conversationId, content, useContext) -2. 使用 `reqwest` 发送 HTTP POST 请求 -3. 使用 `reqwest-eventsource` 建立 SSE 连接 -4. 在后台任务中监听 SSE 事件流 -5. 每收到一个数据块,发送 `chunk` 事件到前端 -6. 流结束时,发送 `done` 事件 -7. 出错时,发送 `error` 事件 - -### 2. 前端 (Vue/TypeScript) - -#### 文件位置 -- `src/utils/ImRequestUtils.ts` - 流式 API 封装 -- `src/plugins/robot/views/Chat.vue` - AI 聊天界面 -- `src/enums/index.ts` - 添加了 `AI_MESSAGE_SEND_STREAM` 命令枚举 - -#### 核心功能 -- **函数**: `messageSendStream` -- **特点**: - 1. 使用 Promise 包装整个 SSE 流程 - 2. 监听 Tauri 的 `ai-stream-event` 事件 - 3. 提供回调函数支持(onChunk, onDone, onError) - 4. 流结束后 resolve Promise - -#### 使用示例 -```typescript -const fullResponse = await messageSendStream( - { - conversationId: 'chat-123', - content: '你好,AI', - useContext: true - }, - { - onChunk: (chunk: string) => { - // 实时接收数据块 - console.log('收到数据:', chunk) - }, - onDone: (fullContent: string) => { - // 流结束 - console.log('完整内容:', fullContent) - }, - onError: (error: string) => { - // 错误处理 - console.error('错误:', error) - } - } -) -``` - -## 数据流向 - -``` -前端 (Chat.vue) - ↓ 调用 messageSendStream() - ↓ -前端 (ImRequestUtils.ts) - ↓ invoke(AI_MESSAGE_SEND_STREAM) - ↓ -后端 (ai_command.rs) - ↓ 发送 HTTP POST 请求 - ↓ -AI 服务器 - ↓ 建立 SSE 连接 - ↓ 返回流式数据 - ↓ -后端 (ai_command.rs) - ↓ emit('ai-stream-event', chunk) - ↓ -前端 (ImRequestUtils.ts) - ↓ listen('ai-stream-event') - ↓ 触发回调 onChunk() - ↓ 累积数据 - ↓ 流结束时 resolve(fullContent) - ↓ -前端 (Chat.vue) - ↓ 显示完整响应 -``` - -## 关键特性 - -### 1. 同步监听 -- HTTP 请求发送后,立即在 Rust 后端建立 SSE 连接 -- 使用 `tokio::spawn` 在后台异步处理流式数据 -- 不阻塞主线程 - -### 2. 事件驱动 -- 使用 Tauri 的事件系统进行前后端通信 -- 通过 `request_id` 区分不同的请求 -- 支持多个并发请求 - -### 3. Promise 包装 -- 前端使用 Promise 包装整个流程 -- 流结束时自动 resolve -- 出错时自动 reject -- 支持 async/await 语法 - -### 4. 回调支持 -- `onChunk`: 实时接收数据块,可用于 UI 更新 -- `onDone`: 流结束时调用 -- `onError`: 错误处理 - -## 测试步骤 - -### 1. 编译项目 -```bash -# 安装依赖 -cd src-tauri -cargo build - -# 或者直接运行 -npm run tauri dev -``` - -### 2. 测试流程 -1. 打开应用 -2. 进入 ChatBot 窗口(robot 插件) -3. 选择一个 AI 模型 -4. 输入消息并发送 -5. 观察控制台输出: - - 应该看到 "🚀 开始发送AI消息" - - 应该看到 "📨 收到AI流式数据块" (多次) - - 应该看到 "✅ AI流式响应完成" - - 应该看到 "✅ AI消息发送成功" - -### 3. 验证点 -- [ ] HTTP 请求成功发送 -- [ ] SSE 连接成功建立 -- [ ] 能够接收流式数据块 -- [ ] 数据块正确传递到前端 -- [ ] Promise 在流结束后正确 resolve -- [ ] 错误情况下 Promise 正确 reject -- [ ] 多个并发请求互不干扰 - -## 注意事项 - -### 1. 窗口 Label -- AI 聊天窗口的 label 是 `robot` -- 事件会发送到所有窗口,通过 `request_id` 过滤 - -### 2. 错误处理 -- 网络错误会触发 `error` 事件 -- SSE 连接关闭会触发 `done` 事件 -- 需要区分正常结束和异常结束 - -### 3. 内存管理 -- 每个请求都会创建一个后台任务 -- 任务在流结束后自动清理 -- 前端监听器在 Promise resolve/reject 后自动取消 - -### 4. 并发请求 -- 使用唯一的 `request_id` 区分不同请求 -- 支持多个并发流式请求 -- 每个请求独立处理,互不影响 - -## 后续优化建议 - -1. **UI 实时更新**: 在 `onChunk` 回调中实时更新聊天界面,显示 AI 正在输出的内容 -2. **取消请求**: 添加取消流式请求的功能 -3. **重试机制**: 添加自动重试机制 -4. **进度指示**: 添加更详细的进度指示 -5. **错误分类**: 区分不同类型的错误(网络错误、服务器错误等) -6. **性能优化**: 对于大量数据块,考虑批量处理 - -## 相关文件清单 - -### Rust 后端 -- `src-tauri/src/command/ai_command.rs` (新建) -- `src-tauri/src/command/mod.rs` (修改) -- `src-tauri/src/lib.rs` (修改) -- `src-tauri/Cargo.toml` (修改) - -### 前端 -- `src/utils/ImRequestUtils.ts` (修改) -- `src/plugins/robot/views/Chat.vue` (修改) -- `src/enums/index.ts` (修改) - -## 技术栈 - -- **后端**: Rust, Tauri, reqwest, reqwest-eventsource, tokio -- **前端**: Vue 3, TypeScript, Tauri API -- **通信**: Tauri Event System, SSE (Server-Sent Events) - diff --git a/package.json b/package.json index 32201133..c12d863b 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "========= 启动vue(tauri项目会连带执行不需要单独执行) =========": "", "dev": "vite", "========= 打包vue(tauri项目会连带执行不需要单独执行) =========": "", - "build": "vue-tsc --noEmit && vite build", + "build": "NODE_OPTIONS=--max-old-space-size=4096 vue-tsc --noEmit && NODE_OPTIONS=--max-old-space-size=4096 vite build", "========= 启动HuLa桌面应用程序 =========": "", "tauri:dev": "tauri dev", "========= 启动HuLa桌面应用程序(简化命令) =========": "", @@ -140,7 +140,7 @@ "vue-virtual-scroller": "2.0.0-beta.8" }, "devDependencies": { - "@biomejs/biome": "^2.2.7", + "@biomejs/biome": "^2.3.4", "@commitlint/cli": "^19.8.1", "@commitlint/config-conventional": "^19.8.1", "@iconify/vue": "^5.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 331275eb..7083bb4f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -187,8 +187,8 @@ importers: version: 2.0.0-beta.8(vue@3.5.22(typescript@5.9.3)) devDependencies: '@biomejs/biome': - specifier: ^2.2.7 - version: 2.2.7 + specifier: ^2.3.4 + version: 2.3.4 '@commitlint/cli': specifier: ^19.8.1 version: 19.8.1(@types/node@24.6.2)(typescript@5.9.3) @@ -480,59 +480,59 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} - '@biomejs/biome@2.2.7': - resolution: {integrity: sha512-1a8j0UP1vXVUf3UzMZEJ/zS2VgAG6wU6Cuh/I764sUGI+MCnJs/9WaojHYBDCxCMLTgU60/WqnYof85emXmSBA==} + '@biomejs/biome@2.3.4': + resolution: {integrity: sha512-TU08LXjBHdy0mEY9APtEtZdNQQijXUDSXR7IK1i45wgoPD5R0muK7s61QcFir6FpOj/RP1+YkPx5QJlycXUU3w==} engines: {node: '>=14.21.3'} hasBin: true - '@biomejs/cli-darwin-arm64@2.2.7': - resolution: {integrity: sha512-xBUUsebnO2/Qj1v7eZmKUy2ZcFkZ4/jLUkxN02Qup1RPoRaiW9AKXHrqS3L7iX6PzofHY2xuZ+Pb9kAcpoe0qA==} + '@biomejs/cli-darwin-arm64@2.3.4': + resolution: {integrity: sha512-w40GvlNzLaqmuWYiDU6Ys9FNhJiclngKqcGld3iJIiy2bpJ0Q+8n3haiaC81uTPY/NA0d8Q/I3Z9+ajc14102Q==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [darwin] - '@biomejs/cli-darwin-x64@2.2.7': - resolution: {integrity: sha512-vsY4NhmxqgfLJufr9XUnC+yGUPJiXAc1mz6FcjaAmuIuLwfghN4uQO7hnW2AneGyoi2mNe9Jbvf6Qtq4AjzrFg==} + '@biomejs/cli-darwin-x64@2.3.4': + resolution: {integrity: sha512-3s7TLVtjJ7ni1xADXsS7x7GMUrLBZXg8SemXc3T0XLslzvqKj/dq1xGeBQ+pOWQzng9MaozfacIHdK2UlJ3jGA==} engines: {node: '>=14.21.3'} cpu: [x64] os: [darwin] - '@biomejs/cli-linux-arm64-musl@2.2.7': - resolution: {integrity: sha512-FrTwvKO/7t5HbVTvhlMOTOVQLAcR7r4O4iFQhEpZXUtBfosHqrX/JJlX7daPawoe14MDcCu9CDg0zLVpTuDvuQ==} + '@biomejs/cli-linux-arm64-musl@2.3.4': + resolution: {integrity: sha512-IruVGQRwMURivWazchiq7gKAqZSFs5so6gi0hJyxk7x6HR+iwZbO2IxNOqyLURBvL06qkIHs7Wffl6Bw30vCbQ==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] libc: [musl] - '@biomejs/cli-linux-arm64@2.2.7': - resolution: {integrity: sha512-nUdco104rjV9dULi1VssQ5R/kX2jE/Z2sDjyqS+siV9sTQda0DwmEUixFNRCWvZJRRiZUWhgiDFJ4n7RowO8Mg==} + '@biomejs/cli-linux-arm64@2.3.4': + resolution: {integrity: sha512-y7efHyyM2gYmHy/AdWEip+VgTMe9973aP7XYKPzu/j8JxnPHuSUXftzmPhkVw0lfm4ECGbdBdGD6+rLmTgNZaA==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] libc: [glibc] - '@biomejs/cli-linux-x64-musl@2.2.7': - resolution: {integrity: sha512-MnsysF5s/iLC5wnYvuMseOy+m8Pd4bWG1uwlVyy2AUbfjAVUgtbYbboc5wMXljFrDY7e6rLjLTR4S2xqDpGlQg==} + '@biomejs/cli-linux-x64-musl@2.3.4': + resolution: {integrity: sha512-mzKFFv/w66e4/jCobFmD3kymCqG+FuWE7sVa4Yjqd9v7qt2UhXo67MSZKY9Ih18V2IwPzRKQPCw6KwdZs6AXSA==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] libc: [musl] - '@biomejs/cli-linux-x64@2.2.7': - resolution: {integrity: sha512-tPTcGAIEOOZrj2tQ7fdraWlaxNKApBw6l4In8wQQV1IyxnAexqi0hykHzKEX8hKKctf5gxGBfNCzyIvqpj4CFQ==} + '@biomejs/cli-linux-x64@2.3.4': + resolution: {integrity: sha512-gKfjWR/6/dfIxPJCw8REdEowiXCkIpl9jycpNVHux8aX2yhWPLjydOshkDL6Y/82PcQJHn95VCj7J+BRcE5o1Q==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] libc: [glibc] - '@biomejs/cli-win32-arm64@2.2.7': - resolution: {integrity: sha512-h5D1jhwA2b7cFXerYiJfXHSzzAMFFoEDL5Mc2BgiaEw0iaSgSso/3Nc6FbOR55aTQISql+IpB4PS7JoV26Gdbw==} + '@biomejs/cli-win32-arm64@2.3.4': + resolution: {integrity: sha512-5TJ6JfVez+yyupJ/iGUici2wzKf0RrSAxJhghQXtAEsc67OIpdwSKAQboemILrwKfHDi5s6mu7mX+VTCTUydkw==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [win32] - '@biomejs/cli-win32-x64@2.2.7': - resolution: {integrity: sha512-URqAJi0kONyBKG4V9NVafHLDtm6IHmF4qPYi/b6x7MD6jxpWeJiTCO6R5+xDlWckX2T/OGv6Yq3nkz6s0M8Ykw==} + '@biomejs/cli-win32-x64@2.3.4': + resolution: {integrity: sha512-FGCijXecmC4IedQ0esdYNlMpx0Jxgf4zceCaMu6fkjWyjgn50ZQtMiqZZQ0Q/77yqPxvtkgZAvt5uGw0gAAjig==} engines: {node: '>=14.21.3'} cpu: [x64] os: [win32] @@ -1289,8 +1289,8 @@ packages: resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} engines: {node: '>=6.0.0'} - '@jridgewell/source-map@0.3.6': - resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} + '@jridgewell/source-map@0.3.11': + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} '@jridgewell/sourcemap-codec@1.5.0': resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} @@ -5643,39 +5643,39 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} - '@biomejs/biome@2.2.7': + '@biomejs/biome@2.3.4': optionalDependencies: - '@biomejs/cli-darwin-arm64': 2.2.7 - '@biomejs/cli-darwin-x64': 2.2.7 - '@biomejs/cli-linux-arm64': 2.2.7 - '@biomejs/cli-linux-arm64-musl': 2.2.7 - '@biomejs/cli-linux-x64': 2.2.7 - '@biomejs/cli-linux-x64-musl': 2.2.7 - '@biomejs/cli-win32-arm64': 2.2.7 - '@biomejs/cli-win32-x64': 2.2.7 + '@biomejs/cli-darwin-arm64': 2.3.4 + '@biomejs/cli-darwin-x64': 2.3.4 + '@biomejs/cli-linux-arm64': 2.3.4 + '@biomejs/cli-linux-arm64-musl': 2.3.4 + '@biomejs/cli-linux-x64': 2.3.4 + '@biomejs/cli-linux-x64-musl': 2.3.4 + '@biomejs/cli-win32-arm64': 2.3.4 + '@biomejs/cli-win32-x64': 2.3.4 - '@biomejs/cli-darwin-arm64@2.2.7': + '@biomejs/cli-darwin-arm64@2.3.4': optional: true - '@biomejs/cli-darwin-x64@2.2.7': + '@biomejs/cli-darwin-x64@2.3.4': optional: true - '@biomejs/cli-linux-arm64-musl@2.2.7': + '@biomejs/cli-linux-arm64-musl@2.3.4': optional: true - '@biomejs/cli-linux-arm64@2.2.7': + '@biomejs/cli-linux-arm64@2.3.4': optional: true - '@biomejs/cli-linux-x64-musl@2.2.7': + '@biomejs/cli-linux-x64-musl@2.3.4': optional: true - '@biomejs/cli-linux-x64@2.2.7': + '@biomejs/cli-linux-x64@2.3.4': optional: true - '@biomejs/cli-win32-arm64@2.2.7': + '@biomejs/cli-win32-arm64@2.3.4': optional: true - '@biomejs/cli-win32-x64@2.2.7': + '@biomejs/cli-win32-x64@2.3.4': optional: true '@braintree/sanitize-url@7.1.1': {} @@ -6292,7 +6292,7 @@ snapshots: '@jridgewell/set-array@1.2.1': {} - '@jridgewell/source-map@0.3.6': + '@jridgewell/source-map@0.3.11': dependencies: '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 @@ -10058,7 +10058,7 @@ snapshots: terser@5.37.0: dependencies: - '@jridgewell/source-map': 0.3.6 + '@jridgewell/source-map': 0.3.11 acorn: 8.15.0 commander: 2.20.3 source-map-support: 0.5.21 diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 303a15d3..6262945e 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2799,7 +2799,7 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hula" -version = "3.0.2" +version = "3.0.3" dependencies = [ "anyhow", "async-walkdir", @@ -7152,9 +7152,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "2.9.1" +version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9871670c6711f50fddd4e20350be6b9dd6e6c2b5d77d8ee8900eb0d58cd837a" +checksum = "8bceb52453e507c505b330afe3398510e87f428ea42b6e76ecb6bd63b15965b5" dependencies = [ "anyhow", "bytes", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 0e433d0a..e6ba2ec3 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hula" -version = "3.0.2" +version = "3.0.3" description = "hula" authors = ["nongyehong"] license = "" @@ -36,7 +36,7 @@ cc = "1.0" [dependencies] # Tauri 官方依赖 -tauri = { version = "2.9.1", features = [ +tauri = { version = "2.9.2", features = [ "protocol-asset", "macos-private-api", "tray-icon", diff --git a/src/App.vue b/src/App.vue index e6da0db5..2cdceac8 100644 --- a/src/App.vue +++ b/src/App.vue @@ -21,7 +21,6 @@ import { ThemeEnum, ChangeTypeEnum, MittEnum, - ModalEnum, OnlineEnum, RoomTypeEnum } from '@/enums' @@ -34,7 +33,6 @@ import { useSettingStore } from '@/stores/setting.ts' import { isDesktop, isIOS, isMobile, isWindows } from '@/utils/PlatformConstants' import LockScreen from '@/views/LockScreen.vue' import { unreadCountManager } from '@/utils/UnreadCountManager' - import { type LoginSuccessResType, type OnStatusChangeType, @@ -49,6 +47,8 @@ import { useAnnouncementStore } from '@/stores/announcement' import { useFeedStore } from '@/stores/feed' import { useFeedNotificationStore } from '@/stores/feedNotification' import type { MarkItemType, RevokedMsgType, UserItem } from '@/services/types.ts' +import * as ImRequestUtils from '@/utils/ImRequestUtils' +import { REMOTE_LOGIN_INFO_KEY } from '@/common/constants' const mobileRtcCallFloatCell = isMobile() ? defineAsyncComponent(() => import('@/mobile/components/RtcCallFloatCell.vue')) @@ -69,7 +69,7 @@ const router = useRouter() // 只在桌面端初始化窗口管理功能 const { createWebviewWindow } = isDesktop() ? useWindow() : { createWebviewWindow: () => {} } const settingStore = useSettingStore() -const { themes, lockScreen, page } = storeToRefs(settingStore) +const { themes, lockScreen, page, login } = storeToRefs(settingStore) // 全局快捷键管理 const { initializeGlobalShortcut, cleanupGlobalShortcut } = useGlobalShortcut() @@ -276,11 +276,9 @@ useMitt.on(WsResponseMessageType.ROOM_INFO_CHANGE, async (data: { roomId: string useMitt.on(WsResponseMessageType.TOKEN_EXPIRED, async (wsTokenExpire: WsTokenExpire) => { if (Number(userUid.value) === Number(wsTokenExpire.uid) && userStore.userInfo!.client === wsTokenExpire.client) { + const { useLogin } = await import('@/hooks/useLogin') + const { resetLoginState, logout } = useLogin() if (isMobile()) { - // 移动端处理:立即清空登录数据并跳转到登录页,然后显示弹窗 - const { useLogin } = await import('@/hooks/useLogin') - const { resetLoginState, logout } = useLogin() - try { // 1. 先重置登录状态(不请求接口,只清理本地) await resetLoginState() @@ -314,13 +312,18 @@ useMitt.on(WsResponseMessageType.TOKEN_EXPIRED, async (wsTokenExpire: WsTokenExp // 桌面端处理:聚焦主窗口并显示远程登录弹窗 const home = await WebviewWindow.getByLabel('home') await home?.setFocus() - - useMitt.emit(MittEnum.LEFT_MODAL_SHOW, { - type: ModalEnum.REMOTE_LOGIN, - props: { - ip: wsTokenExpire.ip - } - }) + const remoteIp = wsTokenExpire.ip || '未知IP' + localStorage.setItem( + REMOTE_LOGIN_INFO_KEY, + JSON.stringify({ + ip: remoteIp, + timestamp: Date.now() + }) + ) + await ImRequestUtils.logout({ autoLogin: login.value.autoLogin }) + await resetLoginState() + await logout() + useMitt.emit(MittEnum.LOGIN_REMOTE_MODAL) } } }) diff --git a/src/common/constants.ts b/src/common/constants.ts index bf8684dc..a36f0ad0 100644 --- a/src/common/constants.ts +++ b/src/common/constants.ts @@ -6,3 +6,6 @@ export const MAX_FOOTER_HEIGHT = 390 export const MIN_FOOTER_HEIGHT = 200 /** 顶部选项栏高度 */ export const TOOLBAR_HEIGHT = 40 + +/** 异地登录信息存储键 */ +export const REMOTE_LOGIN_INFO_KEY = 'REMOTE_LOGIN_INFO' diff --git a/src/enums/index.ts b/src/enums/index.ts index 3b89e956..57ceea5d 100644 --- a/src/enums/index.ts +++ b/src/enums/index.ts @@ -82,6 +82,8 @@ export enum MittEnum { OPEN_GROUP_NICKNAME_MODAL = 'openGroupNicknameModal', /** 左边菜单弹窗 */ LEFT_MODAL_SHOW = 'leftModalShow', + /** 登录窗口异地登录弹窗 */ + LOGIN_REMOTE_MODAL = 'loginRemoteModal', /** 触发home窗口事件 */ HOME_WINDOW_RESIZE = 'homeWindowResize', /** @ AT */ diff --git a/src/hooks/useWindow.ts b/src/hooks/useWindow.ts index fb922f4f..1bbf68d9 100644 --- a/src/hooks/useWindow.ts +++ b/src/hooks/useWindow.ts @@ -187,7 +187,11 @@ export const useWindow = () => { width: number, height: number, parent: string, - payload?: Record + payload?: Record, + options?: { + minWidth?: number + minHeight?: number + } ) => { // 移动端不支持窗口管理 if (!isDesktop()) { @@ -211,8 +215,8 @@ export const useWindow = () => { height: height, resizable: false, center: true, - minWidth: 500, - minHeight: 500, + minWidth: options?.minWidth ?? 500, + minHeight: options?.minHeight ?? 500, focus: true, minimizable: false, parent: parentWindow ? parentWindow : parent, diff --git a/src/layout/left/model.tsx b/src/layout/left/model.tsx index 057eda3b..979472b2 100644 --- a/src/layout/left/model.tsx +++ b/src/layout/left/model.tsx @@ -1,4 +1,3 @@ -import { emit } from '@tauri-apps/api/event' import { type FormInst, NAvatar, @@ -15,6 +14,8 @@ import { NTimeline, NTimelineItem } from 'naive-ui' +import type { PropType } from 'vue' +import { emit } from '@tauri-apps/api/event' import { EventEnum } from '@/enums' import { handRelativeTime } from '@/utils/Day.ts' import './style.scss' @@ -22,33 +23,17 @@ import { getVersion } from '@tauri-apps/api/app' import { confirm } from '@tauri-apps/plugin-dialog' import { relaunch } from '@tauri-apps/plugin-process' import { check } from '@tauri-apps/plugin-updater' -import { useLogin } from '@/hooks/useLogin' import { useSettingStore } from '@/stores/setting.ts' import { useUserStore } from '@/stores/user.ts' import { AvatarUtils } from '@/utils/AvatarUtils' -import * as ImRequestUtils from '@/utils/ImRequestUtils' import { isMac } from '@/utils/PlatformConstants' +import { REMOTE_LOGIN_INFO_KEY } from '@/common/constants' -const { logout: sysLogout, resetLoginState } = useLogin() const formRef = ref() const formValue = ref({ lockPassword: '' }) export const modalShow = ref(false) -export const remotelogin = ref({ - loading: false, - async logout() { - remotelogin.value.loading = true - const settingStore = useSettingStore() - const { login } = storeToRefs(settingStore) - // token已在后端清空,只需要返回登录页 - await ImRequestUtils.logout({ autoLogin: login.value.autoLogin }) - await resetLoginState() - await sysLogout() - modalShow.value = false - remotelogin.value.loading = false - } -}) export const lock = ref({ loading: false, rules: { @@ -432,26 +417,41 @@ export const RemoteLogin = defineComponent({ ip: { type: String, default: '未知IP' + }, + onConfirm: { + type: Function as PropType<() => void | Promise>, + default: void 0 } }, setup(props) { const userStore = useUserStore() + const handleConfirm = async () => { + modalShow.value = false + if (props.onConfirm) { + // 关闭对应子窗口 + await props.onConfirm() + } + } + setInterval(() => { + localStorage.removeItem(REMOTE_LOGIN_INFO_KEY) + }, 300) + return () => ( -
+ class="w-350px h-310px border-rd-8px select-none cursor-default"> +
{isMac() ? (
) : ( - + )} @@ -460,7 +460,7 @@ export const RemoteLogin = defineComponent({ 下线通知
- +
@@ -473,14 +473,8 @@ export const RemoteLogin = defineComponent({ 登录,如非本人登录,请尽快修改密码,建议联系管理员
- - 重新登录 + + 知道了
diff --git a/src/router/index.ts b/src/router/index.ts index 2b107bf7..afe571f6 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -588,6 +588,11 @@ const getCommonRoutes = (): Array => [ path: '/modal-privacyAgreement', name: 'modal-privacyAgreement', component: () => import('@/views/agreementWindow/Privacy.vue') + }, + { + path: '/modal-remoteLogin', + name: 'modal-remoteLogin', + component: () => import('@/views/loginWindow/RemoteLoginModal.vue') } ] diff --git a/src/views/loginWindow/Login.vue b/src/views/loginWindow/Login.vue index 94860003..0b8adb75 100644 --- a/src/views/loginWindow/Login.vue +++ b/src/views/loginWindow/Login.vue @@ -192,8 +192,10 @@ import { useGuideStore } from '@/stores/guide' import { useLoginHistoriesStore } from '@/stores/loginHistory.ts' import { useSettingStore } from '@/stores/setting.ts' import { useUserStore } from '@/stores/user.ts' +import { MittEnum } from '@/enums' +import { REMOTE_LOGIN_INFO_KEY } from '@/common/constants' import { AvatarUtils } from '@/utils/AvatarUtils' -import { isCompatibility, isMac } from '@/utils/PlatformConstants' +import { isCompatibility, isDesktop, isMac } from '@/utils/PlatformConstants' import { clearListener } from '@/utils/ReadCountQueue' import { useLogin } from '@/hooks/useLogin' @@ -311,6 +313,44 @@ watch( } ) +const openRemoteLoginModal = async (ip?: string) => { + if (!isDesktop()) { + return + } + const payloadIp = ip ?? '未知IP' + await createModalWindow( + '异地登录提醒', + 'modal-remoteLogin', + 350, + 310, + 'login', + { + ip: payloadIp + }, + { + minWidth: 350, + minHeight: 310 + } + ) +} + +const handleCachedRemoteLoginModal = () => { + const cached = localStorage.getItem(REMOTE_LOGIN_INFO_KEY) + if (!cached) { + return + } + try { + const parsed = JSON.parse(cached) as { ip?: string } + openRemoteLoginModal(parsed?.ip) + } catch (error) { + localStorage.removeItem(REMOTE_LOGIN_INFO_KEY) + } +} + +useMitt.on(MittEnum.LOGIN_REMOTE_MODAL, (payload: { ip?: string }) => { + openRemoteLoginModal(payload?.ip) +}) + /** 删除账号列表内容 */ const delAccount = (item: UserInfoType) => { // 获取删除前账户列表的长度 @@ -373,6 +413,7 @@ const enterKey = (e: KeyboardEvent) => { } onBeforeMount(async () => { + handleCachedRemoteLoginModal() // 始终初始化托盘菜单状态为false isTrayMenuShow.value = false diff --git a/src/views/loginWindow/RemoteLoginModal.vue b/src/views/loginWindow/RemoteLoginModal.vue new file mode 100644 index 00000000..b1f66c47 --- /dev/null +++ b/src/views/loginWindow/RemoteLoginModal.vue @@ -0,0 +1,83 @@ + + + + + diff --git a/tauri-plugin-hula/permissions/autogenerated/reference.md b/tauri-plugin-hula/permissions/autogenerated/reference.md deleted file mode 100644 index 23041a43..00000000 --- a/tauri-plugin-hula/permissions/autogenerated/reference.md +++ /dev/null @@ -1,43 +0,0 @@ -## Default Permission - -Default permissions for the plugin - -#### This default permission set includes the following: - -- `allow-ping` - -## Permission Table - - - - - - - - - - - - - - - - - -
IdentifierDescription
- -`hula:allow-ping` - - - -Enables the ping command without any pre-configured scope. - -
- -`hula:deny-ping` - - - -Denies the ping command without any pre-configured scope. - -