Merge branch 'dev' into dev

This commit is contained in:
hejinkang
2026-03-30 10:04:56 +08:00
committed by GitHub
23 changed files with 968 additions and 1431 deletions

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}`);
}

View File

@@ -12,27 +12,8 @@ env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
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 +42,31 @@ 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"
run: | run: |
npx electron-builder --mac --arm64 --publish always 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 +101,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 +146,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 +196,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 +224,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 }}
@@ -294,18 +281,10 @@ jobs:
## macOS 安装提示(未知来源) ## macOS 安装提示(未知来源)
- 若打开时提示“来自未知开发者”或“无法验证开发者”,请到「系统设置 -> 隐私与安全性」中允许打开该应用。 - 若打开时提示“来自未知开发者”或“无法验证开发者”,请到「系统设置 -> 隐私与安全性」中允许打开该应用。
- 如果仍被系统拦截,请在终端执行以下命令去除隔离标记: - 如果仍被系统拦截,请在终端执行以下命令去除隔离标记:
- xattr -rd com.apple.quarantine /Applications/WeFlow.app - \`xattr -dr 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

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'''
]

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

@@ -1242,7 +1242,8 @@ function registerIpcHandlers() {
return { return {
hasUpdate: true, hasUpdate: true,
version: latestVersion, version: latestVersion,
releaseNotes: normalizeReleaseNotes(result.updateInfo.releaseNotes) releaseNotes: normalizeReleaseNotes(result.updateInfo.releaseNotes),
minimumVersion: (result.updateInfo as any).minimumVersion
} }
} }
} }
@@ -2706,7 +2707,8 @@ function checkForUpdatesOnStartup() {
// 通知渲染进程有新版本 // 通知渲染进程有新版本
mainWindow.webContents.send('app:updateAvailable', { mainWindow.webContents.send('app:updateAvailable', {
version: latestVersion, version: latestVersion,
releaseNotes: normalizeReleaseNotes(result.updateInfo.releaseNotes) releaseNotes: normalizeReleaseNotes(result.updateInfo.releaseNotes),
minimumVersion: (result.updateInfo as any).minimumVersion
}) })
} }
} }

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

@@ -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) {

1965
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": {
@@ -38,7 +38,7 @@
"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,7 +53,7 @@
"@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.83.0",
"sharp": "^0.34.5", "sharp": "^0.34.5",
"typescript": "^5.6.3", "typescript": "^5.6.3",
@@ -61,6 +61,18 @@
"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": {
@@ -177,5 +189,11 @@
} }
], ],
"icon": "resources/icon.icns" "icon": "resources/icon.icns"
},
"overrides": {
"picomatch": "^4.0.4",
"tar": "^7.5.13",
"immutable": "^5.1.5",
"ajv": ">=6.14.0"
} }
} }

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.

View File

@@ -312,10 +312,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) => {
const isMandatory = !!(info.minimumVersion && currentVersion &&
currentVersion.localeCompare(info.minimumVersion, undefined, { numeric: true, sensitivity: 'base' }) <= 0)
setUpdateInfo({ ...info, hasUpdate: true, isMandatory })
if (!useAppStore.getState().isLocked) { if (!useAppStore.getState().isLocked) {
setShowUpdateDialog(true) setShowUpdateDialog(true)
} }
})
} }
}) })
const removeProgressListener = window.electronAPI?.app?.onDownloadProgress?.((progress: any) => { const removeProgressListener = window.electronAPI?.app?.onDownloadProgress?.((progress: any) => {
@@ -685,10 +689,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

@@ -283,3 +283,12 @@
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