From 8a3f1078f6931452e41a7c80980c1b6b8f21530f Mon Sep 17 00:00:00 2001 From: Jason <159670257+Jasonzhu1207@users.noreply.github.com> Date: Fri, 10 Apr 2026 22:56:41 +0800 Subject: [PATCH 1/9] Add files via upload --- .github/workflows/release.yml | 30 +++++++----------------------- 1 file changed, 7 insertions(+), 23 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 44cf1bb..cc26d08 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -31,28 +31,12 @@ jobs: - name: Install Dependencies run: npm install - - name: Ensure mac key helpers are executable - shell: bash - run: | - set -euo pipefail - for file in \ - resources/key/macos/universal/xkey_helper \ - resources/key/macos/universal/image_scan_helper \ - resources/key/macos/universal/xkey_helper_macos \ - resources/key/macos/universal/libwx_key.dylib - do - if [ -f "$file" ]; then - chmod +x "$file" - ls -l "$file" - fi - done - - name: Sync version with tag shell: bash run: | VERSION=${GITHUB_REF_NAME#v} echo "Syncing package.json version to $VERSION" - npm version $VERSION --no-git-tag-version --allow-same-version + node -e "const fs=require('fs');const p=JSON.parse(fs.readFileSync('package.json','utf8'));p.version='$VERSION';fs.writeFileSync('package.json',JSON.stringify(p,null,2)+'\n')" - name: Build Frontend & Type Check shell: bash @@ -109,7 +93,7 @@ jobs: run: | VERSION=${GITHUB_REF_NAME#v} echo "Syncing package.json version to $VERSION" - npm version $VERSION --no-git-tag-version --allow-same-version + node -e "const fs=require('fs');const p=JSON.parse(fs.readFileSync('package.json','utf8'));p.version='$VERSION';fs.writeFileSync('package.json',JSON.stringify(p,null,2)+'\n')" - name: Build Frontend & Type Check shell: bash @@ -131,7 +115,7 @@ jobs: 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 + gh release download "$TAG" --repo "$REPO" --pattern "latest-linux.yml" --output "/tmp/latest-linux.yml" 2>/dev/null || true 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 @@ -160,7 +144,7 @@ jobs: run: | VERSION=${GITHUB_REF_NAME#v} echo "Syncing package.json version to $VERSION" - npm version $VERSION --no-git-tag-version --allow-same-version + node -e "const fs=require('fs');const p=JSON.parse(fs.readFileSync('package.json','utf8'));p.version='$VERSION';fs.writeFileSync('package.json',JSON.stringify(p,null,2)+'\n')" - name: Build Frontend & Type Check shell: bash @@ -182,7 +166,7 @@ jobs: 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 + gh release download "$TAG" --repo "$REPO" --pattern "latest.yml" --output "/tmp/latest.yml" 2>/dev/null || true 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 @@ -211,7 +195,7 @@ jobs: run: | VERSION=${GITHUB_REF_NAME#v} echo "Syncing package.json version to $VERSION" - npm version $VERSION --no-git-tag-version --allow-same-version + node -e "const fs=require('fs');const p=JSON.parse(fs.readFileSync('package.json','utf8'));p.version='$VERSION';fs.writeFileSync('package.json',JSON.stringify(p,null,2)+'\n')" - name: Build Frontend & Type Check shell: bash @@ -233,7 +217,7 @@ jobs: 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 + gh release download "$TAG" --repo "$REPO" --pattern "latest-arm64.yml" --output "/tmp/latest-arm64.yml" 2>/dev/null || true 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 From a26d5620ca2df0aec6b60cf1a31f189f8410f311 Mon Sep 17 00:00:00 2001 From: Jason <159670257+Jasonzhu1207@users.noreply.github.com> Date: Fri, 10 Apr 2026 22:57:15 +0800 Subject: [PATCH 2/9] Add files via upload --- package.json | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 0f05abe..2aac96c 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/hicccc77/WeFlow" + "url": "https://github.com/Jasonzhu1207/WeFlow" }, "//": "二改不应改变此处的作者与应用信息", "scripts": { @@ -23,7 +23,6 @@ "electron:build": "npm run build" }, "dependencies": { - "@vscode/sudo-prompt": "^9.3.2", "echarts": "^6.0.0", "echarts-for-react": "^3.0.2", "electron-store": "^11.0.2", @@ -42,8 +41,9 @@ "react-router-dom": "^7.14.0", "react-virtuoso": "^4.18.1", "remark-gfm": "^4.0.1", - "sherpa-onnx-node": "^1.12.35", + "sherpa-onnx-node": "^1.10.38", "silk-wasm": "^3.7.1", + "sudo-prompt": "^9.2.1", "wechat-emojis": "^1.0.2", "zustand": "^5.0.2" }, @@ -54,11 +54,11 @@ "@vitejs/plugin-react": "^4.3.4", "electron": "^41.1.1", "electron-builder": "^26.8.1", - "sass": "^1.99.0", + "sass": "^1.98.0", "sharp": "^0.34.5", "typescript": "^6.0.2", - "vite": "^7.3.2", - "vite-plugin-electron": "^0.29.1", + "vite": "^7.0.0", + "vite-plugin-electron": "^0.28.8", "vite-plugin-electron-renderer": "^0.14.6" }, "pnpm": { @@ -70,16 +70,14 @@ "lodash": ">=4.17.21", "brace-expansion": ">=1.1.11", "picomatch": ">=2.3.1", - "ajv": ">=8.18.0", - "ajv-keywords@3>ajv": "^6.12.6", - "@develar/schema-utils>ajv": "^6.12.6" + "ajv": ">=8.18.0" } }, "build": { "appId": "com.WeFlow.app", "publish": { "provider": "github", - "owner": "hicccc77", + "owner": "Jasonzhu1207", "repo": "WeFlow", "releaseType": "release" }, @@ -98,7 +96,7 @@ "gatekeeperAssess": false, "entitlements": "electron/entitlements.mac.plist", "entitlementsInherit": "electron/entitlements.mac.plist", - "icon": "resources/icons/macos/icon.icns" + "icon": "resources/icon.icns" }, "win": { "target": [ @@ -107,19 +105,19 @@ "icon": "public/icon.ico", "extraFiles": [ { - "from": "resources/runtime/win32/msvcp140.dll", + "from": "resources/msvcp140.dll", "to": "." }, { - "from": "resources/runtime/win32/msvcp140_1.dll", + "from": "resources/msvcp140_1.dll", "to": "." }, { - "from": "resources/runtime/win32/vcruntime140.dll", + "from": "resources/vcruntime140.dll", "to": "." }, { - "from": "resources/runtime/win32/vcruntime140_1.dll", + "from": "resources/vcruntime140_1.dll", "to": "." } ] @@ -135,7 +133,7 @@ "synopsis": "WeFlow for Linux", "extraFiles": [ { - "from": "resources/installer/linux/install.sh", + "from": "resources/linux/install.sh", "to": "install.sh" } ] @@ -190,7 +188,7 @@ "node_modules/sherpa-onnx-*/**/*", "node_modules/ffmpeg-static/**/*" ], - "icon": "resources/icons/macos/icon.icns" + "icon": "resources/icon.icns" }, "overrides": { "picomatch": "^4.0.4", From a734cedac119a8b5454c1884871707e0353bea9d Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 12 Apr 2026 15:45:43 +0800 Subject: [PATCH 3/9] feat: add AI insight debug log export toggle --- electron/services/config.ts | 5 +- electron/services/insightService.ts | 127 ++++++++++++++++++++++++++-- src/pages/SettingsPage.tsx | 92 +++++++++++++------- src/services/config.ts | 12 ++- 4 files changed, 194 insertions(+), 42 deletions(-) diff --git a/electron/services/config.ts b/electron/services/config.ts index 250c93d..5a6b868 100644 --- a/electron/services/config.ts +++ b/electron/services/config.ts @@ -102,6 +102,8 @@ interface ConfigSchema { // AI 足迹 aiFootprintEnabled: boolean aiFootprintSystemPrompt: string + /** 是否将 AI 见解调试日志输出到桌面 */ + aiInsightDebugLogEnabled: boolean } // 需要 safeStorage 加密的字段(普通模式) @@ -204,7 +206,8 @@ export class ConfigService { aiInsightTelegramToken: '', aiInsightTelegramChatIds: '', aiFootprintEnabled: false, - aiFootprintSystemPrompt: '' + aiFootprintSystemPrompt: '', + aiInsightDebugLogEnabled: false } const storeOptions: any = { diff --git a/electron/services/insightService.ts b/electron/services/insightService.ts index 6890f7a..ff91a32 100644 --- a/electron/services/insightService.ts +++ b/electron/services/insightService.ts @@ -15,8 +15,10 @@ import https from 'https' import http from 'http' +import fs from 'fs' +import path from 'path' import { URL } from 'url' -import { Notification } from 'electron' +import { app, Notification } from 'electron' import { ConfigService } from './config' import { chatService, ChatSession, Message } from './chatService' @@ -33,6 +35,8 @@ const SILENCE_SCAN_INITIAL_DELAY_MS = 3 * 60 * 1000 /** 单次 API 请求超时(毫秒) */ const API_TIMEOUT_MS = 45_000 +const API_MAX_TOKENS = 200 +const API_TEMPERATURE = 0.7 /** 沉默天数阈值默认值 */ const DEFAULT_SILENCE_DAYS = 3 @@ -62,15 +66,74 @@ interface SharedAiModelConfig { // ─── 日志 ───────────────────────────────────────────────────────────────────── +type InsightLogLevel = 'INFO' | 'WARN' | 'ERROR' + +let debugLogWriteQueue: Promise = Promise.resolve() + +function formatDebugTimestamp(date: Date = new Date()): string { + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + const hours = String(date.getHours()).padStart(2, '0') + const minutes = String(date.getMinutes()).padStart(2, '0') + const seconds = String(date.getSeconds()).padStart(2, '0') + return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}` +} + +function getInsightDebugLogFilePath(date: Date = new Date()): string { + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + return path.join(app.getPath('desktop'), `weflow-ai-insight-debug-${year}-${month}-${day}.log`) +} + +function isInsightDebugLogEnabled(): boolean { + try { + return ConfigService.getInstance().get('aiInsightDebugLogEnabled') === true + } catch { + return false + } +} + +function appendInsightDebugText(text: string): void { + if (!isInsightDebugLogEnabled()) return + + let logFilePath = '' + try { + logFilePath = getInsightDebugLogFilePath() + } catch { + return + } + + debugLogWriteQueue = debugLogWriteQueue + .then(() => fs.promises.appendFile(logFilePath, text, 'utf8')) + .catch(() => undefined) +} + +function insightDebugLine(level: InsightLogLevel, message: string): void { + appendInsightDebugText(`[${formatDebugTimestamp()}] [${level}] ${message}\n`) +} + +function insightDebugSection(level: InsightLogLevel, title: string, payload: unknown): void { + const content = typeof payload === 'string' + ? payload + : JSON.stringify(payload, null, 2) + + appendInsightDebugText( + `\n========== [${formatDebugTimestamp()}] [${level}] ${title} ==========\n${content}\n========== END ==========\n` + ) +} + /** * 仅输出到 console,不落盘到文件。 */ -function insightLog(level: 'INFO' | 'WARN' | 'ERROR', message: string): void { +function insightLog(level: InsightLogLevel, message: string): void { if (level === 'ERROR' || level === 'WARN') { console.warn(`[InsightService] ${message}`) } else { console.log(`[InsightService] ${message}`) } + insightDebugLine(level, message) } // ─── 工具函数 ───────────────────────────────────────────────────────────────── @@ -127,8 +190,8 @@ function callApi( const body = JSON.stringify({ model, messages, - max_tokens: 200, - temperature: 0.7, + max_tokens: API_MAX_TOKENS, + temperature: API_TEMPERATURE, stream: false }) @@ -336,15 +399,34 @@ class InsightService { } try { + const endpoint = buildApiUrl(apiBaseUrl, '/chat/completions') + const requestMessages = [{ role: 'user', content: '请回复"连接成功"四个字。' }] + insightDebugSection('INFO', 'AI 测试连接请求', { + endpoint, + model, + request: { + model, + messages: requestMessages, + max_tokens: API_MAX_TOKENS, + temperature: API_TEMPERATURE, + stream: false + } + }) + const result = await callApi( apiBaseUrl, apiKey, model, - [{ role: 'user', content: '请回复"连接成功"四个字。' }], + requestMessages, 15_000 ) + insightDebugSection('INFO', 'AI 测试连接输出原文', result) return { success: true, message: `连接成功,模型回复:${result.slice(0, 50)}` } } catch (e) { + insightDebugSection('ERROR', 'AI 测试连接失败', { + error: (e as Error).message, + stack: (e as Error).stack ?? null + }) return { success: false, message: `连接失败:${(e as Error).message}` } } } @@ -884,20 +966,40 @@ ${topMentionText} 请给出你的见解(≤80字):` const endpoint = buildApiUrl(apiBaseUrl, '/chat/completions') + const requestMessages = [ + { role: 'system', content: systemPrompt }, + { role: 'user', content: userPrompt } + ] + insightLog('INFO', `准备调用 API: ${endpoint},模型: ${model}`) + insightDebugSection('INFO', `AI 请求 ${displayName} (${sessionId})`, { + sessionId, + displayName, + triggerReason, + silentDays: silentDays ?? null, + endpoint, + model, + allowContext, + contextCount, + request: { + model, + messages: requestMessages, + max_tokens: API_MAX_TOKENS, + temperature: API_TEMPERATURE, + stream: false + } + }) try { const result = await callApi( apiBaseUrl, apiKey, model, - [ - { role: 'system', content: systemPrompt }, - { role: 'user', content: userPrompt } - ] + requestMessages ) insightLog('INFO', `API 返回原文: ${result.slice(0, 150)}`) + insightDebugSection('INFO', `AI 输出原文 ${displayName} (${sessionId})`, result) // 模型主动选择跳过 if (result.trim().toUpperCase() === 'SKIP' || result.trim().startsWith('SKIP')) { @@ -939,6 +1041,13 @@ ${topMentionText} insightLog('INFO', `已为 ${displayName} 推送见解`) } catch (e) { + insightDebugSection('ERROR', `AI 请求失败 ${displayName} (${sessionId})`, { + sessionId, + displayName, + triggerReason, + error: (e as Error).message, + stack: (e as Error).stack ?? null + }) insightLog('ERROR', `API 调用失败 (${displayName}): ${(e as Error).message}`) } } diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index 48f0ae2..b62f101 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -286,6 +286,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { const [aiInsightTelegramChatIds, setAiInsightTelegramChatIds] = useState('') const [aiFootprintEnabled, setAiFootprintEnabled] = useState(false) const [aiFootprintSystemPrompt, setAiFootprintSystemPrompt] = useState('') + const [aiInsightDebugLogEnabled, setAiInsightDebugLogEnabled] = useState(false) // 检查 Hello 可用性 useEffect(() => { @@ -516,35 +517,38 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { const savedAiModelApiKey = await configService.getAiModelApiKey() const savedAiModelApiModel = await configService.getAiModelApiModel() const savedAiInsightSilenceDays = await configService.getAiInsightSilenceDays() - const savedAiInsightAllowContext = await configService.getAiInsightAllowContext() - const savedAiInsightWhitelistEnabled = await configService.getAiInsightWhitelistEnabled() - const savedAiInsightWhitelist = await configService.getAiInsightWhitelist() - const savedAiInsightCooldownMinutes = await configService.getAiInsightCooldownMinutes() - const savedAiInsightScanIntervalHours = await configService.getAiInsightScanIntervalHours() - const savedAiInsightContextCount = await configService.getAiInsightContextCount() - const savedAiInsightSystemPrompt = await configService.getAiInsightSystemPrompt() - const savedAiInsightTelegramEnabled = await configService.getAiInsightTelegramEnabled() - const savedAiInsightTelegramToken = await configService.getAiInsightTelegramToken() - const savedAiInsightTelegramChatIds = await configService.getAiInsightTelegramChatIds() - const savedAiFootprintEnabled = await configService.getAiFootprintEnabled() - const savedAiFootprintSystemPrompt = await configService.getAiFootprintSystemPrompt() - setAiInsightEnabled(savedAiInsightEnabled) - setAiModelApiBaseUrl(savedAiModelApiBaseUrl) - setAiModelApiKey(savedAiModelApiKey) - setAiModelApiModel(savedAiModelApiModel) - setAiInsightSilenceDays(savedAiInsightSilenceDays) - setAiInsightAllowContext(savedAiInsightAllowContext) - setAiInsightWhitelistEnabled(savedAiInsightWhitelistEnabled) - setAiInsightWhitelist(new Set(savedAiInsightWhitelist)) - setAiInsightCooldownMinutes(savedAiInsightCooldownMinutes) - setAiInsightScanIntervalHours(savedAiInsightScanIntervalHours) - setAiInsightContextCount(savedAiInsightContextCount) - setAiInsightSystemPrompt(savedAiInsightSystemPrompt) - setAiInsightTelegramEnabled(savedAiInsightTelegramEnabled) - setAiInsightTelegramToken(savedAiInsightTelegramToken) - setAiInsightTelegramChatIds(savedAiInsightTelegramChatIds) - setAiFootprintEnabled(savedAiFootprintEnabled) - setAiFootprintSystemPrompt(savedAiFootprintSystemPrompt) + const savedAiInsightAllowContext = await configService.getAiInsightAllowContext() + const savedAiInsightWhitelistEnabled = await configService.getAiInsightWhitelistEnabled() + const savedAiInsightWhitelist = await configService.getAiInsightWhitelist() + const savedAiInsightCooldownMinutes = await configService.getAiInsightCooldownMinutes() + const savedAiInsightScanIntervalHours = await configService.getAiInsightScanIntervalHours() + const savedAiInsightContextCount = await configService.getAiInsightContextCount() + const savedAiInsightSystemPrompt = await configService.getAiInsightSystemPrompt() + const savedAiInsightTelegramEnabled = await configService.getAiInsightTelegramEnabled() + const savedAiInsightTelegramToken = await configService.getAiInsightTelegramToken() + const savedAiInsightTelegramChatIds = await configService.getAiInsightTelegramChatIds() + const savedAiFootprintEnabled = await configService.getAiFootprintEnabled() + const savedAiFootprintSystemPrompt = await configService.getAiFootprintSystemPrompt() + const savedAiInsightDebugLogEnabled = await configService.getAiInsightDebugLogEnabled() + + setAiInsightEnabled(savedAiInsightEnabled) + setAiModelApiBaseUrl(savedAiModelApiBaseUrl) + setAiModelApiKey(savedAiModelApiKey) + setAiModelApiModel(savedAiModelApiModel) + setAiInsightSilenceDays(savedAiInsightSilenceDays) + setAiInsightAllowContext(savedAiInsightAllowContext) + setAiInsightWhitelistEnabled(savedAiInsightWhitelistEnabled) + setAiInsightWhitelist(new Set(savedAiInsightWhitelist)) + setAiInsightCooldownMinutes(savedAiInsightCooldownMinutes) + setAiInsightScanIntervalHours(savedAiInsightScanIntervalHours) + setAiInsightContextCount(savedAiInsightContextCount) + setAiInsightSystemPrompt(savedAiInsightSystemPrompt) + setAiInsightTelegramEnabled(savedAiInsightTelegramEnabled) + setAiInsightTelegramToken(savedAiInsightTelegramToken) + setAiInsightTelegramChatIds(savedAiInsightTelegramChatIds) + setAiFootprintEnabled(savedAiFootprintEnabled) + setAiFootprintSystemPrompt(savedAiFootprintSystemPrompt) + setAiInsightDebugLogEnabled(savedAiInsightDebugLogEnabled) } catch (e: any) { console.error('加载配置失败:', e) @@ -2722,7 +2726,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { setIsTestingInsight(true) setInsightTestResult(null) try { - const result = await (window.electronAPI as any).insight.testConnection() + const result = await window.electronAPI.insight.testConnection() setInsightTestResult(result) } catch (e: any) { setInsightTestResult({ success: false, message: `调用失败:${e?.message || String(e)}` }) @@ -2883,7 +2887,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { setIsTriggeringInsightTest(true) setInsightTriggerResult(null) try { - const result = await (window.electronAPI as any).insight.triggerTest() + const result = await window.electronAPI.insight.triggerTest() setInsightTriggerResult(result) } catch (e: any) { setInsightTriggerResult({ success: false, message: `调用失败:${e?.message || String(e)}` }) @@ -3340,6 +3344,32 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { + +
+ +
+ + + 开启后,AI 见解链路会额外把完整调试日志写到桌面上的 weflow-ai-insight-debug-YYYY-MM-DD.log。 + 其中会包含发送给 AI 的完整提示词原文、近期对话上下文原文和模型输出原文,但不会记录 API Key。 + +
+ {aiInsightDebugLogEnabled ? '已开启' : '已关闭'} + +
+
) diff --git a/src/services/config.ts b/src/services/config.ts index afbbee4..7081266 100644 --- a/src/services/config.ts +++ b/src/services/config.ts @@ -106,7 +106,8 @@ export const CONFIG_KEYS = { // AI 足迹 AI_FOOTPRINT_ENABLED: 'aiFootprintEnabled', - AI_FOOTPRINT_SYSTEM_PROMPT: 'aiFootprintSystemPrompt' + AI_FOOTPRINT_SYSTEM_PROMPT: 'aiFootprintSystemPrompt', + AI_INSIGHT_DEBUG_LOG_ENABLED: 'aiInsightDebugLogEnabled' } as const export interface WxidConfig { @@ -1803,3 +1804,12 @@ export async function getAiFootprintSystemPrompt(): Promise { export async function setAiFootprintSystemPrompt(prompt: string): Promise { await config.set(CONFIG_KEYS.AI_FOOTPRINT_SYSTEM_PROMPT, prompt) } + +export async function getAiInsightDebugLogEnabled(): Promise { + const value = await config.get(CONFIG_KEYS.AI_INSIGHT_DEBUG_LOG_ENABLED) + return value === true +} + +export async function setAiInsightDebugLogEnabled(enabled: boolean): Promise { + await config.set(CONFIG_KEYS.AI_INSIGHT_DEBUG_LOG_ENABLED, enabled) +} From f3bb548626692c2833786941688ed04c7c0c9d71 Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 12 Apr 2026 16:24:29 +0800 Subject: [PATCH 4/9] fix: clean AI insight prompt and debug log formatting --- electron/services/insightService.ts | 218 ++++++++++++++++++++-------- 1 file changed, 157 insertions(+), 61 deletions(-) diff --git a/electron/services/insightService.ts b/electron/services/insightService.ts index ff91a32..911af51 100644 --- a/electron/services/insightService.ts +++ b/electron/services/insightService.ts @@ -401,17 +401,17 @@ class InsightService { try { const endpoint = buildApiUrl(apiBaseUrl, '/chat/completions') const requestMessages = [{ role: 'user', content: '请回复"连接成功"四个字。' }] - insightDebugSection('INFO', 'AI 测试连接请求', { - endpoint, - model, - request: { - model, - messages: requestMessages, - max_tokens: API_MAX_TOKENS, - temperature: API_TEMPERATURE, - stream: false - } - }) + insightDebugSection( + 'INFO', + 'AI 测试连接请求', + [ + `Endpoint: ${endpoint}`, + `Model: ${model}`, + '', + '用户提示词:', + requestMessages[0].content + ].join('\n') + ) const result = await callApi( apiBaseUrl, @@ -423,10 +423,11 @@ class InsightService { insightDebugSection('INFO', 'AI 测试连接输出原文', result) return { success: true, message: `连接成功,模型回复:${result.slice(0, 50)}` } } catch (e) { - insightDebugSection('ERROR', 'AI 测试连接失败', { - error: (e as Error).message, - stack: (e as Error).stack ?? null - }) + insightDebugSection( + 'ERROR', + 'AI 测试连接失败', + `错误信息:${(e as Error).message}\n\n堆栈:\n${(e as Error).stack || '[无堆栈]'}` + ) return { success: false, message: `连接失败:${(e as Error).message}` } } } @@ -604,6 +605,105 @@ ${topMentionText} return { apiBaseUrl, apiKey, model } } + private looksLikeWxid(text: string): boolean { + const normalized = String(text || '').trim() + if (!normalized) return false + return /^wxid_[a-z0-9]+$/i.test(normalized) + || /^[a-z0-9_]+@chatroom$/i.test(normalized) + } + + private looksLikeXmlPayload(text: string): boolean { + const normalized = String(text || '').trim() + if (!normalized) return false + return /^(<\?xml| 1_000_000_000_000 ? createTime : createTime * 1000 + const date = new Date(ms) + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + const hours = String(date.getHours()).padStart(2, '0') + const minutes = String(date.getMinutes()).padStart(2, '0') + const seconds = String(date.getSeconds()).padStart(2, '0') + return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}` + } + + private async resolveInsightSessionDisplayName(sessionId: string, fallbackDisplayName: string): Promise { + const fallback = String(fallbackDisplayName || '').trim() + if (fallback && !this.looksLikeWxid(fallback)) { + return fallback + } + + try { + const sessions = await this.getSessionsCached() + const matched = sessions.find((session) => String(session.username || '').trim() === sessionId) + const cachedDisplayName = String(matched?.displayName || '').trim() + if (cachedDisplayName && !this.looksLikeWxid(cachedDisplayName)) { + return cachedDisplayName + } + } catch { + // ignore display name lookup failures + } + + try { + const contact = await chatService.getContactAvatar(sessionId) + const contactDisplayName = String(contact?.displayName || '').trim() + if (contactDisplayName && !this.looksLikeWxid(contactDisplayName)) { + return contactDisplayName + } + } catch { + // ignore display name lookup failures + } + + return fallback || sessionId + } + + private formatInsightMessageContent(message: Message): string { + const parsedContent = this.normalizeInsightText(String(message.parsedContent || '')) + const quotedPreview = this.normalizeInsightText(String(message.quotedContent || '')) + const quotedSender = this.normalizeInsightText(String(message.quotedSender || '')) + + if (quotedPreview) { + const cleanQuotedSender = quotedSender && !this.looksLikeWxid(quotedSender) ? quotedSender : '' + const quoteLabel = cleanQuotedSender ? `${cleanQuotedSender}:${quotedPreview}` : quotedPreview + const replyText = parsedContent && parsedContent !== '[引用消息]' ? parsedContent : '' + return replyText ? `${replyText}[引用 ${quoteLabel}]` : `[引用 ${quoteLabel}]` + } + + if (parsedContent) { + return parsedContent + } + + const rawContent = this.normalizeInsightText(String(message.rawContent || '')) + if (rawContent && !this.looksLikeXmlPayload(rawContent)) { + return rawContent + } + + return '[其他消息]' + } + + private buildInsightContextSection(messages: Message[], peerDisplayName: string): string { + if (!messages.length) return '' + + const lines = messages.map((message) => { + const senderName = message.isSend === 1 ? '我' : peerDisplayName + const content = this.formatInsightMessageContent(message) + return `${this.formatInsightMessageTimestamp(message.createTime)} '${senderName}'\n${content}` + }) + + return `近期聊天记录(最近 ${lines.length} 条):\n\n${lines.join('\n\n')}` + } + /** * 判断某个会话是否允许触发见解。 * 若白名单未启用,则所有私聊会话均允许; @@ -899,6 +999,7 @@ ${topMentionText} const { apiBaseUrl, apiKey, model } = this.getSharedAiModelConfig() const allowContext = this.config.get('aiInsightAllowContext') as boolean const contextCount = (this.config.get('aiInsightContextCount') as number) || 40 + const resolvedDisplayName = await this.resolveInsightSessionDisplayName(sessionId, displayName) insightLog('INFO', `generateInsightForSession: sessionId=${sessionId}, reason=${triggerReason}, contextCount=${contextCount}, api=${apiBaseUrl ? '已配置' : '未配置'}`) @@ -919,14 +1020,8 @@ ${topMentionText} const msgsResult = await chatService.getLatestMessages(sessionId, contextCount) if (msgsResult.success && msgsResult.messages && msgsResult.messages.length > 0) { const messages: Message[] = msgsResult.messages - const msgLines = messages.map((m) => { - const sender = m.isSend === 1 ? '我' : (displayName || sessionId) - const content = m.rawContent || m.parsedContent || '[非文字消息]' - const time = new Date(Number(m.createTime) * 1000).toLocaleString('zh-CN') - return `[${time}] ${sender}:${content}` - }) - contextSection = `\n\n近期对话记录(最近 ${msgLines.length} 条):\n${msgLines.join('\n')}` - insightLog('INFO', `已加载 ${msgLines.length} 条上下文消息`) + contextSection = this.buildInsightContextSection(messages, resolvedDisplayName) + insightLog('INFO', `已加载 ${messages.length} 条上下文消息`) } } catch (e) { insightLog('WARN', `拉取上下文失败: ${(e as Error).message}`) @@ -950,20 +1045,23 @@ ${topMentionText} // 这样 provider 端(Anthropic/OpenAI)能最大化命中 prompt cache,降低费用 const triggerDesc = triggerReason === 'silence' - ? `你已经 ${silentDays} 天没有和「${displayName}」聊天了。` - : `你最近和「${displayName}」有新的聊天动态。` + ? `你已经 ${silentDays} 天没有和「${resolvedDisplayName}」聊天了。` + : `你最近和「${resolvedDisplayName}」有新的聊天动态。` const todayStatsDesc = sessionTriggerTimes.length > 1 - ? `今天你已经针对「${displayName}」收到过 ${sessionTriggerTimes.length - 1} 条见解(时间:${sessionTriggerTimes.slice(0, -1).join('、')}),请适当克制。` - : `今天你还没有针对「${displayName}」发出过见解。` + ? `今天你已经针对「${resolvedDisplayName}」收到过 ${sessionTriggerTimes.length - 1} 条见解(时间:${sessionTriggerTimes.slice(0, -1).join('、')}),请适当克制。` + : `今天你还没有针对「${resolvedDisplayName}」发出过见解。` const globalStatsDesc = `今天全部联系人合计已触发 ${totalTodayTriggers} 条见解。` - const userPrompt = `触发原因:${triggerDesc} -时间统计:${todayStatsDesc} ${globalStatsDesc}${contextSection} - -请给出你的见解(≤80字):` + const userPrompt = [ + `触发原因:${triggerDesc}`, + `时间统计:${todayStatsDesc}`, + `全局统计:${globalStatsDesc}`, + contextSection, + '请给出你的见解(≤80字):' + ].filter(Boolean).join('\n\n') const endpoint = buildApiUrl(apiBaseUrl, '/chat/completions') const requestMessages = [ @@ -972,23 +1070,23 @@ ${topMentionText} ] insightLog('INFO', `准备调用 API: ${endpoint},模型: ${model}`) - insightDebugSection('INFO', `AI 请求 ${displayName} (${sessionId})`, { - sessionId, - displayName, - triggerReason, - silentDays: silentDays ?? null, - endpoint, - model, - allowContext, - contextCount, - request: { - model, - messages: requestMessages, - max_tokens: API_MAX_TOKENS, - temperature: API_TEMPERATURE, - stream: false - } - }) + insightDebugSection( + 'INFO', + `AI 请求 ${resolvedDisplayName} (${sessionId})`, + [ + `接口地址:${endpoint}`, + `模型:${model}`, + `触发原因:${triggerReason}`, + `上下文开关:${allowContext ? '开启' : '关闭'}`, + `上下文条数:${contextCount}`, + '', + '系统提示词:', + systemPrompt, + '', + '用户提示词:', + userPrompt + ].join('\n') + ) try { const result = await callApi( @@ -999,19 +1097,19 @@ ${topMentionText} ) insightLog('INFO', `API 返回原文: ${result.slice(0, 150)}`) - insightDebugSection('INFO', `AI 输出原文 ${displayName} (${sessionId})`, result) + insightDebugSection('INFO', `AI 输出原文 ${resolvedDisplayName} (${sessionId})`, result) // 模型主动选择跳过 if (result.trim().toUpperCase() === 'SKIP' || result.trim().startsWith('SKIP')) { - insightLog('INFO', `模型选择跳过 ${displayName}`) + insightLog('INFO', `模型选择跳过 ${resolvedDisplayName}`) return } if (!this.isEnabled()) return const insight = result.slice(0, 120) - const notifTitle = `见解 · ${displayName}` + const notifTitle = `见解 · ${resolvedDisplayName}` - insightLog('INFO', `推送通知 → ${displayName}: ${insight}`) + insightLog('INFO', `推送通知 → ${resolvedDisplayName}: ${insight}`) // 渠道一:Electron 原生系统通知 if (Notification.isSupported()) { @@ -1039,16 +1137,14 @@ ${topMentionText} } } - insightLog('INFO', `已为 ${displayName} 推送见解`) + insightLog('INFO', `已为 ${resolvedDisplayName} 推送见解`) } catch (e) { - insightDebugSection('ERROR', `AI 请求失败 ${displayName} (${sessionId})`, { - sessionId, - displayName, - triggerReason, - error: (e as Error).message, - stack: (e as Error).stack ?? null - }) - insightLog('ERROR', `API 调用失败 (${displayName}): ${(e as Error).message}`) + insightDebugSection( + 'ERROR', + `AI 请求失败 ${resolvedDisplayName} (${sessionId})`, + `错误信息:${(e as Error).message}\n\n堆栈:\n${(e as Error).stack || '[无堆栈]'}` + ) + insightLog('ERROR', `API 调用失败 (${resolvedDisplayName}): ${(e as Error).message}`) } } From 86daa8ef06166783b8abc245d99bf28b1def190a Mon Sep 17 00:00:00 2001 From: cc <98377878+hicccc77@users.noreply.github.com> Date: Sun, 12 Apr 2026 23:37:26 +0800 Subject: [PATCH 5/9] =?UTF-8?q?=E6=94=AF=E6=8C=81=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E5=8C=96=E6=9D=A1=E4=BB=B6=E5=AF=BC=E5=87=BA=EF=BC=9B=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E5=BC=95=E5=AF=BC=E9=A1=B5=E9=9D=A2=E6=8F=90=E7=A4=BA?= =?UTF-8?q?=EF=BC=9B=E6=94=AF=E6=8C=81=E5=BF=AB=E9=80=9F=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E8=B4=A6=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- electron/main.ts | 53 +- electron/preload.ts | 2 +- electron/services/config.ts | 2 + electron/services/keyService.ts | 6 +- electron/services/keyServiceLinux.ts | 41 +- electron/services/keyServiceMac.ts | 6 +- src/App.tsx | 2 + .../Export/ExportDateRangeDialog.scss | 2 +- src/components/Sidebar.scss | 257 --- src/components/Sidebar.tsx | 263 +-- src/pages/AccountManagementPage.scss | 274 +++ src/pages/AccountManagementPage.tsx | 574 +++++++ src/pages/ExportPage.scss | 924 +++++++++-- src/pages/ExportPage.tsx | 1469 ++++++++++++++++- src/pages/WelcomePage.scss | 58 + src/pages/WelcomePage.tsx | 265 ++- src/services/config.ts | 183 ++ src/types/electron.d.ts | 4 +- src/types/exportAutomation.ts | 68 + 19 files changed, 3765 insertions(+), 688 deletions(-) create mode 100644 src/pages/AccountManagementPage.scss create mode 100644 src/pages/AccountManagementPage.tsx create mode 100644 src/types/exportAutomation.ts diff --git a/electron/main.ts b/electron/main.ts index f6a873a..2794d19 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -950,8 +950,17 @@ function closeSplash() { /** * 创建首次引导窗口 */ -function createOnboardingWindow() { +function createOnboardingWindow(mode: 'default' | 'add-account' = 'default') { + const onboardingHash = mode === 'add-account' + ? '/onboarding-window?mode=add-account' + : '/onboarding-window' + if (onboardingWindow && !onboardingWindow.isDestroyed()) { + if (process.env.VITE_DEV_SERVER_URL) { + onboardingWindow.loadURL(`${process.env.VITE_DEV_SERVER_URL}#${onboardingHash}`) + } else { + onboardingWindow.loadFile(join(__dirname, '../dist/index.html'), { hash: onboardingHash }) + } onboardingWindow.focus() return onboardingWindow } @@ -987,9 +996,9 @@ function createOnboardingWindow() { }) if (process.env.VITE_DEV_SERVER_URL) { - onboardingWindow.loadURL(`${process.env.VITE_DEV_SERVER_URL}#/onboarding-window`) + onboardingWindow.loadURL(`${process.env.VITE_DEV_SERVER_URL}#${onboardingHash}`) } else { - onboardingWindow.loadFile(join(__dirname, '../dist/index.html'), { hash: '/onboarding-window' }) + onboardingWindow.loadFile(join(__dirname, '../dist/index.html'), { hash: onboardingHash }) } onboardingWindow.on('closed', () => { @@ -2260,6 +2269,39 @@ function registerIpcHandlers() { const defaultValue = key === 'lastSession' ? '' : {} cfg.set(key as any, defaultValue as any) } + + try { + const dbPath = String(cfg.get('dbPath') || '').trim() + const automationMapRaw = cfg.get('exportAutomationTaskMap') as Record | undefined + if (automationMapRaw && typeof automationMapRaw === 'object') { + const nextAutomationMap: Record = { ...automationMapRaw } + let changed = false + for (const scopeKey of Object.keys(automationMapRaw)) { + const normalizedScopeKey = String(scopeKey || '').trim() + if (!normalizedScopeKey) continue + const separatorIndex = normalizedScopeKey.lastIndexOf('::') + const scopedDbPath = separatorIndex >= 0 + ? normalizedScopeKey.slice(0, separatorIndex) + : '' + const scopedWxidRaw = separatorIndex >= 0 + ? normalizedScopeKey.slice(separatorIndex + 2) + : normalizedScopeKey + const scopedWxid = normalizeAccountId(scopedWxidRaw) + const wxidMatched = wxidCandidates.includes(scopedWxidRaw) || scopedWxid === normalizedWxid + const dbPathMatched = !dbPath || !scopedDbPath || scopedDbPath === dbPath + if (!wxidMatched || !dbPathMatched) continue + delete nextAutomationMap[scopeKey] + changed = true + } + if (changed) { + cfg.set('exportAutomationTaskMap' as any, nextAutomationMap as any) + } else if (!Object.keys(automationMapRaw).length) { + cfg.set('exportAutomationTaskMap' as any, {} as any) + } + } + } catch (error) { + warnings.push(`清理自动化导出任务失败: ${String(error)}`) + } } if (clearCache) { @@ -3019,12 +3061,13 @@ function registerIpcHandlers() { }) // 重新打开首次引导窗口,并隐藏主窗口 - ipcMain.handle('window:openOnboardingWindow', async () => { + ipcMain.handle('window:openOnboardingWindow', async (_, options?: { mode?: 'add-account' }) => { shouldShowMain = false if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.hide() } - createOnboardingWindow() + const mode = options?.mode === 'add-account' ? 'add-account' : 'default' + createOnboardingWindow(mode) return true }) diff --git a/electron/preload.ts b/electron/preload.ts index 838a305..9739332 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -110,7 +110,7 @@ contextBridge.exposeInMainWorld('electronAPI', { ipcRenderer.invoke('window:respondCloseConfirm', action), openAgreementWindow: () => ipcRenderer.invoke('window:openAgreementWindow'), completeOnboarding: () => ipcRenderer.invoke('window:completeOnboarding'), - openOnboardingWindow: () => ipcRenderer.invoke('window:openOnboardingWindow'), + openOnboardingWindow: (options?: { mode?: 'add-account' }) => ipcRenderer.invoke('window:openOnboardingWindow', options), setTitleBarOverlay: (options: { symbolColor: string }) => ipcRenderer.send('window:setTitleBarOverlay', options), openVideoPlayerWindow: (videoPath: string, videoWidth?: number, videoHeight?: number) => ipcRenderer.invoke('window:openVideoPlayerWindow', videoPath, videoWidth, videoHeight), diff --git a/electron/services/config.ts b/electron/services/config.ts index 250c93d..2c1aa97 100644 --- a/electron/services/config.ts +++ b/electron/services/config.ts @@ -71,6 +71,7 @@ interface ConfigSchema { quoteLayout: 'quote-top' | 'quote-bottom' wordCloudExcludeWords: string[] exportWriteLayout: 'A' | 'B' | 'C' + exportAutomationTaskMap: Record // AI 见解 aiModelApiBaseUrl: string @@ -185,6 +186,7 @@ export class ConfigService { quoteLayout: 'quote-top', wordCloudExcludeWords: [], exportWriteLayout: 'A', + exportAutomationTaskMap: {}, aiModelApiBaseUrl: '', aiModelApiKey: '', aiModelApiModel: 'gpt-4o-mini', diff --git a/electron/services/keyService.ts b/electron/services/keyService.ts index 72c827c..37242a3 100644 --- a/electron/services/keyService.ts +++ b/electron/services/keyService.ts @@ -9,7 +9,7 @@ import crypto from 'crypto' const execFileAsync = promisify(execFile) type DbKeyResult = { success: boolean; key?: string; error?: string; logs?: string[] } -type ImageKeyResult = { success: boolean; xorKey?: number; aesKey?: string; error?: string } +type ImageKeyResult = { success: boolean; xorKey?: number; aesKey?: string; verified?: boolean; error?: string } export class KeyService { private readonly isMac = process.platform === 'darwin' @@ -814,7 +814,7 @@ export class KeyService { if (!this.verifyDerivedAesKey(aesKey, verifyCiphertext)) continue onProgress?.(`密钥获取成功 (wxid: ${candidateWxid}, code: ${code})`) console.log('[ImageKey] 校验命中: wxid=', candidateWxid, 'code=', code) - return { success: true, xorKey, aesKey } + return { success: true, xorKey, aesKey, verified: true } } } return { success: false, error: '缓存 code 与当前账号 wxid 未匹配,请确认账号目录后重试,或使用内存扫描' } @@ -826,7 +826,7 @@ export class KeyService { const { xorKey, aesKey } = this.deriveImageKeys(fallbackCode, fallbackWxid) onProgress?.(`密钥获取成功 (wxid: ${fallbackWxid}, code: ${fallbackCode})`) console.log('[ImageKey] 回退计算: wxid=', fallbackWxid, 'code=', fallbackCode) - return { success: true, xorKey, aesKey } + return { success: true, xorKey, aesKey, verified: false } } // --- 内存扫描备选方案(融合 Dart+Python 优点)--- diff --git a/electron/services/keyServiceLinux.ts b/electron/services/keyServiceLinux.ts index 0e94d6c..e4b5088 100644 --- a/electron/services/keyServiceLinux.ts +++ b/electron/services/keyServiceLinux.ts @@ -3,6 +3,7 @@ import { join } from 'path' import { existsSync, readdirSync, statSync, readFileSync } from 'fs' import { execFile, exec, spawn } from 'child_process' import { promisify } from 'util' +import crypto from 'crypto' import { createRequire } from 'module'; const require = createRequire(import.meta.url); @@ -10,7 +11,7 @@ const execFileAsync = promisify(execFile) const execAsync = promisify(exec) type DbKeyResult = { success: boolean; key?: string; error?: string; logs?: string[] } -type ImageKeyResult = { success: boolean; xorKey?: number; aesKey?: string; error?: string } +type ImageKeyResult = { success: boolean; xorKey?: number; aesKey?: string; verified?: boolean; error?: string } export class KeyServiceLinux { private sudo: any @@ -243,7 +244,14 @@ export class KeyServiceLinux { if (account && account.keys && account.keys.length > 0) { onProgress?.(`已找到匹配的图片密钥 (wxid: ${account.wxid})`); const keyObj = account.keys[0] - return { success: true, xorKey: keyObj.xorKey, aesKey: keyObj.aesKey } + const aesKey = String(keyObj.aesKey || '') + const verified = await this.verifyImageKeyByTemplate(accountPath, aesKey) + if (verified === true) { + onProgress?.('缓存密钥校验成功,已确认可用') + } else if (verified === false) { + onProgress?.('已从缓存计算密钥,但未通过本地模板校验') + } + return { success: true, xorKey: keyObj.xorKey, aesKey, verified: verified === true } } return { success: false, error: '未在缓存中找到匹配的图片密钥' } } catch (err: any) { @@ -251,6 +259,35 @@ export class KeyServiceLinux { } } + private async verifyImageKeyByTemplate(accountPath: string | undefined, aesKey: string): Promise { + const normalizedPath = String(accountPath || '').trim() + if (!normalizedPath || !aesKey || aesKey.length < 16 || !existsSync(normalizedPath)) return null + try { + const template = await this._findTemplateData(normalizedPath, 32) + if (!template.ciphertext) return null + return this.verifyDerivedAesKey(aesKey, template.ciphertext) + } catch { + return null + } + } + + private verifyDerivedAesKey(aesKey: string, ciphertext: Buffer): boolean { + try { + if (!aesKey || aesKey.length < 16 || ciphertext.length !== 16) return false + const decipher = crypto.createDecipheriv('aes-128-ecb', Buffer.from(aesKey, 'ascii').subarray(0, 16), null) + decipher.setAutoPadding(false) + const dec = Buffer.concat([decipher.update(ciphertext), decipher.final()]) + if (dec[0] === 0xFF && dec[1] === 0xD8 && dec[2] === 0xFF) return true + if (dec[0] === 0x89 && dec[1] === 0x50 && dec[2] === 0x4E && dec[3] === 0x47) return true + if (dec[0] === 0x52 && dec[1] === 0x49 && dec[2] === 0x46 && dec[3] === 0x46) return true + if (dec[0] === 0x77 && dec[1] === 0x78 && dec[2] === 0x67 && dec[3] === 0x66) return true + if (dec[0] === 0x47 && dec[1] === 0x49 && dec[2] === 0x46) return true + return false + } catch { + return false + } + } + public async autoGetImageKeyByMemoryScan( accountPath: string, onProgress?: (msg: string) => void diff --git a/electron/services/keyServiceMac.ts b/electron/services/keyServiceMac.ts index fd95372..9900ec3 100644 --- a/electron/services/keyServiceMac.ts +++ b/electron/services/keyServiceMac.ts @@ -7,7 +7,7 @@ import crypto from 'crypto' import { homedir } from 'os' type DbKeyResult = { success: boolean; key?: string; error?: string; logs?: string[] } -type ImageKeyResult = { success: boolean; xorKey?: number; aesKey?: string; error?: string } +type ImageKeyResult = { success: boolean; xorKey?: number; aesKey?: string; verified?: boolean; error?: string } const execFileAsync = promisify(execFile) export class KeyServiceMac { @@ -647,7 +647,7 @@ export class KeyServiceMac { const { xorKey, aesKey } = this.deriveImageKeys(code, candidateWxid) if (!this.verifyDerivedAesKey(aesKey, template.ciphertext)) continue onStatus?.(`密钥获取成功 (wxid: ${candidateWxid}, code: ${code})`) - return { success: true, xorKey, aesKey } + return { success: true, xorKey, aesKey, verified: true } } } } @@ -662,7 +662,7 @@ export class KeyServiceMac { const fallbackCode = codes[0] const { xorKey, aesKey } = this.deriveImageKeys(fallbackCode, fallbackWxid) onStatus?.(`密钥获取成功 (wxid: ${fallbackWxid}, code: ${fallbackCode})`) - return { success: true, xorKey, aesKey } + return { success: true, xorKey, aesKey, verified: false } } catch (e: any) { return { success: false, error: `自动获取图片密钥失败: ${e.message}` } } diff --git a/src/App.tsx b/src/App.tsx index 8cfb8f4..a0f11d4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -26,6 +26,7 @@ import ContactsPage from './pages/ContactsPage' import ResourcesPage from './pages/ResourcesPage' import ChatHistoryPage from './pages/ChatHistoryPage' import NotificationWindow from './pages/NotificationWindow' +import AccountManagementPage from './pages/AccountManagementPage' import { useAppStore } from './stores/appStore' import { themes, useThemeStore, type ThemeId, type ThemeMode } from './stores/themeStore' @@ -678,6 +679,7 @@ function App() { } /> } /> + } /> } /> } /> diff --git a/src/components/Export/ExportDateRangeDialog.scss b/src/components/Export/ExportDateRangeDialog.scss index 215520e..458c7e4 100644 --- a/src/components/Export/ExportDateRangeDialog.scss +++ b/src/components/Export/ExportDateRangeDialog.scss @@ -6,7 +6,7 @@ align-items: center; justify-content: center; padding: 16px; - z-index: 2400; + z-index: 9200; } .export-date-range-dialog { diff --git a/src/components/Sidebar.scss b/src/components/Sidebar.scss index 31d4725..5f153ee 100644 --- a/src/components/Sidebar.scss +++ b/src/components/Sidebar.scss @@ -275,263 +275,6 @@ gap: 4px; } -.sidebar-dialog-overlay { - position: fixed; - inset: 0; - background: rgba(15, 23, 42, 0.3); - display: flex; - align-items: center; - justify-content: center; - z-index: 1100; - padding: 20px; - animation: fadeIn 0.2s ease; -} - -@keyframes fadeIn { - from { - opacity: 0; - } - to { - opacity: 1; - } -} - -.sidebar-dialog { - width: min(420px, 100%); - background: var(--bg-secondary); - border: 1px solid var(--border-color); - border-radius: 16px; - box-shadow: 0 18px 40px rgba(15, 23, 42, 0.24); - padding: 18px 18px 16px; - animation: slideUp 0.25s ease; - - h3 { - margin: 0; - font-size: 16px; - color: var(--text-primary); - } - - p { - margin: 10px 0 0; - font-size: 13px; - line-height: 1.6; - color: var(--text-secondary); - } -} - -@keyframes slideUp { - from { - opacity: 0; - transform: translateY(20px) scale(0.95); - } - to { - opacity: 1; - transform: translateY(0) scale(1); - } -} - -.sidebar-wxid-list { - margin-top: 14px; - display: flex; - flex-direction: column; - gap: 8px; - max-height: 300px; - overflow-y: auto; -} - -.sidebar-wxid-item { - width: 100%; - padding: 12px 14px; - border: 1px solid var(--border-color); - border-radius: 10px; - background: var(--bg-secondary); - color: var(--text-primary); - font-size: 13px; - cursor: pointer; - display: flex; - align-items: center; - gap: 12px; - transition: all 0.2s ease; - - &:hover:not(:disabled) { - border-color: rgba(99, 102, 241, 0.32); - background: var(--bg-tertiary); - } - - &.current { - border-color: rgba(99, 102, 241, 0.5); - background: var(--bg-tertiary); - } - - &:disabled { - cursor: not-allowed; - opacity: 0.6; - } - - .wxid-avatar { - width: 40px; - height: 40px; - border-radius: 10px; - overflow: hidden; - background: linear-gradient(135deg, var(--primary), var(--primary-hover)); - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - - img { - width: 100%; - height: 100%; - object-fit: cover; - } - - span { - color: var(--on-primary); - font-size: 16px; - font-weight: 600; - } - } - - .wxid-info { - flex: 1; - min-width: 0; - text-align: left; - } - - .wxid-name { - font-size: 14px; - font-weight: 600; - color: var(--text-primary); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - .wxid-id { - margin-top: 2px; - font-size: 12px; - color: var(--text-tertiary); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - .current-badge { - padding: 4px 10px; - border-radius: 6px; - background: var(--primary); - color: var(--on-primary); - font-size: 11px; - font-weight: 600; - flex-shrink: 0; - } -} - -.sidebar-dialog-actions { - margin-top: 18px; - display: flex; - justify-content: flex-end; - gap: 10px; - - button { - border: 1px solid var(--border-color); - border-radius: 10px; - padding: 8px 14px; - font-size: 13px; - cursor: pointer; - background: var(--bg-secondary); - color: var(--text-primary); - transition: all 0.2s ease; - - &:hover:not(:disabled) { - background: var(--bg-tertiary); - } - - &:disabled { - cursor: not-allowed; - opacity: 0.6; - } - } -} - -.sidebar-clear-dialog-overlay { - position: fixed; - inset: 0; - background: rgba(15, 23, 42, 0.3); - display: flex; - align-items: center; - justify-content: center; - z-index: 1100; - padding: 20px; - animation: fadeIn 0.2s ease; -} - -.sidebar-clear-dialog { - width: min(460px, 100%); - background: var(--bg-secondary); - border: 1px solid var(--border-color); - border-radius: 16px; - box-shadow: 0 18px 40px rgba(15, 23, 42, 0.24); - padding: 18px 18px 16px; - animation: slideUp 0.25s ease; - - h3 { - margin: 0; - font-size: 16px; - color: var(--text-primary); - } - - p { - margin: 10px 0 0; - font-size: 13px; - line-height: 1.6; - color: var(--text-secondary); - } -} - -.sidebar-clear-options { - margin-top: 14px; - display: flex; - gap: 14px; - flex-wrap: wrap; - - label { - display: inline-flex; - align-items: center; - gap: 6px; - font-size: 13px; - color: var(--text-primary); - } -} - -.sidebar-clear-actions { - margin-top: 18px; - display: flex; - justify-content: flex-end; - gap: 10px; - - button { - border: 1px solid var(--border-color); - border-radius: 10px; - padding: 8px 14px; - font-size: 13px; - cursor: pointer; - background: var(--bg-secondary); - color: var(--text-primary); - } - - button:disabled { - cursor: not-allowed; - opacity: 0.6; - } - - .danger { - border-color: #ef4444; - background: #ef4444; - color: #fff; - } -} - // 繁花如梦主题:侧边栏毛玻璃 + 激活项用主品牌色 [data-theme="blossom-dream"] .sidebar { background: rgba(255, 255, 255, 0.6); diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 9a9f0aa..4b9a0e7 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -1,12 +1,9 @@ import { useState, useEffect, useRef } from 'react' import { NavLink, useLocation, useNavigate } from 'react-router-dom' -import { Home, MessageSquare, BarChart3, FileText, Settings, Download, Aperture, UserCircle, Lock, LockOpen, ChevronUp, RefreshCw, FolderClosed, Footprints } from 'lucide-react' +import { Home, MessageSquare, BarChart3, FileText, Settings, Download, Aperture, UserCircle, Lock, LockOpen, ChevronUp, FolderClosed, Footprints, Users } from 'lucide-react' import { useAppStore } from '../stores/appStore' -import { useChatStore } from '../stores/chatStore' -import { useAnalyticsStore } from '../stores/analyticsStore' import * as configService from '../services/config' import { onExportSessionStatus, requestExportSessionStatus } from '../services/exportBridge' -import { UserRound } from 'lucide-react' import './Sidebar.scss' @@ -19,6 +16,8 @@ interface SidebarUserProfile { const SIDEBAR_USER_PROFILE_CACHE_KEY = 'sidebar_user_profile_cache_v1' const ACCOUNT_PROFILES_CACHE_KEY = 'account_profiles_cache_v1' +const DEFAULT_DISPLAY_NAME = '微信用户' +const DEFAULT_SUBTITLE = '微信账号' interface SidebarUserProfileCache extends SidebarUserProfile { updatedAt: number @@ -33,24 +32,16 @@ interface AccountProfilesCache { } } -interface WxidOption { - wxid: string - modifiedTime: number - nickname?: string - displayName?: string - avatarUrl?: string -} - const readSidebarUserProfileCache = (): SidebarUserProfile | null => { try { const raw = window.localStorage.getItem(SIDEBAR_USER_PROFILE_CACHE_KEY) if (!raw) return null const parsed = JSON.parse(raw) as SidebarUserProfileCache if (!parsed || typeof parsed !== 'object') return null - if (!parsed.wxid || !parsed.displayName) return null + if (!parsed.wxid) return null return { wxid: parsed.wxid, - displayName: parsed.displayName, + displayName: typeof parsed.displayName === 'string' ? parsed.displayName : '', alias: parsed.alias, avatarUrl: parsed.avatarUrl } @@ -60,7 +51,7 @@ const readSidebarUserProfileCache = (): SidebarUserProfile | null => { } const writeSidebarUserProfileCache = (profile: SidebarUserProfile): void => { - if (!profile.wxid || !profile.displayName) return + if (!profile.wxid) return try { const payload: SidebarUserProfileCache = { ...profile, @@ -115,17 +106,11 @@ function Sidebar({ collapsed }: SidebarProps) { const [activeExportTaskCount, setActiveExportTaskCount] = useState(0) const [userProfile, setUserProfile] = useState({ wxid: '', - displayName: '未识别用户' + displayName: DEFAULT_DISPLAY_NAME }) const [isAccountMenuOpen, setIsAccountMenuOpen] = useState(false) - const [showSwitchAccountDialog, setShowSwitchAccountDialog] = useState(false) - const [wxidOptions, setWxidOptions] = useState([]) - const [isSwitchingAccount, setIsSwitchingAccount] = useState(false) const accountCardWrapRef = useRef(null) const setLocked = useAppStore(state => state.setLocked) - const isDbConnected = useAppStore(state => state.isDbConnected) - const resetChatStore = useChatStore(state => state.reset) - const clearAnalyticsStoreCache = useAnalyticsStore(state => state.clearCache) useEffect(() => { window.electronAPI.auth.verifyEnabled().then(setAuthEnabled) @@ -164,18 +149,20 @@ function Sidebar({ collapsed }: SidebarProps) { }, []) useEffect(() => { + let disposed = false + let loadSeq = 0 + const loadCurrentUser = async () => { - const patchUserProfile = (patch: Partial, expectedWxid?: string) => { + const seq = ++loadSeq + const patchUserProfile = (patch: Partial) => { + if (disposed || seq !== loadSeq) return setUserProfile(prev => { - if (expectedWxid && prev.wxid && prev.wxid !== expectedWxid) { - return prev - } const next: SidebarUserProfile = { ...prev, ...patch } - if (!next.displayName) { - next.displayName = next.wxid || '未识别用户' + if (typeof next.displayName !== 'string' || next.displayName.length === 0) { + next.displayName = DEFAULT_DISPLAY_NAME } writeSidebarUserProfileCache(next) return next @@ -184,11 +171,33 @@ function Sidebar({ collapsed }: SidebarProps) { try { const wxid = await configService.getMyWxid() + if (disposed || seq !== loadSeq) return const resolvedWxidRaw = String(wxid || '').trim() const cleanedWxid = normalizeAccountId(resolvedWxidRaw) const resolvedWxid = cleanedWxid || resolvedWxidRaw - if (!resolvedWxidRaw && !resolvedWxid) return + if (!resolvedWxidRaw && !resolvedWxid) { + window.localStorage.removeItem(SIDEBAR_USER_PROFILE_CACHE_KEY) + patchUserProfile({ + wxid: '', + displayName: DEFAULT_DISPLAY_NAME, + alias: undefined, + avatarUrl: undefined + }) + return + } + + setUserProfile((prev) => { + if (prev.wxid === resolvedWxid) return prev + const seeded: SidebarUserProfile = { + wxid: resolvedWxid, + displayName: DEFAULT_DISPLAY_NAME, + alias: undefined, + avatarUrl: undefined + } + writeSidebarUserProfileCache(seeded) + return seeded + }) const wxidCandidates = new Set([ resolvedWxidRaw.toLowerCase(), @@ -197,14 +206,13 @@ function Sidebar({ collapsed }: SidebarProps) { ].filter(Boolean)) const normalizeName = (value?: string | null): string | undefined => { - if (!value) return undefined - const trimmed = value.trim() - if (!trimmed) return undefined - const lowered = trimmed.toLowerCase() + if (typeof value !== 'string') return undefined + if (value.length === 0) return undefined + const lowered = value.trim().toLowerCase() if (lowered === 'self') return undefined if (lowered.startsWith('wxid_')) return undefined if (wxidCandidates.has(lowered)) return undefined - return trimmed + return value } const pickFirstValidName = (...candidates: Array): string | undefined => { @@ -229,18 +237,20 @@ function Sidebar({ collapsed }: SidebarProps) { })(), window.electronAPI.chat.getMyAvatarUrl() ]) + if (disposed || seq !== loadSeq) return const myContact = contactResult.status === 'fulfilled' ? contactResult.value : null const displayName = pickFirstValidName( myContact?.remark, myContact?.nickName, myContact?.alias - ) || resolvedWxid || '未识别用户' + ) || DEFAULT_DISPLAY_NAME + const alias = normalizeName(myContact?.alias) patchUserProfile({ wxid: resolvedWxid, displayName, - alias: myContact?.alias, + alias, avatarUrl: avatarResult.status === 'fulfilled' && avatarResult.value.success ? avatarResult.value.avatarUrl : undefined @@ -257,118 +267,28 @@ function Sidebar({ collapsed }: SidebarProps) { void loadCurrentUser() const onWxidChanged = () => { void loadCurrentUser() } + const onWindowFocus = () => { void loadCurrentUser() } + const onVisibilityChange = () => { + if (document.visibilityState === 'visible') { + void loadCurrentUser() + } + } window.addEventListener('wxid-changed', onWxidChanged as EventListener) - return () => window.removeEventListener('wxid-changed', onWxidChanged as EventListener) + window.addEventListener('focus', onWindowFocus) + document.addEventListener('visibilitychange', onVisibilityChange) + return () => { + disposed = true + loadSeq += 1 + window.removeEventListener('wxid-changed', onWxidChanged as EventListener) + window.removeEventListener('focus', onWindowFocus) + document.removeEventListener('visibilitychange', onVisibilityChange) + } }, []) const getAvatarLetter = (name: string): string => { - if (!name) return '?' - return [...name][0] || '?' - } - - const openSwitchAccountDialog = async () => { - setIsAccountMenuOpen(false) - if (!isDbConnected) { - window.alert('数据库未连接,无法切换账号') - return - } - const dbPath = await configService.getDbPath() - if (!dbPath) { - window.alert('请先在设置中配置数据库路径') - return - } - try { - const wxids = await window.electronAPI.dbPath.scanWxids(dbPath) - const accountsCache = readAccountProfilesCache() - console.log('[切换账号] 账号缓存:', accountsCache) - - const enrichedWxids = wxids.map((option: WxidOption) => { - const normalizedWxid = normalizeAccountId(option.wxid) - const cached = accountsCache[option.wxid] || accountsCache[normalizedWxid] - - let displayName = option.nickname || option.wxid - let avatarUrl = option.avatarUrl - - if (option.wxid === userProfile.wxid || normalizedWxid === userProfile.wxid) { - displayName = userProfile.displayName || displayName - avatarUrl = userProfile.avatarUrl || avatarUrl - } - - else if (cached) { - displayName = cached.displayName || displayName - avatarUrl = cached.avatarUrl || avatarUrl - } - - return { - ...option, - displayName, - avatarUrl - } - }) - - setWxidOptions(enrichedWxids) - setShowSwitchAccountDialog(true) - } catch (error) { - console.error('扫描账号失败:', error) - window.alert('扫描账号失败,请稍后重试') - } - } - - const handleSwitchAccount = async (selectedWxid: string) => { - if (!selectedWxid || isSwitchingAccount) return - setIsSwitchingAccount(true) - try { - console.log('[切换账号] 开始切换到:', selectedWxid) - const currentWxid = userProfile.wxid - if (currentWxid === selectedWxid) { - console.log('[切换账号] 已经是当前账号,跳过') - setShowSwitchAccountDialog(false) - setIsSwitchingAccount(false) - return - } - - console.log('[切换账号] 设置新 wxid') - await configService.setMyWxid(selectedWxid) - - console.log('[切换账号] 获取账号配置') - const wxidConfig = await configService.getWxidConfig(selectedWxid) - console.log('[切换账号] 配置内容:', wxidConfig) - if (wxidConfig?.decryptKey) { - console.log('[切换账号] 设置 decryptKey') - await configService.setDecryptKey(wxidConfig.decryptKey) - } - if (typeof wxidConfig?.imageXorKey === 'number') { - console.log('[切换账号] 设置 imageXorKey:', wxidConfig.imageXorKey) - await configService.setImageXorKey(wxidConfig.imageXorKey) - } - if (wxidConfig?.imageAesKey) { - console.log('[切换账号] 设置 imageAesKey') - await configService.setImageAesKey(wxidConfig.imageAesKey) - } - - console.log('[切换账号] 检查数据库连接状态') - console.log('[切换账号] 数据库连接状态:', isDbConnected) - if (isDbConnected) { - console.log('[切换账号] 关闭数据库连接') - await window.electronAPI.chat.close() - } - - console.log('[切换账号] 清除缓存') - window.localStorage.removeItem(SIDEBAR_USER_PROFILE_CACHE_KEY) - clearAnalyticsStoreCache() - resetChatStore() - - console.log('[切换账号] 触发 wxid-changed 事件') - window.dispatchEvent(new CustomEvent('wxid-changed', { detail: { wxid: selectedWxid } })) - - console.log('[切换账号] 切换成功') - setShowSwitchAccountDialog(false) - } catch (error) { - console.error('[切换账号] 失败:', error) - window.alert('切换账号失败,请稍后重试') - } finally { - setIsSwitchingAccount(false) - } + if (!name) return '微' + const visible = name.trim() + return (visible && [...visible][0]) || '微' } const openSettingsFromAccountMenu = () => { @@ -380,6 +300,11 @@ function Sidebar({ collapsed }: SidebarProps) { }) } + const openAccountManagement = () => { + setIsAccountMenuOpen(false) + navigate('/account-management') + } + const isActive = (path: string) => { return location.pathname === path || location.pathname.startsWith(`${path}/`) } @@ -515,12 +440,12 @@ function Sidebar({ collapsed }: SidebarProps) {
- - {showSwitchAccountDialog && ( -
!isSwitchingAccount && setShowSwitchAccountDialog(false)}> -
event.stopPropagation()}> -

切换账号

-

选择要切换的微信账号

-
- {wxidOptions.map((option) => ( - - ))} -
-
- -
-
-
- )} ) } diff --git a/src/pages/AccountManagementPage.scss b/src/pages/AccountManagementPage.scss new file mode 100644 index 0000000..1c0215e --- /dev/null +++ b/src/pages/AccountManagementPage.scss @@ -0,0 +1,274 @@ +.account-management-page { + padding: 22px 24px; + min-height: 100%; + display: flex; + flex-direction: column; + gap: 14px; + color: var(--text-primary); +} + +.account-management-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; + + h2 { + margin: 0; + font-size: 22px; + font-weight: 700; + letter-spacing: -0.01em; + } + + p { + margin: 6px 0 0; + color: var(--text-secondary); + font-size: 13px; + } +} + +.account-management-actions { + display: inline-flex; + align-items: center; + gap: 8px; +} + +.account-management-summary { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 10px; +} + +.summary-item { + border: 1px solid var(--border-color); + border-radius: 12px; + background: var(--bg-secondary); + padding: 10px 12px; +} + +.summary-label { + display: block; + font-size: 11px; + color: var(--text-tertiary); +} + +.summary-value { + display: block; + margin-top: 4px; + font-size: 13px; + color: var(--text-primary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.account-notice { + border-radius: 10px; + padding: 10px 12px; + font-size: 13px; + border: 1px solid transparent; + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} + +.notice-action { + border: 1px solid currentColor; + border-radius: 999px; + background: transparent; + color: inherit; + font-size: 12px; + font-weight: 600; + padding: 4px 10px; + cursor: pointer; + white-space: nowrap; + transition: opacity 0.2s ease, background 0.2s ease; + + &:hover:not(:disabled) { + background: rgba(255, 255, 255, 0.35); + } + + &:disabled { + opacity: 0.6; + cursor: not-allowed; + } +} + +.account-notice.success { + background: rgba(34, 197, 94, 0.12); + color: #15803d; + border-color: rgba(34, 197, 94, 0.25); +} + +.account-notice.error { + background: rgba(239, 68, 68, 0.12); + color: #b91c1c; + border-color: rgba(239, 68, 68, 0.25); +} + +.account-notice.info { + background: rgba(59, 130, 246, 0.12); + color: #1d4ed8; + border-color: rgba(59, 130, 246, 0.25); +} + +.account-empty { + border: 1px dashed var(--border-color); + border-radius: 12px; + background: var(--bg-secondary); + padding: 18px 14px; + color: var(--text-secondary); + font-size: 13px; + display: flex; + align-items: center; + gap: 8px; +} + +.account-list { + display: flex; + flex-direction: column; + gap: 10px; +} + +.account-card { + border: 1px solid var(--border-color); + border-radius: 14px; + background: var(--bg-secondary); + padding: 12px; + display: flex; + gap: 12px; + + &.is-current { + border-color: color-mix(in srgb, var(--primary) 60%, var(--border-color)); + box-shadow: 0 0 0 1px color-mix(in srgb, var(--primary) 25%, transparent); + } +} + +.account-avatar { + width: 42px; + height: 42px; + border-radius: 10px; + overflow: hidden; + background: linear-gradient(135deg, var(--primary), var(--primary-hover)); + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + + img { + width: 100%; + height: 100%; + object-fit: cover; + } + + span { + color: var(--on-primary); + font-weight: 600; + font-size: 14px; + } +} + +.account-main { + min-width: 0; + flex: 1; +} + +.account-title-row { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + + h3 { + margin: 0; + font-size: 15px; + color: var(--text-primary); + } +} + +.account-badge { + display: inline-flex; + align-items: center; + gap: 4px; + border-radius: 999px; + padding: 1px 8px; + font-size: 11px; + font-weight: 600; + + &.current { + color: #0f766e; + background: rgba(20, 184, 166, 0.14); + } + + &.ok { + color: #166534; + background: rgba(34, 197, 94, 0.12); + } + + &.warn { + color: #b45309; + background: rgba(245, 158, 11, 0.15); + } +} + +.account-meta { + margin-top: 3px; + font-size: 12px; + color: var(--text-tertiary); + word-break: break-all; +} + +.meta-tip { + margin-left: 6px; + color: var(--text-secondary); +} + +.account-card-actions { + display: inline-flex; + flex-direction: column; + gap: 8px; + align-items: stretch; + + .btn { + min-width: 104px; + justify-content: center; + } +} + +.account-management-footer { + margin-top: 2px; + color: var(--text-tertiary); + font-size: 12px; +} + +.account-management-page { + .btn-danger { + background: rgba(239, 68, 68, 0.12); + color: #b91c1c; + + &:hover:not(:disabled) { + background: rgba(239, 68, 68, 0.2); + } + } + + .btn:disabled { + opacity: 0.55; + cursor: not-allowed; + } +} + +@media (max-width: 920px) { + .account-management-summary { + grid-template-columns: 1fr; + } + + .account-card { + flex-direction: column; + } + + .account-card-actions { + flex-direction: row; + flex-wrap: wrap; + } +} diff --git a/src/pages/AccountManagementPage.tsx b/src/pages/AccountManagementPage.tsx new file mode 100644 index 0000000..99e022e --- /dev/null +++ b/src/pages/AccountManagementPage.tsx @@ -0,0 +1,574 @@ +import { useCallback, useEffect, useMemo, useState } from 'react' +import { RefreshCw, UserPlus, Trash2, ArrowRightLeft, CheckCircle2, Database } from 'lucide-react' +import { useAppStore } from '../stores/appStore' +import { useChatStore } from '../stores/chatStore' +import { useAnalyticsStore } from '../stores/analyticsStore' +import * as configService from '../services/config' +import './AccountManagementPage.scss' + +interface ScannedWxidOption { + wxid: string + modifiedTime: number + nickname?: string + avatarUrl?: string +} + +interface ManagedAccountItem { + wxid: string + normalizedWxid: string + displayName: string + avatarUrl?: string + modifiedTime?: number + configUpdatedAt?: number + hasConfig: boolean + isCurrent: boolean + fromScan: boolean +} + +type AccountProfileCacheEntry = { + displayName?: string + avatarUrl?: string + updatedAt?: number +} + +interface DeleteUndoState { + targetWxid: string + deletedConfigEntries: Array<[string, configService.WxidConfig]> + deletedProfileEntries: Array<[string, AccountProfileCacheEntry]> + previousCurrentWxid: string + shouldRestoreAsCurrent: boolean + previousDbConnected: boolean +} + +type NoticeState = + | { type: 'success' | 'error' | 'info'; text: string } + | null + +const SIDEBAR_USER_PROFILE_CACHE_KEY = 'sidebar_user_profile_cache_v1' +const ACCOUNT_PROFILES_CACHE_KEY = 'account_profiles_cache_v1' +const hiddenDeletedAccountIds = new Set() +const DEFAULT_ACCOUNT_DISPLAY_NAME = '微信用户' + +const normalizeAccountId = (value?: string | null): string => { + const trimmed = String(value || '').trim() + if (!trimmed) return '' + if (trimmed.toLowerCase().startsWith('wxid_')) { + const match = trimmed.match(/^(wxid_[^_]+)/i) + return match?.[1] || trimmed + } + const suffixMatch = trimmed.match(/^(.+)_([a-zA-Z0-9]{4})$/) + return suffixMatch ? suffixMatch[1] : trimmed +} + +const resolveAccountDisplayName = ( + candidates: Array, + wxidCandidates: Set +): string => { + for (const candidate of candidates) { + if (typeof candidate !== 'string') continue + if (candidate.length === 0) continue + const normalized = candidate.trim().toLowerCase() + if (normalized.startsWith('wxid_')) continue + if (normalized && wxidCandidates.has(normalized)) continue + return candidate + } + return DEFAULT_ACCOUNT_DISPLAY_NAME +} + +const resolveAccountAvatarText = (displayName?: string): string => { + if (typeof displayName !== 'string' || displayName.length === 0) return '微' + const visible = displayName.trim() + return (visible && [...visible][0]) || '微' +} + +const readAccountProfilesCache = (): Record => { + try { + const raw = window.localStorage.getItem(ACCOUNT_PROFILES_CACHE_KEY) + if (!raw) return {} + const parsed = JSON.parse(raw) + return parsed && typeof parsed === 'object' ? parsed as Record : {} + } catch { + return {} + } +} + +function AccountManagementPage() { + const isDbConnected = useAppStore(state => state.isDbConnected) + const setDbConnected = useAppStore(state => state.setDbConnected) + const resetChatStore = useChatStore(state => state.reset) + const clearAnalyticsStoreCache = useAnalyticsStore(state => state.clearCache) + + const [dbPath, setDbPath] = useState('') + const [currentWxid, setCurrentWxid] = useState('') + const [accounts, setAccounts] = useState([]) + const [isLoading, setIsLoading] = useState(false) + const [workingWxid, setWorkingWxid] = useState('') + const [notice, setNotice] = useState(null) + const [deleteUndoState, setDeleteUndoState] = useState(null) + + const loadAccounts = useCallback(async () => { + setIsLoading(true) + try { + const [path, rawCurrentWxid, wxidConfigs] = await Promise.all([ + configService.getDbPath(), + configService.getMyWxid(), + configService.getWxidConfigs() + ]) + const nextDbPath = String(path || '').trim() + const nextCurrentWxid = String(rawCurrentWxid || '').trim() + const normalizedCurrent = normalizeAccountId(nextCurrentWxid) || nextCurrentWxid + setDbPath(nextDbPath) + setCurrentWxid(nextCurrentWxid) + + let scannedWxids: ScannedWxidOption[] = [] + if (nextDbPath) { + try { + const scanned = await window.electronAPI.dbPath.scanWxids(nextDbPath) + scannedWxids = Array.isArray(scanned) ? scanned as ScannedWxidOption[] : [] + } catch { + scannedWxids = [] + } + } + + const accountProfileCache = readAccountProfilesCache() + const configEntries = Object.entries(wxidConfigs || {}) + const configByNormalized = new Map() + for (const [wxid, cfg] of configEntries) { + const normalized = normalizeAccountId(wxid) || wxid + if (!normalized) continue + const previous = configByNormalized.get(normalized) + if (!previous || Number(cfg?.updatedAt || 0) > Number(previous.value?.updatedAt || 0)) { + configByNormalized.set(normalized, { key: wxid, value: cfg || {} }) + } + } + + const merged = new Map() + for (const scanned of scannedWxids) { + const normalized = normalizeAccountId(scanned.wxid) || scanned.wxid + if (!normalized) continue + const cached = accountProfileCache[scanned.wxid] || accountProfileCache[normalized] + const matchedConfig = configByNormalized.get(normalized) + const wxidCandidates = new Set([ + String(scanned.wxid || '').trim().toLowerCase(), + String(normalized || '').trim().toLowerCase() + ].filter(Boolean)) + const displayName = resolveAccountDisplayName( + [scanned.nickname, cached?.displayName], + wxidCandidates + ) + merged.set(normalized, { + wxid: scanned.wxid, + normalizedWxid: normalized, + displayName, + avatarUrl: scanned.avatarUrl || cached?.avatarUrl, + modifiedTime: Number(scanned.modifiedTime || 0), + configUpdatedAt: Number(matchedConfig?.value?.updatedAt || 0), + hasConfig: Boolean(matchedConfig), + isCurrent: normalizedCurrent && normalized === normalizedCurrent, + fromScan: true + }) + } + + for (const [normalized, matchedConfig] of configByNormalized.entries()) { + if (merged.has(normalized)) continue + const wxid = matchedConfig.key + const cached = accountProfileCache[wxid] || accountProfileCache[normalized] + const wxidCandidates = new Set([ + String(wxid || '').trim().toLowerCase(), + String(normalized || '').trim().toLowerCase() + ].filter(Boolean)) + const displayName = resolveAccountDisplayName( + [cached?.displayName], + wxidCandidates + ) + merged.set(normalized, { + wxid, + normalizedWxid: normalized, + displayName, + avatarUrl: cached?.avatarUrl, + modifiedTime: 0, + configUpdatedAt: Number(matchedConfig.value?.updatedAt || 0), + hasConfig: true, + isCurrent: normalizedCurrent && normalized === normalizedCurrent, + fromScan: false + }) + } + + // 被“删除配置”操作移除的账号,在当前会话中从列表隐藏; + // 若后续再次生成配置,则自动恢复展示。 + for (const [normalized, item] of Array.from(merged.entries())) { + if (!hiddenDeletedAccountIds.has(normalized)) continue + if (item.hasConfig) { + hiddenDeletedAccountIds.delete(normalized) + continue + } + merged.delete(normalized) + } + + const nextAccounts = Array.from(merged.values()).sort((a, b) => { + if (a.isCurrent && !b.isCurrent) return -1 + if (!a.isCurrent && b.isCurrent) return 1 + const scanDiff = Number(b.modifiedTime || 0) - Number(a.modifiedTime || 0) + if (scanDiff !== 0) return scanDiff + return Number(b.configUpdatedAt || 0) - Number(a.configUpdatedAt || 0) + }) + setAccounts(nextAccounts) + } catch (error) { + console.error('加载账号列表失败:', error) + setNotice({ type: 'error', text: '加载账号列表失败,请稍后重试' }) + setAccounts([]) + } finally { + setIsLoading(false) + } + }, []) + + useEffect(() => { + void loadAccounts() + const onWxidChanged = () => { void loadAccounts() } + const onWindowFocus = () => { void loadAccounts() } + const onVisibilityChange = () => { + if (document.visibilityState === 'visible') { + void loadAccounts() + } + } + window.addEventListener('wxid-changed', onWxidChanged as EventListener) + window.addEventListener('focus', onWindowFocus) + document.addEventListener('visibilitychange', onVisibilityChange) + return () => { + window.removeEventListener('wxid-changed', onWxidChanged as EventListener) + window.removeEventListener('focus', onWindowFocus) + document.removeEventListener('visibilitychange', onVisibilityChange) + } + }, [loadAccounts]) + + const clearRuntimeCacheState = useCallback(async () => { + if (isDbConnected) { + await window.electronAPI.chat.close() + } + window.localStorage.removeItem(SIDEBAR_USER_PROFILE_CACHE_KEY) + clearAnalyticsStoreCache() + resetChatStore() + }, [clearAnalyticsStoreCache, isDbConnected, resetChatStore]) + + const applyWxidConfig = useCallback(async (wxid: string, wxidConfig: configService.WxidConfig | null) => { + await configService.setMyWxid(wxid) + await configService.setDecryptKey(wxidConfig?.decryptKey || '') + await configService.setImageXorKey(typeof wxidConfig?.imageXorKey === 'number' ? wxidConfig.imageXorKey : 0) + await configService.setImageAesKey(wxidConfig?.imageAesKey || '') + }, []) + + const handleSwitchAccount = useCallback(async (wxid: string) => { + if (!wxid || workingWxid) return + const targetNormalized = normalizeAccountId(wxid) || wxid + const currentNormalized = normalizeAccountId(currentWxid) || currentWxid + if (targetNormalized && currentNormalized && targetNormalized === currentNormalized) return + + setWorkingWxid(wxid) + setNotice(null) + setDeleteUndoState(null) + try { + const allConfigs = await configService.getWxidConfigs() + const configEntries = Object.entries(allConfigs || {}) + const matched = configEntries.find(([key]) => { + const normalized = normalizeAccountId(key) || key + return key === wxid || normalized === targetNormalized + }) + const targetConfig = matched?.[1] || null + await applyWxidConfig(wxid, targetConfig) + await clearRuntimeCacheState() + window.dispatchEvent(new CustomEvent('wxid-changed', { detail: { wxid } })) + setNotice({ type: 'success', text: `已切换到账号「${wxid}」` }) + await loadAccounts() + } catch (error) { + console.error('切换账号失败:', error) + setNotice({ type: 'error', text: '切换账号失败,请稍后重试' }) + } finally { + setWorkingWxid('') + } + }, [applyWxidConfig, clearRuntimeCacheState, currentWxid, loadAccounts, workingWxid]) + + const handleAddAccount = useCallback(async () => { + if (workingWxid) return + setNotice(null) + setDeleteUndoState(null) + try { + await window.electronAPI.window.openOnboardingWindow({ mode: 'add-account' }) + await loadAccounts() + const latestWxid = String(await configService.getMyWxid() || '').trim() + window.dispatchEvent(new CustomEvent('wxid-changed', { detail: { wxid: latestWxid } })) + } catch (error) { + console.error('打开添加账号引导失败:', error) + setNotice({ type: 'error', text: '打开添加账号引导失败,请稍后重试' }) + } + }, [loadAccounts, workingWxid]) + + const handleDeleteAccountConfig = useCallback(async (targetWxid: string) => { + if (!targetWxid || workingWxid) return + + const normalizedTarget = normalizeAccountId(targetWxid) || targetWxid + + setWorkingWxid(targetWxid) + setNotice(null) + setDeleteUndoState(null) + try { + const allConfigs = await configService.getWxidConfigs() + const nextConfigs: Record = { ...allConfigs } + const matchedKeys = Object.keys(nextConfigs).filter((key) => { + const normalized = normalizeAccountId(key) || key + return key === targetWxid || normalized === normalizedTarget + }) + + if (matchedKeys.length === 0) { + setNotice({ type: 'info', text: `账号「${targetWxid}」暂无可删除配置` }) + return + } + + const deletedConfigEntries: Array<[string, configService.WxidConfig]> = matchedKeys.map((key) => [key, nextConfigs[key] || {}]) + for (const key of matchedKeys) { + delete nextConfigs[key] + } + await configService.setWxidConfigs(nextConfigs) + + const accountProfileCache = readAccountProfilesCache() + const deletedProfileEntries: Array<[string, AccountProfileCacheEntry]> = [] + for (const key of Object.keys(accountProfileCache)) { + const normalized = normalizeAccountId(key) || key + if (key === targetWxid || normalized === normalizedTarget) { + deletedProfileEntries.push([key, accountProfileCache[key]]) + delete accountProfileCache[key] + } + } + window.localStorage.setItem(ACCOUNT_PROFILES_CACHE_KEY, JSON.stringify(accountProfileCache)) + + const currentNormalized = normalizeAccountId(currentWxid) || currentWxid + const isDeletingCurrent = Boolean(currentNormalized && currentNormalized === normalizedTarget) + const undoPayload: DeleteUndoState = { + targetWxid, + deletedConfigEntries, + deletedProfileEntries, + previousCurrentWxid: currentWxid, + shouldRestoreAsCurrent: isDeletingCurrent, + previousDbConnected: isDbConnected + } + + if (isDeletingCurrent) { + await clearRuntimeCacheState() + + const remainingEntries = Object.entries(nextConfigs) + .filter(([wxid]) => Boolean(String(wxid || '').trim())) + .sort((a, b) => Number(b[1]?.updatedAt || 0) - Number(a[1]?.updatedAt || 0)) + + if (remainingEntries.length > 0) { + const [nextWxid, nextConfig] = remainingEntries[0] + await applyWxidConfig(nextWxid, nextConfig || null) + window.dispatchEvent(new CustomEvent('wxid-changed', { detail: { wxid: nextWxid } })) + hiddenDeletedAccountIds.add(normalizedTarget) + setDeleteUndoState(undoPayload) + setNotice({ type: 'success', text: `已删除「${targetWxid}」配置,并切换到「${nextWxid}」` }) + await loadAccounts() + return + } + + await configService.setMyWxid('') + await configService.setDecryptKey('') + await configService.setImageXorKey(0) + await configService.setImageAesKey('') + setDbConnected(false) + window.dispatchEvent(new CustomEvent('wxid-changed', { detail: { wxid: '' } })) + hiddenDeletedAccountIds.add(normalizedTarget) + setDeleteUndoState(undoPayload) + setNotice({ type: 'info', text: `已删除「${targetWxid}」配置,当前无可用账号配置,可撤回或添加账号` }) + await loadAccounts() + return + } + + hiddenDeletedAccountIds.add(normalizedTarget) + setDeleteUndoState(undoPayload) + setNotice({ type: 'success', text: `已删除账号「${targetWxid}」配置` }) + await loadAccounts() + } catch (error) { + console.error('删除账号配置失败:', error) + setNotice({ type: 'error', text: '删除账号配置失败,请稍后重试' }) + } finally { + setWorkingWxid('') + } + }, [applyWxidConfig, clearRuntimeCacheState, currentWxid, isDbConnected, loadAccounts, setDbConnected, workingWxid]) + + const handleUndoDelete = useCallback(async () => { + if (!deleteUndoState || workingWxid) return + + setWorkingWxid(`undo:${deleteUndoState.targetWxid}`) + setNotice(null) + try { + const currentConfigs = await configService.getWxidConfigs() + const restoredConfigs: Record = { ...currentConfigs } + for (const [key, configValue] of deleteUndoState.deletedConfigEntries) { + restoredConfigs[key] = configValue || {} + } + await configService.setWxidConfigs(restoredConfigs) + hiddenDeletedAccountIds.delete(normalizeAccountId(deleteUndoState.targetWxid) || deleteUndoState.targetWxid) + + const accountProfileCache = readAccountProfilesCache() + for (const [key, profile] of deleteUndoState.deletedProfileEntries) { + accountProfileCache[key] = profile + } + window.localStorage.setItem(ACCOUNT_PROFILES_CACHE_KEY, JSON.stringify(accountProfileCache)) + + if (deleteUndoState.shouldRestoreAsCurrent && deleteUndoState.previousCurrentWxid) { + const previousNormalized = normalizeAccountId(deleteUndoState.previousCurrentWxid) || deleteUndoState.previousCurrentWxid + const restoreConfigEntry = Object.entries(restoredConfigs) + .filter(([key]) => { + const normalized = normalizeAccountId(key) || key + return key === deleteUndoState.previousCurrentWxid || normalized === previousNormalized + }) + .sort((a, b) => Number(b[1]?.updatedAt || 0) - Number(a[1]?.updatedAt || 0))[0] + const restoreConfig = restoreConfigEntry?.[1] || null + + await clearRuntimeCacheState() + await applyWxidConfig(deleteUndoState.previousCurrentWxid, restoreConfig) + if (deleteUndoState.previousDbConnected) { + setDbConnected(true, dbPath || undefined) + } + window.dispatchEvent(new CustomEvent('wxid-changed', { detail: { wxid: deleteUndoState.previousCurrentWxid } })) + } + + setNotice({ type: 'success', text: `已撤回删除,账号「${deleteUndoState.targetWxid}」配置已恢复` }) + setDeleteUndoState(null) + await loadAccounts() + } catch (error) { + console.error('撤回删除失败:', error) + setNotice({ type: 'error', text: '撤回删除失败,请稍后重试' }) + } finally { + setWorkingWxid('') + } + }, [applyWxidConfig, clearRuntimeCacheState, dbPath, deleteUndoState, loadAccounts, setDbConnected, workingWxid]) + + const currentAccountLabel = useMemo(() => { + if (!currentWxid) return '未设置' + return currentWxid + }, [currentWxid]) + + const formatTime = (value?: number): string => { + const ts = Number(value || 0) + if (!ts) return '未知' + const date = new Date(ts) + if (Number.isNaN(date.getTime())) return '未知' + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + const hour = String(date.getHours()).padStart(2, '0') + const minute = String(date.getMinutes()).padStart(2, '0') + return `${year}-${month}-${day} ${hour}:${minute}` + } + + return ( +
+
+
+

账号管理

+

统一管理切换账号、添加账号、删除账号配置。

+
+
+ + +
+
+ +
+
+ 数据库目录 + {dbPath || '未配置'} +
+
+ 当前账号 + {currentAccountLabel} +
+
+ 账号数量 + {accounts.length} +
+
+ + {notice && ( +
+ {notice.text} + {deleteUndoState && (notice.type === 'success' || notice.type === 'info') && ( + + )} +
+ )} + + {accounts.length === 0 ? ( +
+ + 未发现可管理账号,请先添加账号或检查数据库目录。 +
+ ) : ( +
+ {accounts.map((account) => ( +
+
+ {account.avatarUrl ? : {resolveAccountAvatarText(account.displayName)}} +
+
+
+

{account.displayName}

+ {account.isCurrent && ( + + 当前 + + )} + {account.hasConfig ? ( + 已保存配置 + ) : ( + 未保存配置 + )} +
+
wxid: {account.wxid}
+
+ 最近数据更新时间: {formatTime(account.modifiedTime)} · 配置更新时间: {formatTime(account.configUpdatedAt)} + {!account.fromScan && (仅配置记录)} +
+
+
+ + +
+
+ ))} +
+ )} + +
+ 删除仅影响 WeFlow 本地配置,不会删除微信原始数据文件。 +
+
+ ) +} + +export default AccountManagementPage diff --git a/src/pages/ExportPage.scss b/src/pages/ExportPage.scss index 0944735..fd4c63f 100644 --- a/src/pages/ExportPage.scss +++ b/src/pages/ExportPage.scss @@ -3,10 +3,8 @@ height: 100%; margin: -24px -24px 0; padding: 18px 22px 12px; - background: - radial-gradient(1200px 520px at 6% -8%, color-mix(in srgb, var(--primary) 11%, transparent), transparent 65%), - radial-gradient(860px 420px at 90% 0%, color-mix(in srgb, var(--primary) 7%, transparent), transparent 66%), - var(--bg-primary); + background: var(--bg-primary); + /* Minimal background matching Footprint */ display: flex; flex-direction: column; gap: 16px; @@ -38,7 +36,9 @@ display: flex; align-items: center; justify-content: flex-start; - gap: 6px; + gap: 12px; + flex-wrap: wrap; + margin-bottom: 8px; animation: exportSectionReveal 0.38s ease both; } @@ -119,7 +119,9 @@ } @keyframes sessionLoadDetailBars { - 0%, 100% { + + 0%, + 100% { transform: scaleY(0.72); opacity: 0.5; } @@ -460,7 +462,7 @@ border-bottom: none; } - > span { + >span { min-width: 0; overflow: hidden; text-overflow: ellipsis; @@ -1308,12 +1310,29 @@ } } -.content-card-grid .content-card:nth-child(1) { animation-delay: 0.03s; } -.content-card-grid .content-card:nth-child(2) { animation-delay: 0.07s; } -.content-card-grid .content-card:nth-child(3) { animation-delay: 0.11s; } -.content-card-grid .content-card:nth-child(4) { animation-delay: 0.15s; } -.content-card-grid .content-card:nth-child(5) { animation-delay: 0.19s; } -.content-card-grid .content-card:nth-child(6) { animation-delay: 0.23s; } +.content-card-grid .content-card:nth-child(1) { + animation-delay: 0.03s; +} + +.content-card-grid .content-card:nth-child(2) { + animation-delay: 0.07s; +} + +.content-card-grid .content-card:nth-child(3) { + animation-delay: 0.11s; +} + +.content-card-grid .content-card:nth-child(4) { + animation-delay: 0.15s; +} + +.content-card-grid .content-card:nth-child(5) { + animation-delay: 0.19s; +} + +.content-card-grid .content-card:nth-child(6) { + animation-delay: 0.23s; +} .count-loading { color: var(--text-tertiary); @@ -1716,19 +1735,20 @@ flex: 0 0 auto; width: auto; max-width: max-content; - border: 1px solid var(--border-color); - background: var(--bg-secondary); + border: none; + background: transparent; color: var(--text-secondary); min-height: 32px; - padding: 7px 10px; + padding: 7px 12px; border-radius: 999px; cursor: pointer; font-size: 13px; + font-weight: 500; white-space: nowrap; display: inline-flex; align-items: center; justify-content: center; - transition: border-color 0.14s ease, background 0.14s ease, color 0.14s ease, transform 0.14s ease, box-shadow 0.14s ease; + transition: all 0.2s ease; .tab-btn-content { display: inline-flex; @@ -1753,21 +1773,18 @@ } &:hover { - border-color: color-mix(in srgb, var(--primary) 36%, var(--border-color)); + background: color-mix(in srgb, var(--text-tertiary) 8%, transparent); color: var(--text-primary); - transform: translateY(-1px); - box-shadow: 0 7px 14px rgba(15, 23, 42, 0.08); } &.active { - border-color: var(--primary); color: var(--primary); - background: rgba(var(--primary-rgb), 0.12); - box-shadow: 0 6px 14px color-mix(in srgb, var(--primary) 24%, transparent); + background: color-mix(in srgb, var(--primary) 12%, transparent); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02); .tab-btn-content span:last-child { - background: color-mix(in srgb, var(--primary) 16%, var(--bg-secondary)); - color: color-mix(in srgb, var(--primary) 84%, var(--text-primary)); + background: color-mix(in srgb, var(--primary) 16%, transparent); + color: var(--primary); } } @@ -1812,15 +1829,17 @@ gap: 6px; padding: 8px 11px; border-radius: 10px; - border: 1px solid var(--border-color); - background: color-mix(in srgb, var(--bg-secondary) 92%, var(--bg-primary)); - min-width: 240px; - transition: border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease; + border: none; + background: color-mix(in srgb, var(--text-tertiary) 4%, transparent); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.03) inset; + flex: 1; + min-width: 180px; + max-width: 320px; + transition: all 0.2s ease; &:focus-within { - border-color: color-mix(in srgb, var(--primary) 42%, var(--border-color)); - box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 16%, transparent); - background: var(--bg-secondary); + box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary) 20%, transparent), 0 1px 3px rgba(0, 0, 0, 0.02) inset; + background: color-mix(in srgb, var(--text-tertiary) 6%, transparent); } input { @@ -1829,13 +1848,14 @@ color: var(--text-primary); font-size: 13px; outline: none; - width: 220px; + flex: 1; + min-width: 0; } .clear-search { - border: 1px solid transparent; - background: color-mix(in srgb, var(--bg-primary) 84%, var(--bg-secondary)); - color: var(--text-tertiary); + border: none; + background: color-mix(in srgb, var(--text-tertiary) 10%, transparent); + color: var(--text-secondary); cursor: pointer; display: inline-flex; align-items: center; @@ -1843,12 +1863,11 @@ width: 18px; height: 18px; border-radius: 999px; - transition: border-color 0.12s ease, background 0.12s ease, color 0.12s ease; + transition: all 0.2s ease; &:hover { - border-color: color-mix(in srgb, var(--primary) 36%, var(--border-color)); color: var(--primary); - background: color-mix(in srgb, var(--primary) 8%, var(--bg-secondary)); + background: color-mix(in srgb, var(--primary) 14%, transparent); } } } @@ -1892,25 +1911,21 @@ --contacts-message-col-width: 120px; --contacts-media-col-width: 72px; --contacts-action-col-width: 140px; - --contacts-actions-sticky-width: max(var(--contacts-action-col-width), 184px); - --contacts-table-min-width: 1200px; + --contacts-actions-sticky-width: 240px; + --contacts-table-min-width: 1240px; overflow: hidden; - border: 1px solid color-mix(in srgb, var(--border-color) 84%, transparent); + border: none; border-radius: 12px; min-height: 320px; height: auto; flex: 1; display: flex; flex-direction: column; - background: linear-gradient(180deg, color-mix(in srgb, var(--bg-secondary) 84%, var(--bg-primary)) 0%, var(--bg-secondary) 100%); - box-shadow: inset 0 1px 0 color-mix(in srgb, #fff 18%, transparent); - transition: border-color 0.16s ease, box-shadow 0.16s ease; + background: var(--bg-secondary); + transition: all 0.2s ease; &:hover { - border-color: color-mix(in srgb, var(--primary) 22%, var(--border-color)); - box-shadow: - inset 0 1px 0 color-mix(in srgb, #fff 24%, transparent), - 0 8px 18px rgba(15, 23, 42, 0.06); + background: color-mix(in srgb, var(--text-tertiary) 2%, var(--bg-secondary)); } } @@ -2031,11 +2046,12 @@ } .issue-btn { - border: 1px solid var(--border-color); - background: var(--bg-secondary); + border: none; + background: color-mix(in srgb, var(--text-tertiary) 10%, transparent); border-radius: 8px; - padding: 7px 10px; + padding: 7px 12px; font-size: 12px; + font-weight: 500; color: var(--text-secondary); display: inline-flex; align-items: center; @@ -2045,14 +2061,17 @@ &:hover { color: var(--text-primary); - border-color: var(--text-tertiary); - background: var(--bg-hover); + background: color-mix(in srgb, var(--text-tertiary) 16%, transparent); + transform: translateY(-1px); } &.primary { - background: color-mix(in srgb, var(--primary) 14%, var(--bg-secondary)); - border-color: color-mix(in srgb, var(--primary) 42%, var(--border-color)); + background: color-mix(in srgb, var(--primary) 14%, transparent); color: var(--primary); + + &:hover { + background: color-mix(in srgb, var(--primary) 20%, transparent); + } } } @@ -2070,20 +2089,20 @@ } .contacts-list-header { - --contacts-header-bg: color-mix(in srgb, var(--bg-secondary) 90%, var(--bg-primary)); + --contacts-header-bg: color-mix(in srgb, var(--bg-secondary) 80%, transparent); display: flex; align-items: center; gap: var(--contacts-column-gap); padding: 10px var(--contacts-inline-padding) 8px; min-width: max(100%, var(--contacts-table-min-width)); - border-bottom: 1px solid color-mix(in srgb, var(--border-color) 85%, transparent); + border-bottom: 1px solid color-mix(in srgb, var(--text-tertiary) 6%, transparent); background: var(--contacts-header-bg); font-size: 12px; color: var(--text-tertiary); font-weight: 600; letter-spacing: 0.01em; flex-shrink: 0; - backdrop-filter: saturate(115%) blur(3px); + backdrop-filter: blur(10px); &.is-draggable { cursor: grab; @@ -2164,7 +2183,7 @@ display: flex; align-items: center; justify-content: flex-end; - gap: 8px; + gap: 10px; flex-wrap: nowrap; flex-shrink: 0; position: sticky; @@ -2248,25 +2267,25 @@ } .selection-clear-btn { - border: 1px solid var(--border-color); + border: none; border-radius: 8px; - background: var(--bg-secondary); + background: color-mix(in srgb, var(--text-tertiary) 10%, transparent); color: var(--text-secondary); font-size: 12px; + font-weight: 500; padding: 6px 10px; cursor: pointer; white-space: nowrap; - transition: border-color 0.14s ease, color 0.14s ease, background 0.14s ease, transform 0.14s ease; + transition: all 0.2s ease; &:hover:not(:disabled) { - border-color: var(--text-tertiary); color: var(--text-primary); - background: color-mix(in srgb, var(--bg-primary) 88%, var(--bg-secondary)); + background: color-mix(in srgb, var(--text-tertiary) 16%, transparent); transform: translateY(-1px); } &:disabled { - opacity: 0.65; + opacity: 0.5; cursor: not-allowed; } } @@ -2275,19 +2294,21 @@ border: none; border-radius: 8px; padding: 6px 10px; - background: linear-gradient(180deg, color-mix(in srgb, var(--primary) 94%, #ffffff) 0%, var(--primary) 100%); + background: var(--primary); color: #fff; font-size: 12px; + font-weight: 500; cursor: pointer; display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; flex-shrink: 0; - transition: transform 0.14s ease, box-shadow 0.14s ease, background 0.14s ease; + transition: all 0.2s ease; + box-shadow: 0 2px 6px color-mix(in srgb, var(--primary) 20%, transparent); &:hover:not(:disabled) { - background: var(--primary-hover); + background: color-mix(in srgb, var(--primary) 85%, #fff); transform: translateY(-1px); box-shadow: 0 8px 14px color-mix(in srgb, var(--primary) 30%, transparent); } @@ -2360,7 +2381,7 @@ } .contact-item { - --contacts-row-bg: var(--bg-secondary); + --contacts-row-bg: transparent; display: flex; align-items: center; gap: var(--contacts-column-gap); @@ -2369,15 +2390,14 @@ height: 72px; box-sizing: border-box; border-radius: 10px; - transition: box-shadow 0.18s ease, background 0.18s ease, transform 0.18s ease; + transition: all 0.2s ease; cursor: default; background: var(--contacts-row-bg); - box-shadow: inset 0 0 0 1px transparent; + box-shadow: none; &:hover { - background: var(--contacts-row-bg); - box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text-tertiary) 24%, transparent); - transform: translateX(1px); + background: color-mix(in srgb, var(--text-tertiary) 6%, transparent); + transform: translateX(2px); } } @@ -2634,32 +2654,34 @@ min-width: 1300px; border-collapse: separate; border-spacing: 0; - background: var(--bg-secondary); + background: transparent; thead th { position: sticky; top: 0; - background: color-mix(in srgb, var(--bg-secondary) 90%, var(--bg-primary)); + background: color-mix(in srgb, var(--bg-secondary) 80%, transparent); + backdrop-filter: blur(8px); z-index: 4; font-size: 12px; text-align: left; color: var(--text-secondary); - border-bottom: 1px solid var(--border-color); + border-bottom: 1px solid color-mix(in srgb, var(--text-tertiary) 6%, transparent); padding: 10px 10px; white-space: nowrap; } tbody td { padding: 10px; - border-bottom: 1px solid color-mix(in srgb, var(--border-color) 70%, transparent); + border-bottom: 1px solid color-mix(in srgb, var(--text-tertiary) 4%, transparent); font-size: 13px; color: var(--text-primary); vertical-align: middle; white-space: nowrap; + transition: background 0.15s ease; } - tbody tr:hover { - background: rgba(var(--primary-rgb), 0.03); + tbody tr:hover td { + background: color-mix(in srgb, var(--text-tertiary) 6%, transparent); } .selected-row, @@ -2797,27 +2819,26 @@ } .row-detail-btn { - border: 1px solid var(--border-color); + border: none; border-radius: 8px; - padding: 7px 10px; - background: var(--bg-secondary); + padding: 7px 12px; + background: color-mix(in srgb, var(--text-tertiary) 8%, transparent); color: var(--text-secondary); font-size: 12px; + font-weight: 500; cursor: pointer; white-space: nowrap; - transition: border-color 0.14s ease, color 0.14s ease, background 0.14s ease, transform 0.14s ease; + transition: all 0.2s ease; &:hover { - border-color: var(--text-tertiary); color: var(--text-primary); - background: var(--bg-hover); + background: color-mix(in srgb, var(--text-tertiary) 14%, transparent); transform: translateY(-1px); } &.active { - border-color: var(--primary); color: var(--primary); - background: rgba(var(--primary-rgb), 0.12); + background: color-mix(in srgb, var(--primary) 12%, transparent); } } @@ -2883,7 +2904,7 @@ text-align: center; } - .row-export-link.state-running + .row-export-time { + .row-export-link.state-running+.row-export-time { color: var(--primary); font-weight: 600; } @@ -2918,23 +2939,22 @@ width: min(448px, calc(100vw - 24px)); height: 100%; max-height: calc(100vh - 24px); - border: 1px solid color-mix(in srgb, var(--border-color) 86%, transparent); - border-radius: 16px; - background: - linear-gradient(180deg, color-mix(in srgb, var(--card-bg) 82%, var(--bg-primary)) 0%, var(--bg-secondary-solid, #ffffff) 100%); + border: 1px solid color-mix(in srgb, var(--border-color) 40%, transparent); + border-radius: 20px; + background: var(--bg-primary); display: flex; flex-direction: column; overflow: hidden; - box-shadow: -18px 0 40px rgba(0, 0, 0, 0.24); + box-shadow: -18px 24px 60px rgba(0, 0, 0, 0.16); animation: exportDetailPanelIn 0.26s cubic-bezier(0.22, 0.8, 0.24, 1) both; .detail-header { display: flex; align-items: center; justify-content: space-between; - padding: 16px 16px 12px; - border-bottom: 1px solid color-mix(in srgb, var(--border-color) 84%, transparent); - background: color-mix(in srgb, var(--bg-primary) 92%, var(--card-bg)); + padding: 20px 20px 16px; + border-bottom: 1px solid color-mix(in srgb, var(--text-tertiary) 6%, transparent); + background: transparent; .detail-header-main { display: flex; @@ -3318,22 +3338,22 @@ .export-session-sns-dialog { width: min(760px, 100%); max-height: min(86vh, 860px); - border-radius: 14px; - border: 1px solid var(--border-color); - background: var(--bg-secondary-solid, #ffffff); - box-shadow: 0 22px 46px rgba(0, 0, 0, 0.24); + border-radius: 20px; + border: 1px solid color-mix(in srgb, var(--text-tertiary) 8%, transparent); + background: var(--bg-primary); + box-shadow: 0 24px 64px rgba(0, 0, 0, 0.16); display: flex; flex-direction: column; overflow: hidden; - animation: exportModalPopIn 0.24s cubic-bezier(0.2, 0.78, 0.26, 1) both; + animation: footprintFadeSlideUp 0.3s cubic-bezier(0.2, 0.78, 0.26, 1) both; .sns-dialog-header { display: flex; align-items: center; justify-content: space-between; gap: 10px; - padding: 14px 16px; - border-bottom: 1px solid var(--border-color); + padding: 16px 20px; + border-bottom: 1px solid color-mix(in srgb, var(--text-tertiary) 6%, transparent); } .sns-dialog-header-main { @@ -3673,12 +3693,10 @@ position: relative; overflow: hidden; border-radius: 8px; - background: linear-gradient( - 90deg, - rgba(255, 255, 255, 0.08) 0%, - rgba(255, 255, 255, 0.35) 50%, - rgba(255, 255, 255, 0.08) 100% - ); + background: linear-gradient(90deg, + rgba(255, 255, 255, 0.08) 0%, + rgba(255, 255, 255, 0.35) 50%, + rgba(255, 255, 255, 0.08) 100%); background-size: 220% 100%; animation: exportSkeletonShimmer 1.2s linear infinite; } @@ -3700,13 +3718,39 @@ height: 12px; } -.skeleton-line.w-12 { width: 48%; min-width: 42px; } -.skeleton-line.w-20 { width: 22%; min-width: 36px; } -.skeleton-line.w-30 { width: 32%; min-width: 120px; } -.skeleton-line.w-40 { width: 45%; min-width: 80px; } -.skeleton-line.w-60 { width: 62%; min-width: 110px; } -.skeleton-line.w-100 { width: 100%; } -.skeleton-line.h-32 { height: 32px; border-radius: 10px; } +.skeleton-line.w-12 { + width: 48%; + min-width: 42px; +} + +.skeleton-line.w-20 { + width: 22%; + min-width: 36px; +} + +.skeleton-line.w-30 { + width: 32%; + min-width: 120px; +} + +.skeleton-line.w-40 { + width: 45%; + min-width: 80px; +} + +.skeleton-line.w-60 { + width: 62%; + min-width: 110px; +} + +.skeleton-line.w-100 { + width: 100%; +} + +.skeleton-line.h-32 { + height: 32px; + border-radius: 10px; +} .export-dialog-overlay { position: fixed; @@ -4440,11 +4484,9 @@ justify-content: flex-end; gap: 10px; flex-shrink: 0; - background: linear-gradient( - 180deg, - transparent, - var(--card-bg) 38% - ); + background: linear-gradient(180deg, + transparent, + var(--card-bg) 38%); } .primary-btn, @@ -4824,6 +4866,7 @@ 0% { background-position: 220% 0; } + 100% { background-position: -20% 0; } @@ -4833,6 +4876,7 @@ 0% { width: 0; } + 100% { width: 1.8em; } @@ -4843,10 +4887,12 @@ transform: scale(1); box-shadow: 0 0 0 0 rgba(255, 77, 79, 0.35); } + 70% { transform: scale(1.02); box-shadow: 0 0 0 6px rgba(255, 77, 79, 0); } + 100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(255, 77, 79, 0); @@ -4854,6 +4900,7 @@ } @media (prefers-reduced-motion: reduce) { + .export-board-page, .export-top-panel, .export-section-title-row, @@ -5207,3 +5254,634 @@ } } } + +.automation-hint-pill { + display: inline-flex; + align-items: center; + gap: 6px; + margin-top: 6px; + padding: 5px 12px; + border-radius: 999px; + background: color-mix(in srgb, var(--primary) 10%, transparent); + color: var(--primary); + font-size: 12px; + font-weight: 500; +} + +.automation-modal-overlay { + position: fixed; + inset: 0; + z-index: 7750; + background: rgba(0, 0, 0, 0.38); + display: flex; + align-items: center; + justify-content: center; + padding: 24px 20px; + animation: exportOverlayFadeIn 0.2s ease both; +} + +.automation-modal { + width: min(680px, 100%); + max-height: min(80vh, 820px); + border-radius: 20px; + border: 1px solid color-mix(in srgb, var(--text-tertiary) 8%, transparent); + background: var(--bg-primary); + box-shadow: 0 24px 64px rgba(0, 0, 0, 0.16); + display: flex; + flex-direction: column; + overflow: hidden; + animation: footprintFadeSlideUp 0.28s cubic-bezier(0.2, 0.78, 0.26, 1) both; +} + +.automation-modal-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; + padding: 20px 20px 16px; + border-bottom: 1px solid color-mix(in srgb, var(--text-tertiary) 6%, transparent); + flex-shrink: 0; + + h3 { + margin: 0; + font-size: 16px; + font-weight: 700; + color: var(--text-primary); + } + + p { + margin: 4px 0 0; + font-size: 12px; + color: var(--text-tertiary); + } +} + +.automation-modal-body { + flex: 1; + min-height: 0; + overflow-y: auto; + padding: 16px 20px 20px; +} + +.automation-empty { + padding: 40px 0; + text-align: center; + font-size: 13px; + color: var(--text-tertiary); +} + +.automation-task-list { + display: flex; + flex-direction: column; + gap: 10px; +} + +.automation-task-card { + border-radius: 14px; + background: color-mix(in srgb, var(--text-tertiary) 5%, transparent); + padding: 14px 16px; + display: flex; + align-items: flex-start; + gap: 12px; + transition: background 0.2s ease; + + &:hover { + background: color-mix(in srgb, var(--text-tertiary) 9%, transparent); + } + + &.disabled { + opacity: 0.6; + } +} + +.automation-task-main { + flex: 1; + min-width: 0; + + p { + margin: 3px 0 0; + font-size: 12px; + color: var(--text-secondary); + line-height: 1.5; + } +} + +.automation-task-title-row { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 6px; + + strong { + font-size: 14px; + color: var(--text-primary); + font-weight: 600; + } +} + +.automation-task-status { + font-size: 11px; + font-weight: 600; + padding: 2px 8px; + border-radius: 999px; + + &.enabled { + background: color-mix(in srgb, var(--primary) 12%, transparent); + color: var(--primary); + } + + &.disabled { + background: color-mix(in srgb, var(--text-tertiary) 12%, transparent); + color: var(--text-tertiary); + } + + &.running { + background: rgba(82, 196, 26, 0.14); + color: #52c41a; + } + + &.queued { + background: color-mix(in srgb, var(--primary) 12%, transparent); + color: var(--primary); + } +} + +.automation-task-actions { + display: flex; + flex-direction: column; + gap: 6px; + flex-shrink: 0; +} + +.automation-editor-overlay { + position: fixed; + inset: 0; + z-index: 7800; + background: rgba(0, 0, 0, 0.42); + display: flex; + align-items: center; + justify-content: center; + padding: 24px 20px; + animation: exportOverlayFadeIn 0.2s ease both; +} + +.automation-editor-modal { + width: min(560px, 100%); + max-height: min(88vh, 900px); + border-radius: 20px; + border: 1px solid color-mix(in srgb, var(--text-tertiary) 8%, transparent); + background: var(--bg-primary); + box-shadow: 0 24px 64px rgba(0, 0, 0, 0.18); + display: flex; + flex-direction: column; + overflow: hidden; + animation: footprintFadeSlideUp 0.28s cubic-bezier(0.2, 0.78, 0.26, 1) both; +} + +.automation-editor-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 20px 20px 16px; + border-bottom: 1px solid color-mix(in srgb, var(--text-tertiary) 6%, transparent); + flex-shrink: 0; + + h3 { + margin: 0; + font-size: 15px; + font-weight: 700; + color: var(--text-primary); + } +} + +.automation-editor-body { + flex: 1; + min-height: 0; + overflow-y: auto; + padding: 16px 20px; + display: flex; + flex-direction: column; + gap: 14px; + + /* 裸 input 统一样式(未套 .automation-form-field 的情况) */ + >input[type='datetime-local'], + >input[type='number'], + >input[type='text'] { + height: 36px; + border: none; + border-radius: 10px; + background: color-mix(in srgb, var(--text-tertiary) 6%, transparent); + color: var(--text-primary); + padding: 0 12px; + font-size: 13px; + width: 100%; + box-sizing: border-box; + outline: none; + transition: background 0.2s ease, box-shadow 0.2s ease; + + &:focus { + background: color-mix(in srgb, var(--text-tertiary) 8%, transparent); + box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary) 20%, transparent); + } + + &::-webkit-inner-spin-button, + &::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; + } + + /* datetime-local 的日历图标调整外观 */ + &::-webkit-calendar-picker-indicator { + opacity: 0.5; + cursor: pointer; + filter: var(--datetime-picker-icon-filter, none); + transition: opacity 0.18s ease; + border-radius: 4px; + padding: 2px; + + &:hover { + opacity: 1; + background: color-mix(in srgb, var(--text-tertiary) 10%, transparent); + } + } + + &::-webkit-datetime-edit { + padding: 0; + } + + &::-webkit-datetime-edit-fields-wrapper { + background: transparent; + } + + &::-webkit-datetime-edit-text { + color: var(--text-tertiary); + padding: 0 1px; + } + + &::-webkit-datetime-edit-year-field, + &::-webkit-datetime-edit-month-field, + &::-webkit-datetime-edit-day-field, + &::-webkit-datetime-edit-hour-field, + &::-webkit-datetime-edit-minute-field, + &::-webkit-datetime-edit-ampm-field { + color: var(--text-primary); + border-radius: 3px; + padding: 0 2px; + + &:focus { + background: color-mix(in srgb, var(--primary) 16%, transparent); + color: var(--primary); + outline: none; + } + } + } + + /* 嵌套在 div 内的裸 input(如 stopAt 在 .automation-form-field > div 里) */ + input[type='datetime-local']:not(.automation-form-field input) { + height: 36px; + border: none; + border-radius: 10px; + background: color-mix(in srgb, var(--text-tertiary) 6%, transparent); + color: var(--text-primary); + padding: 0 12px; + font-size: 13px; + width: 100%; + box-sizing: border-box; + outline: none; + transition: background 0.2s ease, box-shadow 0.2s ease; + + &:focus { + background: color-mix(in srgb, var(--text-tertiary) 8%, transparent); + box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary) 20%, transparent); + } + + &::-webkit-calendar-picker-indicator { + opacity: 0.5; + cursor: pointer; + transition: opacity 0.18s ease; + border-radius: 4px; + padding: 2px; + + &:hover { + opacity: 1; + } + } + + &::-webkit-datetime-edit-year-field, + &::-webkit-datetime-edit-month-field, + &::-webkit-datetime-edit-day-field, + &::-webkit-datetime-edit-hour-field, + &::-webkit-datetime-edit-minute-field { + color: var(--text-primary); + border-radius: 3px; + padding: 0 2px; + + &:focus { + background: color-mix(in srgb, var(--primary) 16%, transparent); + color: var(--primary); + outline: none; + } + } + } + + input[type='number']:not(.automation-form-field input) { + height: 36px; + border: none; + border-radius: 10px; + background: color-mix(in srgb, var(--text-tertiary) 6%, transparent); + color: var(--text-primary); + padding: 0 12px; + font-size: 13px; + width: 100%; + box-sizing: border-box; + outline: none; + transition: background 0.2s ease, box-shadow 0.2s ease; + + &:focus { + background: color-mix(in srgb, var(--text-tertiary) 8%, transparent); + box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary) 20%, transparent); + } + + &::-webkit-inner-spin-button, + &::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; + } + } +} + +.automation-editor-actions { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 8px; + padding: 12px 20px 16px; + border-top: 1px solid color-mix(in srgb, var(--text-tertiary) 6%, transparent); + flex-shrink: 0; +} + +.automation-form-field { + display: flex; + flex-direction: column; + gap: 6px; + font-size: 13px; + display: flex; + flex-direction: column; + gap: 6px; + font-size: 13px; + + >span { + font-size: 12px; + font-weight: 600; + color: var(--text-secondary); + } + + input[type='text'], + input[type='number'], + input[type='datetime-local'] { + height: 36px; + border: none; + border-radius: 10px; + background: color-mix(in srgb, var(--text-tertiary) 6%, transparent); + color: var(--text-primary); + padding: 0 12px; + font-size: 13px; + width: 100%; + box-sizing: border-box; + outline: none; + transition: background 0.2s ease, box-shadow 0.2s ease; + + &:focus { + background: color-mix(in srgb, var(--text-tertiary) 8%, transparent); + box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary) 20%, transparent); + } + + &::-webkit-inner-spin-button, + &::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; + } + + &::-webkit-calendar-picker-indicator { + cursor: pointer; + opacity: 0.6; + transition: opacity 0.2s ease; + + &:hover { + opacity: 1; + } + } + } +} + +.automation-inline-time { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; +} + +.automation-inline-check { + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; + color: var(--text-primary); + cursor: pointer; + user-select: none; + + input[type='checkbox'] { + appearance: none; + -webkit-appearance: none; + width: 16px; + height: 16px; + cursor: pointer; + flex-shrink: 0; + border-radius: 4px; + border: 1px solid color-mix(in srgb, var(--text-tertiary) 30%, transparent); + background: transparent; + transition: all 0.2s ease; + position: relative; + + &:hover { + background: color-mix(in srgb, var(--text-tertiary) 6%, transparent); + } + + &:checked { + background: var(--primary); + border-color: var(--primary); + + &::after { + content: ''; + position: absolute; + left: 5px; + top: 2px; + width: 4px; + height: 7px; + border: solid #fff; + border-width: 0 2px 2px 0; + transform: rotate(45deg); + } + } + } +} + +.automation-segment-row { + display: flex; + gap: 6px; + flex-wrap: wrap; +} + +.automation-segment-btn { + border: none; + border-radius: 8px; + padding: 6px 14px; + background: color-mix(in srgb, var(--text-tertiary) 8%, transparent); + color: var(--text-secondary); + font-size: 12px; + font-weight: 500; + cursor: pointer; + transition: all 0.18s ease; + + &:hover { + background: color-mix(in srgb, var(--text-tertiary) 14%, transparent); + color: var(--text-primary); + } + + &.active { + background: color-mix(in srgb, var(--primary) 14%, transparent); + color: var(--primary); + } +} + +.automation-path-row { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + font-size: 12px; + color: var(--text-tertiary); + padding: 4px 2px; +} + +.automation-draft-summary { + padding: 10px 12px; + border-radius: 10px; + background: color-mix(in srgb, var(--text-tertiary) 5%, transparent); + font-size: 12px; + color: var(--text-secondary); + line-height: 1.6; +} + +.close-icon-btn { + border: none; + background: color-mix(in srgb, var(--text-tertiary) 8%, transparent); + color: var(--text-secondary); + width: 30px; + height: 30px; + border-radius: 8px; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + flex-shrink: 0; + transition: all 0.18s ease; + + &:hover { + background: color-mix(in srgb, var(--text-tertiary) 16%, transparent); + color: var(--text-primary); + } +} + +.primary-btn { + border: none; + border-radius: 9px; + padding: 8px 18px; + background: var(--primary); + color: #fff; + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + box-shadow: 0 2px 6px color-mix(in srgb, var(--primary) 20%, transparent); + + &:hover:not(:disabled) { + background: color-mix(in srgb, var(--primary) 85%, #fff); + transform: translateY(-1px); + box-shadow: 0 8px 14px color-mix(in srgb, var(--primary) 30%, transparent); + } + + &:disabled { + opacity: 0.6; + cursor: not-allowed; + } +} + +/* 终止时间选择器 */ +.automation-stopat-picker { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + + input { + flex: 1; + min-width: 0; + height: 36px; + border: none; + border-radius: 10px; + background: color-mix(in srgb, var(--text-tertiary) 8%, transparent); + color: var(--text-primary); + padding: 0 10px; + font-size: 13px; + outline: none; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + font-variant-numeric: tabular-nums; + + &:hover { + background: color-mix(in srgb, var(--text-tertiary) 12%, transparent); + } + + &:focus { + background: color-mix(in srgb, var(--text-tertiary) 10%, transparent); + box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary) 25%, transparent); + } + } + + .automation-stopat-date { + flex: 1.4; + } + + .automation-stopat-time { + flex: 1; + text-align: center; + } +} + +/* 自动化创建模式提示 */ +.automation-create-mode-pill { + display: flex; + align-items: center; + gap: 12px; + padding: 6px 16px; + border-radius: 999px; + background: color-mix(in srgb, var(--primary) 8%, var(--bg-primary)); + border: 1px solid color-mix(in srgb, var(--primary) 15%, transparent); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); + animation: footprintFadeSlideUp 0.3s ease both; + white-space: nowrap; + margin-left: 8px; + + span { + font-size: 13px; + font-weight: 500; + color: var(--primary); + } + + .secondary-btn { + height: 24px; + padding: 0 10px; + font-size: 11px; + border-radius: 6px; + } +} \ No newline at end of file diff --git a/src/pages/ExportPage.tsx b/src/pages/ExportPage.tsx index 1c70471..b7d6f1c 100644 --- a/src/pages/ExportPage.tsx +++ b/src/pages/ExportPage.tsx @@ -32,6 +32,12 @@ import { import type { ChatSession as AppChatSession, ContactInfo } from '../types/models' import type { ExportOptions as ElectronExportOptions, ExportProgress } from '../types/electron' import type { BackgroundTaskRecord } from '../types/backgroundTask' +import type { + ExportAutomationCondition, + ExportAutomationDateRangeConfig, + ExportAutomationSchedule, + ExportAutomationTask +} from '../types/exportAutomation' import * as configService from '../services/config' import { emitExportSessionStatus, @@ -55,12 +61,15 @@ import type { SnsPost } from '../types/sns' import { cloneExportDateRange, cloneExportDateRangeSelection, + createDateRangeByLastNDays, createDefaultDateRange, createDefaultExportDateRangeSelection, getExportDateRangeLabel, resolveExportDateRangeConfig, + serializeExportDateRangeConfig, startOfDay, endOfDay, + type ExportDateRangePreset, type ExportDateRangeSelection } from '../utils/exportDateRange' import './ExportPage.scss' @@ -147,6 +156,8 @@ interface ExportTaskPayload { outputDir: string options?: ElectronExportOptions scope: TaskScope + source: 'manual' | 'automation' + automationTaskId?: string contentType?: ContentType sessionNames: string[] snsOptions?: { @@ -175,6 +186,7 @@ interface ExportTask { interface ExportDialogState { open: boolean + intent: 'manual' | 'automation-create' scope: TaskScope contentType?: ContentType sessionIds: string[] @@ -182,6 +194,27 @@ interface ExportDialogState { title: string } +interface AutomationTaskDraft { + mode: 'create' | 'edit' + id?: string + name: string + enabled: boolean + sessionIds: string[] + sessionNames: string[] + outputDir: string + useGlobalOutputDir: boolean + scope: Exclude + contentType?: ContentType + optionTemplate: Omit + dateRangeConfig: ExportAutomationDateRangeConfig | string | null + intervalDays: number + intervalHours: number + stopAtEnabled: boolean + stopAtValue: string + maxRunsEnabled: boolean + maxRuns: number +} + const defaultTxtColumns = ['index', 'time', 'senderRole', 'messageType', 'content'] const DETAIL_PRECISE_REFRESH_COOLDOWN_MS = 10 * 60 * 1000 const TASK_PERFORMANCE_UPDATE_MIN_INTERVAL_MS = 900 @@ -589,6 +622,7 @@ const matchesContactTab = (contact: ContactInfo, tab: ConversationTab): boolean } const createTaskId = (): string => `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}` +const createAutomationTaskId = (): string => `auto-${Date.now()}-${Math.random().toString(36).slice(2, 8)}` const CONTACT_ENRICH_TIMEOUT_MS = 7000 const EXPORT_SNS_STATS_CACHE_STALE_MS = 12 * 60 * 60 * 1000 const EXPORT_AVATAR_ENRICH_BATCH_SIZE = 80 @@ -599,6 +633,220 @@ const EXPORT_REENTER_SNS_SOFT_REFRESH_MS = 3 * 60 * 1000 type SessionDataSource = 'cache' | 'network' | null type ContactsDataSource = 'cache' | 'network' | null +const normalizeAutomationIntervalDays = (value: unknown): number => Math.max(0, Math.floor(Number(value) || 0)) +const normalizeAutomationIntervalHours = (value: unknown): number => Math.max(0, Math.min(23, Math.floor(Number(value) || 0))) + +const resolveAutomationIntervalMs = (schedule: ExportAutomationSchedule): number => { + const days = normalizeAutomationIntervalDays(schedule.intervalDays) + const hours = normalizeAutomationIntervalHours(schedule.intervalHours) + const totalHours = (days * 24) + hours + if (totalHours <= 0) return 0 + return totalHours * 60 * 60 * 1000 +} + +const formatAutomationScheduleLabel = (schedule: ExportAutomationSchedule): string => { + const days = normalizeAutomationIntervalDays(schedule.intervalDays) + const hours = normalizeAutomationIntervalHours(schedule.intervalHours) + const parts: string[] = [] + if (days > 0) parts.push(`${days} 天`) + if (hours > 0) parts.push(`${hours} 小时`) + return `每间隔 ${parts.length > 0 ? parts.join(' ') : '0 小时'} 执行一次` +} + +const resolveAutomationDueScheduleKey = (task: ExportAutomationTask, now: Date): string | null => { + const intervalMs = resolveAutomationIntervalMs(task.schedule) + if (intervalMs <= 0) return null + const nowMs = now.getTime() + const anchorAt = Math.max( + 0, + Number(task.runState?.lastTriggeredAt || 0) || Number(task.createdAt || 0) + ) + if (nowMs < anchorAt + intervalMs) return null + return `interval:${anchorAt}:${Math.floor((nowMs - anchorAt) / intervalMs)}` +} + +const toDateTimeLocalValue = (timestamp: number): string => { + const date = new Date(timestamp) + if (Number.isNaN(date.getTime())) return '' + const year = date.getFullYear() + const month = `${date.getMonth() + 1}`.padStart(2, '0') + const day = `${date.getDate()}`.padStart(2, '0') + const hours = `${date.getHours()}`.padStart(2, '0') + const minutes = `${date.getMinutes()}`.padStart(2, '0') + return `${year}-${month}-${day}T${hours}:${minutes}` +} + +const parseDateTimeLocalValue = (value: string): number | null => { + const text = String(value || '').trim() + if (!text) return null + const parsed = new Date(text) + const timestamp = parsed.getTime() + if (!Number.isFinite(timestamp)) return null + return Math.floor(timestamp) +} + +type AutomationRangeMode = 'all' | 'today' | 'yesterday' | 'last7days' | 'last30days' | 'last1year' | 'lastNDays' | 'custom' + +const AUTOMATION_RANGE_OPTIONS: Array<{ mode: AutomationRangeMode; label: string }> = [ + { mode: 'all', label: '全部时间' }, + { mode: 'yesterday', label: '往前1天' }, + { mode: 'last7days', label: '往前7天' }, + { mode: 'last30days', label: '往前30天' }, + { mode: 'last1year', label: '往前1年' }, + { mode: 'lastNDays', label: '往前N天' }, + { mode: 'custom', label: '完整时间' } +] + +const AUTOMATION_LAST_N_DAYS_MIN = 1 +const AUTOMATION_LAST_N_DAYS_MAX = 3650 +const AUTOMATION_LAST_N_DAYS_DEFAULT = 3 + +const normalizeAutomationLastNDays = (value: unknown): number => { + const parsed = Math.floor(Number(value) || 0) + if (!Number.isFinite(parsed) || parsed <= 0) return AUTOMATION_LAST_N_DAYS_DEFAULT + return Math.min(AUTOMATION_LAST_N_DAYS_MAX, Math.max(AUTOMATION_LAST_N_DAYS_MIN, parsed)) +} + +const readAutomationLastNDays = ( + config: ExportAutomationDateRangeConfig | string | null | undefined +): number | null => { + if (!config || typeof config !== 'object') return null + const raw = config as Record + const mode = String(raw.relativeMode || '').trim() + if (mode !== 'last-n-days') return null + const days = Math.floor(Number(raw.relativeDays) || 0) + if (!Number.isFinite(days) || days <= 0) return null + return Math.min(AUTOMATION_LAST_N_DAYS_MAX, Math.max(AUTOMATION_LAST_N_DAYS_MIN, days)) +} + +const buildAutomationLastNDaysConfig = (days: number): ExportAutomationDateRangeConfig => ({ + version: 1, + preset: 'custom', + useAllTime: false, + relativeMode: 'last-n-days', + relativeDays: normalizeAutomationLastNDays(days) +}) + +const resolveAutomationDateRangeSelection = ( + config: ExportAutomationDateRangeConfig | string | null | undefined, + now = new Date() +): ExportDateRangeSelection => { + const relativeDays = readAutomationLastNDays(config) + if (relativeDays) { + return { + preset: 'custom', + useAllTime: false, + dateRange: createDateRangeByLastNDays(relativeDays, now) + } + } + return resolveExportDateRangeConfig(config as any, now) +} + +const resolveAutomationRangeMode = ( + config: ExportAutomationDateRangeConfig | string | null | undefined, + selection: ExportDateRangeSelection +): AutomationRangeMode => { + if (readAutomationLastNDays(config)) return 'lastNDays' + if (selection.useAllTime) return 'all' + if (selection.preset === 'today') return 'today' + if (selection.preset === 'yesterday') return 'yesterday' + if (selection.preset === 'last7days') return 'last7days' + if (selection.preset === 'last30days') return 'last30days' + if (selection.preset === 'last1year') return 'last1year' + return 'custom' +} + +const createAutomationSelectionByMode = ( + mode: Exclude, + now = new Date() +): ExportDateRangeSelection => { + const preset: ExportDateRangePreset = mode + return resolveExportDateRangeConfig({ + version: 1, + preset, + useAllTime: mode === 'all' + }, now) +} + +const formatAutomationRangeLabel = ( + config: ExportAutomationDateRangeConfig | string | null | undefined, + selection?: ExportDateRangeSelection +): string => { + const resolved = selection || resolveAutomationDateRangeSelection(config, new Date()) + const mode = resolveAutomationRangeMode(config, resolved) + if (mode === 'all') return '每次触发导出全部历史消息' + if (mode === 'today') return '每次触发导出当天' + if (mode === 'yesterday') return '每次触发导出前1天(昨日)' + if (mode === 'last7days') return '每次触发导出前7天' + if (mode === 'last30days') return '每次触发导出前30天' + if (mode === 'last1year') return '每次触发导出前1年' + if (mode === 'lastNDays') { + return `每次触发导出前 ${readAutomationLastNDays(config) || AUTOMATION_LAST_N_DAYS_DEFAULT} 天` + } + return `完整时间:${getExportDateRangeLabel(resolved)}` +} + +const formatAutomationStopCondition = (task: ExportAutomationTask): string => { + const endAt = Number(task.stopCondition?.endAt || 0) + const maxRuns = Number(task.stopCondition?.maxRuns || 0) + const labels: string[] = [] + if (endAt > 0) { + labels.push(`截止到 ${new Date(endAt).toLocaleString('zh-CN')}`) + } + if (maxRuns > 0) { + const successCount = Math.max(0, Math.floor(Number(task.runState?.successCount || 0))) + labels.push(`成功 ${successCount}/${maxRuns} 次后停止`) + } + return labels.length > 0 ? labels.join(' · ') : '无' +} + +const resolveAutomationNextTriggerAt = (task: ExportAutomationTask): number | null => { + const intervalMs = resolveAutomationIntervalMs(task.schedule) + if (intervalMs <= 0) return null + const anchorAt = Math.max(0, Number(task.runState?.lastTriggeredAt || 0) || Number(task.createdAt || 0)) + if (!anchorAt) return null + return anchorAt + intervalMs +} + +const formatAutomationCurrentState = ( + task: ExportAutomationTask, + queueState: 'queued' | 'running' | null, + nowMs: number +): string => { + if (!task.enabled) return '已停用' + if (queueState === 'running') return '执行中' + if (queueState === 'queued') return '排队中' + const nextTriggerAt = resolveAutomationNextTriggerAt(task) + if (!nextTriggerAt) return '等待触发' + const diff = nextTriggerAt - nowMs + if (diff <= 0) return '即将触发' + return `等待触发 · 下次 ${new Date(nextTriggerAt).toLocaleString('zh-CN')}(约 ${formatDurationMs(diff)} 后)` +} + +const formatAutomationLastRunSummary = (task: ExportAutomationTask): string => { + const status = task.runState?.lastRunStatus || 'idle' + const label = ( + status === 'idle' ? '尚未执行' : + status === 'queued' ? '已入队' : + status === 'running' ? '执行中' : + status === 'success' ? '执行成功' : + status === 'error' ? '执行失败' : + status === 'skipped' ? '已跳过' : + status + ) + const parts: string[] = [label] + if (task.runState?.lastSuccessAt) { + parts.push(`最近成功于 ${new Date(task.runState.lastSuccessAt).toLocaleString('zh-CN')}`) + } + if (task.runState?.lastSkipReason) { + parts.push(task.runState.lastSkipReason) + } + if (task.runState?.lastError) { + parts.push(task.runState.lastError) + } + return parts.join(' · ') +} + interface ContactsLoadSession { requestId: string startedAt: number @@ -1574,6 +1822,8 @@ function ExportPage() { const [isSnsStatsLoading, setIsSnsStatsLoading] = useState(true) const [isBaseConfigLoading, setIsBaseConfigLoading] = useState(true) const [isTaskCenterOpen, setIsTaskCenterOpen] = useState(false) + const [isAutomationModalOpen, setIsAutomationModalOpen] = useState(false) + const [automationHint, setAutomationHint] = useState(null) const [expandedPerfTaskId, setExpandedPerfTaskId] = useState(null) const [sessions, setSessions] = useState([]) const [sessionDataSource, setSessionDataSource] = useState(null) @@ -1680,14 +1930,22 @@ function ExportPage() { const [exportDialog, setExportDialog] = useState({ open: false, + intent: 'manual', scope: 'single', sessionIds: [], sessionNames: [], title: '' }) + const [isAutomationCreateMode, setIsAutomationCreateMode] = useState(false) const [showSessionFormatSelect, setShowSessionFormatSelect] = useState(false) const [tasks, setTasks] = useState([]) + const [automationTasks, setAutomationTasks] = useState([]) + const [automationTaskDraft, setAutomationTaskDraft] = useState(null) + const [isAutomationRangeDialogOpen, setIsAutomationRangeDialogOpen] = useState(false) + const [isResolvingAutomationRangeBounds, setIsResolvingAutomationRangeBounds] = useState(false) + const [automationRangeBounds, setAutomationRangeBounds] = useState(null) + const [automationRangeSelection, setAutomationRangeSelection] = useState(() => createDefaultExportDateRangeSelection()) const [lastExportBySession, setLastExportBySession] = useState>({}) const [lastExportByContent, setLastExportByContent] = useState>({}) const [exportRecordsBySession, setExportRecordsBySession] = useState>({}) @@ -1714,6 +1972,10 @@ function ExportPage() { const progressUnsubscribeRef = useRef<(() => void) | null>(null) const runningTaskIdRef = useRef(null) const tasksRef = useRef([]) + const automationTasksRef = useRef([]) + const automationTasksReadyRef = useRef(false) + const automationSchedulerRunningRef = useRef(false) + const automationQueueStatusByTaskIdRef = useRef>(new Map()) const hasSeededSnsStatsRef = useRef(false) const sessionLoadTokenRef = useRef(0) const preselectAppliedRef = useRef(false) @@ -1810,6 +2072,46 @@ function ExportPage() { return scopeKey }, []) + const persistAutomationTasks = useCallback(async (nextTasks: ExportAutomationTask[]) => { + if (!automationTasksReadyRef.current) return + const scopeKey = await ensureExportCacheScope() + await configService.setExportAutomationTasks(scopeKey, nextTasks) + }, [ensureExportCacheScope]) + + const updateAutomationTasks = useCallback(( + updater: (prev: ExportAutomationTask[]) => ExportAutomationTask[] + ) => { + setAutomationTasks((prev) => { + const next = updater(prev) + void persistAutomationTasks(next) + return next + }) + }, [persistAutomationTasks]) + + const patchAutomationTask = useCallback(( + taskId: string, + updater: (task: ExportAutomationTask) => ExportAutomationTask + ) => { + updateAutomationTasks((prev) => prev.map((task) => (task.id === taskId ? updater(task) : task))) + }, [updateAutomationTasks]) + + const markAutomationTaskSkipped = useCallback((taskId: string, reason: string, scheduleKey?: string) => { + const now = Date.now() + patchAutomationTask(taskId, (task) => ({ + ...task, + updatedAt: now, + runState: { + ...(task.runState || {}), + lastRunStatus: 'skipped', + lastTriggeredAt: now, + lastSkipAt: now, + lastSkipReason: reason, + lastError: undefined, + lastScheduleKey: scheduleKey || task.runState?.lastScheduleKey + } + })) + }, [patchAutomationTask]) + const loadContactsCaches = useCallback(async (scopeKey: string) => { const [contactsItem, avatarItem] = await Promise.all([ configService.getContactsListCache(scopeKey), @@ -1821,6 +2123,22 @@ function ExportPage() { } }, []) + const ensureAutomationTasksLoaded = useCallback(async () => { + if (automationTasksReadyRef.current) return + try { + const scopeKey = await ensureExportCacheScope() + const automationTaskItem = await configService.getExportAutomationTasks(scopeKey) + setAutomationTasks(automationTaskItem?.tasks || []) + automationTasksReadyRef.current = true + } catch (error) { + console.error('加载自动化导出任务失败:', error) + } + }, [ensureExportCacheScope]) + + useEffect(() => { + void ensureAutomationTasksLoaded() + }, [ensureAutomationTasksLoaded]) + useEffect(() => { let cancelled = false void (async () => { @@ -2234,6 +2552,10 @@ function ExportPage() { tasksRef.current = tasks }, [tasks]) + useEffect(() => { + automationTasksRef.current = automationTasks + }, [automationTasks]) + useEffect(() => { sessionsRef.current = sessions }, [sessions]) @@ -2288,8 +2610,16 @@ function ExportPage() { return () => window.clearInterval(timer) }, [isTaskCenterOpen, expandedPerfTaskId, tasks]) + useEffect(() => { + if (!isAutomationModalOpen) return + setNowTick(Date.now()) + const timer = window.setInterval(() => setNowTick(Date.now()), 1000) + return () => window.clearInterval(timer) + }, [isAutomationModalOpen]) + const loadBaseConfig = useCallback(async (): Promise => { setIsBaseConfigLoading(true) + automationTasksReadyRef.current = false let isReady = true try { const [savedPath, savedFormat, savedAvatars, savedMedia, savedVoiceAsText, savedExcelCompactColumns, savedTxtColumns, savedConcurrency, savedImageDeepSearchOnMiss, savedSessionMap, savedContentMap, savedSessionRecordMap, savedSnsPostCount, savedWriteLayout, savedSessionNameWithTypePrefix, savedDefaultDateRange, savedFileNamingMode, exportCacheScope] = await Promise.all([ @@ -2314,6 +2644,7 @@ function ExportPage() { ]) const cachedSnsStats = await configService.getExportSnsStatsCache(exportCacheScope) + const automationTaskItem = await configService.getExportAutomationTasks(exportCacheScope) if (savedPath) { setExportFolder(savedPath) @@ -2342,6 +2673,8 @@ function ExportPage() { setExportDefaultConcurrency(savedConcurrency ?? 2) setExportDefaultImageDeepSearchOnMiss(savedImageDeepSearchOnMiss ?? true) setExportDefaultFileNamingMode(savedFileNamingMode ?? 'classic') + setAutomationTasks(automationTaskItem?.tasks || []) + automationTasksReadyRef.current = true const resolvedDefaultDateRange = resolveExportDateRangeConfig(savedDefaultDateRange) setExportDefaultDateRangeSelection(resolvedDefaultDateRange) setTimeRangeSelection(resolvedDefaultDateRange) @@ -2381,6 +2714,7 @@ function ExportPage() { })) } catch (error) { isReady = false + automationTasksReadyRef.current = false console.error('加载导出配置失败:', error) } finally { setIsBaseConfigLoading(false) @@ -4046,6 +4380,7 @@ function ExportPage() { useEffect(() => { if (isExportRoute) return // 导出页隐藏时停止后台联系人补齐请求,避免与通讯录页面查询抢占。 + setIsAutomationCreateMode(false) sessionLoadTokenRef.current = Date.now() sessionCountRequestIdRef.current += 1 snsUserPostCountsHydrationTokenRef.current += 1 @@ -4126,8 +4461,8 @@ function ExportPage() { const clearSelection = () => setSelectedSessions(new Set()) - const openExportDialog = useCallback((payload: Omit) => { - setExportDialog({ open: true, ...payload }) + const openExportDialog = useCallback((payload: Omit & { intent?: ExportDialogState['intent'] }) => { + setExportDialog({ open: true, intent: payload.intent || 'manual', ...payload }) setIsTimeRangeDialogOpen(false) setTimeRangeBounds(null) setTimeRangeSelection(exportDefaultDateRangeSelection) @@ -4197,7 +4532,7 @@ function ExportPage() { ]) const closeExportDialog = useCallback(() => { - setExportDialog(prev => ({ ...prev, open: false })) + setExportDialog(prev => ({ ...prev, open: false, intent: 'manual' })) setIsTimeRangeDialogOpen(false) setTimeRangeBounds(null) }, []) @@ -4488,6 +4823,202 @@ function ExportPage() { } } + const openCreateAutomationDraft = useCallback(() => { + setIsAutomationModalOpen(false) + setAutomationTaskDraft(null) + setIsAutomationRangeDialogOpen(false) + setIsAutomationCreateMode(true) + setSelectedSessions(new Set()) + setAutomationHint('已进入自动化任务创建:请勾选联系人,然后点击「加入任务」') + }, []) + + const openEditAutomationTaskDraft = useCallback((task: ExportAutomationTask) => { + const schedule = task.schedule + const stopAt = Number(task.stopCondition?.endAt || 0) + const maxRuns = Number(task.stopCondition?.maxRuns || 0) + const resolvedRange = resolveAutomationDateRangeSelection(task.template.dateRangeConfig as any, new Date()) + setAutomationRangeSelection(resolvedRange) + setAutomationRangeBounds(null) + setAutomationTaskDraft({ + mode: 'edit', + id: task.id, + name: task.name, + enabled: task.enabled, + sessionIds: task.sessionIds, + sessionNames: task.sessionNames, + outputDir: task.outputDir || exportFolder, + useGlobalOutputDir: !task.outputDir, + scope: task.template.scope, + contentType: task.template.contentType, + optionTemplate: task.template.optionTemplate, + dateRangeConfig: task.template.dateRangeConfig, + intervalDays: normalizeAutomationIntervalDays(schedule.intervalDays), + intervalHours: normalizeAutomationIntervalHours(schedule.intervalHours), + stopAtEnabled: stopAt > 0, + stopAtValue: stopAt > 0 ? toDateTimeLocalValue(stopAt) : '', + maxRunsEnabled: maxRuns > 0, + maxRuns: maxRuns > 0 ? maxRuns : 0 + }) + setIsAutomationModalOpen(true) + }, [exportFolder]) + + const openAutomationDateRangeDialog = useCallback(() => { + if (!automationTaskDraft) return + void (async () => { + if (isResolvingAutomationRangeBounds) return + setIsResolvingAutomationRangeBounds(true) + try { + const nextBounds = await resolveChatExportTimeRangeBounds(automationTaskDraft.sessionIds) + setAutomationRangeBounds(nextBounds) + if (nextBounds) { + const nextSelection = clampExportSelectionToBounds(automationRangeSelection, nextBounds) + if (!areExportSelectionsEqual(nextSelection, automationRangeSelection)) { + setAutomationRangeSelection(nextSelection) + setAutomationTaskDraft((prev) => prev ? { + ...prev, + dateRangeConfig: serializeExportDateRangeConfig(nextSelection) + } : prev) + } + } + setIsAutomationRangeDialogOpen(true) + } catch (error) { + console.error('自动化导出解析时间范围边界失败', error) + setAutomationRangeBounds(null) + setIsAutomationRangeDialogOpen(true) + } finally { + setIsResolvingAutomationRangeBounds(false) + } + })() + }, [ + automationRangeSelection, + automationTaskDraft, + isResolvingAutomationRangeBounds, + resolveChatExportTimeRangeBounds + ]) + + const applyAutomationRangeMode = useCallback((mode: AutomationRangeMode) => { + if (!automationTaskDraft) return + if (mode === 'custom') { + openAutomationDateRangeDialog() + return + } + if (mode === 'lastNDays') { + const relativeDays = readAutomationLastNDays(automationTaskDraft.dateRangeConfig) || AUTOMATION_LAST_N_DAYS_DEFAULT + const nextSelection: ExportDateRangeSelection = { + preset: 'custom', + useAllTime: false, + dateRange: createDateRangeByLastNDays(relativeDays, new Date()) + } + setAutomationRangeSelection(nextSelection) + setAutomationTaskDraft((prev) => prev ? { + ...prev, + dateRangeConfig: buildAutomationLastNDaysConfig(relativeDays) + } : prev) + return + } + const nextSelection = createAutomationSelectionByMode(mode, new Date()) + setAutomationRangeSelection(nextSelection) + setAutomationTaskDraft((prev) => prev ? { + ...prev, + dateRangeConfig: serializeExportDateRangeConfig(nextSelection) + } : prev) + }, [automationTaskDraft, openAutomationDateRangeDialog]) + + const updateAutomationLastNDays = useCallback((value: unknown) => { + const days = normalizeAutomationLastNDays(value) + const nextSelection: ExportDateRangeSelection = { + preset: 'custom', + useAllTime: false, + dateRange: createDateRangeByLastNDays(days, new Date()) + } + setAutomationRangeSelection(nextSelection) + setAutomationTaskDraft((prev) => prev ? { + ...prev, + dateRangeConfig: buildAutomationLastNDaysConfig(days) + } : prev) + }, []) + + const saveAutomationTaskDraft = useCallback(() => { + if (!automationTaskDraft) return + if (!automationTasksReadyRef.current) { + automationTasksReadyRef.current = true + } + const normalizedName = automationTaskDraft.name.trim() + if (!normalizedName) { + window.alert('请输入任务名称') + return + } + if (automationTaskDraft.sessionIds.length === 0) { + window.alert('自动化任务至少需要一个会话') + return + } + + const intervalDays = normalizeAutomationIntervalDays(automationTaskDraft.intervalDays) + const intervalHours = normalizeAutomationIntervalHours(automationTaskDraft.intervalHours) + if (intervalDays <= 0 && intervalHours <= 0) { + window.alert('执行间隔不能为 0,请至少设置天数或小时') + return + } + const schedule: ExportAutomationSchedule = { type: 'interval', intervalDays, intervalHours } + const stopAtTimestamp = automationTaskDraft.stopAtEnabled + ? parseDateTimeLocalValue(automationTaskDraft.stopAtValue) + : null + if (automationTaskDraft.stopAtEnabled && !stopAtTimestamp) { + window.alert('请填写有效的终止时间') + return + } + const maxRuns = automationTaskDraft.maxRunsEnabled + ? Math.max(0, Math.floor(Number(automationTaskDraft.maxRuns || 0))) + : 0 + if (automationTaskDraft.maxRunsEnabled && maxRuns <= 0) { + window.alert('请填写大于 0 的最大执行次数') + return + } + const stopCondition = { + endAt: stopAtTimestamp && stopAtTimestamp > 0 ? stopAtTimestamp : undefined, + maxRuns: maxRuns > 0 ? maxRuns : undefined + } + + const now = Date.now() + const condition: ExportAutomationCondition = { type: 'new-message-since-last-success' } + const nextTask: ExportAutomationTask = { + id: automationTaskDraft.mode === 'edit' && automationTaskDraft.id + ? automationTaskDraft.id + : createAutomationTaskId(), + name: normalizedName, + enabled: automationTaskDraft.enabled, + sessionIds: [...automationTaskDraft.sessionIds], + sessionNames: [...automationTaskDraft.sessionNames], + outputDir: automationTaskDraft.useGlobalOutputDir ? undefined : String(automationTaskDraft.outputDir || '').trim(), + schedule, + condition, + stopCondition: (stopCondition.endAt || stopCondition.maxRuns) ? stopCondition : undefined, + template: { + scope: automationTaskDraft.scope, + contentType: automationTaskDraft.contentType, + optionTemplate: { ...automationTaskDraft.optionTemplate }, + dateRangeConfig: automationTaskDraft.dateRangeConfig + }, + runState: automationTaskDraft.mode === 'edit' + ? automationTasksRef.current.find((item) => item.id === automationTaskDraft.id)?.runState + : { lastRunStatus: 'idle', successCount: 0 }, + createdAt: automationTaskDraft.mode === 'edit' + ? (automationTasksRef.current.find((item) => item.id === automationTaskDraft.id)?.createdAt || now) + : now, + updatedAt: now + } + + updateAutomationTasks((prev) => { + if (automationTaskDraft.mode === 'edit' && automationTaskDraft.id) { + return prev.map((task) => (task.id === automationTaskDraft.id ? nextTask : task)) + } + return [nextTask, ...prev] + }) + setAutomationTaskDraft(null) + setIsAutomationRangeDialogOpen(false) + setAutomationHint(automationTaskDraft.mode === 'edit' ? '自动化任务已更新' : '自动化任务已创建') + }, [automationTaskDraft, updateAutomationTasks]) + const markSessionExported = useCallback((sessionIds: string[], timestamp: number) => { setLastExportBySession(prev => { const next = { ...prev } @@ -4963,10 +5494,123 @@ function ExportPage() { } }, []) + const enqueueExportTask = useCallback((title: string, payload: ExportTaskPayload): string => { + const task: ExportTask = { + id: createTaskId(), + title, + status: 'queued', + settledSessionIds: [], + createdAt: Date.now(), + payload, + progress: createEmptyProgress(), + performance: payload.scope === 'content' && payload.contentType === 'text' + ? createEmptyTaskPerformance() + : undefined + } + setTasks(prev => [task, ...prev]) + return task.id + }, []) + + const buildAutomationExportOptions = useCallback((task: ExportAutomationTask): ElectronExportOptions => { + const selection = resolveAutomationDateRangeSelection(task.template.dateRangeConfig as any, new Date()) + const dateRange = selection.useAllTime + ? null + : { + start: Math.floor(selection.dateRange.start.getTime() / 1000), + end: Math.floor(selection.dateRange.end.getTime() / 1000) + } + return { + ...task.template.optionTemplate, + dateRange + } + }, []) + + const enqueueAutomationTask = useCallback(( + task: ExportAutomationTask, + options?: { scheduleKey?: string; force?: boolean; reason?: string } + ): { queued: boolean; reason?: string } => { + const outputDir = String(task.outputDir || exportFolder || '').trim() + if (!outputDir) { + return { queued: false, reason: '导出目录未设置' } + } + + const hasConflict = tasksRef.current.some((item) => { + if (item.status !== 'running' && item.status !== 'queued') return false + return item.payload.automationTaskId === task.id + }) + if (hasConflict) { + return { queued: false, reason: '任务已有执行队列,本次触发已跳过' } + } + + const exportOptions = buildAutomationExportOptions(task) + const contentType = task.template.contentType + const title = `自动化导出:${task.name}` + enqueueExportTask(title, { + sessionIds: task.sessionIds, + sessionNames: task.sessionNames, + outputDir, + options: exportOptions, + scope: task.template.scope, + source: 'automation', + automationTaskId: task.id, + contentType + }) + const now = Date.now() + patchAutomationTask(task.id, (prev) => ({ + ...prev, + updatedAt: now, + runState: { + ...(prev.runState || {}), + lastRunStatus: 'queued', + lastTriggeredAt: now, + lastSkipReason: undefined, + lastError: undefined, + lastScheduleKey: options?.scheduleKey || prev.runState?.lastScheduleKey + } + })) + if (options?.reason) { + setAutomationHint(options.reason) + } + return { queued: true } + }, [ + buildAutomationExportOptions, + enqueueExportTask, + exportFolder, + patchAutomationTask + ]) + + const resolveAutomationHasNewMessages = useCallback(async (task: ExportAutomationTask): Promise<{ shouldRun: boolean; reason?: string }> => { + const lastSuccessAt = Number(task.runState?.lastSuccessAt || 0) + if (!lastSuccessAt) return { shouldRun: true } + const stats = await window.electronAPI.chat.getExportSessionStats(task.sessionIds, { + includeRelations: false, + allowStaleCache: true + }) + if (!stats.success || !stats.data) { + return { shouldRun: false, reason: stats.error || '会话统计失败,已跳过' } + } + let latestTimestamp = 0 + for (const sessionId of task.sessionIds) { + const raw = Number(stats.data?.[sessionId]?.lastTimestamp || 0) + if (Number.isFinite(raw) && raw > latestTimestamp) { + latestTimestamp = Math.max(0, Math.floor(raw)) + } + } + if (latestTimestamp <= 0) { + return { shouldRun: false, reason: '未检测到可用会话时间戳,已跳过' } + } + const lastSuccessSeconds = Math.floor(lastSuccessAt / 1000) + if (latestTimestamp <= lastSuccessSeconds) { + return { shouldRun: false, reason: '目标会话无新消息,本次已跳过' } + } + return { shouldRun: true } + }, []) + const createTask = async () => { if (!exportDialog.open || !exportFolder) return if (exportDialog.scope !== 'sns' && exportDialog.sessionIds.length === 0) return + const isAutomationCreateIntent = exportDialog.intent === 'automation-create' const exportOptions = exportDialog.scope === 'sns' ? undefined : buildExportOptions(exportDialog.scope, exportDialog.contentType) @@ -4982,30 +5626,58 @@ function ExportPage() { ? '朋友圈批量导出' : `${contentTypeLabels[exportDialog.contentType || 'text']}批量导出` - const task: ExportTask = { - id: createTaskId(), - title, - status: 'queued', - settledSessionIds: [], - createdAt: Date.now(), - payload: { - sessionIds: exportDialog.sessionIds, - sessionNames: exportDialog.sessionNames, + if (isAutomationCreateIntent) { + if (!exportOptions || exportDialog.scope === 'sns') { + window.alert('自动化任务仅支持会话导出') + return + } + const { dateRange: _discard, ...optionTemplate } = exportOptions + const normalizedRangeSelection = cloneExportDateRangeSelection(timeRangeSelection) + const scope = exportDialog.scope === 'single' + ? 'single' + : exportDialog.scope === 'content' + ? 'content' + : 'multi' + setAutomationRangeSelection(normalizedRangeSelection) + setAutomationRangeBounds(null) + setAutomationTaskDraft({ + mode: 'create', + name: exportDialog.sessionIds.length === 1 + ? `${exportDialog.sessionNames[0] || '单会话'} 自动化导出` + : `自动化导出(${exportDialog.sessionIds.length} 个会话)`, + enabled: true, + sessionIds: [...exportDialog.sessionIds], + sessionNames: [...exportDialog.sessionNames], outputDir: exportFolder, - options: exportOptions, - scope: exportDialog.scope, - contentType: exportDialog.contentType, - snsOptions - }, - progress: createEmptyProgress(), - performance: exportDialog.scope === 'content' && exportDialog.contentType === 'text' - ? createEmptyTaskPerformance() - : undefined + useGlobalOutputDir: true, + scope, + contentType: scope === 'content' ? exportDialog.contentType : undefined, + optionTemplate, + dateRangeConfig: serializeExportDateRangeConfig(normalizedRangeSelection), + intervalDays: 1, + intervalHours: 0, + stopAtEnabled: false, + stopAtValue: '', + maxRunsEnabled: false, + maxRuns: 0 + }) + setIsAutomationCreateMode(false) + setAutomationHint('导出配置已完成,请继续设置自动化规则并保存任务') + closeExportDialog() + } else { + enqueueExportTask(title, { + sessionIds: exportDialog.sessionIds, + sessionNames: exportDialog.sessionNames, + outputDir: exportFolder, + options: exportOptions, + scope: exportDialog.scope, + source: 'manual', + contentType: exportDialog.contentType, + snsOptions + }) + closeExportDialog() } - setTasks(prev => [task, ...prev]) - closeExportDialog() - await configService.setExportDefaultFormat(options.format) await configService.setExportDefaultAvatars(options.exportAvatars) await configService.setExportDefaultMedia({ @@ -5062,6 +5734,30 @@ function ExportPage() { .map((item) => item.session) }, [resolveSessionExistingMessageCount]) + const exitAutomationCreateMode = useCallback(() => { + setIsAutomationCreateMode(false) + setAutomationHint('已退出自动化任务创建') + }, []) + + const openAutomationExportConfigDialog = useCallback(() => { + const selectedSet = new Set(selectedSessions) + const selectedRows = sessions.filter((session) => selectedSet.has(session.username)) + const orderedRows = orderSessionsForExport(selectedRows) + if (orderedRows.length === 0) { + window.alert('请先勾选至少一个可导出的会话') + return + } + const ids = orderedRows.map((session) => session.username) + const names = orderedRows.map((session) => session.displayName || session.username) + openExportDialog({ + scope: 'multi', + sessionIds: ids, + sessionNames: names, + title: `自动化任务导出配置(${ids.length} 个会话)`, + intent: 'automation-create' + }) + }, [openExportDialog, orderSessionsForExport, selectedSessions, sessions]) + const openBatchExport = () => { const selectedSet = new Set(selectedSessions) const selectedRows = sessions.filter((session) => selectedSet.has(session.username)) @@ -5147,6 +5843,181 @@ function ExportPage() { [tasks] ) + useEffect(() => { + const previous = automationQueueStatusByTaskIdRef.current + const next = new Map() + for (const task of tasks) { + if (task.payload.source !== 'automation' || !task.payload.automationTaskId) continue + const automationTaskId = task.payload.automationTaskId + next.set(task.id, task.status) + const previousStatus = previous.get(task.id) + if (previousStatus === task.status) continue + + const now = Date.now() + if (task.status === 'running') { + patchAutomationTask(automationTaskId, (current) => ({ + ...current, + updatedAt: now, + runState: { + ...(current.runState || {}), + lastRunStatus: 'running', + lastStartedAt: now, + lastSkipReason: undefined, + lastError: undefined + } + })) + } else if (task.status === 'success') { + patchAutomationTask(automationTaskId, (current) => ({ + ...current, + updatedAt: now, + runState: { + ...(current.runState || {}), + lastRunStatus: 'success', + lastFinishedAt: now, + lastSuccessAt: now, + lastSkipReason: undefined, + lastError: undefined, + successCount: Math.max(0, Math.floor(Number(current.runState?.successCount || 0))) + 1 + } + })) + } else if (task.status === 'error') { + patchAutomationTask(automationTaskId, (current) => ({ + ...current, + updatedAt: now, + runState: { + ...(current.runState || {}), + lastRunStatus: 'error', + lastFinishedAt: now, + lastError: task.error || '导出失败' + } + })) + } + } + automationQueueStatusByTaskIdRef.current = next + }, [patchAutomationTask, tasks]) + + const evaluateAutomationSchedules = useCallback(async () => { + if (!automationTasksReadyRef.current) return + if (automationSchedulerRunningRef.current) return + automationSchedulerRunningRef.current = true + try { + const now = new Date() + const enabledTasks = automationTasksRef.current.filter((task) => task.enabled) + for (const task of enabledTasks) { + const successCount = Math.max(0, Math.floor(Number(task.runState?.successCount || 0))) + const maxRuns = Math.max(0, Math.floor(Number(task.stopCondition?.maxRuns || 0))) + if (maxRuns > 0 && successCount >= maxRuns) { + const stopAt = Date.now() + patchAutomationTask(task.id, (current) => ({ + ...current, + enabled: false, + updatedAt: stopAt, + runState: { + ...(current.runState || {}), + lastRunStatus: 'skipped', + lastSkipAt: stopAt, + lastSkipReason: `已达到最大执行次数(${maxRuns} 次),任务已自动停用`, + successCount: Math.max(0, Math.floor(Number(current.runState?.successCount || 0))) + } + })) + continue + } + + const endAt = Number(task.stopCondition?.endAt || 0) + if (endAt > 0 && now.getTime() > endAt) { + const stopAt = Date.now() + patchAutomationTask(task.id, (current) => ({ + ...current, + enabled: false, + updatedAt: stopAt, + runState: { + ...(current.runState || {}), + lastRunStatus: 'skipped', + lastSkipAt: stopAt, + lastSkipReason: '已超过终止时间,任务已自动停用' + } + })) + continue + } + + const scheduleKey = resolveAutomationDueScheduleKey(task, now) + if (!scheduleKey) continue + if (task.runState?.lastScheduleKey === scheduleKey) continue + + const hasConflict = tasksRef.current.some((item) => { + if (item.status !== 'running' && item.status !== 'queued') return false + return item.payload.automationTaskId === task.id + }) + if (hasConflict) { + markAutomationTaskSkipped(task.id, '任务仍在执行中,本次触发已跳过', scheduleKey) + continue + } + + if (task.condition.type === 'new-message-since-last-success') { + const checkResult = await resolveAutomationHasNewMessages(task) + if (!checkResult.shouldRun) { + markAutomationTaskSkipped(task.id, checkResult.reason || '无新消息,本次触发已跳过', scheduleKey) + continue + } + } + + const queued = enqueueAutomationTask(task, { scheduleKey }) + if (!queued.queued) { + markAutomationTaskSkipped(task.id, queued.reason || '触发失败,本次已跳过', scheduleKey) + } + } + } finally { + automationSchedulerRunningRef.current = false + } + }, [ + enqueueAutomationTask, + markAutomationTaskSkipped, + patchAutomationTask, + resolveAutomationHasNewMessages + ]) + + useEffect(() => { + let cancelled = false + const run = async () => { + if (cancelled) return + if (!automationTasksReadyRef.current) return + try { + await evaluateAutomationSchedules() + } catch (error) { + console.error('自动化导出调度失败:', error) + } + } + void run() + const timer = window.setInterval(() => { + void run() + }, 30_000) + return () => { + cancelled = true + window.clearInterval(timer) + } + }, [evaluateAutomationSchedules]) + + const runAutomationTaskNow = useCallback((taskId: string) => { + const target = automationTasksRef.current.find((task) => task.id === taskId) + if (!target) return + const queued = enqueueAutomationTask(target, { + reason: `已手动触发「${target.name}」`, + scheduleKey: target.runState?.lastScheduleKey + }) + if (!queued.queued) { + markAutomationTaskSkipped(taskId, queued.reason || '手动触发失败') + setAutomationHint(queued.reason || '手动触发失败') + return + } + setAutomationHint(`已加入队列:${target.name}`) + }, [enqueueAutomationTask, markAutomationTaskSkipped]) + + useEffect(() => { + if (!automationHint) return + const timer = window.setTimeout(() => setAutomationHint(null), 2600) + return () => window.clearTimeout(timer) + }, [automationHint]) + const inProgressSessionIdsKey = useMemo( () => inProgressSessionIds.join('||'), [inProgressSessionIds] @@ -6497,6 +7368,7 @@ function ExportPage() { const canCreateTask = exportDialog.scope === 'sns' ? Boolean(exportFolder) : Boolean(exportFolder) && exportDialog.sessionIds.length > 0 + const isAutomationCreateDialog = exportDialog.intent === 'automation-create' const scopeLabel = exportDialog.scope === 'single' ? '单会话' : exportDialog.scope === 'multi' @@ -6815,6 +7687,43 @@ function ExportPage() { const toggleTaskPerfDetail = useCallback((taskId: string) => { setExpandedPerfTaskId(prev => (prev === taskId ? null : taskId)) }, []) + + const toggleAutomationTaskEnabled = useCallback((taskId: string, enabled: boolean) => { + const now = Date.now() + patchAutomationTask(taskId, (task) => ({ + ...task, + enabled, + updatedAt: now + })) + setAutomationHint(enabled ? '自动化任务已启用' : '自动化任务已停用') + }, [patchAutomationTask]) + + const deleteAutomationTask = useCallback((taskId: string) => { + const target = automationTasksRef.current.find((task) => task.id === taskId) + if (!target) return + const confirmed = window.confirm(`确认删除自动化任务「${target.name}」吗?`) + if (!confirmed) return + updateAutomationTasks((prev) => prev.filter((task) => task.id !== taskId)) + setAutomationHint('自动化任务已删除') + }, [updateAutomationTasks]) + + const chooseAutomationDraftOutputDir = useCallback(async () => { + if (!automationTaskDraft) return + const result = await window.electronAPI.dialog.openFile({ + title: '选择任务导出目录', + properties: ['openDirectory'] + }) + if (result.canceled || result.filePaths.length === 0) return + const outputDir = result.filePaths[0] + setAutomationTaskDraft((prev) => { + if (!prev) return prev + return { + ...prev, + outputDir, + useGlobalOutputDir: false + } + }) + }, [automationTaskDraft]) const renderContactRow = useCallback((index: number, contact: ContactInfo) => { const matchedSession = sessionRowByUsername.get(contact.username) const canExport = Boolean(matchedSession?.hasSession) @@ -6824,6 +7733,7 @@ function ExportPage() { const isQueued = canExport && queuedSessionIds.has(contact.username) const recentExportTimestamp = lastExportBySession[contact.username] const hasRecentExport = canExport && Boolean(recentExportTimestamp) + const showRecentExport = !isAutomationCreateMode && hasRecentExport const recentExportTime = hasRecentExport ? formatRecentExportTime(recentExportTimestamp, nowTick) : '' const countedMessages = normalizeMessageCount(sessionMessageCounts[contact.username]) const hintedMessages = normalizeMessageCount(matchedSession?.messageCountHint) @@ -7014,24 +7924,26 @@ function ExportPage() { )}
-
-
- - {hasRecentExport && {recentExportTime}} -
+
+ {!isAutomationCreateMode && ( +
+ + {showRecentExport && {recentExportTime}} +
+ )}
+ {automationHint && ( +
{automationHint}
+ )}
+ {isAutomationModalOpen && createPortal( +
{ + setIsAutomationModalOpen(false) + setAutomationTaskDraft(null) + setIsAutomationRangeDialogOpen(false) + }} + > +
event.stopPropagation()} + > +
+
+

自动化导出

+

仅在应用运行期间生效;错过触发不会补跑。

+
+
+ + +
+
+ +
+ {sortedAutomationTasks.length === 0 ? ( +
+ 暂无自动化任务。点击右上角「新建任务」开始配置。 +
+ ) : ( +
+ {sortedAutomationTasks.map((task) => { + const linkedQueueTask = tasks.find((item) => ( + (item.status === 'running' || item.status === 'queued') && + item.payload.automationTaskId === task.id + )) + const queueState: 'queued' | 'running' | null = linkedQueueTask?.status === 'running' + ? 'running' + : linkedQueueTask?.status === 'queued' + ? 'queued' + : null + return ( +
+
+
+ {task.name} + + {task.enabled ? '已启用' : '已停用'} + + {queueState === 'running' && 执行中} + {queueState === 'queued' && 排队中} +
+

{formatAutomationScheduleLabel(task.schedule)}

+

时间范围:{formatAutomationRangeLabel(task.template.dateRangeConfig as any)}

+

会话范围:{task.sessionIds.length} 个

+

导出目录:{task.outputDir || `${exportFolder || '未设置'}(全局)`}

+

当前状态:{formatAutomationCurrentState(task, queueState, nowTick)}

+

终止条件:{formatAutomationStopCondition(task)}

+

最近结果:{formatAutomationLastRunSummary(task)}

+
+
+ + + + +
+
+ ) + })} +
+ )} +
+
+
, + document.body + )} + + {automationTaskDraft && createPortal( +
{ + setAutomationTaskDraft(null) + setIsAutomationRangeDialogOpen(false) + }}> +
event.stopPropagation()} + > +
+

{automationTaskDraft.mode === 'edit' ? '编辑自动化任务' : '创建自动化任务'}

+ +
+
+ + +
+ + +
+ +
+ 导出时间范围(按触发时间动态计算) +
+ {AUTOMATION_RANGE_OPTIONS.map((option) => { + const active = resolveAutomationRangeMode(automationTaskDraft.dateRangeConfig as any, automationRangeSelection) === option.mode + return ( + + ) + })} +
+ {resolveAutomationRangeMode(automationTaskDraft.dateRangeConfig as any, automationRangeSelection) === 'lastNDays' && ( + + )} +
+ {formatAutomationRangeLabel(automationTaskDraft.dateRangeConfig as any, automationRangeSelection)} + {resolveAutomationRangeMode(automationTaskDraft.dateRangeConfig as any, automationRangeSelection) === 'custom' && ( + + )} +
+
+ +
+ 终止条件(可选) + + {automationTaskDraft.stopAtEnabled && ( +
+ { + const datePart = e.target.value + const timePart = automationTaskDraft.stopAtValue?.slice(11) || '23:59' + setAutomationTaskDraft((prev) => prev ? { ...prev, stopAtValue: datePart ? `${datePart}T${timePart}` : '' } : prev) + }} + /> + { + const timePart = e.target.value + const datePart = automationTaskDraft.stopAtValue?.slice(0, 10) || new Date().toISOString().slice(0, 10) + setAutomationTaskDraft((prev) => prev ? { ...prev, stopAtValue: `${datePart}T${timePart}` } : prev) + }} + /> +
+ )} + + + + {automationTaskDraft.maxRunsEnabled && ( + setAutomationTaskDraft((prev) => prev ? { + ...prev, + maxRuns: Math.max(0, Math.floor(Number(event.target.value) || 0)) + } : prev)} + /> + )} +
+ +
+ 导出目录 + + {!automationTaskDraft.useGlobalOutputDir && ( +
+ + {automationTaskDraft.outputDir || '未设置'} +
+ )} +
+ + + +
+ 会话:{automationTaskDraft.sessionIds.length} 个 · 间隔:{automationTaskDraft.intervalDays} 天 {automationTaskDraft.intervalHours} 小时 · 时间:{formatAutomationRangeLabel(automationTaskDraft.dateRangeConfig as any, automationRangeSelection)} · 条件:有新消息才导出 +
+
+
+ + +
+ setIsAutomationRangeDialogOpen(false)} + onConfirm={(nextSelection) => { + setAutomationRangeSelection(nextSelection) + setAutomationTaskDraft((prev) => prev ? { + ...prev, + dateRangeConfig: serializeExportDateRangeConfig(nextSelection) + } : prev) + setIsAutomationRangeDialogOpen(false) + }} + /> +
+
, + document.body + )} + {isExportDefaultsModalOpen && createPortal(
))}
- + {!isAutomationCreateMode && ( + + )}
) })} @@ -7340,6 +8634,18 @@ function ExportPage() { '你可以先在列表中筛选目标会话,再批量导出,结果会保留每个会话的结构与时间线。' ]} /> + {isAutomationCreateMode && ( +
+ 自动化创建中:先勾选联系人,再点击「加入任务」 + +
+ )} + )} {selectedCount > 0 && ( <> @@ -8299,20 +9614,22 @@ function ExportPage() { -
-
-

时间范围

- + {!isAutomationCreateDialog && ( +
+
+

时间范围

+ +
-
+ )} {shouldShowMediaSection && (
@@ -8490,7 +9807,7 @@ function ExportPage() {
diff --git a/src/pages/WelcomePage.scss b/src/pages/WelcomePage.scss index fb5012d..49a77e6 100644 --- a/src/pages/WelcomePage.scss +++ b/src/pages/WelcomePage.scss @@ -304,6 +304,19 @@ } } +.nav-hint { + margin-top: 4px; + display: inline-flex; + align-items: center; + width: fit-content; + padding: 1px 6px; + border-radius: 999px; + font-size: 11px; + font-weight: 600; + color: #0f766e; + background: rgba(20, 184, 166, 0.16); +} + .sidebar-footer { display: flex; @@ -362,6 +375,16 @@ font-size: 15px; margin: 0; } + + .header-mode-tip { + margin: 10px 0 0; + padding: 6px 10px; + border-radius: 10px; + font-size: 12px; + color: var(--text-secondary); + background: var(--bg-tertiary); + border: 1px dashed var(--border-color); + } } .step-icon-wrapper { @@ -556,6 +579,41 @@ gap: 16px; } +.auto-image-key-preview { + display: flex; + flex-direction: column; + gap: 8px; + padding: 12px; + border: 1px solid var(--border-color); + border-radius: 12px; + background: var(--bg-tertiary); +} + +.auto-image-key-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + + code { + padding: 4px 8px; + border-radius: 8px; + background: var(--bg-primary); + border: 1px solid var(--border-color); + font-size: 12px; + color: var(--text-primary); + max-width: 70%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } +} + +.auto-image-key-label { + font-size: 13px; + color: var(--text-secondary); +} + .mt-4 { margin-top: 16px; } diff --git a/src/pages/WelcomePage.tsx b/src/pages/WelcomePage.tsx index 4e83843..1dda111 100644 --- a/src/pages/WelcomePage.tsx +++ b/src/pages/WelcomePage.tsx @@ -1,5 +1,5 @@ import { useState, useEffect, useRef } from 'react' -import { useNavigate } from 'react-router-dom' +import { useLocation, useNavigate } from 'react-router-dom' import { useAppStore } from '../stores/appStore' import { dialog } from '../services/ipc' import * as configService from '../services/config' @@ -31,6 +31,7 @@ const steps = [ { id: 'security', title: '安全防护', desc: '保护你的数据' } ] type SetupStepId = typeof steps[number]['id'] +type ImageKeyResolveSource = 'manual-cache' | 'prefetch-cache' | 'memory-scan' interface WelcomePageProps { standalone?: boolean @@ -61,9 +62,44 @@ const isDbKeyReadyMessage = (message: string): boolean => ( || message.includes('已准备就绪,现在登录微信或退出登录后重新登录微信') ) +const pickWxidByAnchorTime = ( + wxids: Array<{ wxid: string; modifiedTime: number }>, + anchorTime?: number +): string => { + if (!Array.isArray(wxids) || wxids.length === 0) return '' + const fallbackWxid = wxids[0]?.wxid || '' + if (!anchorTime || !Number.isFinite(anchorTime)) return fallbackWxid + + const valid = wxids.filter(item => Number.isFinite(item.modifiedTime) && item.modifiedTime > 0) + if (valid.length === 0) return fallbackWxid + + const anchor = Number(anchorTime) + const nearWindowMs = 10 * 60 * 1000 + + const near = valid + .filter(item => Math.abs(item.modifiedTime - anchor) <= nearWindowMs) + .sort((a, b) => { + const diffGap = Math.abs(a.modifiedTime - anchor) - Math.abs(b.modifiedTime - anchor) + if (diffGap !== 0) return diffGap + if (b.modifiedTime !== a.modifiedTime) return b.modifiedTime - a.modifiedTime + return a.wxid.localeCompare(b.wxid) + }) + if (near.length > 0) return near[0].wxid + + const closest = valid.sort((a, b) => { + const diffGap = Math.abs(a.modifiedTime - anchor) - Math.abs(b.modifiedTime - anchor) + if (diffGap !== 0) return diffGap + if (b.modifiedTime !== a.modifiedTime) return b.modifiedTime - a.modifiedTime + return a.wxid.localeCompare(b.wxid) + }) + return closest[0]?.wxid || fallbackWxid +} + function WelcomePage({ standalone = false }: WelcomePageProps) { const navigate = useNavigate() + const location = useLocation() const { isDbConnected, setDbConnected, setLoading } = useAppStore() + const isAddAccountMode = standalone && new URLSearchParams(location.search).get('mode') === 'add-account' const [stepIndex, setStepIndex] = useState(0) const [dbPath, setDbPath] = useState('') @@ -92,7 +128,11 @@ function WelcomePage({ standalone = false }: WelcomePageProps) { const [imageKeyStatus, setImageKeyStatus] = useState('') const [isManualStartPrompt, setIsManualStartPrompt] = useState(false) const [imageKeyPercent, setImageKeyPercent] = useState(null) + const [isImageKeyVerified, setIsImageKeyVerified] = useState(false) + const [isImageStepAutoCompleted, setIsImageStepAutoCompleted] = useState(false) + const [hasReacquiredDbKey, setHasReacquiredDbKey] = useState(!isAddAccountMode) const [showDbKeyConfirm, setShowDbKeyConfirm] = useState(false) + const imagePrefetchAttemptRef = useRef('') // 安全相关 state const [enableAuth, setEnableAuth] = useState(false) @@ -191,7 +231,79 @@ function WelcomePage({ standalone = false }: WelcomePageProps) { setWxidOptions([]) setWxid('') setShowWxidSelect(false) - }, [dbPath]) + setIsImageKeyVerified(false) + setIsImageStepAutoCompleted(false) + if (isAddAccountMode) { + setHasReacquiredDbKey(false) + setDecryptKey('') + } + imagePrefetchAttemptRef.current = '' + }, [dbPath, isAddAccountMode]) + + useEffect(() => { + if (!isAddAccountMode) return + let cancelled = false + + const hydrateAddAccountMode = async () => { + const keyStepIndex = steps.findIndex(step => step.id === 'key') + if (keyStepIndex >= 0) { + setStepIndex(keyStepIndex) + } + + try { + const [ + savedDbPath, + savedCachePath, + savedWxid, + savedImageXorKey, + savedImageAesKey + ] = await Promise.all([ + configService.getDbPath(), + configService.getCachePath(), + configService.getMyWxid(), + configService.getImageXorKey(), + configService.getImageAesKey() + ]) + if (cancelled) return + + setDbPath(savedDbPath || '') + setCachePath(savedCachePath || '') + setDecryptKey('') + setHasReacquiredDbKey(false) + if (typeof savedImageXorKey === 'number' && Number.isFinite(savedImageXorKey)) { + setImageXorKey(`0x${savedImageXorKey.toString(16).toUpperCase().padStart(2, '0')}`) + } + setImageAesKey(savedImageAesKey || '') + + if (savedDbPath) { + const scannedWxids = await window.electronAPI.dbPath.scanWxids(savedDbPath) + if (cancelled) return + setWxidOptions(scannedWxids) + + const preferredWxid = String(savedWxid || '').trim() + const matched = scannedWxids.find(item => item.wxid === preferredWxid) + if (matched) { + setWxid(matched.wxid) + } else if (preferredWxid) { + setWxid(preferredWxid) + } else if (scannedWxids.length > 0) { + setWxid(scannedWxids[0].wxid) + } + } else if (savedWxid) { + setWxid(savedWxid) + } + } catch (e) { + if (!cancelled) { + setError(`加载当前账号配置失败: ${e}`) + } + } + } + + void hydrateAddAccountMode() + return () => { + cancelled = true + } + }, [isAddAccountMode]) useEffect(() => { const handleClickOutside = (event: MouseEvent) => { @@ -206,10 +318,30 @@ function WelcomePage({ standalone = false }: WelcomePageProps) { return () => document.removeEventListener('mousedown', handleClickOutside) }, [showWxidSelect]) - const currentStep = steps[stepIndex] + const imageStepIndex = steps.findIndex(step => step.id === 'image') + const securityStepIndex = steps.findIndex(step => step.id === 'security') + const currentStep = steps[stepIndex] ?? steps[0] + const imagePreCompletedAhead = isImageStepAutoCompleted && imageStepIndex >= 0 && stepIndex < imageStepIndex const rootClassName = `welcome-page${isClosing ? ' is-closing' : ''}${standalone ? ' is-standalone' : ''}` const showWindowControls = standalone + const isStepCompleted = (index: number, stepId: SetupStepId): boolean => { + if (index < stepIndex) return true + if (stepId === 'image' && isImageStepAutoCompleted) return true + if (isAddAccountMode && stepId !== 'key') return true + return false + } + + const resolveStepDesc = (step: { id: SetupStepId; desc: string }): string => { + if (step.id === 'image' && isImageStepAutoCompleted) { + return '缓存校验成功,已自动完成' + } + if (isAddAccountMode && step.id !== 'key') { + return '已沿用当前配置' + } + return step.desc + } + const handleMinimize = () => { window.electronAPI.window.minimize() } @@ -302,7 +434,7 @@ function WelcomePage({ standalone = false }: WelcomePageProps) { } } - const handleScanWxid = async (silent = false) => { + const handleScanWxid = async (silent = false, anchorTime?: number) => { if (!dbPath) { if (!silent) setError('请先选择数据库目录') return @@ -314,8 +446,10 @@ function WelcomePage({ standalone = false }: WelcomePageProps) { const wxids = await window.electronAPI.dbPath.scanWxids(dbPath) setWxidOptions(wxids) if (wxids.length > 0) { - // scanWxids 已经按时间排过序了,直接取第一个 - setWxid(wxids[0].wxid) + // 密钥成功后使用成功时刻作为锚点,自动选择最接近该时刻的活跃账号; + // 其余场景保持“时间最新”优先。 + const selectedWxid = pickWxidByAnchorTime(wxids, anchorTime) + setWxid(selectedWxid || wxids[0].wxid) if (!silent) setError('') } else { if (!silent) setError('未检测到账号目录,请检查路径') @@ -364,10 +498,15 @@ function WelcomePage({ standalone = false }: WelcomePageProps) { const result = await window.electronAPI.key.autoGetDbKey() if (result.success && result.key) { setDecryptKey(result.key) + setHasReacquiredDbKey(true) setDbKeyStatus('密钥获取成功') setError('') - await handleScanWxid(true) + const keySuccessAt = Date.now() + await handleScanWxid(true, keySuccessAt) } else { + if (isAddAccountMode) { + setHasReacquiredDbKey(false) + } if ( result.error?.includes('未找到微信安装路径') || result.error?.includes('启动微信失败') || @@ -396,25 +535,45 @@ function WelcomePage({ standalone = false }: WelcomePageProps) { handleAutoGetDbKey() } - const handleAutoGetImageKey = async () => { + const handleAutoGetImageKey = async ( + source: ImageKeyResolveSource = 'manual-cache', + options?: { silentError?: boolean } + ) => { if (isFetchingImageKey) return if (!dbPath) { setError('请先选择数据库目录'); return } setIsFetchingImageKey(true) - setError('') + if (!options?.silentError) { + setError('') + } setImageKeyPercent(0) - setImageKeyStatus('正在准备获取图片密钥...') + setImageKeyStatus(source === 'prefetch-cache' ? '正在预计算图片密钥...' : '正在准备获取图片密钥...') try { const accountPath = wxid ? `${dbPath}/${wxid}` : dbPath const result = await window.electronAPI.key.autoGetImageKey(accountPath, wxid) if (result.success && result.aesKey) { if (typeof result.xorKey === 'number') setImageXorKey(`0x${result.xorKey.toString(16).toUpperCase().padStart(2, '0')}`) setImageAesKey(result.aesKey) - setImageKeyStatus('已获取图片密钥') + const verified = result.verified === true + setIsImageKeyVerified(verified) + setIsImageStepAutoCompleted(verified) + if (verified) { + setImageKeyStatus(source === 'prefetch-cache' ? '图片密钥已预先自动完成(缓存校验通过)' : '图片密钥获取成功(缓存校验通过)') + } else { + setImageKeyStatus('已自动计算图片密钥(未完成校验)') + } } else { - setError(result.error || '自动获取图片密钥失败') + setIsImageKeyVerified(false) + setIsImageStepAutoCompleted(false) + if (!options?.silentError) { + setError(result.error || '自动获取图片密钥失败') + } } } catch (e) { - setError(`自动获取图片密钥失败: ${e}`) + setIsImageKeyVerified(false) + setIsImageStepAutoCompleted(false) + if (!options?.silentError) { + setError(`自动获取图片密钥失败: ${e}`) + } } finally { setIsFetchingImageKey(false) } @@ -433,6 +592,8 @@ function WelcomePage({ standalone = false }: WelcomePageProps) { if (result.success && result.aesKey) { if (typeof result.xorKey === 'number') setImageXorKey(`0x${result.xorKey.toString(16).toUpperCase().padStart(2, '0')}`) setImageAesKey(result.aesKey) + setIsImageKeyVerified(false) + setIsImageStepAutoCompleted(false) setImageKeyStatus('内存扫描成功,已获取图片密钥') } else { setError(result.error || '内存扫描获取图片密钥失败') @@ -444,6 +605,14 @@ function WelcomePage({ standalone = false }: WelcomePageProps) { } } + useEffect(() => { + if (!dbPath || !wxid || decryptKey.length !== 64) return + const attemptKey = `${dbPath}::${wxid}::${decryptKey}` + if (imagePrefetchAttemptRef.current === attemptKey) return + imagePrefetchAttemptRef.current = attemptKey + void handleAutoGetImageKey('prefetch-cache', { silentError: true }) + }, [dbPath, wxid, decryptKey]) + const jumpToStep = (stepId: SetupStepId) => { const targetIndex = steps.findIndex(step => step.id === stepId) if (targetIndex >= 0) setStepIndex(targetIndex) @@ -487,6 +656,10 @@ function WelcomePage({ standalone = false }: WelcomePageProps) { } const canGoNext = () => { + if (isAddAccountMode) { + if (currentStep.id === 'key') return hasReacquiredDbKey && decryptKey.length === 64 && Boolean(wxid) + return true + } if (currentStep.id === 'intro') return true if (currentStep.id === 'db') return Boolean(dbPath) && !dbPathValidationError if (currentStep.id === 'cache') return true @@ -502,6 +675,11 @@ function WelcomePage({ standalone = false }: WelcomePageProps) { } const handleNext = async () => { + if (isAddAccountMode) { + await handleConnect() + return + } + if (currentStep.id === 'db') { const dbStepIssue = await validateDbStepBeforeNext() if (dbStepIssue) { @@ -520,15 +698,25 @@ function WelcomePage({ standalone = false }: WelcomePageProps) { return } setError('') + if (currentStep.id === 'key' && isImageStepAutoCompleted && securityStepIndex >= 0) { + setStepIndex(securityStepIndex) + return + } setStepIndex((prev) => Math.min(prev + 1, steps.length - 1)) } const handleBack = () => { + if (isAddAccountMode) return setError('') setStepIndex((prev) => Math.max(prev - 1, 0)) } const handleConnect = async () => { + if (isAddAccountMode && !hasReacquiredDbKey) { + setError('请先在当前流程中自动获取一次数据库密钥') + return + } + const configIssue = await findConfigIssueBeforeConnect() if (configIssue) { setError(configIssue.message) @@ -708,13 +896,16 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
{steps.map((step, index) => ( -
+
- {index < stepIndex ? :
} + {isStepCompleted(index, step.id) ? :
}
{step.title}
-
{step.desc}
+
{resolveStepDesc(step)}
+ {step.id === 'image' && imagePreCompletedAhead && ( +
已预先自动完成
+ )}
))} @@ -731,6 +922,9 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {

{currentStep.title}

{currentStep.desc}

+ {isAddAccountMode && ( +

添加账号模式:其他步骤已沿用当前配置,只需重新获取数据库密钥。

+ )}
@@ -863,6 +1057,9 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
{dbKeyStatus &&
{dbKeyStatus}
} + {isAddAccountMode && !hasReacquiredDbKey && ( +
添加账号模式下需先自动获取一次数据库密钥,才能完成并返回主窗口。
+ )}
)} @@ -936,19 +1133,19 @@ function WelcomePage({ standalone = false }: WelcomePageProps) { {currentStep.id === 'image' && (
-
-
- - setImageXorKey(e.target.value)} /> +
+
+ 图片 XOR 密钥 + {imageXorKey || '等待自动计算'}
-
- - setImageAesKey(e.target.value)} /> +
+ 图片 AES 密钥 + {imageAesKey || '等待自动计算'}
-
)}
@@ -981,11 +1188,15 @@ function WelcomePage({ standalone = false }: WelcomePageProps) { )}
- - {stepIndex < steps.length - 1 ? ( + {isAddAccountMode ? ( + + ) : stepIndex < steps.length - 1 ? ( diff --git a/src/services/config.ts b/src/services/config.ts index afbbee4..179e11d 100644 --- a/src/services/config.ts +++ b/src/services/config.ts @@ -1,6 +1,7 @@ // 配置服务 - 封装 Electron Store import { config } from './ipc' import type { ExportDefaultDateRangeConfig } from '../utils/exportDateRange' +import type { ExportAutomationTask } from '../types/exportAutomation' // 配置键名 export const CONFIG_KEYS = { @@ -48,6 +49,7 @@ export const CONFIG_KEYS = { EXPORT_SNS_STATS_CACHE_MAP: 'exportSnsStatsCacheMap', EXPORT_SNS_USER_POST_COUNTS_CACHE_MAP: 'exportSnsUserPostCountsCacheMap', EXPORT_SESSION_MUTUAL_FRIENDS_CACHE_MAP: 'exportSessionMutualFriendsCacheMap', + EXPORT_AUTOMATION_TASK_MAP: 'exportAutomationTaskMap', SNS_PAGE_CACHE_MAP: 'snsPageCacheMap', CONTACTS_LOAD_TIMEOUT_MS: 'contactsLoadTimeoutMs', CONTACTS_LIST_CACHE_MAP: 'contactsListCacheMap', @@ -190,6 +192,10 @@ export async function getWxidConfigs(): Promise> { return {} } +export async function setWxidConfigs(configs: Record): Promise { + await config.set(CONFIG_KEYS.WXID_CONFIGS, configs || {}) +} + export async function getWxidConfig(wxid: string): Promise { if (!wxid) return null const configs = await getWxidConfigs() @@ -660,6 +666,183 @@ export async function setExportLastSnsPostCount(count: number): Promise { await config.set(CONFIG_KEYS.EXPORT_LAST_SNS_POST_COUNT, normalized) } +export interface ExportAutomationTaskMapItem { + updatedAt: number + tasks: ExportAutomationTask[] +} + +const normalizeAutomationNumeric = (value: unknown, fallback: number): number => { + const numeric = Number(value) + if (!Number.isFinite(numeric)) return fallback + return Math.floor(numeric) +} + +const normalizeAutomationTask = (raw: unknown): ExportAutomationTask | null => { + if (!raw || typeof raw !== 'object') return null + const source = raw as Record + + const id = String(source.id || '').trim() + const name = String(source.name || '').trim() + if (!id || !name) return null + + const sessionIds = Array.isArray(source.sessionIds) + ? Array.from(new Set(source.sessionIds.map((item) => String(item || '').trim()).filter(Boolean))) + : [] + const sessionNames = Array.isArray(source.sessionNames) + ? source.sessionNames.map((item) => String(item || '').trim()).filter(Boolean) + : [] + if (sessionIds.length === 0) return null + + const scheduleRaw = source.schedule + if (!scheduleRaw || typeof scheduleRaw !== 'object') return null + const scheduleObj = scheduleRaw as Record + const scheduleType = String(scheduleObj.type || '').trim() as ExportAutomationTask['schedule']['type'] + let schedule: ExportAutomationTask['schedule'] | null = null + if (scheduleType === 'interval') { + const rawDays = Math.max(0, normalizeAutomationNumeric(scheduleObj.intervalDays, 0)) + const rawHours = Math.max(0, normalizeAutomationNumeric(scheduleObj.intervalHours, 0)) + const totalHours = (rawDays * 24) + rawHours + if (totalHours <= 0) return null + const intervalDays = Math.floor(totalHours / 24) + const intervalHours = totalHours % 24 + schedule = { type: 'interval', intervalDays, intervalHours } + } + if (!schedule) return null + + const conditionRaw = source.condition + if (!conditionRaw || typeof conditionRaw !== 'object') return null + const conditionType = String((conditionRaw as Record).type || '').trim() + if (conditionType !== 'new-message-since-last-success') return null + + const templateRaw = source.template + if (!templateRaw || typeof templateRaw !== 'object') return null + const template = templateRaw as Record + const scope = String(template.scope || '').trim() as ExportAutomationTask['template']['scope'] + if (scope !== 'single' && scope !== 'multi' && scope !== 'content') return null + const optionTemplate = template.optionTemplate + if (!optionTemplate || typeof optionTemplate !== 'object') return null + const dateRangeConfig = template.dateRangeConfig + const outputDirRaw = String(source.outputDir || '').trim() + const runStateRaw = source.runState && typeof source.runState === 'object' + ? (source.runState as Record) + : null + const stopConditionRaw = source.stopCondition && typeof source.stopCondition === 'object' + ? (source.stopCondition as Record) + : null + const rawContentType = String(template.contentType || '').trim() + const contentType = ( + rawContentType === 'text' || + rawContentType === 'voice' || + rawContentType === 'image' || + rawContentType === 'video' || + rawContentType === 'emoji' || + rawContentType === 'file' + ) + ? rawContentType + : undefined + const rawRunStatus = runStateRaw ? String(runStateRaw.lastRunStatus || '').trim() : '' + const lastRunStatus = ( + rawRunStatus === 'idle' || + rawRunStatus === 'queued' || + rawRunStatus === 'running' || + rawRunStatus === 'success' || + rawRunStatus === 'error' || + rawRunStatus === 'skipped' + ) + ? rawRunStatus + : undefined + const endAt = stopConditionRaw ? Math.max(0, normalizeAutomationNumeric(stopConditionRaw.endAt, 0)) : 0 + const maxRuns = stopConditionRaw ? Math.max(0, normalizeAutomationNumeric(stopConditionRaw.maxRuns, 0)) : 0 + + return { + id, + name, + enabled: source.enabled !== false, + sessionIds, + sessionNames, + outputDir: outputDirRaw || undefined, + schedule, + condition: { type: 'new-message-since-last-success' }, + stopCondition: (endAt > 0 || maxRuns > 0) + ? { + endAt: endAt > 0 ? endAt : undefined, + maxRuns: maxRuns > 0 ? maxRuns : undefined + } + : undefined, + template: { + scope, + contentType, + optionTemplate: optionTemplate as ExportAutomationTask['template']['optionTemplate'], + dateRangeConfig: (dateRangeConfig ?? null) as ExportAutomationTask['template']['dateRangeConfig'] + }, + runState: runStateRaw + ? { + lastRunStatus, + lastTriggeredAt: normalizeAutomationNumeric(runStateRaw.lastTriggeredAt, 0) || undefined, + lastStartedAt: normalizeAutomationNumeric(runStateRaw.lastStartedAt, 0) || undefined, + lastFinishedAt: normalizeAutomationNumeric(runStateRaw.lastFinishedAt, 0) || undefined, + lastSuccessAt: normalizeAutomationNumeric(runStateRaw.lastSuccessAt, 0) || undefined, + lastSkipAt: normalizeAutomationNumeric(runStateRaw.lastSkipAt, 0) || undefined, + lastSkipReason: String(runStateRaw.lastSkipReason || '').trim() || undefined, + lastError: String(runStateRaw.lastError || '').trim() || undefined, + lastScheduleKey: String(runStateRaw.lastScheduleKey || '').trim() || undefined, + successCount: Math.max(0, normalizeAutomationNumeric(runStateRaw.successCount, 0)) || undefined + } + : undefined, + createdAt: Math.max(0, normalizeAutomationNumeric(source.createdAt, Date.now())), + updatedAt: Math.max(0, normalizeAutomationNumeric(source.updatedAt, Date.now())) + } +} + +export async function getExportAutomationTasks(scopeKey: string): Promise { + if (!scopeKey) return null + const value = await config.get(CONFIG_KEYS.EXPORT_AUTOMATION_TASK_MAP) + if (!value || typeof value !== 'object') return null + const rawMap = value as Record + const rawItem = rawMap[scopeKey] + if (!rawItem || typeof rawItem !== 'object') return null + + const item = rawItem as Record + const updatedAt = Number(item.updatedAt) + const rawTasks = Array.isArray(item.tasks) + ? item.tasks + : (Array.isArray(rawItem) ? rawItem : []) + const tasks: ExportAutomationTask[] = [] + for (const rawTask of rawTasks) { + const normalized = normalizeAutomationTask(rawTask) + if (normalized) { + tasks.push(normalized) + } + } + return { + updatedAt: Number.isFinite(updatedAt) ? Math.max(0, Math.floor(updatedAt)) : 0, + tasks + } +} + +export async function setExportAutomationTasks(scopeKey: string, tasks: ExportAutomationTask[]): Promise { + if (!scopeKey) return + const current = await config.get(CONFIG_KEYS.EXPORT_AUTOMATION_TASK_MAP) + const map = current && typeof current === 'object' + ? { ...(current as Record) } + : {} + map[scopeKey] = { + updatedAt: Date.now(), + tasks: (Array.isArray(tasks) ? tasks : []).map((task) => ({ ...task })) + } + await config.set(CONFIG_KEYS.EXPORT_AUTOMATION_TASK_MAP, map) +} + +export async function clearExportAutomationTasks(scopeKey: string): Promise { + if (!scopeKey) return + const current = await config.get(CONFIG_KEYS.EXPORT_AUTOMATION_TASK_MAP) + if (!current || typeof current !== 'object') return + const map = { ...(current as Record) } + if (!(scopeKey in map)) return + delete map[scopeKey] + await config.set(CONFIG_KEYS.EXPORT_AUTOMATION_TASK_MAP, map) +} + export interface ExportSessionMessageCountCacheItem { updatedAt: number counts: Record diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index 06a47c4..244896d 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -18,7 +18,7 @@ export interface ElectronAPI { respondCloseConfirm: (action: 'tray' | 'quit' | 'cancel') => Promise openAgreementWindow: () => Promise completeOnboarding: () => Promise - openOnboardingWindow: () => Promise + openOnboardingWindow: (options?: { mode?: 'add-account' }) => Promise setTitleBarOverlay: (options: { symbolColor: string }) => void openVideoPlayerWindow: (videoPath: string, videoWidth?: number, videoHeight?: number) => Promise resizeToFitVideo: (videoWidth: number, videoHeight: number) => Promise @@ -146,7 +146,7 @@ export interface ElectronAPI { } key: { autoGetDbKey: () => Promise<{ success: boolean; key?: string; error?: string; logs?: string[] }> - autoGetImageKey: (manualDir?: string, wxid?: string) => Promise<{ success: boolean; xorKey?: number; aesKey?: string; error?: string }> + autoGetImageKey: (manualDir?: string, wxid?: string) => Promise<{ success: boolean; xorKey?: number; aesKey?: string; verified?: boolean; error?: string }> scanImageKeyFromMemory: (userDir: string) => Promise<{ success: boolean; xorKey?: number; aesKey?: string; error?: string }> onDbKeyStatus: (callback: (payload: { message: string; level: number }) => void) => () => void onImageKeyStatus: (callback: (payload: { message: string }) => void) => () => void diff --git a/src/types/exportAutomation.ts b/src/types/exportAutomation.ts new file mode 100644 index 0000000..2725f6c --- /dev/null +++ b/src/types/exportAutomation.ts @@ -0,0 +1,68 @@ +import type { ExportOptions as ElectronExportOptions } from './electron' + +export type ExportAutomationScope = 'single' | 'multi' | 'content' +export type ExportAutomationContentType = 'text' | 'voice' | 'image' | 'video' | 'emoji' | 'file' + +export type ExportAutomationSchedule = + | { + type: 'interval' + intervalDays: number + intervalHours: number + } + +export interface ExportAutomationCondition { + type: 'new-message-since-last-success' +} + +export interface ExportAutomationDateRangeConfig { + version?: 1 + preset?: string + useAllTime?: boolean + start?: string | number | Date | null + end?: string | number | Date | null + relativeMode?: 'last-n-days' | string + relativeDays?: number +} + +export interface ExportAutomationTemplate { + scope: ExportAutomationScope + contentType?: ExportAutomationContentType + optionTemplate: Omit + dateRangeConfig: ExportAutomationDateRangeConfig | string | null +} + +export interface ExportAutomationStopCondition { + endAt?: number + maxRuns?: number +} + +export type ExportAutomationRunStatus = 'idle' | 'queued' | 'running' | 'success' | 'error' | 'skipped' + +export interface ExportAutomationRunState { + lastRunStatus?: ExportAutomationRunStatus + lastTriggeredAt?: number + lastStartedAt?: number + lastFinishedAt?: number + lastSuccessAt?: number + lastSkipAt?: number + lastSkipReason?: string + lastError?: string + lastScheduleKey?: string + successCount?: number +} + +export interface ExportAutomationTask { + id: string + name: string + enabled: boolean + sessionIds: string[] + sessionNames: string[] + outputDir?: string + schedule: ExportAutomationSchedule + condition: ExportAutomationCondition + stopCondition?: ExportAutomationStopCondition + template: ExportAutomationTemplate + runState?: ExportAutomationRunState + createdAt: number + updatedAt: number +} From a05cde93bd83a2b3853ab7255d89e1e9036bb54d Mon Sep 17 00:00:00 2001 From: H3CoF6 <1707889225@qq.com> Date: Mon, 13 Apr 2026 04:37:23 +0800 Subject: [PATCH 6/9] chore: add aur pkg build file --- package-lock.json | 338 +++++++++++------------ resources/installer/linux/PKGBUILD | 30 ++ resources/installer/linux/icon.png | Bin 0 -> 55144 bytes resources/installer/linux/weflow.desktop | 9 + 4 files changed, 208 insertions(+), 169 deletions(-) create mode 100644 resources/installer/linux/PKGBUILD create mode 100644 resources/installer/linux/icon.png create mode 100644 resources/installer/linux/weflow.desktop diff --git a/package-lock.json b/package-lock.json index 0c06ec1..0151587 100644 --- a/package-lock.json +++ b/package-lock.json @@ -849,9 +849,9 @@ "optional": true }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.5.tgz", - "integrity": "sha512-nGsF/4C7uzUj+Nj/4J+Zt0bYQ6bz33Phz8Lb2N80Mti1HjGclTJdXZ+9APC4kLvONbjxN1zfvYNd8FEcbBK/MQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", "cpu": [ "ppc64" ], @@ -866,9 +866,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.5.tgz", - "integrity": "sha512-Cv781jd0Rfj/paoNrul1/r4G0HLvuFKYh7C9uHZ2Pl8YXstzvCyyeWENTFR9qFnRzNMCjXmsulZuvosDg10Mog==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", "cpu": [ "arm" ], @@ -883,9 +883,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.5.tgz", - "integrity": "sha512-Oeghq+XFgh1pUGd1YKs4DDoxzxkoUkvko+T/IVKwlghKLvvjbGFB3ek8VEDBmNvqhwuL0CQS3cExdzpmUyIrgA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", "cpu": [ "arm64" ], @@ -900,9 +900,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.5.tgz", - "integrity": "sha512-nQD7lspbzerlmtNOxYMFAGmhxgzn8Z7m9jgFkh6kpkjsAhZee1w8tJW3ZlW+N9iRePz0oPUDrYrXidCPSImD0Q==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", "cpu": [ "x64" ], @@ -917,9 +917,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.5.tgz", - "integrity": "sha512-I+Ya/MgC6rr8oRWGRDF3BXDfP8K1BVUggHqN6VI2lUZLdDi1IM1v2cy0e3lCPbP+pVcK3Tv8cgUhHse1kaNZZw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", "cpu": [ "arm64" ], @@ -934,9 +934,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.5.tgz", - "integrity": "sha512-MCjQUtC8wWJn/pIPM7vQaO69BFgwPD1jriEdqwTCKzWjGgkMbcg+M5HzrOhPhuYe1AJjXlHmD142KQf+jnYj8A==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", "cpu": [ "x64" ], @@ -951,9 +951,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.5.tgz", - "integrity": "sha512-X6xVS+goSH0UelYXnuf4GHLwpOdc8rgK/zai+dKzBMnncw7BTQIwquOodE7EKvY2UVUetSqyAfyZC1D+oqLQtg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", "cpu": [ "arm64" ], @@ -968,9 +968,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.5.tgz", - "integrity": "sha512-233X1FGo3a8x1ekLB6XT69LfZ83vqz+9z3TSEQCTYfMNY880A97nr81KbPcAMl9rmOFp11wO0dP+eB18KU/Ucg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", "cpu": [ "x64" ], @@ -985,9 +985,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.5.tgz", - "integrity": "sha512-0wkVrYHG4sdCCN/bcwQ7yYMXACkaHc3UFeaEOwSVW6e5RycMageYAFv+JS2bKLwHyeKVUvtoVH+5/RHq0fgeFw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", "cpu": [ "arm" ], @@ -1002,9 +1002,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.5.tgz", - "integrity": "sha512-euKkilsNOv7x/M1NKsx5znyprbpsRFIzTV6lWziqJch7yWYayfLtZzDxDTl+LSQDJYAjd9TVb/Kt5UKIrj2e4A==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", "cpu": [ "arm64" ], @@ -1019,9 +1019,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.5.tgz", - "integrity": "sha512-hVRQX4+P3MS36NxOy24v/Cdsimy/5HYePw+tmPqnNN1fxV0bPrFWR6TMqwXPwoTM2VzbkA+4lbHWUKDd5ZDA/w==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", "cpu": [ "ia32" ], @@ -1036,9 +1036,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.5.tgz", - "integrity": "sha512-mKqqRuOPALI8nDzhOBmIS0INvZOOFGGg5n1osGIXAx8oersceEbKd4t1ACNTHM3sJBXGFAlEgqM+svzjPot+ZQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", "cpu": [ "loong64" ], @@ -1053,9 +1053,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.5.tgz", - "integrity": "sha512-EE/QXH9IyaAj1qeuIV5+/GZkBTipgGO782Ff7Um3vPS9cvLhJJeATy4Ggxikz2inZ46KByamMn6GqtqyVjhenA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", "cpu": [ "mips64el" ], @@ -1070,9 +1070,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.5.tgz", - "integrity": "sha512-0V2iF1RGxBf1b7/BjurA5jfkl7PtySjom1r6xOK2q9KWw/XCpAdtB6KNMO+9xx69yYfSCRR9FE0TyKfHA2eQMw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", "cpu": [ "ppc64" ], @@ -1087,9 +1087,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.5.tgz", - "integrity": "sha512-rYxThBx6G9HN6tFNuvB/vykeLi4VDsm5hE5pVwzqbAjZEARQrWu3noZSfbEnPZ/CRXP3271GyFk/49up2W190g==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", "cpu": [ "riscv64" ], @@ -1104,9 +1104,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.5.tgz", - "integrity": "sha512-uEP2q/4qgd8goEUc4QIdU/1P2NmEtZ/zX5u3OpLlCGhJIuBIv0s0wr7TB2nBrd3/A5XIdEkkS5ZLF0ULuvaaYQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", "cpu": [ "s390x" ], @@ -1121,9 +1121,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.5.tgz", - "integrity": "sha512-+Gq47Wqq6PLOOZuBzVSII2//9yyHNKZLuwfzCemqexqOQCSz0zy0O26kIzyp9EMNMK+nZ0tFHBZrCeVUuMs/ew==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", "cpu": [ "x64" ], @@ -1138,9 +1138,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.5.tgz", - "integrity": "sha512-3F/5EG8VHfN/I+W5cO1/SV2H9Q/5r7vcHabMnBqhHK2lTWOh3F8vixNzo8lqxrlmBtZVFpW8pmITHnq54+Tq4g==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", "cpu": [ "arm64" ], @@ -1155,9 +1155,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.5.tgz", - "integrity": "sha512-28t+Sj3CPN8vkMOlZotOmDgilQwVvxWZl7b8rxpn73Tt/gCnvrHxQUMng4uu3itdFvrtba/1nHejvxqz8xgEMA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", "cpu": [ "x64" ], @@ -1172,9 +1172,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.5.tgz", - "integrity": "sha512-Doz/hKtiuVAi9hMsBMpwBANhIZc8l238U2Onko3t2xUp8xtM0ZKdDYHMnm/qPFVthY8KtxkXaocwmMh6VolzMA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", "cpu": [ "arm64" ], @@ -1189,9 +1189,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.5.tgz", - "integrity": "sha512-WfGVaa1oz5A7+ZFPkERIbIhKT4olvGl1tyzTRaB5yoZRLqC0KwaO95FeZtOdQj/oKkjW57KcVF944m62/0GYtA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", "cpu": [ "x64" ], @@ -1206,9 +1206,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.5", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.5.tgz", - "integrity": "sha512-Xh+VRuh6OMh3uJ0JkCjI57l+DVe7VRGBYymen8rFPnTVgATBwA6nmToxM2OwTlSvrnWpPKkrQUj93+K9huYC6A==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", "cpu": [ "arm64" ], @@ -1223,9 +1223,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.5.tgz", - "integrity": "sha512-aC1gpJkkaUADHuAdQfuVTnqVUTLqqUNhAvEwHwVWcnVVZvNlDPGA0UveZsfXJJ9T6k9Po4eHi3c02gbdwO3g6w==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", "cpu": [ "x64" ], @@ -1240,9 +1240,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.5.tgz", - "integrity": "sha512-0UNx2aavV0fk6UpZcwXFLztA2r/k9jTUa7OW7SAea1VYUhkug99MW1uZeXEnPn5+cHOd0n8myQay6TlFnBR07w==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", "cpu": [ "arm64" ], @@ -1257,9 +1257,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.5.tgz", - "integrity": "sha512-5nlJ3AeJWCTSzR7AEqVjT/faWyqKU86kCi1lLmxVqmNR+j4HrYdns+eTGjS/vmrzCIe8inGQckUadvS0+JkKdQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", "cpu": [ "ia32" ], @@ -1274,9 +1274,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.5.tgz", - "integrity": "sha512-PWypQR+d4FLfkhBIV+/kHsUELAnMpx1bRvvsn3p+/sAERbnCzFrtDRG2Xw5n+2zPxBK2+iaP+vetsRl4Ti7WgA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", "cpu": [ "x64" ], @@ -2948,9 +2948,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.12.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz", - "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", + "version": "24.12.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", + "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", "dev": true, "license": "MIT", "dependencies": { @@ -3561,9 +3561,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.10.13", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.13.tgz", - "integrity": "sha512-BL2sTuHOdy0YT1lYieUxTw/QMtPBC3pmlJC6xk8BBYVv6vcw3SGdKemQ+Xsx9ik2F/lYDO9tqsFQH1r9PFuHKw==", + "version": "2.10.16", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.16.tgz", + "integrity": "sha512-Lyf3aK28zpsD1yQMiiHD4RvVb6UdMoo8xzG2XzFIfR9luPzOpcBlAsT/qfB1XWS1bxWT+UtE4WmQgsp297FYOA==", "dev": true, "license": "Apache-2.0", "bin": { @@ -3930,9 +3930,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001784", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001784.tgz", - "integrity": "sha512-WU346nBTklUV9YfUl60fqRbU5ZqyXlqvo1SgigE1OAXK5bFL8LL9q1K7aap3N739l4BvNqnkm3YrGHiY9sfUQw==", + "version": "1.0.30001787", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001787.tgz", + "integrity": "sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==", "dev": true, "funding": [ { @@ -4884,9 +4884,9 @@ } }, "node_modules/electron": { - "version": "41.1.1", - "resolved": "https://registry.npmjs.org/electron/-/electron-41.1.1.tgz", - "integrity": "sha512-8bgvDhBjli+3Z2YCKgzzoBPh6391pr7Xv2h/tTJG4ETgvPvUxZomObbZLs31mUzYb6VrlcDDd9cyWyNKtPm3tA==", + "version": "41.2.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-41.2.0.tgz", + "integrity": "sha512-0OKLiymqfV0WK68RBXqAm3Myad2TpI5wwxLCBEUcH5Nugo3YfSk7p1Js/AL9266qTz5xZioUnxt9hG8FFwax0g==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -5051,9 +5051,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.331", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.331.tgz", - "integrity": "sha512-IbxXrsTlD3hRodkLnbxAPP4OuJYdWCeM3IOdT+CpcMoIwIoDfCmRpEtSPfwBXxVkg9xmBeY7Lz2Eo2TDn/HC3Q==", + "version": "1.5.334", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.334.tgz", + "integrity": "sha512-mgjZAz7Jyx1SRCwEpy9wefDS7GvNPazLthHg8eQMJ76wBdGQQDW33TCrUTvQ4wzpmOrv2zrFoD3oNufMdyMpog==", "dev": true, "license": "ISC" }, @@ -5247,9 +5247,9 @@ "optional": true }, "node_modules/esbuild": { - "version": "0.27.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.5.tgz", - "integrity": "sha512-zdQoHBjuDqKsvV5OPaWansOwfSQ0Js+Uj9J85TBvj3bFW1JjWTSULMRwdQAc8qMeIScbClxeMK0jlrtB9linhA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -5260,32 +5260,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.5", - "@esbuild/android-arm": "0.27.5", - "@esbuild/android-arm64": "0.27.5", - "@esbuild/android-x64": "0.27.5", - "@esbuild/darwin-arm64": "0.27.5", - "@esbuild/darwin-x64": "0.27.5", - "@esbuild/freebsd-arm64": "0.27.5", - "@esbuild/freebsd-x64": "0.27.5", - "@esbuild/linux-arm": "0.27.5", - "@esbuild/linux-arm64": "0.27.5", - "@esbuild/linux-ia32": "0.27.5", - "@esbuild/linux-loong64": "0.27.5", - "@esbuild/linux-mips64el": "0.27.5", - "@esbuild/linux-ppc64": "0.27.5", - "@esbuild/linux-riscv64": "0.27.5", - "@esbuild/linux-s390x": "0.27.5", - "@esbuild/linux-x64": "0.27.5", - "@esbuild/netbsd-arm64": "0.27.5", - "@esbuild/netbsd-x64": "0.27.5", - "@esbuild/openbsd-arm64": "0.27.5", - "@esbuild/openbsd-x64": "0.27.5", - "@esbuild/openharmony-arm64": "0.27.5", - "@esbuild/sunos-x64": "0.27.5", - "@esbuild/win32-arm64": "0.27.5", - "@esbuild/win32-ia32": "0.27.5", - "@esbuild/win32-x64": "0.27.5" + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" } }, "node_modules/escalade": { @@ -6537,9 +6537,9 @@ } }, "node_modules/koffi": { - "version": "2.15.2", - "resolved": "https://registry.npmjs.org/koffi/-/koffi-2.15.2.tgz", - "integrity": "sha512-r9tjJLVRSOhCRWdVyQlF3/Ugzeg13jlzS4czS82MAgLff4W+BcYOW7g8Y62t9O5JYjYOLAjAovAZDNlDfZNu+g==", + "version": "2.15.6", + "resolved": "https://registry.npmjs.org/koffi/-/koffi-2.15.6.tgz", + "integrity": "sha512-WQBpM5uo74UQ17UpsFN+PUOrQQg4/nYdey4SGVluQun2drYYfePziLLWdSmFb4wSdWlJC1aimXQnjhPCheRKuw==", "hasInstallScript": true, "license": "MIT", "funding": { @@ -6743,9 +6743,9 @@ } }, "node_modules/lucide-react": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.7.0.tgz", - "integrity": "sha512-yI7BeItCLZJTXikmK4KNUGCKoGzSvbKlfCvw44bU4fXAL6v3gYS4uHD1jzsLkfwODYwI6Drw5Tu9Z5ulDe0TSg==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.8.0.tgz", + "integrity": "sha512-WuvlsjngSk7TnTBJ1hsCy3ql9V9VOdcPkd3PKcSmM34vJD8KG6molxz7m7zbYFgICwsanQWmJ13JlYs4Zp7Arw==", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -8316,9 +8316,9 @@ } }, "node_modules/postcss": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", - "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz", + "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==", "dev": true, "funding": [ { @@ -8470,24 +8470,24 @@ } }, "node_modules/react": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", - "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", + "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", - "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", "license": "MIT", "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.4" + "react": "^19.2.5" } }, "node_modules/react-markdown": { @@ -9093,9 +9093,9 @@ } }, "node_modules/sherpa-onnx-darwin-arm64": { - "version": "1.12.35", - "resolved": "https://registry.npmjs.org/sherpa-onnx-darwin-arm64/-/sherpa-onnx-darwin-arm64-1.12.35.tgz", - "integrity": "sha512-WGIABo3ruBXE/7FhAdaVNuM+ZKx0B7jkA+jT22k5TxUcw58nWzgkY6k+CPdM14lfaaXR+jPWdDrM4gXl/bP4RQ==", + "version": "1.12.36", + "resolved": "https://registry.npmjs.org/sherpa-onnx-darwin-arm64/-/sherpa-onnx-darwin-arm64-1.12.36.tgz", + "integrity": "sha512-c1C7f2zO2BXNusrDlid5Mq5USfqq4oJrimnnHqNYtm4Kk2UzrxA9l6lAu2FZDj4gTQYRY4IJnlo0T7UXeOsjtQ==", "cpu": [ "arm64" ], @@ -9106,9 +9106,9 @@ ] }, "node_modules/sherpa-onnx-darwin-x64": { - "version": "1.12.35", - "resolved": "https://registry.npmjs.org/sherpa-onnx-darwin-x64/-/sherpa-onnx-darwin-x64-1.12.35.tgz", - "integrity": "sha512-hzWQm4CJhGyf3N9Sd1Oobcdz49FauuSCmhrm5vRqydyNsANjs89wATHAuatPAtinpBkgEqacDPrGz+1A/BWpNA==", + "version": "1.12.36", + "resolved": "https://registry.npmjs.org/sherpa-onnx-darwin-x64/-/sherpa-onnx-darwin-x64-1.12.36.tgz", + "integrity": "sha512-pRaELEwQ60cfBTtERiw6muN+0+FhVom0As0erRcRqdPKLNK4HCCSUaoHPYLFT6W1VUiJBzABH+WE2+LTDyx5JA==", "cpu": [ "x64" ], @@ -9119,9 +9119,9 @@ ] }, "node_modules/sherpa-onnx-linux-arm64": { - "version": "1.12.35", - "resolved": "https://registry.npmjs.org/sherpa-onnx-linux-arm64/-/sherpa-onnx-linux-arm64-1.12.35.tgz", - "integrity": "sha512-9glJ+dRv/rFWz/61tiKfaR9Gj+8B6sXi7NBgwBAnO/+ygu/WAjBfQRz2+S0YIy1dxqu7ng246TBNnx1M2XaNXA==", + "version": "1.12.36", + "resolved": "https://registry.npmjs.org/sherpa-onnx-linux-arm64/-/sherpa-onnx-linux-arm64-1.12.36.tgz", + "integrity": "sha512-Lv+j1Bq0Blp44O/i4gT4RieSDpiCoEPXfvNv0ABR2Gp6IbziI7gEsHgAf8HGjrA7EirtHAgX0o4hzUPW9yc+uw==", "cpu": [ "arm64" ], @@ -9132,9 +9132,9 @@ ] }, "node_modules/sherpa-onnx-linux-x64": { - "version": "1.12.35", - "resolved": "https://registry.npmjs.org/sherpa-onnx-linux-x64/-/sherpa-onnx-linux-x64-1.12.35.tgz", - "integrity": "sha512-h+v4Yed8T+k1qLlKX2LTGoXP/11ycz7jbqC2f80kDWgz9J8m46mOBa/H20wVkLyQPy1vG1O5iH5Fe5Wh4QlLhw==", + "version": "1.12.36", + "resolved": "https://registry.npmjs.org/sherpa-onnx-linux-x64/-/sherpa-onnx-linux-x64-1.12.36.tgz", + "integrity": "sha512-Wul2WAaUt0e2zZaaQR4N3GTDhcZdz44w3FiStr195TI9U4uNxVbFgZbw9YsEMj8K7gm7JhoH2bnCn8fUJv88EQ==", "cpu": [ "x64" ], @@ -9145,23 +9145,23 @@ ] }, "node_modules/sherpa-onnx-node": { - "version": "1.12.35", - "resolved": "https://registry.npmjs.org/sherpa-onnx-node/-/sherpa-onnx-node-1.12.35.tgz", - "integrity": "sha512-RHCgV+9fos/ZxP0MsIL7JPU9K3YHnIDmwtX674ChQZY6DLVaIQaju+J3hDqzRu1R3agnDg9WDf01zsT46NC7SQ==", + "version": "1.12.36", + "resolved": "https://registry.npmjs.org/sherpa-onnx-node/-/sherpa-onnx-node-1.12.36.tgz", + "integrity": "sha512-AjdOE0qa3jAmS/zh0BwNVXn9ZRz8PpgCgcLIyf7IYxDfTMWIgHaRFisDL4QzttuDe3apvBXjP1Y7FtZPSRFqqQ==", "license": "Apache-2.0", "optionalDependencies": { - "sherpa-onnx-darwin-arm64": "^1.12.35", - "sherpa-onnx-darwin-x64": "^1.12.35", - "sherpa-onnx-linux-arm64": "^1.12.35", - "sherpa-onnx-linux-x64": "^1.12.35", - "sherpa-onnx-win-ia32": "^1.12.35", - "sherpa-onnx-win-x64": "^1.12.35" + "sherpa-onnx-darwin-arm64": "^1.12.36", + "sherpa-onnx-darwin-x64": "^1.12.36", + "sherpa-onnx-linux-arm64": "^1.12.36", + "sherpa-onnx-linux-x64": "^1.12.36", + "sherpa-onnx-win-ia32": "^1.12.36", + "sherpa-onnx-win-x64": "^1.12.36" } }, "node_modules/sherpa-onnx-win-ia32": { - "version": "1.12.35", - "resolved": "https://registry.npmjs.org/sherpa-onnx-win-ia32/-/sherpa-onnx-win-ia32-1.12.35.tgz", - "integrity": "sha512-6H6BSdXXWtz92AuvOmr4w/QvCofxXbfbNKT7jCxdE7Nd4AvinLJxT02vbnL6T54vuXd9chu0QvQrDl1tuRphAA==", + "version": "1.12.36", + "resolved": "https://registry.npmjs.org/sherpa-onnx-win-ia32/-/sherpa-onnx-win-ia32-1.12.36.tgz", + "integrity": "sha512-An3O5tEq4P5xKBSZMq10F/w6q79fgdbFZ4NKPoflSKSW37TSyNz9MhHLHwhse/BMPa35eW14J9dsGG3DZTC/xA==", "cpu": [ "ia32" ], @@ -9172,9 +9172,9 @@ ] }, "node_modules/sherpa-onnx-win-x64": { - "version": "1.12.35", - "resolved": "https://registry.npmjs.org/sherpa-onnx-win-x64/-/sherpa-onnx-win-x64-1.12.35.tgz", - "integrity": "sha512-+GLrxwaEvpJAO0KZgKulfd4qUR089MD+TjE5jVSugMTq4Eh/R/TpPPqYQGibRZVPHW7Se1ABfHGapZQoFMHH5Q==", + "version": "1.12.36", + "resolved": "https://registry.npmjs.org/sherpa-onnx-win-x64/-/sherpa-onnx-win-x64-1.12.36.tgz", + "integrity": "sha512-wZLQflcvy8ynsU6B8GvqWIhOCAjP6+rnzyadF+qGWgZzMSCduapD63q++0QDRabGRBL8qfsd2n7O2dF/W2kccQ==", "cpu": [ "x64" ], @@ -9643,14 +9643,14 @@ "license": "MIT" }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" diff --git a/resources/installer/linux/PKGBUILD b/resources/installer/linux/PKGBUILD new file mode 100644 index 0000000..573a5e9 --- /dev/null +++ b/resources/installer/linux/PKGBUILD @@ -0,0 +1,30 @@ +# Maintainer: H3CoF6 +pkgname=weflow +pkgver=4.3.0 +pkgrel=1 +pkgdesc="A local WeChat database decryption and analysis tool" +arch=('x86_64') +url="https://github.com/hicccc77/weflow" +license=('CC-BY-NC-SA-4.0') +depends=('alsa-lib' 'gtk3' 'nss' 'glibc') +options=('!strip' '!debug') + +source=("WeFlow-${pkgver}-Setup.tar.gz::${url}/releases/download/v${pkgver}/WeFlow-${pkgver}-Setup.tar.gz" + "weflow.desktop" + "icon.png") + +sha256sums=('2859aca2f57c42f4d1516ed229613623c57d3e78b9cb152fcb2b9c1096ab9340' + '2cf03766f5c2f1915ad136f060a66f5788ed32b06defe1956e406c73d7e733b7' + 'b1c412d9c08ae683e231173c16fe73958ad1063f14c9b3852373385e4fcb6f33') + +package() { + install -dm755 "${pkgdir}/opt/${pkgname}" + + cp -a "${srcdir}/WeFlow-${pkgver}-Setup/"* "${pkgdir}/opt/${pkgname}/" + + install -dm755 "${pkgdir}/usr/bin" + ln -s "/opt/${pkgname}/weflow" "${pkgdir}/usr/bin/${pkgname}" + + install -Dm644 "${srcdir}/weflow.desktop" -t "${pkgdir}/usr/share/applications/" + install -Dm644 "${srcdir}/icon.png" "${pkgdir}/usr/share/pixmaps/${pkgname}.png" +} diff --git a/resources/installer/linux/icon.png b/resources/installer/linux/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..7372ec7cff94700e052e504063d11f6287efedd3 GIT binary patch literal 55144 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

!OBl%4i7Xh~}Z(3q}fbZRXhC|L`&r2{b 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 WnlsRbc3KMn0000 Date: Mon, 13 Apr 2026 04:52:07 +0800 Subject: [PATCH 7/9] =?UTF-8?q?fix:=20=E4=BF=AE=E6=94=B9AUR=E5=8F=91?= =?UTF-8?q?=E5=B8=83=E7=9A=84action?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 02f3255..b17ef49 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -311,10 +311,22 @@ jobs: with: fetch-depth: 0 + - name: Update PKGBUILD version + run: | + NEW_VER=$(echo "${{ github.ref_name }}" | sed 's/^v//') + sed -i "s/^pkgver=.*/pkgver=${NEW_VER}/" resources/installer/linux/PKGBUILD + sed -i "s/^pkgrel=.*/pkgrel=1/" resources/installer/linux/PKGBUILD + - name: Publish AUR package uses: KSXGitHub/github-actions-deploy-aur@master with: pkgname: weflow + pkgbuild: resources/installer/linux/PKGBUILD + updpkgsums: true + assets: | + resources/installer/linux/weflow.desktop + resources/installer/linux/icon.png + ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} commit_username: H3CoF6 commit_email: h3cof6@gmail.com From f40b039426fb434a5988593b4f7fbda35ab379dd Mon Sep 17 00:00:00 2001 From: H3CoF6 <1707889225@qq.com> Date: Mon, 13 Apr 2026 04:58:58 +0800 Subject: [PATCH 8/9] style: revert lock file changes --- package-lock.json | 338 +++++++++++++++++++++++----------------------- 1 file changed, 169 insertions(+), 169 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0151587..0c06ec1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -849,9 +849,9 @@ "optional": true }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", - "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.5.tgz", + "integrity": "sha512-nGsF/4C7uzUj+Nj/4J+Zt0bYQ6bz33Phz8Lb2N80Mti1HjGclTJdXZ+9APC4kLvONbjxN1zfvYNd8FEcbBK/MQ==", "cpu": [ "ppc64" ], @@ -866,9 +866,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", - "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.5.tgz", + "integrity": "sha512-Cv781jd0Rfj/paoNrul1/r4G0HLvuFKYh7C9uHZ2Pl8YXstzvCyyeWENTFR9qFnRzNMCjXmsulZuvosDg10Mog==", "cpu": [ "arm" ], @@ -883,9 +883,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", - "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.5.tgz", + "integrity": "sha512-Oeghq+XFgh1pUGd1YKs4DDoxzxkoUkvko+T/IVKwlghKLvvjbGFB3ek8VEDBmNvqhwuL0CQS3cExdzpmUyIrgA==", "cpu": [ "arm64" ], @@ -900,9 +900,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", - "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.5.tgz", + "integrity": "sha512-nQD7lspbzerlmtNOxYMFAGmhxgzn8Z7m9jgFkh6kpkjsAhZee1w8tJW3ZlW+N9iRePz0oPUDrYrXidCPSImD0Q==", "cpu": [ "x64" ], @@ -917,9 +917,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", - "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.5.tgz", + "integrity": "sha512-I+Ya/MgC6rr8oRWGRDF3BXDfP8K1BVUggHqN6VI2lUZLdDi1IM1v2cy0e3lCPbP+pVcK3Tv8cgUhHse1kaNZZw==", "cpu": [ "arm64" ], @@ -934,9 +934,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", - "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.5.tgz", + "integrity": "sha512-MCjQUtC8wWJn/pIPM7vQaO69BFgwPD1jriEdqwTCKzWjGgkMbcg+M5HzrOhPhuYe1AJjXlHmD142KQf+jnYj8A==", "cpu": [ "x64" ], @@ -951,9 +951,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", - "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.5.tgz", + "integrity": "sha512-X6xVS+goSH0UelYXnuf4GHLwpOdc8rgK/zai+dKzBMnncw7BTQIwquOodE7EKvY2UVUetSqyAfyZC1D+oqLQtg==", "cpu": [ "arm64" ], @@ -968,9 +968,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", - "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.5.tgz", + "integrity": "sha512-233X1FGo3a8x1ekLB6XT69LfZ83vqz+9z3TSEQCTYfMNY880A97nr81KbPcAMl9rmOFp11wO0dP+eB18KU/Ucg==", "cpu": [ "x64" ], @@ -985,9 +985,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", - "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.5.tgz", + "integrity": "sha512-0wkVrYHG4sdCCN/bcwQ7yYMXACkaHc3UFeaEOwSVW6e5RycMageYAFv+JS2bKLwHyeKVUvtoVH+5/RHq0fgeFw==", "cpu": [ "arm" ], @@ -1002,9 +1002,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", - "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.5.tgz", + "integrity": "sha512-euKkilsNOv7x/M1NKsx5znyprbpsRFIzTV6lWziqJch7yWYayfLtZzDxDTl+LSQDJYAjd9TVb/Kt5UKIrj2e4A==", "cpu": [ "arm64" ], @@ -1019,9 +1019,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", - "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.5.tgz", + "integrity": "sha512-hVRQX4+P3MS36NxOy24v/Cdsimy/5HYePw+tmPqnNN1fxV0bPrFWR6TMqwXPwoTM2VzbkA+4lbHWUKDd5ZDA/w==", "cpu": [ "ia32" ], @@ -1036,9 +1036,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", - "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.5.tgz", + "integrity": "sha512-mKqqRuOPALI8nDzhOBmIS0INvZOOFGGg5n1osGIXAx8oersceEbKd4t1ACNTHM3sJBXGFAlEgqM+svzjPot+ZQ==", "cpu": [ "loong64" ], @@ -1053,9 +1053,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", - "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.5.tgz", + "integrity": "sha512-EE/QXH9IyaAj1qeuIV5+/GZkBTipgGO782Ff7Um3vPS9cvLhJJeATy4Ggxikz2inZ46KByamMn6GqtqyVjhenA==", "cpu": [ "mips64el" ], @@ -1070,9 +1070,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", - "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.5.tgz", + "integrity": "sha512-0V2iF1RGxBf1b7/BjurA5jfkl7PtySjom1r6xOK2q9KWw/XCpAdtB6KNMO+9xx69yYfSCRR9FE0TyKfHA2eQMw==", "cpu": [ "ppc64" ], @@ -1087,9 +1087,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", - "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.5.tgz", + "integrity": "sha512-rYxThBx6G9HN6tFNuvB/vykeLi4VDsm5hE5pVwzqbAjZEARQrWu3noZSfbEnPZ/CRXP3271GyFk/49up2W190g==", "cpu": [ "riscv64" ], @@ -1104,9 +1104,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", - "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.5.tgz", + "integrity": "sha512-uEP2q/4qgd8goEUc4QIdU/1P2NmEtZ/zX5u3OpLlCGhJIuBIv0s0wr7TB2nBrd3/A5XIdEkkS5ZLF0ULuvaaYQ==", "cpu": [ "s390x" ], @@ -1121,9 +1121,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", - "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.5.tgz", + "integrity": "sha512-+Gq47Wqq6PLOOZuBzVSII2//9yyHNKZLuwfzCemqexqOQCSz0zy0O26kIzyp9EMNMK+nZ0tFHBZrCeVUuMs/ew==", "cpu": [ "x64" ], @@ -1138,9 +1138,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", - "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.5.tgz", + "integrity": "sha512-3F/5EG8VHfN/I+W5cO1/SV2H9Q/5r7vcHabMnBqhHK2lTWOh3F8vixNzo8lqxrlmBtZVFpW8pmITHnq54+Tq4g==", "cpu": [ "arm64" ], @@ -1155,9 +1155,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", - "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.5.tgz", + "integrity": "sha512-28t+Sj3CPN8vkMOlZotOmDgilQwVvxWZl7b8rxpn73Tt/gCnvrHxQUMng4uu3itdFvrtba/1nHejvxqz8xgEMA==", "cpu": [ "x64" ], @@ -1172,9 +1172,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", - "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.5.tgz", + "integrity": "sha512-Doz/hKtiuVAi9hMsBMpwBANhIZc8l238U2Onko3t2xUp8xtM0ZKdDYHMnm/qPFVthY8KtxkXaocwmMh6VolzMA==", "cpu": [ "arm64" ], @@ -1189,9 +1189,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", - "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.5.tgz", + "integrity": "sha512-WfGVaa1oz5A7+ZFPkERIbIhKT4olvGl1tyzTRaB5yoZRLqC0KwaO95FeZtOdQj/oKkjW57KcVF944m62/0GYtA==", "cpu": [ "x64" ], @@ -1206,9 +1206,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", - "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.5.tgz", + "integrity": "sha512-Xh+VRuh6OMh3uJ0JkCjI57l+DVe7VRGBYymen8rFPnTVgATBwA6nmToxM2OwTlSvrnWpPKkrQUj93+K9huYC6A==", "cpu": [ "arm64" ], @@ -1223,9 +1223,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", - "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.5.tgz", + "integrity": "sha512-aC1gpJkkaUADHuAdQfuVTnqVUTLqqUNhAvEwHwVWcnVVZvNlDPGA0UveZsfXJJ9T6k9Po4eHi3c02gbdwO3g6w==", "cpu": [ "x64" ], @@ -1240,9 +1240,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", - "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.5.tgz", + "integrity": "sha512-0UNx2aavV0fk6UpZcwXFLztA2r/k9jTUa7OW7SAea1VYUhkug99MW1uZeXEnPn5+cHOd0n8myQay6TlFnBR07w==", "cpu": [ "arm64" ], @@ -1257,9 +1257,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", - "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.5.tgz", + "integrity": "sha512-5nlJ3AeJWCTSzR7AEqVjT/faWyqKU86kCi1lLmxVqmNR+j4HrYdns+eTGjS/vmrzCIe8inGQckUadvS0+JkKdQ==", "cpu": [ "ia32" ], @@ -1274,9 +1274,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", - "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.5.tgz", + "integrity": "sha512-PWypQR+d4FLfkhBIV+/kHsUELAnMpx1bRvvsn3p+/sAERbnCzFrtDRG2Xw5n+2zPxBK2+iaP+vetsRl4Ti7WgA==", "cpu": [ "x64" ], @@ -2948,9 +2948,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.12.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", - "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", + "version": "24.12.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz", + "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3561,9 +3561,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.10.16", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.16.tgz", - "integrity": "sha512-Lyf3aK28zpsD1yQMiiHD4RvVb6UdMoo8xzG2XzFIfR9luPzOpcBlAsT/qfB1XWS1bxWT+UtE4WmQgsp297FYOA==", + "version": "2.10.13", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.13.tgz", + "integrity": "sha512-BL2sTuHOdy0YT1lYieUxTw/QMtPBC3pmlJC6xk8BBYVv6vcw3SGdKemQ+Xsx9ik2F/lYDO9tqsFQH1r9PFuHKw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -3930,9 +3930,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001787", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001787.tgz", - "integrity": "sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==", + "version": "1.0.30001784", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001784.tgz", + "integrity": "sha512-WU346nBTklUV9YfUl60fqRbU5ZqyXlqvo1SgigE1OAXK5bFL8LL9q1K7aap3N739l4BvNqnkm3YrGHiY9sfUQw==", "dev": true, "funding": [ { @@ -4884,9 +4884,9 @@ } }, "node_modules/electron": { - "version": "41.2.0", - "resolved": "https://registry.npmjs.org/electron/-/electron-41.2.0.tgz", - "integrity": "sha512-0OKLiymqfV0WK68RBXqAm3Myad2TpI5wwxLCBEUcH5Nugo3YfSk7p1Js/AL9266qTz5xZioUnxt9hG8FFwax0g==", + "version": "41.1.1", + "resolved": "https://registry.npmjs.org/electron/-/electron-41.1.1.tgz", + "integrity": "sha512-8bgvDhBjli+3Z2YCKgzzoBPh6391pr7Xv2h/tTJG4ETgvPvUxZomObbZLs31mUzYb6VrlcDDd9cyWyNKtPm3tA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -5051,9 +5051,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.334", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.334.tgz", - "integrity": "sha512-mgjZAz7Jyx1SRCwEpy9wefDS7GvNPazLthHg8eQMJ76wBdGQQDW33TCrUTvQ4wzpmOrv2zrFoD3oNufMdyMpog==", + "version": "1.5.331", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.331.tgz", + "integrity": "sha512-IbxXrsTlD3hRodkLnbxAPP4OuJYdWCeM3IOdT+CpcMoIwIoDfCmRpEtSPfwBXxVkg9xmBeY7Lz2Eo2TDn/HC3Q==", "dev": true, "license": "ISC" }, @@ -5247,9 +5247,9 @@ "optional": true }, "node_modules/esbuild": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", - "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.5.tgz", + "integrity": "sha512-zdQoHBjuDqKsvV5OPaWansOwfSQ0Js+Uj9J85TBvj3bFW1JjWTSULMRwdQAc8qMeIScbClxeMK0jlrtB9linhA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -5260,32 +5260,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.7", - "@esbuild/android-arm": "0.27.7", - "@esbuild/android-arm64": "0.27.7", - "@esbuild/android-x64": "0.27.7", - "@esbuild/darwin-arm64": "0.27.7", - "@esbuild/darwin-x64": "0.27.7", - "@esbuild/freebsd-arm64": "0.27.7", - "@esbuild/freebsd-x64": "0.27.7", - "@esbuild/linux-arm": "0.27.7", - "@esbuild/linux-arm64": "0.27.7", - "@esbuild/linux-ia32": "0.27.7", - "@esbuild/linux-loong64": "0.27.7", - "@esbuild/linux-mips64el": "0.27.7", - "@esbuild/linux-ppc64": "0.27.7", - "@esbuild/linux-riscv64": "0.27.7", - "@esbuild/linux-s390x": "0.27.7", - "@esbuild/linux-x64": "0.27.7", - "@esbuild/netbsd-arm64": "0.27.7", - "@esbuild/netbsd-x64": "0.27.7", - "@esbuild/openbsd-arm64": "0.27.7", - "@esbuild/openbsd-x64": "0.27.7", - "@esbuild/openharmony-arm64": "0.27.7", - "@esbuild/sunos-x64": "0.27.7", - "@esbuild/win32-arm64": "0.27.7", - "@esbuild/win32-ia32": "0.27.7", - "@esbuild/win32-x64": "0.27.7" + "@esbuild/aix-ppc64": "0.27.5", + "@esbuild/android-arm": "0.27.5", + "@esbuild/android-arm64": "0.27.5", + "@esbuild/android-x64": "0.27.5", + "@esbuild/darwin-arm64": "0.27.5", + "@esbuild/darwin-x64": "0.27.5", + "@esbuild/freebsd-arm64": "0.27.5", + "@esbuild/freebsd-x64": "0.27.5", + "@esbuild/linux-arm": "0.27.5", + "@esbuild/linux-arm64": "0.27.5", + "@esbuild/linux-ia32": "0.27.5", + "@esbuild/linux-loong64": "0.27.5", + "@esbuild/linux-mips64el": "0.27.5", + "@esbuild/linux-ppc64": "0.27.5", + "@esbuild/linux-riscv64": "0.27.5", + "@esbuild/linux-s390x": "0.27.5", + "@esbuild/linux-x64": "0.27.5", + "@esbuild/netbsd-arm64": "0.27.5", + "@esbuild/netbsd-x64": "0.27.5", + "@esbuild/openbsd-arm64": "0.27.5", + "@esbuild/openbsd-x64": "0.27.5", + "@esbuild/openharmony-arm64": "0.27.5", + "@esbuild/sunos-x64": "0.27.5", + "@esbuild/win32-arm64": "0.27.5", + "@esbuild/win32-ia32": "0.27.5", + "@esbuild/win32-x64": "0.27.5" } }, "node_modules/escalade": { @@ -6537,9 +6537,9 @@ } }, "node_modules/koffi": { - "version": "2.15.6", - "resolved": "https://registry.npmjs.org/koffi/-/koffi-2.15.6.tgz", - "integrity": "sha512-WQBpM5uo74UQ17UpsFN+PUOrQQg4/nYdey4SGVluQun2drYYfePziLLWdSmFb4wSdWlJC1aimXQnjhPCheRKuw==", + "version": "2.15.2", + "resolved": "https://registry.npmjs.org/koffi/-/koffi-2.15.2.tgz", + "integrity": "sha512-r9tjJLVRSOhCRWdVyQlF3/Ugzeg13jlzS4czS82MAgLff4W+BcYOW7g8Y62t9O5JYjYOLAjAovAZDNlDfZNu+g==", "hasInstallScript": true, "license": "MIT", "funding": { @@ -6743,9 +6743,9 @@ } }, "node_modules/lucide-react": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.8.0.tgz", - "integrity": "sha512-WuvlsjngSk7TnTBJ1hsCy3ql9V9VOdcPkd3PKcSmM34vJD8KG6molxz7m7zbYFgICwsanQWmJ13JlYs4Zp7Arw==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.7.0.tgz", + "integrity": "sha512-yI7BeItCLZJTXikmK4KNUGCKoGzSvbKlfCvw44bU4fXAL6v3gYS4uHD1jzsLkfwODYwI6Drw5Tu9Z5ulDe0TSg==", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -8316,9 +8316,9 @@ } }, "node_modules/postcss": { - "version": "8.5.9", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz", - "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==", + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", "dev": true, "funding": [ { @@ -8470,24 +8470,24 @@ } }, "node_modules/react": { - "version": "19.2.5", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", - "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.2.5", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", - "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.5" + "react": "^19.2.4" } }, "node_modules/react-markdown": { @@ -9093,9 +9093,9 @@ } }, "node_modules/sherpa-onnx-darwin-arm64": { - "version": "1.12.36", - "resolved": "https://registry.npmjs.org/sherpa-onnx-darwin-arm64/-/sherpa-onnx-darwin-arm64-1.12.36.tgz", - "integrity": "sha512-c1C7f2zO2BXNusrDlid5Mq5USfqq4oJrimnnHqNYtm4Kk2UzrxA9l6lAu2FZDj4gTQYRY4IJnlo0T7UXeOsjtQ==", + "version": "1.12.35", + "resolved": "https://registry.npmjs.org/sherpa-onnx-darwin-arm64/-/sherpa-onnx-darwin-arm64-1.12.35.tgz", + "integrity": "sha512-WGIABo3ruBXE/7FhAdaVNuM+ZKx0B7jkA+jT22k5TxUcw58nWzgkY6k+CPdM14lfaaXR+jPWdDrM4gXl/bP4RQ==", "cpu": [ "arm64" ], @@ -9106,9 +9106,9 @@ ] }, "node_modules/sherpa-onnx-darwin-x64": { - "version": "1.12.36", - "resolved": "https://registry.npmjs.org/sherpa-onnx-darwin-x64/-/sherpa-onnx-darwin-x64-1.12.36.tgz", - "integrity": "sha512-pRaELEwQ60cfBTtERiw6muN+0+FhVom0As0erRcRqdPKLNK4HCCSUaoHPYLFT6W1VUiJBzABH+WE2+LTDyx5JA==", + "version": "1.12.35", + "resolved": "https://registry.npmjs.org/sherpa-onnx-darwin-x64/-/sherpa-onnx-darwin-x64-1.12.35.tgz", + "integrity": "sha512-hzWQm4CJhGyf3N9Sd1Oobcdz49FauuSCmhrm5vRqydyNsANjs89wATHAuatPAtinpBkgEqacDPrGz+1A/BWpNA==", "cpu": [ "x64" ], @@ -9119,9 +9119,9 @@ ] }, "node_modules/sherpa-onnx-linux-arm64": { - "version": "1.12.36", - "resolved": "https://registry.npmjs.org/sherpa-onnx-linux-arm64/-/sherpa-onnx-linux-arm64-1.12.36.tgz", - "integrity": "sha512-Lv+j1Bq0Blp44O/i4gT4RieSDpiCoEPXfvNv0ABR2Gp6IbziI7gEsHgAf8HGjrA7EirtHAgX0o4hzUPW9yc+uw==", + "version": "1.12.35", + "resolved": "https://registry.npmjs.org/sherpa-onnx-linux-arm64/-/sherpa-onnx-linux-arm64-1.12.35.tgz", + "integrity": "sha512-9glJ+dRv/rFWz/61tiKfaR9Gj+8B6sXi7NBgwBAnO/+ygu/WAjBfQRz2+S0YIy1dxqu7ng246TBNnx1M2XaNXA==", "cpu": [ "arm64" ], @@ -9132,9 +9132,9 @@ ] }, "node_modules/sherpa-onnx-linux-x64": { - "version": "1.12.36", - "resolved": "https://registry.npmjs.org/sherpa-onnx-linux-x64/-/sherpa-onnx-linux-x64-1.12.36.tgz", - "integrity": "sha512-Wul2WAaUt0e2zZaaQR4N3GTDhcZdz44w3FiStr195TI9U4uNxVbFgZbw9YsEMj8K7gm7JhoH2bnCn8fUJv88EQ==", + "version": "1.12.35", + "resolved": "https://registry.npmjs.org/sherpa-onnx-linux-x64/-/sherpa-onnx-linux-x64-1.12.35.tgz", + "integrity": "sha512-h+v4Yed8T+k1qLlKX2LTGoXP/11ycz7jbqC2f80kDWgz9J8m46mOBa/H20wVkLyQPy1vG1O5iH5Fe5Wh4QlLhw==", "cpu": [ "x64" ], @@ -9145,23 +9145,23 @@ ] }, "node_modules/sherpa-onnx-node": { - "version": "1.12.36", - "resolved": "https://registry.npmjs.org/sherpa-onnx-node/-/sherpa-onnx-node-1.12.36.tgz", - "integrity": "sha512-AjdOE0qa3jAmS/zh0BwNVXn9ZRz8PpgCgcLIyf7IYxDfTMWIgHaRFisDL4QzttuDe3apvBXjP1Y7FtZPSRFqqQ==", + "version": "1.12.35", + "resolved": "https://registry.npmjs.org/sherpa-onnx-node/-/sherpa-onnx-node-1.12.35.tgz", + "integrity": "sha512-RHCgV+9fos/ZxP0MsIL7JPU9K3YHnIDmwtX674ChQZY6DLVaIQaju+J3hDqzRu1R3agnDg9WDf01zsT46NC7SQ==", "license": "Apache-2.0", "optionalDependencies": { - "sherpa-onnx-darwin-arm64": "^1.12.36", - "sherpa-onnx-darwin-x64": "^1.12.36", - "sherpa-onnx-linux-arm64": "^1.12.36", - "sherpa-onnx-linux-x64": "^1.12.36", - "sherpa-onnx-win-ia32": "^1.12.36", - "sherpa-onnx-win-x64": "^1.12.36" + "sherpa-onnx-darwin-arm64": "^1.12.35", + "sherpa-onnx-darwin-x64": "^1.12.35", + "sherpa-onnx-linux-arm64": "^1.12.35", + "sherpa-onnx-linux-x64": "^1.12.35", + "sherpa-onnx-win-ia32": "^1.12.35", + "sherpa-onnx-win-x64": "^1.12.35" } }, "node_modules/sherpa-onnx-win-ia32": { - "version": "1.12.36", - "resolved": "https://registry.npmjs.org/sherpa-onnx-win-ia32/-/sherpa-onnx-win-ia32-1.12.36.tgz", - "integrity": "sha512-An3O5tEq4P5xKBSZMq10F/w6q79fgdbFZ4NKPoflSKSW37TSyNz9MhHLHwhse/BMPa35eW14J9dsGG3DZTC/xA==", + "version": "1.12.35", + "resolved": "https://registry.npmjs.org/sherpa-onnx-win-ia32/-/sherpa-onnx-win-ia32-1.12.35.tgz", + "integrity": "sha512-6H6BSdXXWtz92AuvOmr4w/QvCofxXbfbNKT7jCxdE7Nd4AvinLJxT02vbnL6T54vuXd9chu0QvQrDl1tuRphAA==", "cpu": [ "ia32" ], @@ -9172,9 +9172,9 @@ ] }, "node_modules/sherpa-onnx-win-x64": { - "version": "1.12.36", - "resolved": "https://registry.npmjs.org/sherpa-onnx-win-x64/-/sherpa-onnx-win-x64-1.12.36.tgz", - "integrity": "sha512-wZLQflcvy8ynsU6B8GvqWIhOCAjP6+rnzyadF+qGWgZzMSCduapD63q++0QDRabGRBL8qfsd2n7O2dF/W2kccQ==", + "version": "1.12.35", + "resolved": "https://registry.npmjs.org/sherpa-onnx-win-x64/-/sherpa-onnx-win-x64-1.12.35.tgz", + "integrity": "sha512-+GLrxwaEvpJAO0KZgKulfd4qUR089MD+TjE5jVSugMTq4Eh/R/TpPPqYQGibRZVPHW7Se1ABfHGapZQoFMHH5Q==", "cpu": [ "x64" ], @@ -9643,14 +9643,14 @@ "license": "MIT" }, "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.4" + "picomatch": "^4.0.3" }, "engines": { "node": ">=12.0.0" From 7aeff80bf9d6896a9793647fba546d9c8bc000d4 Mon Sep 17 00:00:00 2001 From: cc <98377878+hicccc77@users.noreply.github.com> Date: Mon, 13 Apr 2026 19:32:54 +0800 Subject: [PATCH 9/9] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BA=86=E4=B8=80?= =?UTF-8?q?=E5=A4=84=E8=B5=8B=E5=80=BC=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/AccountManagementPage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/AccountManagementPage.tsx b/src/pages/AccountManagementPage.tsx index 99e022e..e4efdb2 100644 --- a/src/pages/AccountManagementPage.tsx +++ b/src/pages/AccountManagementPage.tsx @@ -164,7 +164,7 @@ function AccountManagementPage() { modifiedTime: Number(scanned.modifiedTime || 0), configUpdatedAt: Number(matchedConfig?.value?.updatedAt || 0), hasConfig: Boolean(matchedConfig), - isCurrent: normalizedCurrent && normalized === normalizedCurrent, + isCurrent: Boolean(normalizedCurrent) && normalized === normalizedCurrent, fromScan: true }) } @@ -189,7 +189,7 @@ function AccountManagementPage() { modifiedTime: 0, configUpdatedAt: Number(matchedConfig.value?.updatedAt || 0), hasConfig: true, - isCurrent: normalizedCurrent && normalized === normalizedCurrent, + isCurrent: Boolean(normalizedCurrent) && normalized === normalizedCurrent, fromScan: false }) }