Compare commits

...

108 Commits

Author SHA1 Message Date
cc
a58518ccb5 Merge pull request #611 from hicccc77/dev
Dev
2026-04-03 00:01:58 +08:00
cc
cdd17d919e Merge branch 'main' into dev 2026-04-03 00:01:49 +08:00
cc
4580cef7f2 Merge branch 'dev' of https://github.com/hicccc77/WeFlow into dev 2026-04-03 00:01:18 +08:00
cc
6f9c765bab 更新依赖 2026-04-03 00:01:05 +08:00
cc
b0cc811807 Merge pull request #610 from hicccc77/dev
Dev
2026-04-02 23:30:36 +08:00
cc
eb540d5c13 Merge branch 'main' into dev 2026-04-02 23:30:29 +08:00
cc
e308293cf6 Merge branch 'dev' of https://github.com/hicccc77/WeFlow into dev 2026-04-02 23:29:50 +08:00
cc
9ed4659c5c 修复依赖问题 2026-04-02 23:28:55 +08:00
cc
f5f2b76914 Merge pull request #609 from hicccc77/dev
Dev
2026-04-02 23:23:07 +08:00
cc
551a065497 Merge branch 'main' into dev 2026-04-02 23:22:59 +08:00
cc
88d7e38d82 Merge branch 'dev' of https://github.com/hicccc77/WeFlow into dev 2026-04-02 23:22:16 +08:00
cc
65e6cb22dd 修复下载源问题 2026-04-02 23:22:12 +08:00
cc
689a396f6e Merge pull request #608 from hicccc77/dev
Dev
2026-04-02 23:16:30 +08:00
cc
512ea84850 Merge branch 'main' into dev 2026-04-02 23:16:19 +08:00
cc
1542e583f7 修复了一些问题 2026-04-02 23:15:39 +08:00
cc
c488dcc3c6 Merge branch 'dev' of https://github.com/hicccc77/WeFlow into dev 2026-04-02 23:15:20 +08:00
cc
1594e0e24b 修复安装问题 2026-04-02 23:15:17 +08:00
cc
1e1fa77621 Merge pull request #607 from hicccc77/dev
Dev
2026-04-02 23:04:50 +08:00
cc
3270c17514 Merge branch 'main' into dev 2026-04-02 23:04:28 +08:00
cc
e635942e3d 补充更新 2026-04-02 23:03:50 +08:00
cc
64dc2858a7 补充更新 2026-04-02 23:02:36 +08:00
cc
d05496bb3d 补充更新 2026-04-02 23:02:24 +08:00
cc
bb94553fff 补充更新 2026-04-02 23:02:15 +08:00
cc
113216b7ba 修复打包问题 2026-04-02 23:01:54 +08:00
cc
55181edaa8 Merge pull request #599 from hicccc77/dependabot/npm_and_yarn/dev/lucide-react-1.7.0
chore(deps): bump lucide-react from 0.562.0 to 1.7.0
2026-04-02 23:01:20 +08:00
cc
4f01a7b577 Merge branch 'dev' into dependabot/npm_and_yarn/dev/lucide-react-1.7.0 2026-04-02 23:01:08 +08:00
cc
ea21111037 Merge pull request #600 from hicccc77/dependabot/npm_and_yarn/dev/typescript-6.0.2
chore(deps-dev): bump typescript from 5.9.3 to 6.0.2
2026-04-02 23:00:57 +08:00
dependabot[bot]
fba9f1de42 chore(deps): bump lucide-react from 0.562.0 to 1.7.0
Bumps [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) from 0.562.0 to 1.7.0.
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.7.0/packages/lucide-react)

---
updated-dependencies:
- dependency-name: lucide-react
  dependency-version: 1.7.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-02 14:53:21 +00:00
dependabot[bot]
7d0b8db7a6 chore(deps-dev): bump typescript from 5.9.3 to 6.0.2
Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.9.3 to 6.0.2.
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.9.3...v6.0.2)

---
updated-dependencies:
- dependency-name: typescript
  dependency-version: 6.0.2
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-02 14:53:14 +00:00
cc
12faa31e34 Merge branch 'dev' of https://github.com/hicccc77/WeFlow into dev 2026-04-02 22:51:48 +08:00
cc
74945b1752 修复了一些打包问题 2026-04-02 22:51:44 +08:00
cc
a7c66517d2 Merge pull request #606 from hicccc77/dev
Dev
2026-04-02 22:42:32 +08:00
cc
adf187ddf5 Merge branch 'main' into dev 2026-04-02 22:42:13 +08:00
cc
614d897dd2 Merge branch 'dev' of https://github.com/hicccc77/WeFlow into dev 2026-04-02 22:41:42 +08:00
cc
ec9a7d68e6 修复了一些问题 2026-04-02 22:41:36 +08:00
cc
79dd91b270 Merge pull request #605 from hicccc77/dev
Dev
2026-04-02 22:31:26 +08:00
cc
b26bcd7603 Merge branch 'main' into dev 2026-04-02 22:31:25 +08:00
cc
a65468191b Merge branch 'dev' of https://github.com/hicccc77/WeFlow into dev 2026-04-02 22:30:53 +08:00
cc
4ed5271703 修复管理器问题 2026-04-02 22:30:49 +08:00
cc
f2afe2a977 Merge pull request #604 from hicccc77/dev
Dev
2026-04-02 22:28:35 +08:00
cc
430b0f30c9 Merge branch 'main' into dev 2026-04-02 22:28:24 +08:00
cc
8aac9a795e 修复检测问题与依赖问题 2026-04-02 22:27:45 +08:00
cc
b9405765f9 Merge pull request #601 from hicccc77/dependabot/npm_and_yarn/dev/echarts-6.0.0
chore(deps): bump echarts from 5.6.0 to 6.0.0
2026-04-02 22:25:00 +08:00
cc
90856b3812 Merge pull request #602 from hicccc77/dependabot/npm_and_yarn/dev/sass-1.98.0
chore(deps-dev): bump sass from 1.97.2 to 1.98.0
2026-04-02 22:24:49 +08:00
cc
1e78af3c25 Merge pull request #603 from hicccc77/dependabot/npm_and_yarn/dev/vite-8.0.3
chore(deps-dev): bump vite from 6.4.1 to 8.0.3
2026-04-02 22:24:38 +08:00
dependabot[bot]
4be232b951 chore(deps-dev): bump vite from 6.4.1 to 8.0.3
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.4.1 to 8.0.3.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/create-vite@8.0.3/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.0.3
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-02 14:22:00 +00:00
dependabot[bot]
59d5c2762d chore(deps-dev): bump sass from 1.97.2 to 1.98.0
Bumps [sass](https://github.com/sass/dart-sass) from 1.97.2 to 1.98.0.
- [Release notes](https://github.com/sass/dart-sass/releases)
- [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sass/dart-sass/compare/1.97.2...1.98.0)

---
updated-dependencies:
- dependency-name: sass
  dependency-version: 1.98.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-02 14:21:28 +00:00
dependabot[bot]
21a55439ec chore(deps): bump echarts from 5.6.0 to 6.0.0
Bumps [echarts](https://github.com/apache/echarts) from 5.6.0 to 6.0.0.
- [Release notes](https://github.com/apache/echarts/releases)
- [Commits](https://github.com/apache/echarts/compare/5.6.0...6.0.0)

---
updated-dependencies:
- dependency-name: echarts
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-02 14:21:19 +00:00
cc
c4a35f5c15 Merge pull request #598 from hicccc77/dev
Dev
2026-04-02 22:20:04 +08:00
cc
b42f761011 配置更新 2026-04-02 22:18:50 +08:00
cc
46e2e64e65 Merge branch 'dev' of https://github.com/hicccc77/WeFlow into dev 2026-04-02 22:14:52 +08:00
cc
54ed35ffb3 新增渠道更新 2026-04-02 22:14:46 +08:00
cc
3e379957e1 Merge pull request #596 from hicccc77/dependabot/npm_and_yarn/npm_and_yarn-282a1442c2
chore(deps-dev): bump @xmldom/xmldom from 0.8.11 to 0.8.12 in the npm_and_yarn group across 1 directory
2026-04-02 20:53:57 +08:00
dependabot[bot]
b64525487e chore(deps-dev): bump @xmldom/xmldom
Bumps the npm_and_yarn group with 1 update in the / directory: [@xmldom/xmldom](https://github.com/xmldom/xmldom).


Updates `@xmldom/xmldom` from 0.8.11 to 0.8.12
- [Release notes](https://github.com/xmldom/xmldom/releases)
- [Changelog](https://github.com/xmldom/xmldom/blob/master/CHANGELOG.md)
- [Commits](https://github.com/xmldom/xmldom/compare/0.8.11...0.8.12)

---
updated-dependencies:
- dependency-name: "@xmldom/xmldom"
  dependency-version: 0.8.12
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-01 05:11:30 +00:00
hicccc77
e544f5c862 fix: add forceReopen retry logic for openMessageCursorLite 2026-04-01 04:04:53 +08:00
hicccc77
669759c52e chore: 更新资源文件 2026-03-31 21:36:53 +08:00
cc
4a13d3209b 更新 2026-03-31 21:24:45 +08:00
cc
be069e9aed 实现 #584 2026-03-31 21:24:31 +08:00
hicccc77
0b20ee1aa2 chore: 更新资源文件 2026-03-31 20:02:07 +08:00
hicccc77
69128062fe chore: 更新资源文件 2026-03-30 23:56:34 +08:00
hicccc77
6d652130e6 chore: 更新资源文件 2026-03-30 20:10:35 +08:00
hicccc77
9e6f8077f7 fix: 数据匿名收集受 analyticsConsent 开关控制 (#589)
- 新增 analyticsConsent state 追踪用户同意状态
- initCloudControl() 仅在用户明确同意后执行
- recordPage() 同样受 analyticsConsent 守卫
- handleAnalyticsAllow 同步更新 state,用户同意后立即生效

Fixes #589
2026-03-30 12:05:41 +08:00
hicccc77
40342ca824 fix(deps): 修复 npm install postinstall 阶段 ajv-keywords 兼容性错误
将 npm overrides 中的 ajv 版本范围从 >=6.14.0 改为 ^6.14.0,
确保 electron-builder 依赖链使用 ajv v6,避免在 Node.js v22 上
@develar/schema-utils 加载 ajv-keywords 时访问 formats 返回 undefined 的问题。

Fixes #588
2026-03-30 11:04:44 +08:00
hicccc77
4da9f1e6cf feat: 添加 anti-spam workflow,自动检测并关闭垃圾 issue 2026-03-29 23:19:55 +08:00
hicccc77
93b55fe370 feat: 添加 anti-spam workflow,自动检测并关闭垃圾 issue 2026-03-29 23:18:24 +08:00
hicccc77
ee5e7d2586 fix: 修复微信重装后 openMessageCursor 返回 -3 (no message db) 的问题
- 新增 forceReopen() 方法:清空路径缓存后强制重新初始化账号连接
- openMessageCursor 在 result=-3 时自动触发 forceReopen 并重试一次
- 改善 -3 错误的提示文案,引导用户重新指定数据目录

修复 #591
2026-03-29 19:18:20 +08:00
hicccc77
4f4e09c3de fix: 修复微信重装后 openMessageCursor 返回 -3 (no message db) 的问题
- 新增 forceReopen() 方法:清空路径缓存后强制重新初始化账号连接
- openMessageCursor 在 result=-3 时自动触发 forceReopen 并重试一次
- 改善 -3 错误的提示文案,引导用户重新指定数据目录

修复 #591
2026-03-29 16:07:09 +08:00
hicccc77
d537d81f1c fix(deps): 修复安全漏洞 2026-03-28 21:15:14 +08:00
hicccc77
26c6700152 fix: 修复 CodeQL code scanning warning 问题 2026-03-28 21:12:29 +08:00
hicccc77
49fb96d7a3 Revert "Revert "fix(deps): 修复安全漏洞""
This reverts commit d256ee5696.
2026-03-28 19:29:17 +08:00
hicccc77
d256ee5696 Revert "fix(deps): 修复安全漏洞"
This reverts commit 06079659af.
2026-03-28 19:28:45 +08:00
hicccc77
bd70a7bfa8 Revert "fix: 修复 Linux 下内存扫描找不到微信进程的问题\n\n增加 pidof/pgrep/ps aux 三重兜底逻辑,兼容不同发行版\n(flatpak、AppImage、wechat-bin 等安装方式),解决 #575"
This reverts commit 3fb09bad0d.
2026-03-28 19:28:45 +08:00
hicccc77
3fb09bad0d fix: 修复 Linux 下内存扫描找不到微信进程的问题\n\n增加 pidof/pgrep/ps aux 三重兜底逻辑,兼容不同发行版\n(flatpak、AppImage、wechat-bin 等安装方式),解决 #575 2026-03-28 19:05:21 +08:00
hicccc77
06079659af fix(deps): 修复安全漏洞 2026-03-28 17:36:28 +08:00
hicccc77
22d8049c2c Revert "fix: 兼容微信新目录结构多一层嵌套导致账号目录识别失败的问题"
This reverts commit 5f6b0e8960.
2026-03-28 17:30:56 +08:00
hicccc77
5f6b0e8960 fix: 兼容微信新目录结构多一层嵌套导致账号目录识别失败的问题
修复 scanWxids 和 scanWxidCandidates 在 2.0b4.0.9/xwechat_files/wxid_xxx
结构下扫描不到账号目录的问题,增加往下多扫一层的兜底逻辑

Fixes #541
2026-03-28 17:28:52 +08:00
hicccc77
9b8da7774d fix: 替换失效的 downloads badge 为 shields.io 2026-03-28 17:05:33 +08:00
hicccc77
eabed55a7a fix: 修复 README Downloads badge 嵌套在 Issues 链接内的问题 2026-03-28 17:03:28 +08:00
hicccc77
32cc74f99c merge: 同步 main 最新代码到 dev(依赖更新、版本 4.3.0、资源文件) 2026-03-28 16:54:47 +08:00
cc
ffc4cc3d96 Merge pull request #574 from hicccc77/dependabot/npm_and_yarn/npm_and_yarn-8abc9b7730
chore(deps): bump the npm_and_yarn group across 1 directory with 3 updates
2026-03-28 16:50:28 +08:00
dependabot[bot]
007cf57efd chore(deps): bump the npm_and_yarn group across 1 directory with 3 updates
Bumps the npm_and_yarn group with 3 updates in the / directory: [minimatch](https://github.com/isaacs/minimatch), [brace-expansion](https://github.com/juliangruber/brace-expansion) and [rollup](https://github.com/rollup/rollup).


Updates `minimatch` from 3.1.2 to 3.1.5
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v3.1.2...v3.1.5)

Updates `brace-expansion` from 1.1.12 to 1.1.13
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v1.1.12...v1.1.13)

Updates `rollup` from 4.55.1 to 4.60.0
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.55.1...v4.60.0)

---
updated-dependencies:
- dependency-name: minimatch
  dependency-version: 3.1.5
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: brace-expansion
  dependency-version: 1.1.13
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: rollup
  dependency-version: 4.60.0
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-28 07:56:41 +00:00
hicccc77
c6dba71197 fix: 修复 macOS release notes 中 xattr 命令被 bash 吞掉的问题 2026-03-28 15:55:11 +08:00
cc
8aa162e294 Merge pull request #568 from hicccc77/dependabot/npm_and_yarn/npm_and_yarn-1ca40131d0
chore(deps): bump @tootallnate/once from 2.0.0 to removed in the npm_and_yarn group across 1 directory
2026-03-28 14:51:11 +08:00
dependabot[bot]
51d6dec7ff chore(deps): bump @tootallnate/once
Bumps the npm_and_yarn group with 1 update in the / directory: [@tootallnate/once](https://github.com/TooTallNate/once).


Removes `@tootallnate/once`

---
updated-dependencies:
- dependency-name: "@tootallnate/once"
  dependency-version: 
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-27 22:39:12 +00:00
hicccc77
f1b2762769 fix(deps): 修复安全漏洞 2026-03-28 06:37:44 +08:00
hicccc77
d126be2aa5 chore: 更新资源文件 2026-03-27 22:03:27 +08:00
hicccc77
ea034ee76a ci: fix pnpm audit exit code causing job failure 2026-03-27 21:43:16 +08:00
hicccc77
39634a690c fix(deps): remove ajv override to fix electron-builder compatibility 2026-03-27 21:09:28 +08:00
hicccc77
a7001eb6da fix(deps): upgrade react-router-dom to 7.13.2 and add pnpm overrides for security vulnerabilities
- Upgrade react-router-dom ^7.1.1 -> ^7.13.2
- Add pnpm.overrides to force safe versions of: tar, minimatch, rollup,
  immutable, lodash, ajv, brace-expansion, picomatch
2026-03-27 21:04:44 +08:00
hicccc77
71e3540f18 ci: add gitleaks config to suppress false positives 2026-03-27 20:58:14 +08:00
hicccc77
78cadfd352 ci: add security-events write permission for CodeQL 2026-03-27 19:12:31 +08:00
hicccc77
da15f829d3 ci: add security-events write permission for CodeQL 2026-03-27 19:12:20 +08:00
hicccc77
bb60694013 ci: fix pnpm install frozen-lockfile issue 2026-03-27 18:17:26 +08:00
hicccc77
b3758d2baf ci: fix pnpm install frozen-lockfile issue 2026-03-27 18:17:14 +08:00
hicccc77
bc794e9a44 ci: add daily security scan workflow for all branches 2026-03-27 17:59:09 +08:00
hicccc77
c80115d0f7 ci: add daily security scan workflow for all branches 2026-03-27 17:56:35 +08:00
xuncha
6277576249 Merge pull request #560 from JiQingzhe2004/main
feat: 强制更新支持 minimumVersion,阻止低版本用户继续使用
2026-03-27 15:23:02 +08:00
JiQingzhe2004
2201d369fa chore: bump version to 4.3.0 2026-03-27 14:43:33 +08:00
JiQingzhe2004
9f4e4790f5 feat: 强制更新支持 minimumVersion,阻止低版本用户继续使用 2026-03-27 14:43:08 +08:00
xuncha
501e373e38 Merge pull request #559 from xunchahaha/main
更新打包
2026-03-27 13:10:50 +08:00
xuncha
b2cf7c92d5 更新打包 2026-03-27 13:10:27 +08:00
xuncha
e92e13c045 Merge pull request #558 from hicccc77/xunchahaha-patch-1
Delete preinstall.js
2026-03-27 12:59:45 +08:00
xuncha
f3dec958b0 Delete preinstall.js 2026-03-27 12:48:42 +08:00
cc
cfa335564a Merge pull request #549 from hicccc77/dev
Dev
2026-03-25 20:02:58 +08:00
Forrest
61ef10de9b Merge pull request #545 from JiQingzhe2004/main
更新图标
2026-03-25 02:09:50 +08:00
Forrest
73f36d6b29 更新图标 2026-03-25 01:36:04 +08:00
Forrest
666a1a3296 Merge branch 'hicccc77:main' into main 2026-03-25 00:18:12 +08:00
xuncha
b5a371da87 Merge pull request #349 from hicccc77/dev
Dev
2026-03-13 08:55:32 +03:00
42 changed files with 3598 additions and 2611 deletions

7
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,7 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "daily"
target-branch: "dev"

134
.github/workflows/anti-spam.yml vendored Normal file
View File

@@ -0,0 +1,134 @@
name: Anti-Spam
on:
issues:
types: [opened, edited]
permissions:
issues: write
jobs:
check-spam:
runs-on: ubuntu-latest
steps:
- name: Check for spam
uses: actions/github-script@v7
with:
script: |
const issue = context.payload.issue;
const title = (issue.title || '').toLowerCase();
const body = (issue.body || '').toLowerCase();
const text = title + ' ' + body;
// 博彩/赌球类
const gamblingPatterns = [
/世界杯.*买球/, /买球.*世界杯/,
/世界杯.*下注/, /世界杯.*竞猜/,
/世界杯.*投注/, /世界杯.*押注/,
/世界杯.*彩票/, /世界杯.*平台/,
/世界杯.*app/, /世界杯.*软件/,
/世界杯.*网站/, /世界杯.*网址/,
/足球.*买球/, /买球.*足球/,
/足球.*投注/, /足球.*押注/,
/足球.*竞猜/, /足球.*平台/,
/篮球.*买球/, /篮球.*投注/,
/体育.*投注/, /体育.*竞猜/,
/体育.*买球/, /体育.*押注/,
/赌球/, /赌博.*网站/, /赌博.*平台/,
/博彩/, /博彩.*网站/, /博彩.*平台/,
/正规.*买球/, /官方.*买球/,
/买球.*网站/, /买球.*app/,
/买球.*软件/, /买球.*网址/,
/买球.*平台/, /买球.*技巧/,
/投注.*网站/, /投注.*平台/,
/押注.*网站/, /押注.*平台/,
/竞猜.*网站/, /竞猜.*平台/,
/彩票.*网站/, /彩票.*平台/,
/欧洲杯.*买球/, /欧冠.*买球/,
/nba.*买球/, /nba.*投注/,
];
// 色情/交友类
const adultPatterns = [
/约炮/, /一夜情/, /外围/,
/包养/, /援交/, /陪聊/,
/成人.*网站/, /成人.*视频/,
/av.*网站/, /黄色.*网站/,
];
// 贷款/金融诈骗类
const financePatterns = [
/秒到账.*贷款/, /无抵押.*贷款/,
/征信.*贷款/, /黑户.*贷款/,
/快速.*放款/, /私人.*放贷/,
/刷单/, /兼职.*日入/, /兼职.*月入/,
/网赚/, /躺赚/, /被动收入.*平台/,
/虚拟货币.*投资/, /usdt.*投资/,
/炒币.*平台/, /数字货币.*平台/,
];
// 垃圾推广类
const spamPromoPatterns = [
/代刷/, /粉丝.*购买/, /涨粉/,
/seo.*优化/, /快速排名/,
/微商/, /代理.*招募/,
];
// 账号特征检测(新账号 + 无 contribution
const allPatterns = [
...gamblingPatterns,
...adultPatterns,
...financePatterns,
...spamPromoPatterns,
];
const isSpam = allPatterns.some(pattern => pattern.test(text));
// 额外检测:标题超短且含可疑关键词(常见于批量刷单)
const suspiciousShort = title.length < 10 && /(买球|投注|博彩|赌博|下注|押注)/.test(title);
if (isSpam || suspiciousShort) {
// 确保 spam label 存在
try {
await github.rest.issues.createLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name: 'spam',
color: 'e4e669',
description: 'Spam issue'
});
} catch (e) {
// label 已存在,忽略
}
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: '此 issue 已被自动识别为垃圾内容并关闭。\n\nThis issue has been automatically identified as spam and closed.'
});
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: ['spam']
});
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
state: 'closed',
state_reason: 'not_planned'
});
await github.rest.issues.lock({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
lock_reason: 'spam'
});
console.log(`Closed spam issue #${issue.number}: ${issue.title}`);
}

310
.github/workflows/dev-daily-fixed.yml vendored Normal file
View File

@@ -0,0 +1,310 @@
name: Dev Daily
on:
schedule:
# GitHub Actions schedule uses UTC. 16:00 UTC = 北京时间次日 00:00
- cron: "0 16 * * *"
workflow_dispatch:
permissions:
contents: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
FIXED_DEV_TAG: nightly-dev
ELECTRON_BUILDER_BINARIES_MIRROR: https://github.com/electron-userland/electron-builder-binaries/releases/download/
jobs:
prepare:
runs-on: ubuntu-latest
outputs:
dev_version: ${{ steps.meta.outputs.dev_version }}
steps:
- name: Check out git repository
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Install Node.js
uses: actions/setup-node@v5
with:
node-version: 24
cache: "npm"
- name: Generate daily dev version
id: meta
shell: bash
run: |
set -euo pipefail
BASE_VERSION="$(node -p "require('./package.json').version.split('-')[0]")"
YEAR_2="$(TZ=Asia/Shanghai date +%y)"
MONTH="$(TZ=Asia/Shanghai date +%-m)"
DAY="$(TZ=Asia/Shanghai date +%-d)"
DEV_VERSION="${BASE_VERSION}-dev.${YEAR_2}.${MONTH}.${DAY}"
echo "dev_version=$DEV_VERSION" >> "$GITHUB_OUTPUT"
echo "Dev version: $DEV_VERSION"
- name: Ensure fixed prerelease exists
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: |
set -euo pipefail
if gh release view "$FIXED_DEV_TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
gh release edit "$FIXED_DEV_TAG" --repo "$GITHUB_REPOSITORY" --title "Daily Dev Build" --prerelease
else
gh release create "$FIXED_DEV_TAG" --repo "$GITHUB_REPOSITORY" --title "Daily Dev Build" --notes "开发版发布页" --prerelease
fi
dev-mac-arm64:
needs: prepare
runs-on: macos-14
steps:
- name: Check out git repository
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Install Node.js
uses: actions/setup-node@v5
with:
node-version: 24
cache: "npm"
- name: Install Dependencies
run: npm install
- name: Set dev version
shell: bash
run: npm version "${{ needs.prepare.outputs.dev_version }}" --no-git-tag-version --allow-same-version
- name: Build Frontend & Type Check
run: |
npx tsc
npx vite build
- name: Package macOS arm64 dev artifacts
shell: bash
run: |
export ELECTRON_BUILDER_BINARIES_MIRROR="https://github.com/electron-userland/electron-builder-binaries/releases/download/"
echo "Using ELECTRON_BUILDER_BINARIES_MIRROR=$ELECTRON_BUILDER_BINARIES_MIRROR"
npx electron-builder --mac dmg --arm64 --publish never '--config.publish.channel=dev' '--config.artifactName=${productName}-dev-arm64.${ext}'
- name: Upload macOS arm64 assets to fixed release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: |
mapfile -t assets < <(find release -maxdepth 1 -type f | sort)
if [ "${#assets[@]}" -eq 0 ]; then
echo "No release files found in ./release"
exit 1
fi
gh release upload "$FIXED_DEV_TAG" "${assets[@]}" --repo "$GITHUB_REPOSITORY" --clobber
dev-linux:
needs: prepare
runs-on: ubuntu-latest
steps:
- name: Check out git repository
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Install Node.js
uses: actions/setup-node@v5
with:
node-version: 24
cache: "npm"
- name: Install Dependencies
run: npm install
- name: Set dev version
shell: bash
run: npm version "${{ needs.prepare.outputs.dev_version }}" --no-git-tag-version --allow-same-version
- name: Build Frontend & Type Check
run: |
npx tsc
npx vite build
- name: Package Linux dev artifacts
run: |
npx electron-builder --linux --publish never '--config.publish.channel=dev' '--config.artifactName=${productName}-dev-linux.${ext}'
- name: Upload Linux assets to fixed release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: |
mapfile -t assets < <(find release -maxdepth 1 -type f | sort)
if [ "${#assets[@]}" -eq 0 ]; then
echo "No release files found in ./release"
exit 1
fi
gh release upload "$FIXED_DEV_TAG" "${assets[@]}" --repo "$GITHUB_REPOSITORY" --clobber
dev-win-x64:
needs: prepare
runs-on: windows-latest
steps:
- name: Check out git repository
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Install Node.js
uses: actions/setup-node@v5
with:
node-version: 24
cache: "npm"
- name: Install Dependencies
run: npm install
- name: Set dev version
shell: bash
run: npm version "${{ needs.prepare.outputs.dev_version }}" --no-git-tag-version --allow-same-version
- name: Build Frontend & Type Check
run: |
npx tsc
npx vite build
- name: Package Windows x64 dev artifacts
run: |
npx electron-builder --win nsis --x64 --publish never '--config.publish.channel=dev' '--config.artifactName=${productName}-dev-x64-Setup.${ext}'
- name: Upload Windows x64 assets to fixed release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: |
mapfile -t assets < <(find release -maxdepth 1 -type f | sort)
if [ "${#assets[@]}" -eq 0 ]; then
echo "No release files found in ./release"
exit 1
fi
gh release upload "$FIXED_DEV_TAG" "${assets[@]}" --repo "$GITHUB_REPOSITORY" --clobber
dev-win-arm64:
needs: prepare
runs-on: windows-latest
steps:
- name: Check out git repository
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Install Node.js
uses: actions/setup-node@v5
with:
node-version: 24
cache: "npm"
- name: Install Dependencies
run: npm install
- name: Set dev version
shell: bash
run: npm version "${{ needs.prepare.outputs.dev_version }}" --no-git-tag-version --allow-same-version
- name: Build Frontend & Type Check
run: |
npx tsc
npx vite build
- name: Package Windows arm64 dev artifacts
run: |
npx electron-builder --win nsis --arm64 --publish never '--config.publish.channel=dev-arm64' '--config.artifactName=${productName}-dev-arm64-Setup.${ext}'
- name: Upload Windows arm64 assets to fixed release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: |
mapfile -t assets < <(find release -maxdepth 1 -type f | sort)
if [ "${#assets[@]}" -eq 0 ]; then
echo "No release files found in ./release"
exit 1
fi
gh release upload "$FIXED_DEV_TAG" "${assets[@]}" --repo "$GITHUB_REPOSITORY" --clobber
update-dev-release-notes:
needs:
- prepare
- dev-mac-arm64
- dev-linux
- dev-win-x64
- dev-win-arm64
if: always() && needs.prepare.result == 'success'
runs-on: ubuntu-latest
steps:
- name: Update fixed dev release notes
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
FIXED_DEV_TAG: ${{ env.FIXED_DEV_TAG }}
shell: bash
run: |
set -euo pipefail
TAG="$FIXED_DEV_TAG"
REPO="$GITHUB_REPOSITORY"
RELEASE_PAGE="https://github.com/$REPO/releases/tag/$TAG"
if ! gh release view "$TAG" --repo "$REPO" >/dev/null 2>&1; then
echo "Release $TAG not found, skip notes update."
exit 0
fi
ASSETS_JSON="$(gh release view "$TAG" --repo "$REPO" --json assets)"
pick_asset() {
local pattern="$1"
echo "$ASSETS_JSON" | jq -r --arg p "$pattern" '[.assets[].name | select(test($p))][0] // ""'
}
WINDOWS_ASSET="$(pick_asset "dev-x64-Setup[.]exe$")"
WINDOWS_ARM64_ASSET="$(pick_asset "dev-arm64-Setup[.]exe$")"
MAC_ASSET="$(pick_asset "dev-arm64[.]dmg$")"
LINUX_TAR_ASSET="$(pick_asset "dev-linux[.]tar[.]gz$")"
LINUX_APPIMAGE_ASSET="$(pick_asset "dev-linux[.]AppImage$")"
build_link() {
local name="$1"
if [ -n "$name" ]; then
echo "https://github.com/$REPO/releases/download/$TAG/$name"
fi
}
WINDOWS_URL="$(build_link "$WINDOWS_ASSET")"
WINDOWS_ARM64_URL="$(build_link "$WINDOWS_ARM64_ASSET")"
MAC_URL="$(build_link "$MAC_ASSET")"
LINUX_TAR_URL="$(build_link "$LINUX_TAR_ASSET")"
LINUX_APPIMAGE_URL="$(build_link "$LINUX_APPIMAGE_ASSET")"
cat > dev_release_notes.md <<EOF
## Daily Dev Build
- 该发布页为 **开发版**。
- 当前构建版本:\`${{ needs.prepare.outputs.dev_version }}\`
- 此版本为每日构建的开发版,包含最新的功能和修复,但可能不够稳定,仅推荐用于测试和验证。
## 下载
- Windows x64: ${WINDOWS_URL:-$RELEASE_PAGE}
- Windows arm64: ${WINDOWS_ARM64_URL:-$RELEASE_PAGE}
- macOSApple Silicon: ${MAC_URL:-$RELEASE_PAGE}
- Linux (.tar.gz): ${LINUX_TAR_URL:-$RELEASE_PAGE}
- Linux (.AppImage): ${LINUX_APPIMAGE_URL:-$RELEASE_PAGE}
## macOS 安装提示
- 如果被系统提示已损坏,你需要在终端执行以下命令去除隔离标记:
- \`xattr -dr com.apple.quarantine "/Applications/WeFlow.app"\`
- 执行后重新打开 WeFlow。
## 说明
- 该发布页的同名资源会被后续构建覆盖,请勿将其视作长期归档版本。
- 如某个平台资源暂未生成,请进入发布页查看最新状态:$RELEASE_PAGE
EOF
gh release edit "$TAG" --repo "$REPO" --title "Daily Dev Build" --notes-file dev_release_notes.md

View File

@@ -0,0 +1,278 @@
name: Preview Nightly
on:
schedule:
# GitHub Actions schedule uses UTC. 16:00 UTC = 北京时间次日 00:00
- cron: "0 16 * * *"
workflow_dispatch:
permissions:
contents: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
ELECTRON_BUILDER_BINARIES_MIRROR: https://github.com/electron-userland/electron-builder-binaries/releases/download/
jobs:
prepare:
runs-on: ubuntu-latest
outputs:
should_build: ${{ steps.meta.outputs.should_build }}
preview_version: ${{ steps.meta.outputs.preview_version }}
steps:
- name: Check out git repository
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Install Node.js
uses: actions/setup-node@v5
with:
node-version: 24
cache: "npm"
- name: Decide whether to build and generate preview version
id: meta
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: |
set -euo pipefail
git fetch origin main --depth=1
COMMITS_24H="$(git rev-list --count --since='24 hours ago' origin/main)"
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
SHOULD_BUILD=true
elif [ "$COMMITS_24H" -gt 0 ]; then
SHOULD_BUILD=true
else
SHOULD_BUILD=false
fi
BASE_VERSION="$(node -p "require('./package.json').version.split('-')[0]")"
YEAR_2="$(TZ=Asia/Shanghai date +%y)"
EXISTING_COUNT="$(gh api --paginate "repos/${GITHUB_REPOSITORY}/releases" --jq "[.[].tag_name | select(test(\"^v${BASE_VERSION}-preview[.]${YEAR_2}[.][0-9]+$\"))] | length")"
NEXT_COUNT=$((EXISTING_COUNT + 1))
PREVIEW_VERSION="${BASE_VERSION}-preview.${YEAR_2}.${NEXT_COUNT}"
echo "should_build=$SHOULD_BUILD" >> "$GITHUB_OUTPUT"
echo "preview_version=$PREVIEW_VERSION" >> "$GITHUB_OUTPUT"
echo "Preview version: $PREVIEW_VERSION (commits in last 24h on main: $COMMITS_24H)"
preview-mac-arm64:
needs: prepare
if: needs.prepare.outputs.should_build == 'true'
runs-on: macos-14
steps:
- name: Check out git repository
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Install Node.js
uses: actions/setup-node@v5
with:
node-version: 24
cache: "npm"
- name: Install Dependencies
run: npm install
- name: Set preview version
shell: bash
run: npm version "${{ needs.prepare.outputs.preview_version }}" --no-git-tag-version --allow-same-version
- name: Build Frontend & Type Check
run: |
npx tsc
npx vite build
- name: Package and Publish macOS arm64 preview
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CSC_IDENTITY_AUTO_DISCOVERY: "false"
shell: bash
run: |
export ELECTRON_BUILDER_BINARIES_MIRROR="https://github.com/electron-userland/electron-builder-binaries/releases/download/"
echo "Using ELECTRON_BUILDER_BINARIES_MIRROR=$ELECTRON_BUILDER_BINARIES_MIRROR"
npx electron-builder --mac dmg --arm64 --publish always '--config.publish.releaseType=prerelease' '--config.publish.channel=preview'
preview-linux:
needs: prepare
if: needs.prepare.outputs.should_build == 'true'
runs-on: ubuntu-latest
steps:
- name: Check out git repository
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Install Node.js
uses: actions/setup-node@v5
with:
node-version: 24
cache: "npm"
- name: Install Dependencies
run: npm install
- name: Set preview version
shell: bash
run: npm version "${{ needs.prepare.outputs.preview_version }}" --no-git-tag-version --allow-same-version
- name: Build Frontend & Type Check
run: |
npx tsc
npx vite build
- name: Package and Publish Linux preview
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
npx electron-builder --linux --publish always '--config.publish.releaseType=prerelease' '--config.publish.channel=preview'
preview-win-x64:
needs: prepare
if: needs.prepare.outputs.should_build == 'true'
runs-on: windows-latest
steps:
- name: Check out git repository
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Install Node.js
uses: actions/setup-node@v5
with:
node-version: 24
cache: "npm"
- name: Install Dependencies
run: npm install
- name: Set preview version
shell: bash
run: npm version "${{ needs.prepare.outputs.preview_version }}" --no-git-tag-version --allow-same-version
- name: Build Frontend & Type Check
run: |
npx tsc
npx vite build
- name: Package and Publish Windows x64 preview
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
npx electron-builder --win nsis --x64 --publish always '--config.publish.releaseType=prerelease' '--config.publish.channel=preview' '--config.artifactName=${productName}-${version}-x64-Setup.${ext}'
preview-win-arm64:
needs: prepare
if: needs.prepare.outputs.should_build == 'true'
runs-on: windows-latest
steps:
- name: Check out git repository
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Install Node.js
uses: actions/setup-node@v5
with:
node-version: 24
cache: "npm"
- name: Install Dependencies
run: npm install
- name: Set preview version
shell: bash
run: npm version "${{ needs.prepare.outputs.preview_version }}" --no-git-tag-version --allow-same-version
- name: Build Frontend & Type Check
run: |
npx tsc
npx vite build
- name: Package and Publish Windows arm64 preview
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
npx electron-builder --win nsis --arm64 --publish always '--config.publish.releaseType=prerelease' '--config.publish.channel=preview-arm64' '--config.artifactName=${productName}-${version}-arm64-Setup.${ext}'
update-preview-release-notes:
needs:
- prepare
- preview-mac-arm64
- preview-linux
- preview-win-x64
- preview-win-arm64
if: needs.prepare.outputs.should_build == 'true' && always()
runs-on: ubuntu-latest
steps:
- name: Update preview release notes
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: |
set -euo pipefail
TAG="v${{ needs.prepare.outputs.preview_version }}"
REPO="$GITHUB_REPOSITORY"
RELEASE_PAGE="https://github.com/$REPO/releases/tag/$TAG"
if ! gh release view "$TAG" --repo "$REPO" >/dev/null 2>&1; then
echo "Release $TAG not found (possibly all publish jobs failed), skip notes update."
exit 0
fi
ASSETS_JSON="$(gh release view "$TAG" --repo "$REPO" --json assets)"
pick_asset() {
local pattern="$1"
echo "$ASSETS_JSON" | jq -r --arg p "$pattern" '[.assets[].name | select(test($p))][0] // ""'
}
WINDOWS_ASSET="$(pick_asset "x64.*[.]exe$")"
if [ -z "$WINDOWS_ASSET" ]; then
WINDOWS_ASSET="$(echo "$ASSETS_JSON" | jq -r '[.assets[].name | select(test("[.]exe$")) | select(test("arm64") | not)][0] // ""')"
fi
WINDOWS_ARM64_ASSET="$(pick_asset "arm64.*[.]exe$")"
MAC_ASSET="$(pick_asset "[.]dmg$")"
LINUX_TAR_ASSET="$(pick_asset "[.]tar[.]gz$")"
LINUX_APPIMAGE_ASSET="$(pick_asset "[.]AppImage$")"
build_link() {
local name="$1"
if [ -n "$name" ]; then
echo "https://github.com/$REPO/releases/download/$TAG/$name"
fi
}
WINDOWS_URL="$(build_link "$WINDOWS_ASSET")"
WINDOWS_ARM64_URL="$(build_link "$WINDOWS_ARM64_ASSET")"
MAC_URL="$(build_link "$MAC_ASSET")"
LINUX_TAR_URL="$(build_link "$LINUX_TAR_ASSET")"
LINUX_APPIMAGE_URL="$(build_link "$LINUX_APPIMAGE_ASSET")"
cat > preview_release_notes.md <<EOF
## Preview Nightly 说明
- 该版本为 **预览版**,用于提前体验即将发布的功能与修复。
- 可能包含尚未完全稳定的改动,不建议长期使用
## 下载
- Windows x64: ${WINDOWS_URL:-$RELEASE_PAGE}
- Windows arm64: ${WINDOWS_ARM64_URL:-$RELEASE_PAGE}
- macOSApple Silicon: ${MAC_URL:-$RELEASE_PAGE}
- Linux (.tar.gz): ${LINUX_TAR_URL:-$RELEASE_PAGE}
- Linux (.AppImage): ${LINUX_APPIMAGE_URL:-$RELEASE_PAGE}
## macOS 安装提示
- 如果被系统提示已损坏,你需要在终端执行以下命令去除隔离标记:
- \`xattr -dr com.apple.quarantine "/Applications/WeFlow.app"\`
- 执行后重新打开 WeFlow。
> 如某个平台链接暂未生成,请前往发布页查看最新资源:$RELEASE_PAGE
EOF
gh release edit "$TAG" --repo "$REPO" --notes-file preview_release_notes.md

View File

@@ -10,29 +10,11 @@ permissions:
env: env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
ELECTRON_BUILDER_BINARIES_MIRROR: https://github.com/electron-userland/electron-builder-binaries/releases/download/
jobs: jobs:
prepare-release:
runs-on: ubuntu-latest
steps:
- name: Mark release as pre-release (building)
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: |
set -euo pipefail
TAG="$GITHUB_REF_NAME"
REPO="$GITHUB_REPOSITORY"
# Create or update the release as a pre-release with a placeholder note
if gh release view "$TAG" --repo "$REPO" > /dev/null 2>&1; then
gh release edit "$TAG" --repo "$REPO" --prerelease --notes $'## ⚠️ 正在自动构建中,请勿下载\n\n各平台安装包正在构建完成后将自动更新本页面并正式发布。\n\n**请勿在此期间下载任何文件。**'
else
gh release create "$TAG" --repo "$REPO" --prerelease --title "$TAG" --notes $'## ⚠️ 正在自动构建中,请勿下载\n\n各平台安装包正在构建完成后将自动更新本页面并正式发布。\n\n**请勿在此期间下载任何文件。**'
fi
release-mac-arm64: release-mac-arm64:
runs-on: macos-14 runs-on: macos-14
needs: prepare-release
steps: steps:
- name: Check out git repository - name: Check out git repository
@@ -61,16 +43,34 @@ jobs:
npx tsc npx tsc
npx vite build npx vite build
- name: Package and Publish macOS arm64 (unsigned DMG + ZIP) - name: Package and Publish macOS arm64 (unsigned DMG)
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CSC_IDENTITY_AUTO_DISCOVERY: "false" CSC_IDENTITY_AUTO_DISCOVERY: "false"
shell: bash
run: | run: |
npx electron-builder --mac --arm64 --publish always export ELECTRON_BUILDER_BINARIES_MIRROR="https://github.com/electron-userland/electron-builder-binaries/releases/download/"
echo "Using ELECTRON_BUILDER_BINARIES_MIRROR=$ELECTRON_BUILDER_BINARIES_MIRROR"
npx electron-builder --mac dmg --arm64 --publish always
- name: Inject minimumVersion into latest yml
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: |
TAG=${GITHUB_REF_NAME}
REPO=${{ github.repository }}
MINIMUM_VERSION="4.1.7"
for YML_FILE in latest-mac.yml latest-arm64-mac.yml; do
gh release download "$TAG" --repo "$REPO" --pattern "$YML_FILE" --output "/tmp/$YML_FILE" 2>/dev/null || continue
if ! grep -q 'minimumVersion' "/tmp/$YML_FILE"; then
echo "minimumVersion: $MINIMUM_VERSION" >> "/tmp/$YML_FILE"
fi
gh release upload "$TAG" --repo "$REPO" "/tmp/$YML_FILE" --clobber
done
release-linux: release-linux:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: prepare-release
steps: steps:
- name: Check out git repository - name: Check out git repository
@@ -105,9 +105,22 @@ jobs:
run: | run: |
npx electron-builder --linux --publish always npx electron-builder --linux --publish always
- name: Inject minimumVersion into latest yml
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: |
TAG=${GITHUB_REF_NAME}
REPO=${{ github.repository }}
MINIMUM_VERSION="4.1.7"
gh release download "$TAG" --repo "$REPO" --pattern "latest-linux.yml" --output "/tmp/latest-linux.yml" 2>/dev/null
if [ -f /tmp/latest-linux.yml ] && ! grep -q 'minimumVersion' /tmp/latest-linux.yml; then
echo "minimumVersion: $MINIMUM_VERSION" >> /tmp/latest-linux.yml
gh release upload "$TAG" --repo "$REPO" /tmp/latest-linux.yml --clobber
fi
release: release:
runs-on: windows-latest runs-on: windows-latest
needs: prepare-release
steps: steps:
- name: Check out git repository - name: Check out git repository
@@ -137,15 +150,27 @@ jobs:
npx vite build npx vite build
- name: Package and Publish - name: Package and Publish
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
npx electron-builder --win nsis --x64 --publish always '--config.artifactName=${productName}-${version}-x64-Setup.${ext}'
- name: Inject minimumVersion into latest yml
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash shell: bash
run: | run: |
npx electron-builder --win nsis --x64 --publish always "-c.artifactName=\${productName}-\${version}-x64-Setup.\${ext}" TAG=${GITHUB_REF_NAME}
REPO=${{ github.repository }}
MINIMUM_VERSION="4.1.7"
gh release download "$TAG" --repo "$REPO" --pattern "latest.yml" --output "/tmp/latest.yml" 2>/dev/null
if [ -f /tmp/latest.yml ] && ! grep -q 'minimumVersion' /tmp/latest.yml; then
echo "minimumVersion: $MINIMUM_VERSION" >> /tmp/latest.yml
gh release upload "$TAG" --repo "$REPO" /tmp/latest.yml --clobber
fi
release-windows-arm64: release-windows-arm64:
runs-on: windows-latest runs-on: windows-latest
needs: prepare-release
steps: steps:
- name: Check out git repository - name: Check out git repository
@@ -175,11 +200,24 @@ jobs:
npx vite build npx vite build
- name: Package and Publish Windows arm64 - name: Package and Publish Windows arm64
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
npx electron-builder --win nsis --arm64 --publish always '--config.publish.channel=latest-arm64' '--config.artifactName=${productName}-${version}-arm64-Setup.${ext}'
- name: Inject minimumVersion into latest yml
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash shell: bash
run: | run: |
npx electron-builder --win nsis --arm64 --publish always -c.publish.channel=latest-arm64 "-c.artifactName=\${productName}-\${version}-arm64-Setup.\${ext}" TAG=${GITHUB_REF_NAME}
REPO=${{ github.repository }}
MINIMUM_VERSION="4.1.7"
gh release download "$TAG" --repo "$REPO" --pattern "latest-arm64.yml" --output "/tmp/latest-arm64.yml" 2>/dev/null
if [ -f /tmp/latest-arm64.yml ] && ! grep -q 'minimumVersion' /tmp/latest-arm64.yml; then
echo "minimumVersion: $MINIMUM_VERSION" >> /tmp/latest-arm64.yml
gh release upload "$TAG" --repo "$REPO" /tmp/latest-arm64.yml --clobber
fi
update-release-notes: update-release-notes:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -190,53 +228,6 @@ jobs:
- release-windows-arm64 - release-windows-arm64
steps: steps:
- name: Fix latest.yml to point to x64 installer
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: |
set -euo pipefail
TAG="$GITHUB_REF_NAME"
VERSION="${TAG#v}"
REPO="$GITHUB_REPOSITORY"
# Find the x64 exe asset name
ASSETS_JSON="$(gh release view "$TAG" --repo "$REPO" --json assets)"
X64_ASSET="$(echo "$ASSETS_JSON" | jq -r '[.assets[].name | select(test("x64.*\\.exe$"))][0] // ""')"
if [ -z "$X64_ASSET" ]; then
X64_ASSET="$(echo "$ASSETS_JSON" | jq -r '[.assets[].name | select(test("\\.exe$")) | select(test("arm64") | not)][0] // ""')"
fi
if [ -z "$X64_ASSET" ]; then
echo "ERROR: Could not find x64 exe asset"
exit 1
fi
echo "Downloading x64 installer: $X64_ASSET"
gh release download "$TAG" --repo "$REPO" --pattern "$X64_ASSET" --dir /tmp/weflow-x64
SHA512_B64="$(sha512sum "/tmp/weflow-x64/$X64_ASSET" | awk '{print $1}' | xxd -r -p | base64 -w 0)"
SIZE="$(stat -c%s "/tmp/weflow-x64/$X64_ASSET")"
RELEASE_DATE="$(gh release view "$TAG" --repo "$REPO" --json publishedAt -q .publishedAt)"
cat > /tmp/latest.yml <<YMLEOF
version: $VERSION
files:
- url: $X64_ASSET
sha512: $SHA512_B64
size: $SIZE
path: $X64_ASSET
sha512: $SHA512_B64
releaseDate: '$RELEASE_DATE'
YMLEOF
# Strip leading spaces (heredoc indentation)
sed -i 's/^ //' /tmp/latest.yml
cat /tmp/latest.yml
gh release upload "$TAG" --repo "$REPO" /tmp/latest.yml --clobber
echo "latest.yml updated successfully to point to $X64_ASSET"
- name: Generate release notes with platform download links - name: Generate release notes with platform download links
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -289,23 +280,14 @@ jobs:
- Windows arm64: ${WINDOWS_ARM64_URL:-$RELEASE_PAGE} - Windows arm64: ${WINDOWS_ARM64_URL:-$RELEASE_PAGE}
- macOSM系列芯片: ${MAC_URL:-$RELEASE_PAGE} - macOSM系列芯片: ${MAC_URL:-$RELEASE_PAGE}
- Linux (.tar.gz): ${LINUX_TAR_URL:-$RELEASE_PAGE} - Linux (.tar.gz): ${LINUX_TAR_URL:-$RELEASE_PAGE}
- linux (.AppImage): ${LINUX_APPIMAGE_URL:-$RELEASE_PAGE} - Linux (.AppImage): ${LINUX_APPIMAGE_URL:-$RELEASE_PAGE}
## macOS 安装提示(未知来源) ## macOS 安装提示
- 若打开时提示“来自未知开发者”或“无法验证开发者”,请到「系统设置 -> 隐私与安全性」中允许打开该应用。 - 如果被系统提示已损坏,你需要在终端执行以下命令去除隔离标记:
- 如果仍被系统拦截,请在终端执行以下命令去除隔离标记: - \`xattr -dr com.apple.quarantine "/Applications/WeFlow.app"\`
- xattr -rd com.apple.quarantine /Applications/WeFlow.app
- 执行后重新打开 WeFlow。 - 执行后重新打开 WeFlow。
> 如果某个平台链接暂时未生成,可进入完整发布页查看全部资源:$RELEASE_PAGE > 如果某个平台链接暂时未生成,可进入完整发布页查看全部资源:$RELEASE_PAGE
EOF EOF
gh release edit "$TAG" --repo "$REPO" --notes-file release_notes.md gh release edit "$TAG" --repo "$REPO" --notes-file release_notes.md
- name: Mark release as published (no longer pre-release)
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: |
set -euo pipefail
gh release edit "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" --latest --draft=false --prerelease=false

87
.github/workflows/security-scan.yml vendored Normal file
View File

@@ -0,0 +1,87 @@
name: Security Scan
on:
schedule:
- cron: '0 2 * * *' # 每天 UTC 02:00
workflow_dispatch: # 手动触发
pull_request: # PR 时触发
branches: [ main, dev ]
permissions:
contents: read
security-events: write
actions: read
jobs:
security-scan:
name: Security Scan (${{ matrix.branch }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
branch:
- main
steps:
- name: Checkout ${{ matrix.branch }}
uses: actions/checkout@v4
with:
ref: ${{ matrix.branch }}
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm' # 使用 npm 缓存加速
- name: Install dependencies
run: npm ci --ignore-scripts
# 1. npm audit - 检查依赖漏洞
- name: Dependency vulnerability audit
run: npm audit --audit-level=moderate
continue-on-error: true
# 2. CodeQL 静态分析
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: javascript, typescript
queries: security-and-quality
- name: Autobuild
uses: github/codeql-action/autobuild@v3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: '/language:javascript-typescript/branch:${{ matrix.branch }}'
# 3. 密钥/敏感信息扫描
- name: Secret scanning with Gitleaks
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
continue-on-error: true
# 动态获取所有分支并扫描
scan-all-branches:
name: Scan additional branches
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Run npm audit on all branches
run: |
git branch -r | grep -v HEAD | sed 's|origin/||' | tr -d ' ' | while read branch; do
echo "===== Auditing branch: $branch ====="
git checkout "$branch" 2>/dev/null || continue
# 尝试安装并审计
npm ci --ignore-scripts --silent 2>/dev/null || npm install --ignore-scripts --silent 2>/dev/null || true
npm audit --audit-level=moderate 2>/dev/null || true
done
continue-on-error: true

3
.gitignore vendored
View File

@@ -70,4 +70,5 @@ resources/wx_send
概述.md 概述.md
pnpm-lock.yaml pnpm-lock.yaml
/pnpm-workspace.yaml /pnpm-workspace.yaml
wechat-research-site wechat-research-site
.codex

23
.gitleaks.toml Normal file
View File

@@ -0,0 +1,23 @@
title = "Gitleaks Config"
[extend]
# 继承默认规则
useDefault = true
# 排除误报路径
[[rules]]
id = "curl-auth-header"
[rules.allowlist]
paths = [
'''docs/HTTP-API\.md'''
]
regexes = [
'''YOUR_TOKEN'''
]
[[rules]]
id = "generic-api-key"
[rules.allowlist]
paths = [
'''src/pages/ChatPage\.tsx'''
]

4
.npmrc
View File

@@ -1,3 +1 @@
registry=https://registry.npmmirror.com registry=https://registry.npmjs.org
electron-mirror=https://npmmirror.com/mirrors/electron/
electron-builder-binaries-mirror=https://npmmirror.com/mirrors/electron-builder-binaries/

View File

@@ -19,7 +19,9 @@ WeFlow 是一个**完全本地**的微信**实时**聊天记录查看、分析
</a> </a>
<a href="https://github.com/hicccc77/WeFlow/issues"> <a href="https://github.com/hicccc77/WeFlow/issues">
<img src="https://img.shields.io/github/issues/hicccc77/WeFlow?style=flat-square" alt="Issues"> <img src="https://img.shields.io/github/issues/hicccc77/WeFlow?style=flat-square" alt="Issues">
<img src="https://gh-down-badges.linkof.link/hicccc77/WeFlow/" alt="Downloads" /> </a>
<a href="https://github.com/hicccc77/WeFlow/releases">
<img src="https://img.shields.io/github/downloads/hicccc77/WeFlow/total?style=flat-square" alt="Downloads" />
</a> </a>
<a href="https://t.me/weflow_cc"> <a href="https://t.me/weflow_cc">
<img src="https://img.shields.io/badge/Telegram%20频道-0088cc?style=flat-square&logo=telegram&logoColor=0088cc&labelColor=white" alt="Telegram"> <img src="https://img.shields.io/badge/Telegram%20频道-0088cc?style=flat-square&logo=telegram&logoColor=0088cc&labelColor=white" alt="Telegram">

View File

@@ -36,10 +36,45 @@ import { messagePushService } from './services/messagePushService'
autoUpdater.autoDownload = false autoUpdater.autoDownload = false
autoUpdater.autoInstallOnAppQuit = true autoUpdater.autoInstallOnAppQuit = true
autoUpdater.disableDifferentialDownload = true // 禁用差分更新,强制全量下载 autoUpdater.disableDifferentialDownload = true // 禁用差分更新,强制全量下载
// Windows x64 与 arm64 使用不同更新通道,避免 latest.yml 互相覆盖导致下错架构安装包。 // 更新通道策略:
if (process.platform === 'win32' && process.arch === 'arm64') { // - 稳定版(如 4.3.0)默认走 latest
autoUpdater.channel = 'latest-arm64' // - 预览版(如 4.3.0-preview.26.1)默认走 preview
// - 开发版(如 4.3.0-dev.26.3.4)默认走 dev
// - 用户可在设置页切换稳定/预览/开发,切换后即时生效
// 同时区分 Windows x64 / arm64避免更新清单互相覆盖。
const appVersion = app.getVersion()
const defaultUpdateTrack: 'stable' | 'preview' | 'dev' = (() => {
if (/-preview\.\d+\.\d+$/i.test(appVersion)) return 'preview'
if (/-dev\.\d+\.\d+\.\d+$/i.test(appVersion)) return 'dev'
if (/(alpha|beta|rc)/i.test(appVersion)) return 'dev'
return 'stable'
})()
const isPrereleaseBuild = defaultUpdateTrack !== 'stable'
let configService: ConfigService | null = null
const normalizeUpdateTrack = (raw: unknown): 'stable' | 'preview' | 'dev' | null => {
if (raw === 'stable' || raw === 'preview' || raw === 'dev') return raw
return null
} }
const getEffectiveUpdateTrack = (): 'stable' | 'preview' | 'dev' => {
const configuredTrack = normalizeUpdateTrack(configService?.get('updateChannel'))
return configuredTrack || defaultUpdateTrack
}
const applyAutoUpdateChannel = (reason: 'startup' | 'settings' = 'startup') => {
const track = getEffectiveUpdateTrack()
const baseUpdateChannel = track === 'stable' ? 'latest' : track
autoUpdater.allowPrerelease = track !== 'stable'
autoUpdater.allowDowngrade = isPrereleaseBuild && track === 'stable'
autoUpdater.channel =
process.platform === 'win32' && process.arch === 'arm64'
? `${baseUpdateChannel}-arm64`
: baseUpdateChannel
console.log(`[Update](${reason}) 当前版本 ${appVersion},渠道偏好: ${track},更新通道: ${autoUpdater.channel}`)
}
applyAutoUpdateChannel('startup')
const AUTO_UPDATE_ENABLED = const AUTO_UPDATE_ENABLED =
process.env.AUTO_UPDATE_ENABLED === 'true' || process.env.AUTO_UPDATE_ENABLED === 'true' ||
process.env.AUTO_UPDATE_ENABLED === '1' || process.env.AUTO_UPDATE_ENABLED === '1' ||
@@ -87,7 +122,6 @@ function sanitizePathEnv() {
sanitizePathEnv() sanitizePathEnv()
// 单例服务 // 单例服务
let configService: ConfigService | null = null
// 协议窗口实例 // 协议窗口实例
let agreementWindow: BrowserWindow | null = null let agreementWindow: BrowserWindow | null = null
@@ -243,6 +277,14 @@ const normalizeReleaseNotes = (rawReleaseNotes: unknown): string => {
return cleaned return cleaned
} }
const getDialogReleaseNotes = (rawReleaseNotes: unknown): string => {
const track = getEffectiveUpdateTrack()
if (track !== 'stable') {
return '修复了一些已知问题'
}
return normalizeReleaseNotes(rawReleaseNotes)
}
type AnnualReportYearsLoadStrategy = 'cache' | 'native' | 'hybrid' type AnnualReportYearsLoadStrategy = 'cache' | 'native' | 'hybrid'
type AnnualReportYearsLoadPhase = 'cache' | 'native' | 'scan' | 'done' type AnnualReportYearsLoadPhase = 'cache' | 'native' | 'scan' | 'done'
@@ -1117,6 +1159,9 @@ function registerIpcHandlers() {
ipcMain.handle('config:set', async (_, key: string, value: any) => { ipcMain.handle('config:set', async (_, key: string, value: any) => {
const result = configService?.set(key as any, value) const result = configService?.set(key as any, value)
if (key === 'updateChannel') {
applyAutoUpdateChannel('settings')
}
void messagePushService.handleConfigChanged(key) void messagePushService.handleConfigChanged(key)
return result return result
}) })
@@ -1242,7 +1287,8 @@ function registerIpcHandlers() {
return { return {
hasUpdate: true, hasUpdate: true,
version: latestVersion, version: latestVersion,
releaseNotes: normalizeReleaseNotes(result.updateInfo.releaseNotes) releaseNotes: getDialogReleaseNotes(result.updateInfo.releaseNotes),
minimumVersion: (result.updateInfo as any).minimumVersion
} }
} }
} }
@@ -2706,7 +2752,8 @@ function checkForUpdatesOnStartup() {
// 通知渲染进程有新版本 // 通知渲染进程有新版本
mainWindow.webContents.send('app:updateAvailable', { mainWindow.webContents.send('app:updateAvailable', {
version: latestVersion, version: latestVersion,
releaseNotes: normalizeReleaseNotes(result.updateInfo.releaseNotes) releaseNotes: getDialogReleaseNotes(result.updateInfo.releaseNotes),
minimumVersion: (result.updateInfo as any).minimumVersion
}) })
} }
} }
@@ -2739,6 +2786,7 @@ app.whenReady().then(async () => {
// 初始化配置服务 // 初始化配置服务
updateSplashProgress(5, '正在加载配置...') updateSplashProgress(5, '正在加载配置...')
configService = new ConfigService() configService = new ConfigService()
applyAutoUpdateChannel('startup')
// 将用户主题配置推送给 Splash 窗口 // 将用户主题配置推送给 Splash 窗口
if (splashWindow && !splashWindow.isDestroyed()) { if (splashWindow && !splashWindow.isDestroyed()) {

View File

@@ -1135,7 +1135,7 @@ class AnnualReportService {
const now = Date.now() const now = Date.now()
if (now - lastProgressAt > 200) { if (now - lastProgressAt > 200) {
let progress = 30 let progress: number
if (totalMessagesForProgress > 0) { if (totalMessagesForProgress > 0) {
const ratio = Math.min(1, processedMessages / totalMessagesForProgress) const ratio = Math.min(1, processedMessages / totalMessagesForProgress)
progress = 30 + Math.floor(ratio * 50) progress = 30 + Math.floor(ratio * 50)

View File

@@ -2009,7 +2009,7 @@ class ChatService {
selectableColumns = resolvedColumns selectableColumns = resolvedColumns
} }
if (!selectableColumns || selectableColumns.length === 0) return rows if (selectableColumns.length === 0) return rows
const selectColumns = ['username', ...selectableColumns] const selectColumns = ['username', ...selectableColumns]
const sql = `SELECT ${selectColumns.map((column) => this.quoteSqlIdentifier(column)).join(', ')} FROM contact WHERE username IS NOT NULL AND username != ''` const sql = `SELECT ${selectColumns.map((column) => this.quoteSqlIdentifier(column)).join(', ')} FROM contact WHERE username IS NOT NULL AND username != ''`

View File

@@ -15,15 +15,31 @@ class CloudControlService {
private timer: NodeJS.Timeout | null = null private timer: NodeJS.Timeout | null = null
private pages: Set<string> = new Set() private pages: Set<string> = new Set()
private platformVersionCache: string | null = null private platformVersionCache: string | null = null
private pendingReports: UsageStats[] = []
private flushInProgress = false
private retryDelayMs = 5_000
private consecutiveFailures = 0
private circuitOpenedAt = 0
private nextDelayOverrideMs: number | null = null
private initialized = false
private static readonly BASE_FLUSH_MS = 300_000
private static readonly JITTER_MS = 30_000
private static readonly MAX_BUFFER_REPORTS = 200
private static readonly MAX_BATCH_REPORTS = 20
private static readonly MAX_RETRY_MS = 120_000
private static readonly CIRCUIT_FAIL_THRESHOLD = 5
private static readonly CIRCUIT_COOLDOWN_MS = 120_000
async init() { async init() {
if (this.initialized) return
this.initialized = true
this.deviceId = this.getDeviceId() this.deviceId = this.getDeviceId()
await wcdbService.cloudInit(300) await wcdbService.cloudInit(300)
await this.reportOnline() this.enqueueCurrentReport()
await this.flushQueue(true)
this.timer = setInterval(() => { this.scheduleNextFlush(this.nextDelayOverrideMs ?? undefined)
this.reportOnline() this.nextDelayOverrideMs = null
}, 300000)
} }
private getDeviceId(): string { private getDeviceId(): string {
@@ -33,8 +49,8 @@ class CloudControlService {
return crypto.createHash('md5').update(machineId).digest('hex') return crypto.createHash('md5').update(machineId).digest('hex')
} }
private async reportOnline() { private buildCurrentReport(): UsageStats {
const data: UsageStats = { return {
appVersion: app.getVersion(), appVersion: app.getVersion(),
platform: this.getPlatformVersion(), platform: this.getPlatformVersion(),
deviceId: this.deviceId, deviceId: this.deviceId,
@@ -42,11 +58,69 @@ class CloudControlService {
online: true, online: true,
pages: Array.from(this.pages) pages: Array.from(this.pages)
} }
}
await wcdbService.cloudReport(JSON.stringify(data)) private enqueueCurrentReport() {
const report = this.buildCurrentReport()
this.pendingReports.push(report)
if (this.pendingReports.length > CloudControlService.MAX_BUFFER_REPORTS) {
this.pendingReports.splice(0, this.pendingReports.length - CloudControlService.MAX_BUFFER_REPORTS)
}
this.pages.clear() this.pages.clear()
} }
private isCircuitOpen(nowMs: number): boolean {
if (this.circuitOpenedAt <= 0) return false
return nowMs-this.circuitOpenedAt < CloudControlService.CIRCUIT_COOLDOWN_MS
}
private scheduleNextFlush(delayMs?: number) {
if (this.timer) {
clearTimeout(this.timer)
this.timer = null
}
const jitter = Math.floor(Math.random() * CloudControlService.JITTER_MS)
const nextDelay = Math.max(1_000, Number(delayMs) > 0 ? Number(delayMs) : CloudControlService.BASE_FLUSH_MS + jitter)
this.timer = setTimeout(() => {
this.enqueueCurrentReport()
this.flushQueue(false).finally(() => {
this.scheduleNextFlush(this.nextDelayOverrideMs ?? undefined)
this.nextDelayOverrideMs = null
})
}, nextDelay)
}
private async flushQueue(force: boolean) {
if (this.flushInProgress) return
if (this.pendingReports.length === 0) return
const now = Date.now()
if (!force && this.isCircuitOpen(now)) {
return
}
this.flushInProgress = true
try {
while (this.pendingReports.length > 0) {
const batch = this.pendingReports.slice(0, CloudControlService.MAX_BATCH_REPORTS)
const result = await wcdbService.cloudReport(JSON.stringify(batch))
if (!result || result.success !== true) {
this.consecutiveFailures += 1
this.retryDelayMs = Math.min(CloudControlService.MAX_RETRY_MS, this.retryDelayMs * 2)
if (this.consecutiveFailures >= CloudControlService.CIRCUIT_FAIL_THRESHOLD) {
this.circuitOpenedAt = Date.now()
}
this.nextDelayOverrideMs = this.retryDelayMs
return
}
this.pendingReports.splice(0, batch.length)
this.consecutiveFailures = 0
this.retryDelayMs = 5_000
this.circuitOpenedAt = 0
}
} finally {
this.flushInProgress = false
}
}
private getPlatformVersion(): string { private getPlatformVersion(): string {
if (this.platformVersionCache) { if (this.platformVersionCache) {
return this.platformVersionCache return this.platformVersionCache
@@ -146,9 +220,16 @@ class CloudControlService {
stop() { stop() {
if (this.timer) { if (this.timer) {
clearInterval(this.timer) clearTimeout(this.timer)
this.timer = null this.timer = null
} }
this.pendingReports = []
this.flushInProgress = false
this.retryDelayMs = 5_000
this.consecutiveFailures = 0
this.circuitOpenedAt = 0
this.nextDelayOverrideMs = null
this.initialized = false
wcdbService.cloudStop() wcdbService.cloudStop()
} }
@@ -158,4 +239,3 @@ class CloudControlService {
} }
export const cloudControlService = new CloudControlService() export const cloudControlService = new CloudControlService()

View File

@@ -45,6 +45,7 @@ interface ConfigSchema {
// 更新相关 // 更新相关
ignoredUpdateVersion: string ignoredUpdateVersion: string
updateChannel: 'auto' | 'stable' | 'preview' | 'dev'
// 通知 // 通知
notificationEnabled: boolean notificationEnabled: boolean
@@ -119,6 +120,7 @@ export class ConfigService {
authUseHello: false, authUseHello: false,
authHelloSecret: '', authHelloSecret: '',
ignoredUpdateVersion: '', ignoredUpdateVersion: '',
updateChannel: 'auto',
notificationEnabled: true, notificationEnabled: true,
notificationPosition: 'top-right', notificationPosition: 'top-right',
notificationFilterMode: 'all', notificationFilterMode: 'all',

View File

@@ -562,14 +562,14 @@ export class ImageDecryptService {
if (allowThumbnail || !isThumb) { if (allowThumbnail || !isThumb) {
this.logInfo('[ImageDecrypt] hardlink hit (datName)', { imageMd5: imageDatName, path: preferredPath }) this.logInfo('[ImageDecrypt] hardlink hit (datName)', { imageMd5: imageDatName, path: preferredPath })
this.cacheDatPath(accountDir, imageDatName, preferredPath) this.cacheDatPath(accountDir, imageDatName, preferredPath)
if (imageMd5) this.cacheDatPath(accountDir, imageMd5, preferredPath) this.cacheDatPath(accountDir, imageMd5, preferredPath)
return preferredPath return preferredPath
} }
// 找到缩略图但要求高清图,尝试同目录查找高清图变体 // 找到缩略图但要求高清图,尝试同目录查找高清图变体
const hdPath = this.findHdVariantInSameDir(preferredPath) const hdPath = this.findHdVariantInSameDir(preferredPath)
if (hdPath) { if (hdPath) {
this.cacheDatPath(accountDir, imageDatName, hdPath) this.cacheDatPath(accountDir, imageDatName, hdPath)
if (imageMd5) this.cacheDatPath(accountDir, imageMd5, hdPath) this.cacheDatPath(accountDir, imageMd5, hdPath)
return hdPath return hdPath
} }
return null return null

View File

@@ -2596,13 +2596,34 @@ export class WcdbCore {
} }
} }
/**
* 强制重新打开账号连接(绕过路径缓存),用于微信重装后消息数据库刷新失败时的自动恢复。
* 返回重新打开是否成功。
*/
private async forceReopen(): Promise<boolean> {
if (!this.currentPath || !this.currentKey || !this.currentWxid) return false
const path = this.currentPath
const key = this.currentKey
const wxid = this.currentWxid
this.writeLog('forceReopen: clearing cached handle and reopening...', true)
// 清空缓存状态,让 open() 真正重新打开
try { this.wcdbShutdown() } catch { }
this.handle = null
this.currentPath = null
this.currentKey = null
this.currentWxid = null
this.currentDbStoragePath = null
this.initialized = false
return this.open(path, key, wxid)
}
async openMessageCursor(sessionId: string, batchSize: number, ascending: boolean, beginTimestamp: number, endTimestamp: number): Promise<{ success: boolean; cursor?: number; error?: string }> { async openMessageCursor(sessionId: string, batchSize: number, ascending: boolean, beginTimestamp: number, endTimestamp: number): Promise<{ success: boolean; cursor?: number; error?: string }> {
if (!this.ensureReady()) { if (!this.ensureReady()) {
return { success: false, error: 'WCDB 未连接' } return { success: false, error: 'WCDB 未连接' }
} }
try { try {
const outCursor = [0] const outCursor = [0]
const result = this.wcdbOpenMessageCursor( let result = this.wcdbOpenMessageCursor(
this.handle, this.handle,
sessionId, sessionId,
batchSize, batchSize,
@@ -2611,13 +2632,37 @@ export class WcdbCore {
endTimestamp, endTimestamp,
outCursor outCursor
) )
// result=-3 表示 WCDB_STATUS_NO_MESSAGE_DB消息数据库缓存为空常见于微信重装后
// 自动强制重连并重试一次
if (result === -3 && outCursor[0] <= 0) {
this.writeLog('openMessageCursor: result=-3 (no message db), attempting forceReopen...', true)
const reopened = await this.forceReopen()
if (reopened && this.handle !== null) {
outCursor[0] = 0
result = this.wcdbOpenMessageCursor(
this.handle,
sessionId,
batchSize,
ascending ? 1 : 0,
beginTimestamp,
endTimestamp,
outCursor
)
this.writeLog(`openMessageCursor retry after forceReopen: result=${result} cursor=${outCursor[0]}`, true)
} else {
this.writeLog('openMessageCursor forceReopen failed, giving up', true)
}
}
if (result !== 0 || outCursor[0] <= 0) { if (result !== 0 || outCursor[0] <= 0) {
await this.printLogs(true) await this.printLogs(true)
this.writeLog( this.writeLog(
`openMessageCursor failed: sessionId=${sessionId} batchSize=${batchSize} ascending=${ascending ? 1 : 0} begin=${beginTimestamp} end=${endTimestamp} result=${result} cursor=${outCursor[0]}`, `openMessageCursor failed: sessionId=${sessionId} batchSize=${batchSize} ascending=${ascending ? 1 : 0} begin=${beginTimestamp} end=${endTimestamp} result=${result} cursor=${outCursor[0]}`,
true true
) )
return { success: false, error: `创建游标失败: ${result},请查看日志` } const hint = result === -3
? `创建游标失败: ${result}(消息数据库未找到)。如果你最近重装过微信,请尝试重新指定数据目录后重试`
: `创建游标失败: ${result},请查看日志`
return { success: false, error: hint }
} }
return { success: true, cursor: outCursor[0] } return { success: true, cursor: outCursor[0] }
} catch (e) { } catch (e) {
@@ -2636,7 +2681,7 @@ export class WcdbCore {
} }
try { try {
const outCursor = [0] const outCursor = [0]
const result = this.wcdbOpenMessageCursorLite( let result = this.wcdbOpenMessageCursorLite(
this.handle, this.handle,
sessionId, sessionId,
batchSize, batchSize,
@@ -2645,6 +2690,29 @@ export class WcdbCore {
endTimestamp, endTimestamp,
outCursor outCursor
) )
// result=-3 表示 WCDB_STATUS_NO_MESSAGE_DB消息数据库缓存为空
// 自动强制重连并重试一次
if (result === -3 && outCursor[0] <= 0) {
this.writeLog('openMessageCursorLite: result=-3 (no message db), attempting forceReopen...', true)
const reopened = await this.forceReopen()
if (reopened && this.handle !== null) {
outCursor[0] = 0
result = this.wcdbOpenMessageCursorLite(
this.handle,
sessionId,
batchSize,
ascending ? 1 : 0,
beginTimestamp,
endTimestamp,
outCursor
)
this.writeLog(`openMessageCursorLite retry after forceReopen: result=${result} cursor=${outCursor[0]}`, true)
} else {
this.writeLog('openMessageCursorLite forceReopen failed, giving up', true)
}
}
if (result !== 0 || outCursor[0] <= 0) { if (result !== 0 || outCursor[0] <= 0) {
await this.printLogs(true) await this.printLogs(true)
this.writeLog( this.writeLog(

4071
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "weflow", "name": "weflow",
"version": "4.2.0", "version": "4.3.0",
"description": "WeFlow", "description": "WeFlow",
"main": "dist-electron/main.js", "main": "dist-electron/main.js",
"author": { "author": {
@@ -23,7 +23,7 @@
"electron:build": "npm run build" "electron:build": "npm run build"
}, },
"dependencies": { "dependencies": {
"echarts": "^5.5.1", "echarts": "^6.0.0",
"echarts-for-react": "^3.0.2", "echarts-for-react": "^3.0.2",
"electron-store": "^10.0.0", "electron-store": "^10.0.0",
"electron-updater": "^6.3.9", "electron-updater": "^6.3.9",
@@ -34,11 +34,11 @@
"jieba-wasm": "^2.2.0", "jieba-wasm": "^2.2.0",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"koffi": "^2.9.0", "koffi": "^2.9.0",
"lucide-react": "^0.562.0", "lucide-react": "^1.7.0",
"react": "^19.2.3", "react": "^19.2.3",
"react-dom": "^19.2.3", "react-dom": "^19.2.3",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
"react-router-dom": "^7.1.1", "react-router-dom": "^7.13.2",
"react-virtuoso": "^4.18.1", "react-virtuoso": "^4.18.1",
"remark-gfm": "^4.0.1", "remark-gfm": "^4.0.1",
"sherpa-onnx-node": "^1.10.38", "sherpa-onnx-node": "^1.10.38",
@@ -53,14 +53,26 @@
"@types/react-dom": "^19.1.0", "@types/react-dom": "^19.1.0",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.3.4",
"electron": "^39.2.7", "electron": "^39.2.7",
"electron-builder": "^25.1.8", "electron-builder": "^26.8.1",
"sass": "^1.83.0", "sass": "^1.98.0",
"sharp": "^0.34.5", "sharp": "^0.34.5",
"typescript": "^5.6.3", "typescript": "^6.0.2",
"vite": "^6.0.5", "vite": "^7.0.0",
"vite-plugin-electron": "^0.28.8", "vite-plugin-electron": "^0.28.8",
"vite-plugin-electron-renderer": "^0.14.6" "vite-plugin-electron-renderer": "^0.14.6"
}, },
"pnpm": {
"overrides": {
"tar": ">=6.2.1",
"minimatch": ">=3.1.2",
"rollup": ">=4.0.0",
"immutable": ">=4.0.0",
"lodash": ">=4.17.21",
"brace-expansion": ">=1.1.11",
"picomatch": ">=2.3.1",
"ajv": ">=8.18.0"
}
},
"build": { "build": {
"appId": "com.WeFlow.app", "appId": "com.WeFlow.app",
"publish": { "publish": {
@@ -90,7 +102,25 @@
"target": [ "target": [
"nsis" "nsis"
], ],
"icon": "public/icon.ico" "icon": "public/icon.ico",
"extraFiles": [
{
"from": "resources/msvcp140.dll",
"to": "."
},
{
"from": "resources/msvcp140_1.dll",
"to": "."
},
{
"from": "resources/vcruntime140.dll",
"to": "."
},
{
"from": "resources/vcruntime140_1.dll",
"to": "."
}
]
}, },
"linux": { "linux": {
"icon": "public/icon.png", "icon": "public/icon.png",
@@ -158,24 +188,11 @@
"node_modules/sherpa-onnx-*/**/*", "node_modules/sherpa-onnx-*/**/*",
"node_modules/ffmpeg-static/**/*" "node_modules/ffmpeg-static/**/*"
], ],
"extraFiles": [
{
"from": "resources/msvcp140.dll",
"to": "."
},
{
"from": "resources/msvcp140_1.dll",
"to": "."
},
{
"from": "resources/vcruntime140.dll",
"to": "."
},
{
"from": "resources/vcruntime140_1.dll",
"to": "."
}
],
"icon": "resources/icon.icns" "icon": "resources/icon.icns"
},
"overrides": {
"picomatch": "^4.0.4",
"tar": "^7.5.13",
"immutable": "^5.1.5"
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 212 KiB

After

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 570 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 570 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -103,6 +103,7 @@ function App() {
// 数据收集同意状态 // 数据收集同意状态
const [showAnalyticsConsent, setShowAnalyticsConsent] = useState(false) const [showAnalyticsConsent, setShowAnalyticsConsent] = useState(false)
const [analyticsConsent, setAnalyticsConsent] = useState<boolean | null>(null)
const [showWaylandWarning, setShowWaylandWarning] = useState(false) const [showWaylandWarning, setShowWaylandWarning] = useState(false)
@@ -252,6 +253,7 @@ function App() {
// 协议已同意,检查数据收集同意状态 // 协议已同意,检查数据收集同意状态
const consent = await configService.getAnalyticsConsent() const consent = await configService.getAnalyticsConsent()
const denyCount = await configService.getAnalyticsDenyCount() const denyCount = await configService.getAnalyticsDenyCount()
setAnalyticsConsent(consent)
// 如果未设置同意状态且拒绝次数小于2次显示弹窗 // 如果未设置同意状态且拒绝次数小于2次显示弹窗
if (consent === null && denyCount < 2) { if (consent === null && denyCount < 2) {
setShowAnalyticsConsent(true) setShowAnalyticsConsent(true)
@@ -266,18 +268,21 @@ function App() {
checkAgreement() checkAgreement()
}, []) }, [])
// 初始化数据收集 // 初始化数据收集(仅在用户同意后)
useEffect(() => { useEffect(() => {
cloudControl.initCloudControl() if (analyticsConsent === true) {
}, []) cloudControl.initCloudControl()
}
}, [analyticsConsent])
// 记录页面访问 // 记录页面访问(仅在用户同意后)
useEffect(() => { useEffect(() => {
if (analyticsConsent !== true) return
const path = location.pathname const path = location.pathname
if (path && path !== '/') { if (path && path !== '/') {
cloudControl.recordPage(path) cloudControl.recordPage(path)
} }
}, [location.pathname]) }, [location.pathname, analyticsConsent])
const handleAgree = async () => { const handleAgree = async () => {
if (!agreementChecked) return if (!agreementChecked) return
@@ -296,6 +301,7 @@ function App() {
const handleAnalyticsAllow = async () => { const handleAnalyticsAllow = async () => {
await configService.setAnalyticsConsent(true) await configService.setAnalyticsConsent(true)
setAnalyticsConsent(true)
setShowAnalyticsConsent(false) setShowAnalyticsConsent(false)
} }
@@ -312,10 +318,14 @@ function App() {
const removeUpdateListener = window.electronAPI?.app?.onUpdateAvailable?.((info: any) => { const removeUpdateListener = window.electronAPI?.app?.onUpdateAvailable?.((info: any) => {
// 发现新版本时保存更新信息,锁定状态下不弹窗,解锁后再显示 // 发现新版本时保存更新信息,锁定状态下不弹窗,解锁后再显示
if (info) { if (info) {
setUpdateInfo({ ...info, hasUpdate: true }) window.electronAPI.app.getVersion().then((currentVersion: string) => {
if (!useAppStore.getState().isLocked) { const isMandatory = !!(info.minimumVersion && currentVersion &&
setShowUpdateDialog(true) currentVersion.localeCompare(info.minimumVersion, undefined, { numeric: true, sensitivity: 'base' }) <= 0)
} setUpdateInfo({ ...info, hasUpdate: true, isMandatory })
if (!useAppStore.getState().isLocked) {
setShowUpdateDialog(true)
}
})
} }
}) })
const removeProgressListener = window.electronAPI?.app?.onDownloadProgress?.((progress: any) => { const removeProgressListener = window.electronAPI?.app?.onDownloadProgress?.((progress: any) => {
@@ -685,10 +695,11 @@ function App() {
<UpdateDialog <UpdateDialog
open={showUpdateDialog} open={showUpdateDialog}
updateInfo={updateInfo} updateInfo={updateInfo}
onClose={() => setShowUpdateDialog(false)} onClose={() => { if (!(updateInfo as any)?.isMandatory) setShowUpdateDialog(false) }}
onUpdate={handleUpdateNow} onUpdate={handleUpdateNow}
onIgnore={handleIgnoreUpdate} onIgnore={handleIgnoreUpdate}
isDownloading={isDownloading} isDownloading={isDownloading}
isMandatory={!!(updateInfo as any)?.isMandatory}
progress={downloadProgress} progress={downloadProgress}
/> />

View File

@@ -75,6 +75,8 @@
font-size: 15px; font-size: 15px;
font-weight: 600; font-weight: 600;
color: var(--text-primary); color: var(--text-primary);
border: none;
background: transparent;
&.clickable { &.clickable {
cursor: pointer; cursor: pointer;
@@ -172,6 +174,33 @@
} }
} }
} }
.year-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 6px;
.year-btn {
padding: 10px 0;
border: none;
background: transparent;
border-radius: 8px;
cursor: pointer;
font-size: 13px;
color: var(--text-secondary);
transition: all 0.15s;
&:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
&.active {
background: var(--primary);
color: #fff;
}
}
}
} }
} }
@@ -372,4 +401,4 @@
opacity: 1; opacity: 1;
transform: translateY(0); transform: translateY(0);
} }
} }

View File

@@ -1,4 +1,4 @@
import React, { useState, useMemo } from 'react' import React, { useState } from 'react'
import { X, ChevronLeft, ChevronRight, Calendar as CalendarIcon, Loader2 } from 'lucide-react' import { X, ChevronLeft, ChevronRight, Calendar as CalendarIcon, Loader2 } from 'lucide-react'
import './JumpToDateDialog.scss' import './JumpToDateDialog.scss'
@@ -21,10 +21,15 @@ const JumpToDateDialog: React.FC<JumpToDateDialogProps> = ({
messageDates, messageDates,
loadingDates = false loadingDates = false
}) => { }) => {
type CalendarViewMode = 'day' | 'month' | 'year'
const getYearPageStart = (year: number): number => Math.floor(year / 12) * 12
const isValidDate = (d: any) => d instanceof Date && !isNaN(d.getTime()) const isValidDate = (d: any) => d instanceof Date && !isNaN(d.getTime())
const [calendarDate, setCalendarDate] = useState(isValidDate(currentDate) ? new Date(currentDate) : new Date()) const [calendarDate, setCalendarDate] = useState(isValidDate(currentDate) ? new Date(currentDate) : new Date())
const [selectedDate, setSelectedDate] = useState(new Date(currentDate)) const [selectedDate, setSelectedDate] = useState(new Date(currentDate))
const [showYearMonthPicker, setShowYearMonthPicker] = useState(false) const [viewMode, setViewMode] = useState<CalendarViewMode>('day')
const [yearPageStart, setYearPageStart] = useState<number>(
getYearPageStart((isValidDate(currentDate) ? new Date(currentDate) : new Date()).getFullYear())
)
if (!isOpen) return null if (!isOpen) return null
@@ -116,6 +121,57 @@ const JumpToDateDialog: React.FC<JumpToDateDialogProps> = ({
const weekdays = ['日', '一', '二', '三', '四', '五', '六'] const weekdays = ['日', '一', '二', '三', '四', '五', '六']
const days = generateCalendar() const days = generateCalendar()
const monthNames = ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月']
const updateCalendarDate = (nextDate: Date) => {
setCalendarDate(nextDate)
}
const openMonthView = () => setViewMode('month')
const openYearView = () => {
setYearPageStart(getYearPageStart(calendarDate.getFullYear()))
setViewMode('year')
}
const handleTitleClick = () => {
if (viewMode === 'day') {
openMonthView()
return
}
if (viewMode === 'month') {
openYearView()
}
}
const handlePrev = () => {
if (viewMode === 'day') {
updateCalendarDate(new Date(calendarDate.getFullYear(), calendarDate.getMonth() - 1, 1))
return
}
if (viewMode === 'month') {
updateCalendarDate(new Date(calendarDate.getFullYear() - 1, calendarDate.getMonth(), 1))
return
}
setYearPageStart((prev) => prev - 12)
}
const handleNext = () => {
if (viewMode === 'day') {
updateCalendarDate(new Date(calendarDate.getFullYear(), calendarDate.getMonth() + 1, 1))
return
}
if (viewMode === 'month') {
updateCalendarDate(new Date(calendarDate.getFullYear() + 1, calendarDate.getMonth(), 1))
return
}
setYearPageStart((prev) => prev + 12)
}
const navTitle = viewMode === 'day'
? `${calendarDate.getFullYear()}${calendarDate.getMonth() + 1}`
: viewMode === 'month'
? `${calendarDate.getFullYear()}`
: `${yearPageStart}年 - ${yearPageStart + 11}`
return ( return (
<div className="jump-date-overlay" onClick={onClose}> <div className="jump-date-overlay" onClick={onClose}>
@@ -134,45 +190,57 @@ const JumpToDateDialog: React.FC<JumpToDateDialogProps> = ({
<div className="calendar-nav"> <div className="calendar-nav">
<button <button
className="nav-btn" className="nav-btn"
onClick={() => setCalendarDate(new Date(calendarDate.getFullYear(), calendarDate.getMonth() - 1, 1))} onClick={handlePrev}
> >
<ChevronLeft size={18} /> <ChevronLeft size={18} />
</button> </button>
<span className="current-month clickable" onClick={() => setShowYearMonthPicker(!showYearMonthPicker)}> <button
{calendarDate.getFullYear()}{calendarDate.getMonth() + 1} className={`current-month ${viewMode === 'year' ? '' : 'clickable'}`.trim()}
</span> onClick={handleTitleClick}
type="button"
>
{navTitle}
</button>
<button <button
className="nav-btn" className="nav-btn"
onClick={() => setCalendarDate(new Date(calendarDate.getFullYear(), calendarDate.getMonth() + 1, 1))} onClick={handleNext}
> >
<ChevronRight size={18} /> <ChevronRight size={18} />
</button> </button>
</div> </div>
{showYearMonthPicker ? ( {viewMode === 'month' ? (
<div className="year-month-picker"> <div className="year-month-picker">
<div className="year-selector">
<button className="nav-btn" onClick={() => setCalendarDate(new Date(calendarDate.getFullYear() - 1, calendarDate.getMonth(), 1))}>
<ChevronLeft size={16} />
</button>
<span className="year-label">{calendarDate.getFullYear()}</span>
<button className="nav-btn" onClick={() => setCalendarDate(new Date(calendarDate.getFullYear() + 1, calendarDate.getMonth(), 1))}>
<ChevronRight size={16} />
</button>
</div>
<div className="month-grid"> <div className="month-grid">
{['一月','二月','三月','四月','五月','六月','七月','八月','九月','十月','十一月','十二月'].map((name, i) => ( {monthNames.map((name, i) => (
<button <button
key={i} key={i}
className={`month-btn ${i === calendarDate.getMonth() ? 'active' : ''}`} className={`month-btn ${i === calendarDate.getMonth() ? 'active' : ''}`}
onClick={() => { onClick={() => {
setCalendarDate(new Date(calendarDate.getFullYear(), i, 1)) updateCalendarDate(new Date(calendarDate.getFullYear(), i, 1))
setShowYearMonthPicker(false) setViewMode('day')
}} }}
>{name}</button> >{name}</button>
))} ))}
</div> </div>
</div> </div>
) : viewMode === 'year' ? (
<div className="year-month-picker">
<div className="year-grid">
{Array.from({ length: 12 }, (_, i) => yearPageStart + i).map((year) => (
<button
key={year}
className={`year-btn ${year === calendarDate.getFullYear() ? 'active' : ''}`}
onClick={() => {
updateCalendarDate(new Date(year, calendarDate.getMonth(), 1))
setViewMode('month')
}}
>
{year}
</button>
))}
</div>
</div>
) : ( ) : (
<div className={`calendar-grid ${loadingDates ? 'loading' : ''}`}> <div className={`calendar-grid ${loadingDates ? 'loading' : ''}`}>
{loadingDates && ( {loadingDates && (
@@ -208,18 +276,21 @@ const JumpToDateDialog: React.FC<JumpToDateDialogProps> = ({
const d = new Date() const d = new Date()
setSelectedDate(d) setSelectedDate(d)
setCalendarDate(new Date(d)) setCalendarDate(new Date(d))
setViewMode('day')
}}></button> }}></button>
<button onClick={() => { <button onClick={() => {
const d = new Date() const d = new Date()
d.setDate(d.getDate() - 7) d.setDate(d.getDate() - 7)
setSelectedDate(d) setSelectedDate(d)
setCalendarDate(new Date(d)) setCalendarDate(new Date(d))
setViewMode('day')
}}></button> }}></button>
<button onClick={() => { <button onClick={() => {
const d = new Date() const d = new Date()
d.setMonth(d.getMonth() - 1) d.setMonth(d.getMonth() - 1)
setSelectedDate(d) setSelectedDate(d)
setCalendarDate(new Date(d)) setCalendarDate(new Date(d))
setViewMode('day')
}}></button> }}></button>
</div> </div>

View File

@@ -28,6 +28,20 @@
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
color: var(--text-primary); color: var(--text-primary);
border: none;
background: transparent;
border-radius: 8px;
padding: 4px 8px;
}
.jump-date-popover .current-month.clickable {
cursor: pointer;
transition: all 0.18s ease;
}
.jump-date-popover .current-month.clickable:hover {
color: var(--primary);
background: var(--bg-hover);
} }
.jump-date-popover .nav-btn { .jump-date-popover .nav-btn {
@@ -83,6 +97,37 @@
gap: 4px; gap: 4px;
} }
.jump-date-popover .month-grid,
.jump-date-popover .year-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 6px;
min-height: 256px;
}
.jump-date-popover .month-cell,
.jump-date-popover .year-cell {
border: none;
border-radius: 8px;
background: transparent;
color: var(--text-secondary);
cursor: pointer;
font-size: 13px;
transition: all 0.18s ease;
}
.jump-date-popover .month-cell:hover,
.jump-date-popover .year-cell:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.jump-date-popover .month-cell.active,
.jump-date-popover .year-cell.active {
background: var(--primary);
color: #fff;
}
.jump-date-popover .day-cell { .jump-date-popover .day-cell {
position: relative; position: relative;
border: 1px solid transparent; border: 1px solid transparent;

View File

@@ -31,14 +31,20 @@ const JumpToDatePopover: React.FC<JumpToDatePopoverProps> = ({
loadingDates = false, loadingDates = false,
loadingDateCounts = false loadingDateCounts = false
}) => { }) => {
type CalendarViewMode = 'day' | 'month' | 'year'
const getYearPageStart = (year: number): number => Math.floor(year / 12) * 12
const [calendarDate, setCalendarDate] = useState<Date>(new Date(currentDate)) const [calendarDate, setCalendarDate] = useState<Date>(new Date(currentDate))
const [selectedDate, setSelectedDate] = useState<Date>(new Date(currentDate)) const [selectedDate, setSelectedDate] = useState<Date>(new Date(currentDate))
const [viewMode, setViewMode] = useState<CalendarViewMode>('day')
const [yearPageStart, setYearPageStart] = useState<number>(getYearPageStart(new Date(currentDate).getFullYear()))
useEffect(() => { useEffect(() => {
if (!isOpen) return if (!isOpen) return
const normalized = new Date(currentDate) const normalized = new Date(currentDate)
setCalendarDate(normalized) setCalendarDate(normalized)
setSelectedDate(normalized) setSelectedDate(normalized)
setViewMode('day')
setYearPageStart(getYearPageStart(normalized.getFullYear()))
}, [isOpen, currentDate]) }, [isOpen, currentDate])
if (!isOpen) return null if (!isOpen) return null
@@ -114,25 +120,78 @@ const JumpToDatePopover: React.FC<JumpToDatePopoverProps> = ({
const weekdays = ['日', '一', '二', '三', '四', '五', '六'] const weekdays = ['日', '一', '二', '三', '四', '五', '六']
const days = generateCalendar() const days = generateCalendar()
const mergedClassName = ['jump-date-popover', className || ''].join(' ').trim() const mergedClassName = ['jump-date-popover', className || ''].join(' ').trim()
const updateCalendarDate = (nextDate: Date) => { const updateCalendarDate = (nextDate: Date) => {
setCalendarDate(nextDate) setCalendarDate(nextDate)
onMonthChange?.(nextDate) onMonthChange?.(nextDate)
} }
const openMonthView = () => setViewMode('month')
const openYearView = () => {
setYearPageStart(getYearPageStart(calendarDate.getFullYear()))
setViewMode('year')
}
const handleTitleClick = () => {
if (viewMode === 'day') {
openMonthView()
return
}
if (viewMode === 'month') {
openYearView()
}
}
const handlePrev = () => {
if (viewMode === 'day') {
updateCalendarDate(new Date(calendarDate.getFullYear(), calendarDate.getMonth() - 1, 1))
return
}
if (viewMode === 'month') {
updateCalendarDate(new Date(calendarDate.getFullYear() - 1, calendarDate.getMonth(), 1))
return
}
setYearPageStart((prev) => prev - 12)
}
const handleNext = () => {
if (viewMode === 'day') {
updateCalendarDate(new Date(calendarDate.getFullYear(), calendarDate.getMonth() + 1, 1))
return
}
if (viewMode === 'month') {
updateCalendarDate(new Date(calendarDate.getFullYear() + 1, calendarDate.getMonth(), 1))
return
}
setYearPageStart((prev) => prev + 12)
}
const navTitle = viewMode === 'day'
? `${calendarDate.getFullYear()}${calendarDate.getMonth() + 1}`
: viewMode === 'month'
? `${calendarDate.getFullYear()}`
: `${yearPageStart}年 - ${yearPageStart + 11}`
return ( return (
<div className={mergedClassName} style={style} role="dialog" aria-label="跳转日期"> <div className={mergedClassName} style={style} role="dialog" aria-label="跳转日期">
<div className="calendar-nav"> <div className="calendar-nav">
<button <button
className="nav-btn" className="nav-btn"
onClick={() => updateCalendarDate(new Date(calendarDate.getFullYear(), calendarDate.getMonth() - 1, 1))} onClick={handlePrev}
aria-label="上一月" aria-label="上一月"
> >
<ChevronLeft size={16} /> <ChevronLeft size={16} />
</button> </button>
<span className="current-month">{calendarDate.getFullYear()}{calendarDate.getMonth() + 1}</span> <button
className={`current-month ${viewMode === 'year' ? '' : 'clickable'}`.trim()}
onClick={handleTitleClick}
type="button"
>
{navTitle}
</button>
<button <button
className="nav-btn" className="nav-btn"
onClick={() => updateCalendarDate(new Date(calendarDate.getFullYear(), calendarDate.getMonth() + 1, 1))} onClick={handleNext}
aria-label="下一月" aria-label="下一月"
> >
<ChevronRight size={16} /> <ChevronRight size={16} />
@@ -154,36 +213,74 @@ const JumpToDatePopover: React.FC<JumpToDatePopoverProps> = ({
)} )}
</div> </div>
<div className="calendar-grid"> {viewMode === 'day' && (
<div className="weekdays"> <div className="calendar-grid">
{weekdays.map(day => ( <div className="weekdays">
<div key={day} className="weekday">{day}</div> {weekdays.map(day => (
<div key={day} className="weekday">{day}</div>
))}
</div>
<div className="days">
{days.map((day, index) => {
if (day === null) return <div key={index} className="day-cell empty" />
const dateKey = toDateKey(day)
const hasMessageOnDay = hasMessage(day)
const count = Number(messageDateCounts?.[dateKey] || 0)
const showCount = count > 0
const showCountLoading = hasMessageOnDay && loadingDateCounts && !showCount
return (
<button
key={index}
className={getDayClassName(day)}
onClick={() => handleDateClick(day)}
disabled={hasLoadedMessageDates && !hasMessageOnDay}
type="button"
>
<span className="day-number">{day}</span>
{showCount && <span className="day-count">{count}</span>}
{showCountLoading && <Loader2 size={11} className="day-count-loading spin" />}
</button>
)
})}
</div>
</div>
)}
{viewMode === 'month' && (
<div className="month-grid">
{['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'].map((name, monthIndex) => (
<button
key={name}
className={`month-cell ${monthIndex === calendarDate.getMonth() ? 'active' : ''}`}
onClick={() => {
updateCalendarDate(new Date(calendarDate.getFullYear(), monthIndex, 1))
setViewMode('day')
}}
type="button"
>
{name}
</button>
))} ))}
</div> </div>
<div className="days"> )}
{days.map((day, index) => {
if (day === null) return <div key={index} className="day-cell empty" /> {viewMode === 'year' && (
const dateKey = toDateKey(day) <div className="year-grid">
const hasMessageOnDay = hasMessage(day) {Array.from({ length: 12 }, (_, i) => yearPageStart + i).map((year) => (
const count = Number(messageDateCounts?.[dateKey] || 0) <button
const showCount = count > 0 key={year}
const showCountLoading = hasMessageOnDay && loadingDateCounts && !showCount className={`year-cell ${year === calendarDate.getFullYear() ? 'active' : ''}`}
return ( onClick={() => {
<button updateCalendarDate(new Date(year, calendarDate.getMonth(), 1))
key={index} setViewMode('month')
className={getDayClassName(day)} }}
onClick={() => handleDateClick(day)} type="button"
disabled={hasLoadedMessageDates && !hasMessageOnDay} >
type="button" {year}
> </button>
<span className="day-number">{day}</span> ))}
{showCount && <span className="day-count">{count}</span>}
{showCountLoading && <Loader2 size={11} className="day-count-loading spin" />}
</button>
)
})}
</div> </div>
</div> )}
</div> </div>
) )
} }

View File

@@ -282,4 +282,13 @@
transform: translateY(0); transform: translateY(0);
opacity: 1; opacity: 1;
} }
} }
.mandatory-tip {
color: #e53e3e;
font-size: 13px;
text-align: center;
margin: 0 0 8px;
padding: 6px 12px;
background: rgba(229, 62, 62, 0.08);
border-radius: 6px;
}

View File

@@ -14,6 +14,7 @@ interface UpdateDialogProps {
onUpdate: () => void onUpdate: () => void
onIgnore?: () => void onIgnore?: () => void
isDownloading: boolean isDownloading: boolean
isMandatory?: boolean
progress: number | { progress: number | {
percent: number percent: number
bytesPerSecond?: number bytesPerSecond?: number
@@ -30,6 +31,7 @@ const UpdateDialog: React.FC<UpdateDialogProps> = ({
onUpdate, onUpdate,
onIgnore, onIgnore,
isDownloading, isDownloading,
isMandatory,
progress progress
}) => { }) => {
if (!open || !updateInfo) return null if (!open || !updateInfo) return null
@@ -69,7 +71,7 @@ const UpdateDialog: React.FC<UpdateDialogProps> = ({
return ( return (
<div className="update-dialog-overlay"> <div className="update-dialog-overlay">
<div className="update-dialog"> <div className="update-dialog">
{!isDownloading && ( {!isDownloading && !isMandatory && (
<button className="close-btn" onClick={onClose}> <button className="close-btn" onClick={onClose}>
<X size={20} /> <X size={20} />
</button> </button>
@@ -119,11 +121,14 @@ const UpdateDialog: React.FC<UpdateDialogProps> = ({
</div> </div>
) : ( ) : (
<div className="actions"> <div className="actions">
{onIgnore && ( {onIgnore && !isMandatory && (
<button className="btn-ignore" onClick={onIgnore}> <button className="btn-ignore" onClick={onIgnore}>
</button> </button>
)} )}
{isMandatory && (
<p className="mandatory-tip">使</p>
)}
<button className="btn-update" onClick={onUpdate}> <button className="btn-update" onClick={onUpdate}>
</button> </button>

View File

@@ -3227,7 +3227,7 @@ function ChatPage(props: ChatPageProps) {
const session = sessionMapRef.current.get(sessionId) const session = sessionMapRef.current.get(sessionId)
const unreadCount = session?.unreadCount ?? 0 const unreadCount = session?.unreadCount ?? 0
let messageLimit = currentBatchSizeRef.current let messageLimit: number
if (offset === 0) { if (offset === 0) {
const preferredLimit = Number.isFinite(options.forceInitialLimit) const preferredLimit = Number.isFinite(options.forceInitialLimit)
@@ -7901,7 +7901,7 @@ function MessageBubble({
useEffect(() => { useEffect(() => {
if (emojiLocalPath) return if (emojiLocalPath) return
// 后端已从本地缓存找到文件(转发表情包无 CDN URL 的情况) // 后端已从本地缓存找到文件(转发表情包无 CDN URL 的情况)
if (isEmoji && message.emojiLocalPath && !emojiLocalPath) { if (isEmoji && message.emojiLocalPath) {
captureEmojiResizeBaseline() captureEmojiResizeBaseline()
setEmojiLocalPath(message.emojiLocalPath) setEmojiLocalPath(message.emojiLocalPath)
return return

View File

@@ -1401,6 +1401,220 @@
} }
} }
// 版本更新页面
.updates-tab {
display: flex;
flex-direction: column;
gap: 16px;
}
.updates-hero {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 20px 22px;
border-radius: 18px;
border: 1px solid color-mix(in srgb, var(--primary) 18%, var(--border-color));
background:
linear-gradient(110deg, color-mix(in srgb, var(--bg-primary) 98%, #ffffff 2%), color-mix(in srgb, var(--bg-primary) 97%, var(--primary) 3%));
box-shadow: 0 8px 22px color-mix(in srgb, var(--primary) 8%, transparent);
}
.updates-hero-main {
min-width: 0;
h2 {
margin: 10px 0 6px;
font-size: 30px;
line-height: 1;
letter-spacing: -0.03em;
color: var(--text-primary);
}
p {
margin: 0;
font-size: 13px;
color: var(--text-secondary);
}
}
.updates-chip {
display: inline-flex;
align-items: center;
height: 24px;
padding: 0 10px;
border-radius: 999px;
font-size: 12px;
font-weight: 600;
color: var(--primary);
background: color-mix(in srgb, var(--primary) 12%, transparent);
}
.updates-hero-action {
display: flex;
flex-direction: column;
gap: 8px;
align-items: flex-end;
}
.updates-card {
padding: 18px;
border-radius: 16px;
border: 1px solid var(--border-color);
background: color-mix(in srgb, var(--bg-primary) 96%, var(--bg-secondary));
}
.updates-progress-card {
padding: 16px 18px;
border-radius: 16px;
border: 1px solid color-mix(in srgb, var(--primary) 34%, var(--border-color));
background: color-mix(in srgb, var(--bg-primary) 95%, var(--primary) 4%);
}
.updates-progress-header {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 10px;
margin-bottom: 10px;
h3 {
margin: 0;
font-size: 15px;
color: var(--text-primary);
}
strong {
color: var(--primary);
font-size: 14px;
font-variant-numeric: tabular-nums;
}
span {
font-size: 12px;
color: var(--text-secondary);
}
}
.updates-card-header {
margin-bottom: 12px;
h3 {
margin: 0 0 4px;
font-size: 16px;
color: var(--text-primary);
}
span {
font-size: 12px;
color: var(--text-tertiary);
}
}
.update-channel-grid {
display: grid;
gap: 10px;
}
.update-channel-card {
width: 100%;
text-align: left;
border: 1px solid var(--border-color);
border-radius: 14px;
padding: 12px 14px;
background: var(--bg-primary);
color: var(--text-primary);
transition: border-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
cursor: pointer;
&:hover:not(:disabled) {
border-color: color-mix(in srgb, var(--primary) 42%, var(--border-color));
transform: translateY(-1px);
box-shadow: 0 8px 24px color-mix(in srgb, var(--primary) 12%, transparent);
}
&:disabled {
cursor: default;
}
&.active {
border-color: color-mix(in srgb, var(--primary) 60%, var(--border-color));
background: color-mix(in srgb, var(--primary) 10%, var(--bg-primary));
}
}
.update-channel-title-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6px;
.title {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
}
svg {
color: var(--primary);
}
}
.update-channel-card .desc {
display: block;
font-size: 12px;
color: var(--text-secondary);
}
.updates-progress-track {
height: 10px;
border-radius: 999px;
background: var(--bg-tertiary);
overflow: hidden;
}
.updates-progress-fill {
height: 100%;
border-radius: inherit;
background: linear-gradient(90deg, var(--primary), color-mix(in srgb, var(--primary) 72%, #ffffff));
transition: width 0.3s ease;
position: relative;
&::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.26), transparent);
animation: updatesShimmer 1.7s linear infinite;
}
}
.updates-ignore-btn {
margin-top: 12px;
}
@media (max-width: 920px) {
.updates-hero {
flex-direction: column;
align-items: flex-start;
}
.updates-hero-action {
align-items: flex-start;
}
}
@keyframes updatesShimmer {
from {
transform: translateX(-100%);
}
to {
transform: translateX(100%);
}
}
// 关于页面 // 关于页面
.about-tab { .about-tab {

View File

@@ -15,7 +15,7 @@ import {
import { Avatar } from '../components/Avatar' import { Avatar } from '../components/Avatar'
import './SettingsPage.scss' import './SettingsPage.scss'
type SettingsTab = 'appearance' | 'notification' | 'database' | 'models' | 'cache' | 'api' | 'security' | 'about' | 'analytics' type SettingsTab = 'appearance' | 'notification' | 'database' | 'models' | 'cache' | 'api' | 'updates' | 'security' | 'about' | 'analytics'
const tabs: { id: SettingsTab; label: string; icon: React.ElementType }[] = [ const tabs: { id: SettingsTab; label: string; icon: React.ElementType }[] = [
{ id: 'appearance', label: '外观', icon: Palette }, { id: 'appearance', label: '外观', icon: Palette },
@@ -24,9 +24,9 @@ const tabs: { id: SettingsTab; label: string; icon: React.ElementType }[] = [
{ id: 'models', label: '模型管理', icon: Mic }, { id: 'models', label: '模型管理', icon: Mic },
{ id: 'cache', label: '缓存', icon: HardDrive }, { id: 'cache', label: '缓存', icon: HardDrive },
{ id: 'api', label: 'API 服务', icon: Globe }, { id: 'api', label: 'API 服务', icon: Globe },
{ id: 'analytics', label: '分析', icon: BarChart2 }, { id: 'analytics', label: '分析', icon: BarChart2 },
{ id: 'security', label: '安全', icon: ShieldCheck }, { id: 'security', label: '安全', icon: ShieldCheck },
{ id: 'updates', label: '版本更新', icon: RefreshCw },
{ id: 'about', label: '关于', icon: Info } { id: 'about', label: '关于', icon: Info }
] ]
@@ -68,7 +68,6 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
setDownloadProgress, setDownloadProgress,
showUpdateDialog, showUpdateDialog,
setShowUpdateDialog, setShowUpdateDialog,
setUpdateError
} = useAppStore() } = useAppStore()
const resetChatStore = useChatStore((state) => state.reset) const resetChatStore = useChatStore((state) => state.reset)
@@ -141,6 +140,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
const [notificationFilterList, setNotificationFilterList] = useState<string[]>([]) const [notificationFilterList, setNotificationFilterList] = useState<string[]>([])
const [windowCloseBehavior, setWindowCloseBehavior] = useState<configService.WindowCloseBehavior>('ask') const [windowCloseBehavior, setWindowCloseBehavior] = useState<configService.WindowCloseBehavior>('ask')
const [quoteLayout, setQuoteLayout] = useState<configService.QuoteLayout>('quote-top') const [quoteLayout, setQuoteLayout] = useState<configService.QuoteLayout>('quote-top')
const [updateChannel, setUpdateChannel] = useState<configService.UpdateChannel>('stable')
const [filterSearchKeyword, setFilterSearchKeyword] = useState('') const [filterSearchKeyword, setFilterSearchKeyword] = useState('')
const [filterModeDropdownOpen, setFilterModeDropdownOpen] = useState(false) const [filterModeDropdownOpen, setFilterModeDropdownOpen] = useState(false)
const [positionDropdownOpen, setPositionDropdownOpen] = useState(false) const [positionDropdownOpen, setPositionDropdownOpen] = useState(false)
@@ -339,6 +339,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
const savedMessagePushEnabled = await configService.getMessagePushEnabled() const savedMessagePushEnabled = await configService.getMessagePushEnabled()
const savedWindowCloseBehavior = await configService.getWindowCloseBehavior() const savedWindowCloseBehavior = await configService.getWindowCloseBehavior()
const savedQuoteLayout = await configService.getQuoteLayout() const savedQuoteLayout = await configService.getQuoteLayout()
const savedUpdateChannel = await configService.getUpdateChannel()
const savedAuthEnabled = await window.electronAPI.auth.verifyEnabled() const savedAuthEnabled = await window.electronAPI.auth.verifyEnabled()
const savedAuthUseHello = await configService.getAuthUseHello() const savedAuthUseHello = await configService.getAuthUseHello()
@@ -387,6 +388,18 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
setMessagePushEnabled(savedMessagePushEnabled) setMessagePushEnabled(savedMessagePushEnabled)
setWindowCloseBehavior(savedWindowCloseBehavior) setWindowCloseBehavior(savedWindowCloseBehavior)
setQuoteLayout(savedQuoteLayout) setQuoteLayout(savedQuoteLayout)
if (savedUpdateChannel) {
setUpdateChannel(savedUpdateChannel)
} else {
const currentVersion = await window.electronAPI.app.getVersion()
if (/-preview\.\d+\.\d+$/i.test(currentVersion)) {
setUpdateChannel('preview')
} else if (/-dev\.\d+\.\d+\.\d+$/i.test(currentVersion) || /(alpha|beta|rc)/i.test(currentVersion)) {
setUpdateChannel('dev')
} else {
setUpdateChannel('stable')
}
}
const savedExcludeWords = await configService.getWordCloudExcludeWords() const savedExcludeWords = await configService.getWordCloudExcludeWords()
setWordCloudExcludeWords(savedExcludeWords) setWordCloudExcludeWords(savedExcludeWords)
@@ -512,7 +525,22 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
} }
} }
const handleUpdateChannelChange = async (channel: configService.UpdateChannel) => {
if (channel === updateChannel) return
try {
setUpdateChannel(channel)
await configService.setUpdateChannel(channel)
await configService.setIgnoredUpdateVersion('')
setUpdateInfo(null)
setShowUpdateDialog(false)
const channelLabel = channel === 'stable' ? '稳定版' : channel === 'preview' ? '预览版' : '开发版'
showMessage(`已切换到${channelLabel}更新渠道,正在检查更新`, true)
await handleCheckUpdate()
} catch (e: any) {
showMessage(`切换更新渠道失败: ${e}`, false)
}
}
const showMessage = (text: string, success: boolean) => { const showMessage = (text: string, success: boolean) => {
setMessage({ text, success }) setMessage({ text, success })
@@ -1474,6 +1502,16 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
const renderDatabaseTab = () => ( const renderDatabaseTab = () => (
<div className="tab-content"> <div className="tab-content">
<div className="form-group">
<label></label>
<span className="form-hint"></span>
<button className="btn btn-secondary" onClick={handleTestConnection} disabled={isLoading || isTesting}>
<Plug size={16} /> {isTesting ? '测试中...' : '测试连接'}
</button>
</div>
<div className="divider" />
<div className="form-group"> <div className="form-group">
<label></label> <label></label>
<span className="form-hint">64</span> <span className="form-hint">64</span>
@@ -2400,35 +2438,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
<img src="./logo.png" alt="WeFlow" /> <img src="./logo.png" alt="WeFlow" />
</div> </div>
<h2 className="about-name">WeFlow</h2> <h2 className="about-name">WeFlow</h2>
<p className="about-slogan">WeFlow</p>
<p className="about-version">v{appVersion || '...'}</p> <p className="about-version">v{appVersion || '...'}</p>
<div className="about-update">
{updateInfo?.hasUpdate ? (
<>
<p className="update-hint"> v{updateInfo.version} </p>
{isDownloading ? (
<div className="update-progress">
<div className="progress-bar">
<div className="progress-inner" style={{ width: `${(downloadProgress?.percent || 0)}%` }} />
</div>
<span>{(downloadProgress?.percent || 0).toFixed(0)}%</span>
</div>
) : (
<button className="btn btn-primary" onClick={() => setShowUpdateDialog(true)}>
<Download size={16} />
</button>
)}
</>
) : (
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
<button className="btn btn-secondary" onClick={handleCheckUpdate} disabled={isCheckingUpdate}>
<RefreshCw size={16} className={isCheckingUpdate ? 'spin' : ''} />
{isCheckingUpdate ? '检查中...' : '检查更新'}
</button>
</div>
)}
</div>
</div> </div>
<div className="about-footer"> <div className="about-footer">
@@ -2440,7 +2450,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
<span>·</span> <span>·</span>
<a href="#" onClick={(e) => { e.preventDefault(); window.electronAPI.window.openAgreementWindow() }}></a> <a href="#" onClick={(e) => { e.preventDefault(); window.electronAPI.window.openAgreementWindow() }}></a>
</div> </div>
<p className="copyright">© 2025 WeFlow. All rights reserved.</p> <p className="copyright">© 2026 WeFlow. All rights reserved.</p>
<div className="log-toggle-line" style={{ marginTop: '16px', justifyContent: 'center' }}> <div className="log-toggle-line" style={{ marginTop: '16px', justifyContent: 'center' }}>
<span style={{ fontSize: '13px', opacity: 0.7 }}></span> <span style={{ fontSize: '13px', opacity: 0.7 }}></span>
@@ -2463,6 +2473,82 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
</div> </div>
) )
const renderUpdatesTab = () => {
const downloadPercent = Math.max(0, Math.min(100, Number(downloadProgress?.percent || 0)))
const channelCards: { id: configService.UpdateChannel; title: string; desc: string }[] = [
{ id: 'stable', title: '稳定版', desc: '正式发布的版本,适合日常使用' },
{ id: 'preview', title: '预览版', desc: '正式发布前的预览体验版本' },
{ id: 'dev', title: '开发版', desc: '即刻体验我们的屎山代码' }
]
return (
<div className="tab-content updates-tab">
<div className="updates-hero">
<div className="updates-hero-main">
<span className="updates-chip"></span>
<h2>{appVersion || '...'}</h2>
<p>{updateInfo?.hasUpdate ? `发现新版本 v${updateInfo.version}` : '当前已是最新版本,可手动检查更新'}</p>
</div>
<div className="updates-hero-action">
{updateInfo?.hasUpdate ? (
<button className="btn btn-primary" onClick={() => setShowUpdateDialog(true)}>
<Download size={16} />
</button>
) : (
<button className="btn btn-secondary" onClick={handleCheckUpdate} disabled={isCheckingUpdate}>
<RefreshCw size={16} className={isCheckingUpdate ? 'spin' : ''} />
{isCheckingUpdate ? '检查中...' : '检查更新'}
</button>
)}
</div>
</div>
{(isDownloading || updateInfo?.hasUpdate) && (
<div className="updates-progress-card">
<div className="updates-progress-header">
<h3>{isDownloading ? `正在下载 v${updateInfo?.version || ''}` : `新版本 v${updateInfo?.version} 已就绪`}</h3>
{isDownloading ? <strong>{downloadPercent.toFixed(0)}%</strong> : <span></span>}
</div>
<div className="updates-progress-track">
<div className="updates-progress-fill" style={{ width: `${isDownloading ? downloadPercent : 100}%` }} />
</div>
{updateInfo?.hasUpdate && !isDownloading && (
<button className="btn btn-secondary updates-ignore-btn" onClick={handleIgnoreUpdate}>
</button>
)}
</div>
)}
<div className="updates-card">
<div className="updates-card-header">
<h3></h3>
<span></span>
</div>
<div className="update-channel-grid">
{channelCards.map((channel) => {
const active = updateChannel === channel.id
return (
<button
key={channel.id}
className={`update-channel-card ${active ? 'active' : ''}`}
onClick={() => void handleUpdateChannelChange(channel.id)}
disabled={active}
>
<div className="update-channel-title-row">
<span className="title">{channel.title}</span>
{active && <Check size={16} />}
</div>
<span className="desc">{channel.desc}</span>
</button>
)
})}
</div>
</div>
</div>
)
}
return ( return (
<div className={`settings-modal-overlay ${isClosing ? 'closing' : ''}`} onClick={handleClose}> <div className={`settings-modal-overlay ${isClosing ? 'closing' : ''}`} onClick={handleClose}>
<div className={`settings-page ${isClosing ? 'closing' : ''}`} onClick={(event) => event.stopPropagation()}> <div className={`settings-page ${isClosing ? 'closing' : ''}`} onClick={(event) => event.stopPropagation()}>
@@ -2510,9 +2596,6 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
<h1></h1> <h1></h1>
</div> </div>
<div className="settings-actions"> <div className="settings-actions">
<button className="btn btn-secondary" onClick={handleTestConnection} disabled={isLoading || isTesting}>
<Plug size={16} /> {isTesting ? '测试中...' : '测试连接'}
</button>
{onClose && ( {onClose && (
<button type="button" className="settings-close-btn" onClick={handleClose} aria-label="关闭设置"> <button type="button" className="settings-close-btn" onClick={handleClose} aria-label="关闭设置">
<X size={18} /> <X size={18} />
@@ -2542,6 +2625,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
{activeTab === 'models' && renderModelsTab()} {activeTab === 'models' && renderModelsTab()}
{activeTab === 'cache' && renderCacheTab()} {activeTab === 'cache' && renderCacheTab()}
{activeTab === 'api' && renderApiTab()} {activeTab === 'api' && renderApiTab()}
{activeTab === 'updates' && renderUpdatesTab()}
{activeTab === 'analytics' && renderAnalyticsTab()} {activeTab === 'analytics' && renderAnalyticsTab()}
{activeTab === 'security' && renderSecurityTab()} {activeTab === 'security' && renderSecurityTab()}
{activeTab === 'about' && renderAboutTab()} {activeTab === 'about' && renderAboutTab()}

View File

@@ -58,6 +58,7 @@ export const CONFIG_KEYS = {
// 更新 // 更新
IGNORED_UPDATE_VERSION: 'ignoredUpdateVersion', IGNORED_UPDATE_VERSION: 'ignoredUpdateVersion',
UPDATE_CHANNEL: 'updateChannel',
// 通知 // 通知
NOTIFICATION_ENABLED: 'notificationEnabled', NOTIFICATION_ENABLED: 'notificationEnabled',
@@ -96,6 +97,7 @@ export interface ExportDefaultMediaConfig {
export type WindowCloseBehavior = 'ask' | 'tray' | 'quit' export type WindowCloseBehavior = 'ask' | 'tray' | 'quit'
export type QuoteLayout = 'quote-top' | 'quote-bottom' export type QuoteLayout = 'quote-top' | 'quote-bottom'
export type UpdateChannel = 'stable' | 'preview' | 'dev'
const DEFAULT_EXPORT_MEDIA_CONFIG: ExportDefaultMediaConfig = { const DEFAULT_EXPORT_MEDIA_CONFIG: ExportDefaultMediaConfig = {
images: true, images: true,
@@ -1381,6 +1383,18 @@ export async function setIgnoredUpdateVersion(version: string): Promise<void> {
await config.set(CONFIG_KEYS.IGNORED_UPDATE_VERSION, version) await config.set(CONFIG_KEYS.IGNORED_UPDATE_VERSION, version)
} }
// 获取更新渠道(空值/auto 视为未显式设置,交由安装包类型决定默认渠道)
export async function getUpdateChannel(): Promise<UpdateChannel | null> {
const value = await config.get(CONFIG_KEYS.UPDATE_CHANNEL)
if (value === 'stable' || value === 'preview' || value === 'dev') return value
return null
}
// 设置更新渠道
export async function setUpdateChannel(channel: UpdateChannel): Promise<void> {
await config.set(CONFIG_KEYS.UPDATE_CHANNEL, channel)
}
// 获取通知开关 // 获取通知开关
export async function getNotificationEnabled(): Promise<boolean> { export async function getNotificationEnabled(): Promise<boolean> {
const value = await config.get(CONFIG_KEYS.NOTIFICATION_ENABLED) const value = await config.get(CONFIG_KEYS.NOTIFICATION_ENABLED)

View File

@@ -17,8 +17,7 @@
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": ["./src/*"]
}, }
"baseUrl": "."
}, },
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.d.ts"], "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.d.ts"],
"references": [{ "path": "./tsconfig.node.json" }] "references": [{ "path": "./tsconfig.node.json" }]

View File

@@ -15,8 +15,41 @@ export default defineConfig({
strictPort: false // 如果3000被占用自动尝试下一个 strictPort: false // 如果3000被占用自动尝试下一个
}, },
build: { build: {
chunkSizeWarningLimit: 900,
commonjsOptions: { commonjsOptions: {
ignoreDynamicRequires: true ignoreDynamicRequires: true
},
rollupOptions: {
output: {
manualChunks(id) {
if (!id.includes('node_modules')) return
if (id.includes('/react/') || id.includes('/react-dom/') || id.includes('/react-router')) {
return 'vendor-react'
}
if (id.includes('/echarts') || id.includes('/echarts-for-react')) {
return 'vendor-echarts'
}
if (
id.includes('/react-markdown') ||
id.includes('/remark-gfm') ||
id.includes('/mdast-') ||
id.includes('/micromark-') ||
id.includes('/unified') ||
id.includes('/vfile')
) {
return 'vendor-markdown'
}
if (id.includes('/jszip') || id.includes('/exceljs')) {
return 'vendor-export'
}
return 'vendor-misc'
}
}
} }
}, },
optimizeDeps: { optimizeDeps: {