Merge branch 'master' into feat/AIChat

This commit is contained in:
OrionMark
2025-06-06 21:45:13 +08:00
47 changed files with 1133 additions and 171 deletions

View File

@@ -1,5 +1,32 @@
## [2.6.12](https://github.com/HuLaSpark/HuLa/compare/v2.6.11...v2.6.12) (2025-05-25)
### ✨ Features | 新功能
* **component:** :sparkles: 增加右键emoji可以另存为 ([0dfdfc1](https://github.com/HuLaSpark/HuLa/commit/0dfdfc1269256baa59316b7936db94468b733b69))
* **hook:** :sparkles: 增强文本选择复制 ([791b9cc](https://github.com/HuLaSpark/HuLa/commit/791b9cc69485d9c8245396d72cca834ac9a61aa0)), closes [#279](https://github.com/HuLaSpark/HuLa/issues/279)
### 🐛 Bug Fixes | Bug 修复
* 修复ws链接断开没有重试问题 ([#276](https://github.com/HuLaSpark/HuLa/issues/276)) ([e068a25](https://github.com/HuLaSpark/HuLa/commit/e068a253e5215aacb606f852087e5e9e67010404))
* **agreement:** :bug: 安装界面乱码 ([#277](https://github.com/HuLaSpark/HuLa/issues/277)) ([13c528a](https://github.com/HuLaSpark/HuLa/commit/13c528a35e11ee5fa7325beb1fcef8d28f2550e3)), closes [#275](https://github.com/HuLaSpark/HuLa/issues/275)
* **hook:** :bug: 修复除emoji类型和图片类型都可以另存为的bug ([d2b6ab2](https://github.com/HuLaSpark/HuLa/commit/d2b6ab25d6fd7ea5a4e9df1d287fe3d3cc9a1b58))
* **hook:** :bug: 修复mac系统右键会选中文本的问题 ([7e762e8](https://github.com/HuLaSpark/HuLa/commit/7e762e8524df0d17f85ca71eedb95d01dea7c8d3))
* **hook:** :bug: 暂时移除/唤起ai快捷键识别 ([4a1a05c](https://github.com/HuLaSpark/HuLa/commit/4a1a05cf51b4bab670b2da5faac43bf33f902998))
* **input:** :bug: 修复mac下输入框检查拼写和字母大小写问题 ([f8602e5](https://github.com/HuLaSpark/HuLa/commit/f8602e56ebbf4ae90f5f5dc7e7cebee317bf4ab4))
* **mac:** :bug: 修复mac下点击关闭按钮无法关闭home窗口问题 ([2a63046](https://github.com/HuLaSpark/HuLa/commit/2a63046bbd2d7c4cd484d456c708ab47bdc8e792))
* **view:** :bug: 修复托盘菜单内容不展示问题和托盘图标闪烁后不展示问题 ([c927be4](https://github.com/HuLaSpark/HuLa/commit/c927be4c3fd00cdde9f93c15793ea56ce5b11d14))
* **view:** :bug: 修改邮箱输入框长度限制 ([61618db](https://github.com/HuLaSpark/HuLa/commit/61618db93cbe9512eceb66fbc50006a90f7d44f1)), closes [#278](https://github.com/HuLaSpark/HuLa/issues/278)
* **worker:** :bug: 修复ws在重连后清空token导致无法对应获取消息问题 ([030fed7](https://github.com/HuLaSpark/HuLa/commit/030fed7d60a6eb03dccb49e6f108b2b5d67161e4))
### ⚡️ Performance Improvements | 性能优化
* **global:** :zap: 增加ws健康检查兜底刷新最新消息内容、网络断线恢复重连 ([f734dca](https://github.com/HuLaSpark/HuLa/commit/f734dca910b17e3dd8a4d8e5e58cae3e7caaa333))
## [2.6.11](https://github.com/HuLaSpark/HuLa/compare/v2.6.10...v2.6.11) (2025-05-18)

View File

@@ -216,6 +216,7 @@ Thanks to the following sponsors for their support!
| Date | Sponsor | Sum | Platform |
|------|--------|------|------|
| 2025-04-25 | 上官俊斌 | ¥200 | 微信赞赏码 |
| 2025-05-27 | 临安居士 | ¥188 | 微信赞赏码 |
| 2025-04-20 | 姜兴(Simon) | ¥188 | 微信赞赏码 |
| 2025-02-17 | 禾硕 | ¥168 | 支付宝赞赏 |
| 2025-02-8 | Boom.... | ¥100 | 微信赞赏码 |
@@ -225,6 +226,8 @@ Thanks to the following sponsors for their support!
| 2025-02-7 | dennis | ¥80 | gitee码云赞赏 |
| 2025-05-15 | 孤鸿影 | ¥56 | 微信红包 |
| 2025-02-6 | 小二 | ¥62 | 微信转账 |
| 2025-05-27 | 刘启成 | ¥20 | 微信赞赏码 |
| 2025-05-20 | 没有留名的赞助者 | ¥20 | 微信赞赏码 |
> Note: This list is manually updated. If you have sponsored but are not displayed in the list, please contact us by:
1. Submit Issue on GitHub

View File

@@ -216,6 +216,7 @@ sudo xattr -r -d com.apple.quarantine /Applications/应用名称.app
| 日期 | 赞助者 | 金额 | 平台 |
|------|--------|------|------|
| 2025-04-25 | 上官俊斌 | ¥200 | 微信赞赏码 |
| 2025-05-27 | 临安居士 | ¥188 | 微信赞赏码 |
| 2025-04-20 | 姜兴(Simon) | ¥188 | 微信赞赏码 |
| 2025-02-17 | 禾硕 | ¥168 | 支付宝赞赏 |
| 2025-02-8 | Boom.... | ¥100 | 微信赞赏码 |
@@ -225,6 +226,8 @@ sudo xattr -r -d com.apple.quarantine /Applications/应用名称.app
| 2025-02-7 | dennis | ¥80 | gitee码云赞赏 |
| 2025-05-15 | 孤鸿影 | ¥56 | 微信红包 |
| 2025-02-6 | 小二 | ¥62 | 微信转账 |
| 2025-05-27 | 刘启成 | ¥20 | 微信赞赏码 |
| 2025-05-20 | 没有留名的赞助者 | ¥20 | 微信赞赏码 |
> 注:该名单为手动更新。如果您已赞助但未显示在列表中,请通过以下方式联系我们:
1. 在GitHub上提交Issue

View File

@@ -1,7 +1,7 @@
{
"name": "hula",
"type": "module",
"version": "2.6.11",
"version": "2.6.12",
"license": "Apache-2.0",
"engines": {
"node": ">=18.x",
@@ -114,7 +114,7 @@
"@vitest/coverage-v8": "^3.0.5",
"@vitest/ui": "^3.0.5",
"@vue/test-utils": "^2.4.6",
"@vueuse/core": "^13.1.0",
"@vueuse/core": "^13.2.0",
"chalk": "^5.3.0",
"commitizen": "^4.3.1",
"cz-git": "^1.11.0",

40
pnpm-lock.yaml generated
View File

@@ -178,8 +178,8 @@ importers:
specifier: ^2.4.6
version: 2.4.6
'@vueuse/core':
specifier: ^13.1.0
version: 13.1.0(vue@3.5.13(typescript@5.7.2))
specifier: ^13.2.0
version: 13.2.0(vue@3.5.13(typescript@5.7.2))
chalk:
specifier: ^5.3.0
version: 5.4.1
@@ -230,7 +230,7 @@ importers:
version: 5.7.2
unplugin-auto-import:
specifier: ^19.1.1
version: 19.1.1(@nuxt/kit@3.15.4(magicast@0.3.5))(@vueuse/core@13.1.0(vue@3.5.13(typescript@5.7.2)))
version: 19.1.1(@nuxt/kit@3.15.4(magicast@0.3.5))(@vueuse/core@13.2.0(vue@3.5.13(typescript@5.7.2)))
unplugin-vue-components:
specifier: ^28.4.1
version: 28.4.1(@babel/parser@7.26.9)(@nuxt/kit@3.15.4(magicast@0.3.5))(vue@3.5.13(typescript@5.7.2))
@@ -1850,16 +1850,16 @@ packages:
'@vue/test-utils@2.4.6':
resolution: {integrity: sha512-FMxEjOpYNYiFe0GkaHsnJPXFHxQ6m4t8vI/ElPGpMWxZKpmRvQ33OIrvRXemy6yha03RxhOlQuy+gZMC3CQSow==}
'@vueuse/core@13.1.0':
resolution: {integrity: sha512-PAauvdRXZvTWXtGLg8cPUFjiZEddTqmogdwYpnn60t08AA5a8Q4hZokBnpTOnVNqySlFlTcRYIC8OqreV4hv3Q==}
'@vueuse/core@13.2.0':
resolution: {integrity: sha512-n5TZoIAxbWAQ3PqdVPDzLgIRQOujFfMlatdI+f7ditSmoEeNpPBvp7h2zamzikCmrhFIePAwdEQB6ENccHr7Rg==}
peerDependencies:
vue: ^3.5.0
'@vueuse/metadata@13.1.0':
resolution: {integrity: sha512-+TDd7/a78jale5YbHX9KHW3cEDav1lz1JptwDvep2zSG8XjCsVE+9mHIzjTOaPbHUAk5XiE4jXLz51/tS+aKQw==}
'@vueuse/metadata@13.2.0':
resolution: {integrity: sha512-kPpzuQCU0+D8DZCzK0iPpIcXI+6ufWSgwnjJ6//GNpEn+SHViaCtR+XurzORChSgvpHO9YC8gGM97Y1kB+UabA==}
'@vueuse/shared@13.1.0':
resolution: {integrity: sha512-IVS/qRRjhPTZ6C2/AM3jieqXACGwFZwWTdw5sNTSKk2m/ZpkuuN+ri+WCVUP8TqaKwJYt/KuMwmXspMAw8E6ew==}
'@vueuse/shared@13.2.0':
resolution: {integrity: sha512-vx9ZPDF5HcU9up3Jgt3G62dMUfZEdk6tLyBAHYAG4F4n73vpaA7J5hdncDI/lS9Vm7GA/FPlbOmh9TrDZROTpg==}
peerDependencies:
vue: ^3.5.0
@@ -2945,8 +2945,8 @@ packages:
resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==}
engines: {node: '>= 0.4'}
get-tsconfig@4.10.0:
resolution: {integrity: sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==}
get-tsconfig@4.10.1:
resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==}
get-uri@6.0.4:
resolution: {integrity: sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ==}
@@ -6710,16 +6710,16 @@ snapshots:
js-beautify: 1.15.1
vue-component-type-helpers: 2.2.0
'@vueuse/core@13.1.0(vue@3.5.13(typescript@5.7.2))':
'@vueuse/core@13.2.0(vue@3.5.13(typescript@5.7.2))':
dependencies:
'@types/web-bluetooth': 0.0.21
'@vueuse/metadata': 13.1.0
'@vueuse/shared': 13.1.0(vue@3.5.13(typescript@5.7.2))
'@vueuse/metadata': 13.2.0
'@vueuse/shared': 13.2.0(vue@3.5.13(typescript@5.7.2))
vue: 3.5.13(typescript@5.7.2)
'@vueuse/metadata@13.1.0': {}
'@vueuse/metadata@13.2.0': {}
'@vueuse/shared@13.1.0(vue@3.5.13(typescript@5.7.2))':
'@vueuse/shared@13.2.0(vue@3.5.13(typescript@5.7.2))':
dependencies:
vue: 3.5.13(typescript@5.7.2)
@@ -7997,7 +7997,7 @@ snapshots:
es-errors: 1.3.0
get-intrinsic: 1.2.7
get-tsconfig@4.10.0:
get-tsconfig@4.10.1:
dependencies:
resolve-pkg-maps: 1.0.0
optional: true
@@ -9825,7 +9825,7 @@ snapshots:
tsx@4.19.2:
dependencies:
esbuild: 0.23.1
get-tsconfig: 4.10.0
get-tsconfig: 4.10.1
optionalDependencies:
fsevents: 2.3.3
optional: true
@@ -9961,7 +9961,7 @@ snapshots:
unload@2.4.1: {}
unplugin-auto-import@19.1.1(@nuxt/kit@3.15.4(magicast@0.3.5))(@vueuse/core@13.1.0(vue@3.5.13(typescript@5.7.2))):
unplugin-auto-import@19.1.1(@nuxt/kit@3.15.4(magicast@0.3.5))(@vueuse/core@13.2.0(vue@3.5.13(typescript@5.7.2))):
dependencies:
local-pkg: 1.0.0
magic-string: 0.30.17
@@ -9971,7 +9971,7 @@ snapshots:
unplugin-utils: 0.2.4
optionalDependencies:
'@nuxt/kit': 3.15.4(magicast@0.3.5)
'@vueuse/core': 13.1.0(vue@3.5.13(typescript@5.7.2))
'@vueuse/core': 13.2.0(vue@3.5.13(typescript@5.7.2))
unplugin-utils@0.2.4:
dependencies:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 242 KiB

After

Width:  |  Height:  |  Size: 212 KiB

68
src-tauri/Cargo.lock generated
View File

@@ -2220,11 +2220,13 @@ dependencies = [
"cocoa",
"lazy_static",
"objc",
"rand 0.8.5",
"reqwest 0.11.27",
"rodio",
"screenshots",
"serde",
"serde_json",
"surge-ping",
"tauri",
"tauri-build",
"tauri-plugin-autostart",
@@ -2241,6 +2243,7 @@ dependencies = [
"tauri-plugin-sql",
"tauri-plugin-updater",
"tauri-plugin-upload",
"tokio",
"tungstenite",
]
@@ -3091,6 +3094,12 @@ dependencies = [
"memoffset",
]
[[package]]
name = "no-std-net"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65"
[[package]]
name = "nodrop"
version = "0.1.14"
@@ -3877,6 +3886,48 @@ dependencies = [
"time",
]
[[package]]
name = "pnet_base"
version = "0.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe4cf6fb3ab38b68d01ab2aea03ed3d1132b4868fa4e06285f29f16da01c5f4c"
dependencies = [
"no-std-net",
]
[[package]]
name = "pnet_macros"
version = "0.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "688b17499eee04a0408aca0aa5cba5fc86401d7216de8a63fdf7a4c227871804"
dependencies = [
"proc-macro2",
"quote",
"regex",
"syn 2.0.101",
]
[[package]]
name = "pnet_macros_support"
version = "0.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eea925b72f4bd37f8eab0f221bbe4c78b63498350c983ffa9dd4bcde7e030f56"
dependencies = [
"pnet_base",
]
[[package]]
name = "pnet_packet"
version = "0.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9a005825396b7fe7a38a8e288dbc342d5034dac80c15212436424fef8ea90ba"
dependencies = [
"glob",
"pnet_base",
"pnet_macros",
"pnet_macros_support",
]
[[package]]
name = "png"
version = "0.17.16"
@@ -5309,6 +5360,22 @@ version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "surge-ping"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fda78103d8016bb25c331ddc54af634e801806463682cc3e549d335df644d95"
dependencies = [
"hex",
"parking_lot",
"pnet_packet",
"rand 0.9.1",
"socket2",
"thiserror 1.0.69",
"tokio",
"tracing",
]
[[package]]
name = "swift-rs"
version = "1.0.7"
@@ -6198,6 +6265,7 @@ dependencies = [
"bytes",
"libc",
"mio",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",

View File

@@ -37,7 +37,10 @@ tauri-plugin-os = "2.2.1"
tauri-plugin-shell = "2.2.1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tauri-plugin-http = { version = "2.4.3", features = ["unsafe-headers", "rustls-tls"] }
tauri-plugin-http = { version = "2.4.3", features = [
"unsafe-headers",
"rustls-tls",
] }
tauri-plugin-process = "2.2.1"
tauri-plugin-fs = "2.2.1"
tauri-plugin-dialog = "2.2.1"
@@ -48,7 +51,8 @@ tauri-plugin-updater = "2.7.1"
tauri-plugin-sql = { version = "2.2.0", features = ["sqlite"] }
tauri-plugin-single-instance = "2.2.3"
tauri-plugin-notification = "2.2.2"
tungstenite = { version = "0.26.2", features = ["rustls-tls-webpki-roots"] }
tungstenite = { version = "0.26.2", features = ["rustls-tls-webpki-roots"] }
tokio = { version = "1", features = ["full"] }
[target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies]
tauri-plugin-autostart = "2.3.0"
@@ -60,7 +64,10 @@ reqwest = { version = "0.11", features = [
"json",
"socks",
"rustls-tls",
"blocking",
] }
surge-ping = "0.8.0"
rand = "0.8.5"
[target."cfg(target_os =\"macos\")".dependencies]
cocoa = "0.26.0"
objc = "0.2.7"

View File

@@ -1,19 +1,19 @@
use std::{env, fs, io};
fn main() -> Result<(), Box<dyn std::error::Error>> {
fn main() -> Result<(), Box<dyn std::error::Error>> {
ensure_frontend_dist()?;
tauri_build::build();
Ok(())
}
fn ensure_frontend_dist() -> Result<(), Box<dyn std::error::Error>> {
fn ensure_frontend_dist() -> Result<(), Box<dyn std::error::Error>> {
let current_dir = env::current_dir()?;
let parent_dir = current_dir
.parent()
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Cannot find parent directory!"))?;
let frontend_dist = parent_dir.join("dist");
// There should not be this directory.
let exists = frontend_dist.exists() && frontend_dist.is_dir();

View File

@@ -1,12 +1,10 @@
// 桌面端依赖
#[cfg(desktop)]
mod desktops;
#[cfg(desktop)]
use common_cmd::{
audio, default_window_icon, screenshot, set_badge_count, set_height,
};
#[cfg(target_os = "macos")]
use common_cmd::{hide_title_bar_buttons};
use common_cmd::hide_title_bar_buttons;
#[cfg(desktop)]
use common_cmd::{audio, default_window_icon, screenshot, set_badge_count, set_height};
#[cfg(desktop)]
mod proxy;
#[cfg(desktop)]

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "HuLa",
"version": "2.6.11",
"version": "2.6.12",
"identifier": "com.hula-app.app",
"build": {
"beforeDevCommand": "pnpm dev",

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "HuLa",
"version": "2.6.11",
"version": "2.6.12",
"identifier": "com.hula.pc",
"build": {
"beforeDevCommand": "pnpm dev",

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "HuLa",
"version": "2.6.11",
"version": "2.6.12",
"identifier": "com.hula-app.app",
"build": {
"beforeDevCommand": "pnpm dev",

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "HuLa",
"version": "2.6.11",
"version": "2.6.12",
"identifier": "com.hula.pc",
"build": {
"beforeDevCommand": "pnpm dev",

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "HuLa",
"version": "2.6.11",
"version": "2.6.12",
"identifier": "com.hula.pc",
"build": {
"beforeDevCommand": "pnpm dev",

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "HuLa",
"version": "2.6.11",
"version": "2.6.12",
"identifier": "com.hula.pc",
"build": {
"beforeDevCommand": "pnpm dev",

View File

@@ -125,6 +125,10 @@
class="border-(1px solid #90909080)"
placeholder="请输入群聊备注"
clearable
spellCheck="false"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
@blur="handleRemarkUpdate"
@keydown.enter="handleRemarkUpdate" />
</div>
@@ -147,6 +151,10 @@
class="border-(1px solid #90909080)"
placeholder="请输入本群昵称"
clearable
spellCheck="false"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
@blur="handleNicknameUpdate"
@keydown.enter="handleNicknameUpdate" />
</div>

View File

@@ -165,6 +165,10 @@
@keydown.enter="saveGroupName"
size="tiny"
maxlength="12"
spellCheck="false"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
class="border-(solid 1px [--line-color])"
placeholder="请输入群名称(最多12字)" />
</div>
@@ -235,6 +239,10 @@
<p class="text-(12px [--chat-text-color]) mt-20px mb-10px">我本群的昵称</p>
<n-input
class="border-(solid 1px [--line-color]) custom-shadow"
spellCheck="false"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
v-model:value="groupDetail.myNickname"
@update:value="updateGroupInfo($event, 'nickname')" />
<!-- 群备注 -->
@@ -245,6 +253,10 @@
<n-input
class="border-(solid 1px [--line-color]) custom-shadow"
v-model:value="groupDetail.groupRemark"
spellCheck="false"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
@update:value="updateGroupInfo($event, 'remark')" />
<!-- 群设置选项 -->

View File

@@ -1,7 +1,7 @@
<template>
<!-- 网络断开提示 -->
<!-- 网络状态提示 -->
<n-flex
v-if="!isOnline"
v-if="!networkStatus.isOnline.value"
align="center"
justify="center"
class="z-999 absolute w-full h-40px rounded-4px text-(12px [--danger-text]) bg-[--danger-bg]">
@@ -448,7 +448,6 @@ import { useUserInfo, useBadgeInfo } from '@/hooks/useCached.ts'
import { useChatStore } from '@/stores/chat.ts'
import { type } from '@tauri-apps/plugin-os'
import { useUserStore } from '@/stores/user.ts'
import { useNetwork } from '@vueuse/core'
import { AvatarUtils } from '@/utils/AvatarUtils'
import VirtualList, { type VirtualListExpose } from '@/components/common/VirtualList.vue'
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
@@ -458,6 +457,7 @@ import { useGlobalStore } from '@/stores/global'
import { useDebounceFn } from '@vueuse/core'
import { useCachedStore } from '@/stores/cached'
import apis from '@/services/apis'
import { useNetworkStatus } from '@/hooks/useNetworkStatus'
const appWindow = WebviewWindow.getCurrent()
const { addListener } = useTauriListener()
@@ -470,6 +470,7 @@ const userStore = useUserStore()
const groupStore = useGroupStore()
const globalStore = useGlobalStore()
const cachedStore = useCachedStore()
const networkStatus = useNetworkStatus()
// 记录当前滚动位置相关信息
const isAutoScrolling = ref(false)
@@ -520,8 +521,6 @@ const hoverBubble = ref<{
})
/** 记录右键菜单时选中的气泡的元素(用于处理mac右键会选中文本的问题) */
const recordEL = ref()
/** 网络连接是否正常 */
const { isOnline } = useNetwork()
const isMac = computed(() => type() === 'macos')
// 公告展示时需要减去的高度
const announcementHeight = computed(() => (isGroup.value && topAnnouncement.value ? 300 : 260))

View File

@@ -73,6 +73,10 @@
placeholder="搜索"
type="text"
size="tiny"
spellCheck="false"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
class="h-26px w-95% lh-26px rounded-6px">
<template #prefix>
<svg class="w-12px h-12px">

View File

@@ -14,6 +14,24 @@ export const useContextMenu = (ContextMenuRef: Ref, isNull?: Ref<boolean>) => {
// 禁止滚动的默认行为
const preventDefault = (e: Event) => e.preventDefault()
// 禁止选中文本的默认行为
const preventTextSelection = (e: Event) => e.preventDefault()
// 禁用文本选择
const disableTextSelection = () => {
// 清除当前选择
window.getSelection()?.removeAllRanges()
// 添加禁止选择事件
document.body.classList.add('no-select')
window.addEventListener('selectstart', preventTextSelection)
}
// 启用文本选择
const enableTextSelection = () => {
document.body.classList.remove('no-select')
window.removeEventListener('selectstart', preventTextSelection)
}
/**! 解决使用n-virtual-list时右键菜单出现还可以滚动的问题 */
const handleVirtualListScroll = (isBan: boolean) => {
const scrollbar_main = document.querySelector('#image-chat-main') as HTMLElement
@@ -27,6 +45,10 @@ export const useContextMenu = (ContextMenuRef: Ref, isNull?: Ref<boolean>) => {
e.preventDefault()
e.stopPropagation()
if (isNull?.value) return
// 在显示菜单前清除选择
disableTextSelection()
handleVirtualListScroll(true)
showMenu.value = true
x.value = e.clientX
@@ -39,11 +61,36 @@ export const useContextMenu = (ContextMenuRef: Ref, isNull?: Ref<boolean>) => {
if (!event.target.matches('.context-menu, .context-menu *')) {
handleVirtualListScroll(false)
showMenu.value = false
enableTextSelection() // 恢复文本选择功能
}
window.removeEventListener('wheel', preventDefault) // 移除禁止滚轮滚动
}
// 监听showMenu状态变化
watch(
() => showMenu.value,
(newValue) => {
if (!newValue) {
// 当菜单关闭时,恢复文本选择功能
enableTextSelection()
}
}
)
onMounted(() => {
// 添加全局样式
if (!document.querySelector('#no-select-style')) {
const style = document.createElement('style')
style.id = 'no-select-style'
style.textContent = `.no-select {
user-select: none !important;
-webkit-user-select: none !important;
-moz-user-select: none !important;
-ms-user-select: none !important;
}`
document.head.appendChild(style)
}
const div = ContextMenuRef.value
//这里只监听了div的右键如果需要监听其他元素的右键需要在其他元素上监听
div.addEventListener('contextmenu', handleContextMenu)
@@ -65,8 +112,16 @@ export const useContextMenu = (ContextMenuRef: Ref, isNull?: Ref<boolean>) => {
div?.removeEventListener('contextmenu', handleContextMenu)
window.removeEventListener('contextmenu', preventDefault)
window.removeEventListener('wheel', preventDefault)
window.removeEventListener('selectstart', preventTextSelection)
window.removeEventListener('click', closeMenu, true)
window.removeEventListener('contextmenu', closeMenu, true)
// 确保恢复选择功能
enableTextSelection()
// 移除样式
const style = document.querySelector('#no-select-style')
if (style) style.remove()
})
return {

View File

@@ -0,0 +1,173 @@
import { useNetwork, useEventListener, useTimeoutFn } from '@vueuse/core'
import { useChatStore } from '@/stores/chat'
import { useGlobalStore } from '@/stores/global'
import { useGroupStore } from '@/stores/group'
import { useCachedStore } from '@/stores/cached'
import { useContactStore } from '@/stores/contacts'
import { type } from '@tauri-apps/plugin-os'
import webSocket from '@/services/webSocket'
/**
* 网络重连Hook监测网络恢复并自动刷新数据
* 在网络从离线变为在线时触发数据刷新
* 处理系统睡眠唤醒和长时间空闲后的连接恢复
* 刷新当前聊天室消息、会话列表、群组信息等
*/
export const useNetworkReconnect = () => {
// 获取所需的stores
const chatStore = useChatStore()
const globalStore = useGlobalStore()
const groupStore = useGroupStore()
const cachedStore = useCachedStore()
const contactStore = useContactStore()
// 使用VueUse的useNetwork获取网络状态
const { isOnline, offlineAt, onlineAt } = useNetwork()
// 上次离线的时间戳
let lastOfflineTimestamp = 0
// 上次活动时间戳(用于检测长时间空闲)
let lastActivityTimestamp = Date.now()
// 判断是否是移动设备
const isMobile = computed(() => type() === 'android' || type() === 'ios')
// 最长空闲时间超过这个时间会检查连接15分钟
const MAX_IDLE_TIME = 15 * 60 * 1000
// 定期健康检查定时器
const healthCheckTimer = useTimeoutFn(
() => {
checkConnectionHealth()
// 执行完一次后,再次启动定时器,实现循环检查
healthCheckTimer.start()
},
5 * 60 * 1000,
{ immediate: true, immediateCallback: false }
)
// 监听网络状态变化
watch([isOnline, offlineAt], ([newIsOnline, newOfflineAt], [oldIsOnline]) => {
// 网络从离线变为在线时
if (!oldIsOnline && newIsOnline) {
console.log('[Network] 网络已重新连接,开始刷新数据')
// 如果知道离线时间,记录下来
if (newOfflineAt) {
lastOfflineTimestamp = newOfflineAt
}
// 执行数据刷新
refreshAllData()
}
// 网络变为离线时记录时间
if (oldIsOnline && !newIsOnline && newOfflineAt) {
lastOfflineTimestamp = newOfflineAt
console.log('[Network] 网络已断开连接')
}
})
/**
* 在挂起/恢复时可能会改变网络状态
* 对所有平台处理可见性变化和潜在的连接问题
*/
useEventListener(window, 'visibilitychange', () => {
const currentTime = Date.now()
const idleTime = currentTime - lastActivityTimestamp
// 页面变为可见时
if (document.visibilityState === 'visible') {
console.log(`[Network] 应用从后台恢复,已离线 ${idleTime / 1000}`)
lastActivityTimestamp = currentTime
// 在移动设备上的恢复逻辑
if (isMobile.value && isOnline.value) {
// 如果离线超过30秒则刷新数据
if (lastOfflineTimestamp > 0 && currentTime - lastOfflineTimestamp > 30000) {
console.log('[Network] 移动端应用从后台恢复,刷新数据')
refreshAllData()
lastOfflineTimestamp = 0
}
}
// 在所有设备上,如果空闲时间过长,执行连接健康检查
if (idleTime > 15 * 60 * 1000) {
// 如果超过15分钟没活动
checkConnectionHealth(true)
}
} else {
// 页面变为不可见时,记录时间戳
lastActivityTimestamp = currentTime
}
})
/**
* 睡眠/唤醒事件
* TODO 实现
*/
// const setupSystemEventListeners = async () => {
// }
/**
* 检查WebSocket连接健康状态
* @param forceReconnect 是否在不健康时强制重连
*/
const checkConnectionHealth = (forceReconnect = false) => {
// 调用WebSocket的健康检查
const health = webSocket.checkConnectionHealth()
if (!health) return
console.log('[Network] 连接健康检查:', health)
const currentTime = Date.now()
const idleTime = currentTime - lastActivityTimestamp
// 1. 明确要求强制重连
// 2. 连接不健康且空闲时间超过MAX_IDLE_TIME
if (forceReconnect || (!health.isHealthy && idleTime > MAX_IDLE_TIME)) {
console.log('[Network] 检测到不健康或空闲时间过长的连接,正在重新连接...')
webSocket.forceReconnect()
// 刷新数据
refreshAllData()
}
}
/**
* 刷新所有重要数据
*/
const refreshAllData = async () => {
// 刷新会话列表
await chatStore.getSessionList(true)
// 如果当前有选中的聊天室,重置并刷新消息列表
if (globalStore.currentSession?.roomId) {
// 获取最新消息
await chatStore.resetAndRefreshCurrentRoomMessages()
}
// 如果当前是群聊,刷新群组信息
if (globalStore.currentSession?.type === 2) {
await groupStore.getGroupUserList(true)
await groupStore.getCountStatistic()
await cachedStore.getGroupAtUserBaseInfo()
}
// 刷新联系人列表
await contactStore.getContactList(true)
// 更新未读消息计数
globalStore.updateGlobalUnreadCount()
// 刷新在线用户列表
await groupStore.refreshGroupMembers()
console.log('[Network] 数据刷新完成')
}
return {
isOnline,
offlineAt,
onlineAt,
refreshAllData,
checkConnectionHealth
}
}

View File

@@ -0,0 +1,42 @@
/**
* 网络状态监测钩子
*/
export const useNetworkStatus = () => {
// 网络状态 - 基于浏览器navigator.onLine
const isOnline = ref(navigator.onLine)
// 监听浏览器网络状态变化
const handleOnline = () => {
isOnline.value = true
}
const handleOffline = () => {
isOnline.value = false
}
// 初始化网络状态监听
const initNetworkListener = () => {
window.addEventListener('online', handleOnline)
window.addEventListener('offline', handleOffline)
}
// 清理网络状态监听
const cleanupNetworkListener = () => {
window.removeEventListener('online', handleOnline)
window.removeEventListener('offline', handleOffline)
}
// 自动初始化监听器
initNetworkListener()
// 在组件卸载时清理监听器
onUnmounted(() => {
cleanupNetworkListener()
})
return {
isOnline,
initNetworkListener,
cleanupNetworkListener
}
}

View File

@@ -37,6 +37,10 @@
style="background: var(--search-bg-color)"
:maxlength="20"
clearable
spellCheck="false"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
size="small"
:placeholder="isSearchMode ? '' : '搜索'">
<template #prefix>

View File

@@ -61,6 +61,10 @@
:passively-activated="true"
class="rounded-6px"
clearable
spellCheck="false"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
:allow-input="noSideSpace"
placeholder="请输入你的昵称"
show-count

View File

@@ -11,6 +11,10 @@
minlength="6"
v-model:value="info.account"
type="text"
spellCheck="false"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
:placeholder="accountPH"
@focus="accountPH = ''"
@blur="accountPH = '输入HuLa账号'"
@@ -56,6 +60,10 @@
show-password-on="click"
v-model:value="info.password"
type="password"
spellCheck="false"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
:placeholder="passwordPH"
@focus="passwordPH = ''"
@blur="passwordPH = '输入HuLa密码'"

View File

@@ -8,6 +8,10 @@
class="rounded-6px w-full relative text-12px"
:maxlength="20"
clearable
spellCheck="false"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
:placeholder="'搜索'">
<template #prefix>
<svg class="w-12px h-12px"><use href="#search"></use></svg>

View File

@@ -86,7 +86,7 @@
</n-image-group>
</n-scrollbar>
<n-input />
<n-input spellCheck="false" autoComplete="off" autoCorrect="off" autoCapitalize="off" />
</n-flex>
</n-flex>
</n-flex>

View File

@@ -73,6 +73,10 @@
placeholder="输入标题"
type="text"
size="tiny"
spellCheck="false"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
style="width: 200px"
class="h-22px lh-22px rounded-6px">
</n-input>

View File

@@ -19,6 +19,10 @@
placeholder="输入标题"
type="text"
size="tiny"
spellCheck="false"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
style="width: 200px"
class="leading-7 text-14px rounded-6px">
</n-input>

View File

@@ -13,8 +13,10 @@ import { useUserStore } from '@/stores/user'
import { getEnhancedFingerprint } from '@/services/fingerprint.ts'
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
import { useTauriListener } from '@/hooks/useTauriListener'
import { listen, emit } from '@tauri-apps/api/event'
import { listen } from '@tauri-apps/api/event'
import { useDebounceFn } from '@vueuse/core'
// 使用类型导入避免直接执行代码
import type { useNetworkReconnect as UseNetworkReconnectType } from '@/hooks/useNetworkReconnect'
// 创建 webSocket worker
const worker: Worker = new Worker(new URL('../workers/webSocket.worker.ts', import.meta.url), {
@@ -29,14 +31,48 @@ const timerWorker: Worker = new Worker(new URL('../workers/timer.worker.ts', imp
// 添加一个标识是否是主窗口的变量
let isMainWindow = false
// LRU缓存实现
class LRUCache<K, V> {
private maxSize: number
private cache = new Map<K, V>()
constructor(maxSize: number = 1000) {
this.maxSize = maxSize
}
set(key: K, value: V) {
if (this.cache.has(key)) {
this.cache.delete(key)
} else if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value
if (firstKey) {
this.cache.delete(firstKey)
}
}
this.cache.set(key, value)
}
has(key: K): boolean {
return this.cache.has(key)
}
clear() {
this.cache.clear()
}
get size(): number {
return this.cache.size
}
}
class WS {
// 添加消息队列大小限制
readonly #MAX_QUEUE_SIZE = 100
readonly #MAX_QUEUE_SIZE = 50 // 减少队列大小
#tasks: WsReqMsgContentType[] = []
// 重连🔐
#connectReady = false
// 使用LRU缓存替代简单的Set
#processedMsgCache = new Map<number, number>()
#processedMsgCache = new LRUCache<number, number>(1000) // 使用LRU缓存
#tauriListener: ReturnType<typeof useTauriListener> | null = null
@@ -50,6 +86,12 @@ class WS {
timeSinceLastPong: null as number | null
}
// 网络重连工具,延迟初始化
#networkReconnect: ReturnType<typeof UseNetworkReconnectType> | null = null
// 存储watch清理函数
#unwatchFunctions: (() => void)[] = []
constructor() {
this.initWindowType()
if (isMainWindow) {
@@ -60,9 +102,40 @@ class WS {
timerWorker.addEventListener('message', this.onTimerWorkerMsg)
// 添加页面可见性监听
this.initVisibilityListener()
this.initNetworkReconnect()
}
}
// 初始化网络重连工具
private initNetworkReconnect() {
// 动态导入以延迟执行
import('@/hooks/useNetworkReconnect')
.then(({ useNetworkReconnect }) => {
this.#networkReconnect = useNetworkReconnect()
console.log('[WebSocket] 网络重连工具初始化完成')
// 监听网络在线状态变化
if (this.#networkReconnect.isOnline) {
const unwatch = watch(this.#networkReconnect.isOnline, (newValue, oldValue) => {
// 只在网络从离线变为在线时执行重连
if (newValue === true && oldValue === false) {
console.log('[WebSocket] 网络恢复在线状态主动重新初始化WebSocket连接')
// 重置重连计数并重新初始化连接
this.forceReconnect()
}
})
// 存储清理函数
this.#unwatchFunctions = this.#unwatchFunctions || []
this.#unwatchFunctions.push(unwatch)
}
})
.catch((err) => {
console.error('[WebSocket] 网络重连工具初始化失败:', err)
})
}
// 初始化页面可见性监听
private async initVisibilityListener() {
const handleVisibilityChange = (isVisible: boolean) => {
@@ -72,6 +145,18 @@ class WS {
value: { isHidden: !isVisible }
})
)
// 优化的可见性恢复检查
if (isVisible && this.#networkReconnect?.isOnline?.value) {
// 检查最后一次通信时间,如果太久没有通信,刷新数据
const now = Date.now()
const lastPongTime = this.#connectionHealth.lastPongTime
const heartbeatTimeout = 90000 // 增加到90秒减少误触发
if (lastPongTime && now - lastPongTime > heartbeatTimeout) {
console.log('[Network] 应用从后台恢复且长时间无心跳,刷新数据')
this.#networkReconnect?.refreshAllData()
}
}
}
const debouncedVisibilityChange = useDebounceFn((isVisible: boolean) => {
@@ -169,31 +254,6 @@ class WS {
private async initWindowType() {
const currentWindow = WebviewWindow.getCurrent()
isMainWindow = currentWindow.label === 'home'
if (!isMainWindow) {
// 非主窗口监听来自主窗口的消息
await this.initChildWindowListeners()
}
}
// 为子窗口初始化监听器
private async initChildWindowListeners() {
this.#tauriListener = useTauriListener()
// 监听主窗口发来的WebSocket消息
this.#tauriListener.addListener(
listen('ws-message', (event) => {
this.onMessage(event.payload as string)
})
)
// 监听连接状态变化
this.#tauriListener.addListener(
listen('ws-state-change', (event) => {
const state = event.payload as ConnectionState
useMitt.emit('wsConnectionStateChange', state)
})
)
}
initConnect = async () => {
@@ -223,10 +283,6 @@ class WS {
switch (params.type) {
case WorkerMsgEnum.MESSAGE: {
await this.onMessage(params.value as string)
// 广播消息给其他窗口
if (isMainWindow) {
await emit('ws-message', params.value)
}
break
}
case WorkerMsgEnum.OPEN: {
@@ -243,7 +299,6 @@ class WS {
useMitt.emit(WsResponseMessageType.NO_INTERNET, params.value)
// 如果是重连失败,可以提示用户刷新页面
if ((params.value as { msg: string }).msg.includes('连接失败次数过多')) {
useMitt.emit('showMainMessage', { title: '连接断开', content: '连接已断开,请刷新页面或重新登录。' })
// 可以触发UI提示让用户刷新页面
useMitt.emit('wsReconnectFailed', params.value)
}
@@ -299,21 +354,21 @@ class WS {
const { state } = params.value as { state: ConnectionState }
// 检测重连成功: 从RECONNECTING状态变为CONNECTED状态
// TODO 重连的时候没有执行这里
if (this.#previousConnectionState === ConnectionState.RECONNECTING && state === ConnectionState.CONNECTED) {
console.log('🔄 WebSocket 重连成功')
// 可以添加UI提示
useMitt.emit('showMainMessage', { title: '连接恢复', content: '网络连接已恢复' })
// 网络重连成功后刷新数据
if (isMainWindow && this.#networkReconnect) {
console.log('开始刷新数据...')
this.#networkReconnect.refreshAllData()
} else if (isMainWindow) {
// 如果还没初始化,延迟初始化后再刷新
this.initNetworkReconnect()
}
}
// 更新前一状态
this.#previousConnectionState = state
console.log('连接状态改变', state)
useMitt.emit('wsConnectionStateChange', state)
// 广播状态变化给其他窗口
if (isMainWindow) {
await emit('ws-state-change', state)
}
break
}
// 处理心跳响应
@@ -363,16 +418,22 @@ class WS {
if (this.#connectReady) {
this.#send(params)
} else {
// 队列限制
// 优化的队列管理
if (this.#tasks.length >= this.#MAX_QUEUE_SIZE) {
console.warn('消息队列已满,正在丢弃最旧的消息')
this.#tasks.shift()
// 优先丢弃非关键消息
const nonCriticalIndex = this.#tasks.findIndex(
(task) => typeof task === 'object' && task.type !== 1 && task.type !== 2
)
if (nonCriticalIndex !== -1) {
this.#tasks.splice(nonCriticalIndex, 1)
console.warn('消息队列已满,丢弃非关键消息')
} else {
this.#tasks.shift()
console.warn('消息队列已满,丢弃最旧消息')
}
}
this.#tasks.push(params)
}
} else {
// 子窗口通过事件发送消息到主窗口
emit('ws-send', params)
}
}
@@ -562,18 +623,67 @@ class WS {
return this.#connectionHealth
}
destroy() {
// 强制重新连接WebSocket
forceReconnect() {
console.log('[WebSocket] 强制重新初始化WebSocket连接')
// 停止当前的重连计时器
worker.postMessage(JSON.stringify({ type: 'clearReconnectTimer' }))
worker.terminate()
// 同时终止timer worker相关的心跳
timerWorker.postMessage({
type: 'stopPeriodicHeartbeat'
})
this.#tasks = []
this.#processedMsgCache.clear()
this.#connectReady = false
// 清理 Tauri 事件监听器
this.#tauriListener?.cleanup()
// 停止心跳
worker.postMessage(JSON.stringify({ type: 'stopHeartbeat' }))
// 重置重连计数并重新初始化
worker.postMessage(JSON.stringify({ type: 'resetReconnectCount' }))
// 重新初始化连接
this.initConnect()
}
destroy() {
try {
// 优化的资源清理顺序
worker.postMessage(JSON.stringify({ type: 'clearReconnectTimer' }))
worker.postMessage(JSON.stringify({ type: 'stopHeartbeat' }))
// 同时终止timer worker相关的心跳
timerWorker.postMessage({
type: 'stopPeriodicHeartbeat'
})
// 清理内存
this.#tasks.length = 0 // 更高效的数组清空
this.#processedMsgCache.clear()
this.#connectReady = false
// 重置连接健康状态
this.#connectionHealth = {
isHealthy: true,
lastPongTime: null,
timeSinceLastPong: null
}
// 清理 Tauri 事件监听器
this.#tauriListener?.cleanup()
this.#tauriListener = null
// 清理所有watch
this.#unwatchFunctions.forEach((unwatch) => {
try {
unwatch()
} catch (error) {
console.warn('清理watch函数时出错:', error)
}
})
this.#unwatchFunctions.length = 0
// 最后终止workers
setTimeout(() => {
worker.terminate()
timerWorker.terminate()
}, 100) // 给一点时间让消息处理完成
} catch (error) {
console.error('销毁WebSocket时出错:', error)
}
}
}

View File

@@ -664,7 +664,7 @@ export const useChatStore = defineStore(
expirationTimers.clear()
}
// 在 useChatStore 中添加新方法
// 更新未读消息计数
const updateTotalUnreadCount = () => {
// 使用 Array.from 确保遍历的是最新的 sessionList
const totalUnread = Array.from(sessionList.value).reduce((total, session) => {
@@ -683,9 +683,8 @@ export const useChatStore = defineStore(
invoke('set_badge_count', { count: totalUnread > 0 ? totalUnread : null })
}
// 在 useChatStore 中添加新方法
// 清空所有会话的未读数
const clearUnreadCount = () => {
// 清空所有会话的未读数
sessionList.value.forEach((session) => {
session.unreadCount = 0
})
@@ -693,6 +692,50 @@ export const useChatStore = defineStore(
updateTotalUnreadCount()
}
// 重置当前聊天室的消息并刷新最新消息
const resetAndRefreshCurrentRoomMessages = async () => {
if (!currentRoomId.value) return
// 保存当前房间ID用于后续比较
const requestRoomId = currentRoomId.value
try {
// 1. 清空当前消息列表
if (currentMessageMap.value) {
currentMessageMap.value.clear()
}
// 2. 重置消息加载状态
if (currentMessageOptions.value) {
currentMessageOptions.value = {
isLast: false,
isLoading: true,
cursor: ''
}
}
// 3. 清空回复映射
if (currentReplyMap.value) {
currentReplyMap.value.clear()
}
// 4. 从服务器获取最新的消息默认20条
await getMsgList(pageSize)
console.log('[Network] 已重置并刷新当前聊天室的消息列表')
} catch (error) {
console.error('[Network] 重置并刷新消息列表失败:', error)
// 如果获取失败,确保重置加载状态
if (currentRoomId.value === requestRoomId && currentMessageOptions.value) {
currentMessageOptions.value = {
isLast: false,
isLoading: false,
cursor: ''
}
}
}
}
return {
getMsgIndex,
chatMessageList,
@@ -726,7 +769,8 @@ export const useChatStore = defineStore(
recalledMessages,
clearAllExpirationTimers,
updateTotalUnreadCount,
clearUnreadCount
clearUnreadCount,
resetAndRefreshCurrentRoomMessages
}
},
{

View File

@@ -101,6 +101,9 @@ svg {
// 危险样式
--danger-text: #c14053;
--danger-bg: #f6dfe3;
// 警告样式
--warning-text: #f7b668ff;
--warning-bg: #fdeeddff;
// 头像边框颜色
--avatar-border-color: #fff;
// hover头像样式
@@ -197,7 +200,10 @@ html[data-theme='dark'] {
// 危险样式
--danger-text: #da8583;
--danger-bg: #37292c;
// 头像边框颜色
// 警告样式
--warning-text: #f4c375ff;
--warning-bg: #402f1dff;
// 头像边框颜色
--avatar-border-color: #1b1b1b;
// hover头像样式
--avatar-hover-bg: rgba(30, 30, 30, 0.6);

View File

@@ -22,6 +22,7 @@ declare module 'vue' {
Emoji: typeof import('./../components/rightBox/renderMessage/Emoji.vue')['default']
Emoticon: typeof import('./../components/rightBox/emoticon/index.vue')['default']
FloatBlockList: typeof import('./../components/common/FloatBlockList.vue')['default']
HInput: typeof import('./../components/common/HInput.vue')['default']
Image: typeof import('./../components/rightBox/renderMessage/Image.vue')['default']
InfoPopover: typeof import('./../components/common/InfoPopover.vue')['default']
LoadingSpinner: typeof import('./../components/common/LoadingSpinner.vue')['default']

View File

@@ -66,6 +66,10 @@
background-color: #404040;
color: #fff;
"
spellCheck="false"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
placeholder="锁屏密码"
show-password-on="click"
type="password"

View File

@@ -12,6 +12,10 @@
placeholder="填写公告1600字"
:autosize="{ minRows: 20 }"
maxlength="600"
spellCheck="false"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
show-count
autofocus />
</div>

View File

@@ -26,6 +26,10 @@
class="border-(1px solid #90909080)"
v-model:value="formData.email"
placeholder="请输入您的邮箱"
spellCheck="false"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
clearable />
</n-form-item>
@@ -37,6 +41,10 @@
class="border-(1px solid #90909080)"
v-model:value="formData.imgCode"
placeholder="请输入图片验证码"
spellCheck="false"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
maxlength="5" />
<n-image
width="120"
@@ -60,6 +68,10 @@
class="border-(1px solid #90909080)"
v-model:value="formData.emailCode"
placeholder="请输入邮箱验证码"
spellCheck="false"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
maxlength="6" />
<n-button
color="#13987f"
@@ -98,6 +110,10 @@
show-password-on="click"
placeholder="请输入6-16位新密码"
maxlength="16"
spellCheck="false"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
minlength="6" />
<n-flex vertical :size="4" class="space-y-4px">
<Validation :value="passwordForm.password" message="密码长度为6-16位" :validator="validateMinLength" />
@@ -123,6 +139,10 @@
type="password"
show-password-on="click"
placeholder="请再次输入密码"
spellCheck="false"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
maxlength="16"
minlength="6" />
<n-flex vertical :size="4">

View File

@@ -36,6 +36,10 @@
:maxlength="60"
:count-graphemes="countGraphemes"
show-count
spellCheck="false"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
type="textarea"
placeholder="输入几句话对TA说些什么吧" />

View File

@@ -36,6 +36,10 @@
:maxlength="60"
:count-graphemes="countGraphemes"
show-count
spellCheck="false"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
type="textarea"
placeholder="输入验证消息" />

View File

@@ -26,6 +26,10 @@
:placeholder="searchPlaceholder[searchType]"
:maxlength="20"
round
spellCheck="false"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
clearable
@keydown.enter="handleSearch"
@clear="handleClear">

View File

@@ -43,7 +43,7 @@
<template v-else-if="searchResults.length > 0">
<p class="text-(12px #909090) mb-6px">搜索结果</p>
<n-scrollbar style="max-height: calc(100vh - 212px)">
<n-scrollbar style="max-height: calc(100vh - 142px)">
<template v-for="item in searchResults" :key="item.roomId">
<n-flex
align="center"

View File

@@ -134,12 +134,6 @@ const sessionList = computed(() => {
return (
chatStore.sessionList
.map((item) => {
// 获取该会话的所有消息,避免重复转换
const messages = Array.from(chatStore.messageMap.get(item.roomId)?.values() || [])
// 获取最后一条消息
const lastMsg = messages[messages.length - 1]
let LastUserMsg = ''
// 获取最新的头像
let latestAvatar = item.avatar
if (item.type === RoomTypeEnum.SINGLE && item.id) {
@@ -153,27 +147,26 @@ const sessionList = computed(() => {
displayName = item.remark
}
if (lastMsg) {
// 使用 useAtMention hook 检查是否有@我的消息
const { checkRoomAtMe, formatMessageContent, getMessageSenderName } = useReplaceMsg()
// 获取该会话的所有消息用于检查@我
const messages = Array.from(chatStore.messageMap.get(item.roomId)?.values() || [])
const { checkRoomAtMe, getAtMeContent, getMessageSenderName, formatMessageContent } = useReplaceMsg()
// 检查是否有@我的消息
const isAtMe = checkRoomAtMe(item.roomId, item.type, currentSession.value.roomId, messages, item.unreadCount)
// 获取发送者信
const senderName = getMessageSenderName(lastMsg)
// 处理显示消
let displayMsg = ''
// 检查是否有@我的消息
const isAtMe = checkRoomAtMe(item.roomId, item.type, currentSession.value.roomId, messages, item.unreadCount)
// 使用封装后的方法处理消息内容,包括撤回消息和@提醒
LastUserMsg = formatMessageContent(lastMsg, item.type, senderName, isAtMe)
// 返回带有isAtMe标记的对象和修改后的名称
return {
...item,
avatar: latestAvatar,
name: displayName, // 使用可能修改过的显示名称
lastMsg: LastUserMsg || item.text || '欢迎使用HuLa',
lastMsgTime: formatTimestamp(item?.activeTime),
isAtMe: isAtMe
// 优先使用session.text作为内容来源
if (item.text) {
// 如果有@我对text应用[有人@我]的样式
displayMsg = isAtMe ? getAtMeContent(true, item.text) : item.text
}
// 如果没有text则尝试从消息列表中获取
else if (messages.length > 0) {
const lastMsg = messages[messages.length - 1]
if (lastMsg) {
const senderName = getMessageSenderName(lastMsg)
displayMsg = formatMessageContent(lastMsg, item.type, senderName, isAtMe)
}
}
@@ -182,9 +175,9 @@ const sessionList = computed(() => {
...item,
avatar: latestAvatar,
name: displayName, // 使用可能修改过的显示名称
lastMsg: item.text || '欢迎使用HuLa',
lastMsg: displayMsg || '欢迎使用HuLa',
lastMsgTime: formatTimestamp(item?.activeTime),
isAtMe: false
isAtMe
}
})
// 添加排序逻辑:先按置顶状态排序,再按活跃时间排序

View File

@@ -23,6 +23,10 @@
:placeholder="accountPH"
@focus="accountPH = ''"
@blur="accountPH = '邮箱/HuLa账号'"
spellCheck="false"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
clearable>
<template #suffix>
<n-flex v-if="loginHistories.length > 0" @click="arrowStatus = !arrowStatus">
@@ -65,6 +69,10 @@
show-password-on="click"
v-model:value="info.password"
type="password"
spellCheck="false"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
:placeholder="passwordPH"
@focus="passwordPH = ''"
@blur="passwordPH = '输入HuLa密码'"

View File

@@ -20,6 +20,10 @@
class="rounded-6px text-12px"
v-model:value="savedProxy.apiIp"
type="text"
spellCheck="false"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
placeholder="127.0.0.1或hulaspark.com" />
<p class="text-12px pt-6px">端口</p>
@@ -27,12 +31,20 @@
class="rounded-6px text-12px"
v-model:value="savedProxy.apiPort"
type="text"
spellCheck="false"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
placeholder="443" />
<p class="text-12px pt-6px">后缀</p>
<n-input
class="rounded-6px text-12px"
v-model:value="savedProxy.apiSuffix"
type="text"
spellCheck="false"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
placeholder="api" />
</n-flex>
</n-collapse-transition>
@@ -52,6 +64,10 @@
class="rounded-6px text-12px"
v-model:value="savedProxy.wsIp"
type="text"
spellCheck="false"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
placeholder="127.0.0.1或hulaspark.com" />
<p class="text-12px pt-6px">端口</p>
@@ -59,12 +75,20 @@
class="rounded-6px text-12px"
v-model:value="savedProxy.wsPort"
type="text"
spellCheck="false"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
placeholder="443" />
<p class="text-12px pt-6px">后缀</p>
<n-input
class="rounded-6px text-12px"
v-model:value="savedProxy.wsSuffix"
type="text"
spellCheck="false"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
placeholder="websocket" />
</n-flex>
</n-collapse-transition>

View File

@@ -23,6 +23,10 @@
size="large"
v-model:value="info.name"
type="text"
spellCheck="false"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
:allow-input="noSideSpace"
:placeholder="showNamePrefix ? '' : placeholders.name"
@focus="handleInputState($event, 'name')"
@@ -40,6 +44,10 @@
maxlength="16"
minlength="6"
size="large"
spellCheck="false"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
show-password-on="click"
v-model:value="info.password"
type="password"
@@ -60,6 +68,10 @@
maxlength="16"
minlength="6"
size="large"
spellCheck="false"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
show-password-on="click"
v-model:value="confirmPassword"
type="password"
@@ -123,6 +135,10 @@
<n-input
style="width: 140px"
size="large"
spellCheck="false"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
v-model:value="info.code"
:allow-input="noSideSpace"
:placeholder="showCodePrefix ? '' : placeholders.code"

View File

@@ -24,17 +24,20 @@ const checkAllTimersCompleted = () => {
}
}
// 添加调试信息打印函数
// 优化的调试信息打印函数
const logDebugInfo = (msgId: number, remainingTime: number) => {
// 只有开启日志功能时才打印
if (enableLogging) {
// 只有开启日志功能时才打印,且只在关键时间点打印
if (enableLogging && (remainingTime <= 5000 || remainingTime % 10000 < 1000)) {
console.log(`[Worker Debug] 消息ID: ${msgId}, 剩余时间: ${(remainingTime / 1000).toFixed(1)}`)
self.postMessage({
type: 'debug',
msgId,
remainingTime,
timestamp: Date.now()
})
// 减少向主线程发送调试消息的频率
if (remainingTime <= 3000) {
self.postMessage({
type: 'debug',
msgId,
remainingTime,
timestamp: Date.now()
})
}
}
}
@@ -103,7 +106,8 @@ self.onmessage = (e) => {
// 立即打印一次初始状态
logDebugInfo(msgId, duration)
// 创建定时打印的间隔器每1000ms打印一次
// 优化的调试间隔器,减少打印频率
const debugInterval = duration > 10000 ? 5000 : 1000 // 长任务每5秒打印短任务每秒打印
const debugId = setInterval(() => {
const elapsed = Date.now() - startTime
const remaining = duration - elapsed
@@ -112,7 +116,7 @@ self.onmessage = (e) => {
} else {
clearInterval(debugId)
}
}, 1000) // 每秒打印一次
}, debugInterval)
const timerId = setTimeout(() => {
clearInterval(debugId)

View File

@@ -5,9 +5,20 @@ const postMsg = ({ type, value }: { type: string; value?: object }) => {
self.postMessage(JSON.stringify({ type, value }))
}
// 连接状态
let connectionState = ConnectionState.DISCONNECTED
// 最后一次收到pong消息的时间
let lastPongTime: number | null = null
// 连续心跳失败计数未收到pong响应
let consecutiveHeartbeatFailures = 0
// 最大允许的连续心跳失败次数,超过这个值会触发重连
const MAX_HEARTBEAT_FAILURES = 3
// 心跳日志记录开关
let heartbeatLoggingEnabled = false
// ws instance
let connection: WebSocket
@@ -22,6 +33,9 @@ let clientId: null | string = null
let serverUrl: null | string = null
// 心跳状态
let heartbeatActive = false
// 往 ws 发消息
const connectionSend = (value: object) => {
connection?.send(JSON.stringify(value))
@@ -32,32 +46,131 @@ let heartbeatTimeout: string | null = null
const HEARTBEAT_TIMEOUT = 15000 // 15秒超时
const HEARTBEAT_INTERVAL = 9900 // 心跳间隔
// 上次发送心跳的时间
let lastPingSent: number | null = null
// 健康检查间隔
const HEALTH_CHECK_INTERVAL = 30000 // 30秒检查一次连接健康状态
// 健康检查定时器ID
let healthCheckTimerId: string | null = null
// 发送心跳请求使用timer.worker
const sendHeartPack = () => {
// 启动健康检查定时器
startHealthCheck()
// 标记心跳活跃
heartbeatActive = true
// 请求主线程启动心跳定时器
postMsg({
type: 'startHeartbeatTimer',
value: { interval: HEARTBEAT_INTERVAL }
})
// 记录日志
logHeartbeat('心跳定时器已启动')
}
// 启动定期健康检查
const startHealthCheck = () => {
// 清除之前的健康检查定时器
if (healthCheckTimerId) {
postMsg({
type: 'clearTimer',
value: { msgId: healthCheckTimerId }
})
healthCheckTimerId = null
}
// 设置新的健康检查定时器
const timerId = `health_check_${Date.now()}`
healthCheckTimerId = timerId
postMsg({
type: 'startTimer',
value: { msgId: timerId, duration: HEALTH_CHECK_INTERVAL }
})
logHeartbeat('健康检查定时器已启动')
}
// 心跳日志记录
const logHeartbeat = (message: string, data?: any) => {
if (heartbeatLoggingEnabled) {
console.log(`[WebSocket心跳] ${message}`, data || '')
postMsg({
type: 'heartbeatLog',
value: { message, data, timestamp: Date.now() }
})
}
}
// 发送单次心跳
const sendSingleHeartbeat = () => {
// 心跳消息类型 2
connectionSend({ type: 2 })
const pingTime = Date.now()
// 检查WebSocket连接状态
if (connection?.readyState !== WebSocket.OPEN) {
logHeartbeat('尝试发送心跳时发现连接未打开', { readyState: connection?.readyState })
tryReconnect()
return
}
// 检测连接健康状态
// 记录本次发送心跳时间
lastPingSent = Date.now()
// 心跳消息类型 2
try {
connectionSend({ type: 2 })
logHeartbeat('心跳已发送', { timestamp: lastPingSent })
} catch (err) {
logHeartbeat('心跳发送失败', { error: err })
// 发送失败,可能连接已经中断但状态未更新
tryReconnect()
return
}
// 优化的连接健康检测机制
if (lastPongTime !== null) {
const timeSinceLastPong = pingTime - lastPongTime
const isConnectionHealthy = timeSinceLastPong < HEARTBEAT_INTERVAL * 2
const timeSinceLastPong = lastPingSent - lastPongTime
const healthThreshold = HEARTBEAT_INTERVAL * 2.5 // 增加容错时间
const isConnectionHealthy = timeSinceLastPong < healthThreshold
// 如果连接不健康,通知主线程
if (!isConnectionHealthy) {
postMsg({
type: WorkerMsgEnum.ERROR,
value: { msg: '连接响应较慢,可能存在网络问题', timeSinceLastPong }
})
consecutiveHeartbeatFailures++
// 只在关键阈值时记录日志,减少日志开销
if (consecutiveHeartbeatFailures === 1 || consecutiveHeartbeatFailures % 3 === 0) {
logHeartbeat('连接响应缓慢', {
consecutiveFailures: consecutiveHeartbeatFailures,
timeSinceLastPong
})
}
// 延迟错误通知,避免频繁触发
if (consecutiveHeartbeatFailures >= 2) {
postMsg({
type: WorkerMsgEnum.ERROR,
value: {
msg: '连接响应较慢,可能存在网络问题',
timeSinceLastPong,
consecutiveFailures: consecutiveHeartbeatFailures
}
})
}
// 连续失败次数过多,尝试重连
if (consecutiveHeartbeatFailures >= MAX_HEARTBEAT_FAILURES) {
logHeartbeat('连续心跳失败次数过多,触发重连', { consecutiveFailures: consecutiveHeartbeatFailures })
tryReconnect()
return
}
} else {
// 重置连续失败计数
if (consecutiveHeartbeatFailures > 0) {
logHeartbeat('心跳恢复正常', { previousFailures: consecutiveHeartbeatFailures })
consecutiveHeartbeatFailures = 0
}
}
}
@@ -79,8 +192,16 @@ const sendSingleHeartbeat = () => {
})
}
// 更新连接状态
const updateConnectionState = (newState: ConnectionState) => {
connectionState = newState
postMsg({ type: 'connectionStateChange', value: { state: connectionState } })
}
// 清除心跳定时器
const clearHeartPackTimer = () => {
logHeartbeat('清除心跳定时器')
heartbeatActive = false
postMsg({ type: 'stopHeartbeatTimer' })
// 清除超时定时器
@@ -91,13 +212,55 @@ const clearHeartPackTimer = () => {
})
heartbeatTimeout = null
}
// 清除健康检查定时器
if (healthCheckTimerId) {
postMsg({
type: 'clearTimer',
value: { msgId: healthCheckTimerId }
})
healthCheckTimerId = null
}
}
// 主动尝试重连
const tryReconnect = () => {
logHeartbeat('触发主动重连')
// 主动关闭当前连接
connection?.close()
// 重置心跳状态
heartbeatActive = false
// 清除心跳定时器
clearHeartPackTimer()
// 触发重连流程
updateConnectionState(ConnectionState.RECONNECTING)
if (!lockReconnect) {
lockReconnect = true
// 使用短延迟立即重连
postMsg({
type: 'startReconnectTimer',
value: {
delay: 1000, // 快速重连1秒后
reconnectCount
}
})
}
}
// 优化的智能退避算法
const getBackoffDelay = (retryCount: number) => {
const baseDelay = 1000 // 基础延迟1秒
const maxDelay = 30000 // 最大延迟30
const delay = Math.min(baseDelay * Math.pow(2, retryCount), maxDelay)
return delay + Math.random() * 1000 // 添加随机抖动
const maxDelay = 15000 // 减少最大延迟到15
const multiplier = Math.min(1.5, 2 - retryCount * 0.1)
const delay = Math.min(baseDelay * Math.pow(multiplier, retryCount), maxDelay)
// 减少随机抖动范围
return delay + Math.random() * 500
}
const onCloseHandler = () => {
@@ -147,8 +310,18 @@ const onConnectClose = () => {
// ws 连接成功
const onConnectOpen = () => {
console.log('✅ WebSocket 连接成功')
// 重置心跳相关状态
consecutiveHeartbeatFailures = 0
lastPongTime = null
lastPingSent = null
updateConnectionState(ConnectionState.CONNECTED)
postMsg({ type: WorkerMsgEnum.OPEN })
// 连接成功后立即发送一次心跳
sendSingleHeartbeat()
// 然后开始定期心跳
sendHeartPack()
}
// ws 连接 接收到消息
@@ -159,11 +332,42 @@ const onConnectMsg = (e: any) => {
if (data && (data.type === 'pong' || data.type === 3)) {
// 3是pong的消息类型
lastPongTime = Date.now()
// 计算心跳往返时间
let roundTripTime = null
if (lastPingSent) {
roundTripTime = lastPongTime - lastPingSent
}
// 重置连续失败计数
if (consecutiveHeartbeatFailures > 0) {
logHeartbeat('收到pong响应重置连续失败计数', {
previousFailures: consecutiveHeartbeatFailures,
roundTripTime
})
consecutiveHeartbeatFailures = 0
} else {
logHeartbeat('收到pong响应', { roundTripTime })
}
// 告知主线程收到了pong
postMsg({ type: 'pongReceived', value: { timestamp: lastPongTime } })
postMsg({
type: 'pongReceived',
value: {
timestamp: lastPongTime,
roundTripTime,
consecutiveFailures: consecutiveHeartbeatFailures
}
})
}
} catch (err) {
// 解析失败则当作普通消息处理
logHeartbeat('解析消息失败', { error: err })
}
// 如果收到任何消息,说明连接是有效的,更新连接状态
if (connectionState !== ConnectionState.CONNECTED) {
updateConnectionState(ConnectionState.CONNECTED)
}
// 转发消息给主线程
@@ -196,12 +400,18 @@ const initConnection = () => {
connection.addEventListener('error', onConnectError)
}
let connectionState = ConnectionState.DISCONNECTED
// 停止所有心跳相关活动
const stopAllHeartbeat = () => {
console.log('停止所有心跳活动')
heartbeatActive = false
clearHeartPackTimer()
}
// 更新连接状态
const updateConnectionState = (newState: ConnectionState) => {
connectionState = newState
postMsg({ type: 'connectionStateChange', value: { state: connectionState } })
// 重置重连状态
const resetReconnection = () => {
reconnectCount = 0
lockReconnect = false
console.log('重置重连计数和状态')
}
self.onmessage = (e: MessageEvent<string>) => {
@@ -250,6 +460,22 @@ self.onmessage = (e: MessageEvent<string>) => {
postMsg({ type: 'heartbeatTimeout' })
break
}
// 停止心跳
case 'stopHeartbeat': {
stopAllHeartbeat()
break
}
// 重置重连计数
case 'resetReconnectCount': {
resetReconnection()
break
}
// 清除重连计时器
case 'clearReconnectTimer': {
lockReconnect = true // 锁定重连,阻止旧的重连流程
console.log('清除重连计时器')
break
}
// 页面可见性变化
case 'visibilityChange': {
const { isHidden } = value
@@ -267,15 +493,78 @@ self.onmessage = (e: MessageEvent<string>) => {
case 'checkConnectionHealth': {
const now = Date.now()
const isHealthy = lastPongTime !== null && now - lastPongTime < HEARTBEAT_INTERVAL * 2
// 连续失败次数也是健康状态的一个指标
const healthStatus = {
isHealthy,
lastPongTime,
lastPingSent,
connectionState,
heartbeatActive,
consecutiveFailures: consecutiveHeartbeatFailures,
timeSinceLastPong: lastPongTime ? now - lastPongTime : null,
readyState: connection?.readyState
}
logHeartbeat('健康检查', healthStatus)
// 如果连接不健康但状态显示已连接,尝试修复
if (!isHealthy && connection?.readyState === WebSocket.OPEN && heartbeatActive) {
logHeartbeat('健康检查发现异常,尝试恢复心跳', healthStatus)
// 立即发送一次心跳
sendSingleHeartbeat()
}
// 如果心跳应该活跃但心跳定时器未运行,重启心跳
if (connectionState === ConnectionState.CONNECTED && !heartbeatActive) {
logHeartbeat('发现心跳停止但连接正常,重启心跳', healthStatus)
sendHeartPack()
}
postMsg({
type: 'connectionHealthStatus',
value: {
isHealthy,
lastPongTime,
timeSinceLastPong: lastPongTime ? now - lastPongTime : null
}
value: healthStatus
})
break
}
// 健康检查定时器触发
case 'healthCheckTimeout': {
// 定期健康检查触发
const { msgId } = value
if (msgId === healthCheckTimerId) {
// 执行健康检查
const now = Date.now()
const isHealthy = lastPongTime !== null && now - lastPongTime < HEARTBEAT_INTERVAL * 2
logHeartbeat('定期健康检查', {
isHealthy,
timeSinceLastPong: lastPongTime ? now - lastPongTime : null,
heartbeatActive,
readyState: connection?.readyState
})
// 如果不健康且连接状态异常,尝试重连
if (!isHealthy && consecutiveHeartbeatFailures >= 1) {
logHeartbeat('定期健康检查发现连接异常,尝试重连')
tryReconnect()
}
// 如果心跳定时器应该在运行但实际没有运行,重启心跳
else if (connectionState === ConnectionState.CONNECTED && !heartbeatActive) {
logHeartbeat('发现心跳停止但连接正常,重启心跳')
sendHeartPack()
}
// 继续启动下一次健康检查
startHealthCheck()
}
break
}
// 控制心跳日志记录
case 'setHeartbeatLogging': {
heartbeatLoggingEnabled = !!value.enabled
logHeartbeat(`心跳日志${heartbeatLoggingEnabled ? '已开启' : '已关闭'}`)
break
}
}
}