mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-04-08 15:08:44 +00:00
fix: 将通知头像存储在缓存文件中,通过LRU缓存维护头像缓存数量,点击通知后可以跳转到对应的会话窗口(fixes #654)
This commit is contained in:
@@ -19,6 +19,11 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
onShow: (callback: (event: any, data: any) => void) => {
|
onShow: (callback: (event: any, data: any) => void) => {
|
||||||
ipcRenderer.on('notification:show', callback)
|
ipcRenderer.on('notification:show', callback)
|
||||||
return () => ipcRenderer.removeAllListeners('notification:show')
|
return () => ipcRenderer.removeAllListeners('notification:show')
|
||||||
|
}, // 监听原本发送出来的navigate-to-session事件,跳转到具体的会话
|
||||||
|
onNavigateToSession: (callback: (sessionId: string) => void) => {
|
||||||
|
const listener = (_: any, sessionId: string) => callback(sessionId)
|
||||||
|
ipcRenderer.on('navigate-to-session', listener)
|
||||||
|
return () => ipcRenderer.removeListener('navigate-to-session', listener)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
219
electron/services/avatarFileCacheService.ts
Normal file
219
electron/services/avatarFileCacheService.ts
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
import https from "https";
|
||||||
|
import http, { IncomingMessage } from "http";
|
||||||
|
import { promises as fs } from "fs";
|
||||||
|
import { join } from "path";
|
||||||
|
import { ConfigService } from "./config";
|
||||||
|
|
||||||
|
// 头像文件缓存服务 - 复用项目已有的缓存目录结构
|
||||||
|
export class AvatarFileCacheService {
|
||||||
|
private static instance: AvatarFileCacheService | null = null;
|
||||||
|
|
||||||
|
// 头像文件缓存目录
|
||||||
|
private readonly cacheDir: string;
|
||||||
|
// 头像URL -> 本地文件路径的内存缓存(仅追踪正在下载的)
|
||||||
|
private readonly pendingDownloads: Map<string, Promise<string | null>> =
|
||||||
|
new Map();
|
||||||
|
// LRU 追踪:文件路径->最后访问时间
|
||||||
|
private readonly lruOrder: string[] = [];
|
||||||
|
private readonly maxCacheFiles = 100;
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
const basePath = ConfigService.getInstance().getCacheBasePath();
|
||||||
|
this.cacheDir = join(basePath, "avatar-files");
|
||||||
|
this.ensureCacheDir();
|
||||||
|
this.loadLruOrder();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getInstance(): AvatarFileCacheService {
|
||||||
|
if (!AvatarFileCacheService.instance) {
|
||||||
|
AvatarFileCacheService.instance = new AvatarFileCacheService();
|
||||||
|
}
|
||||||
|
return AvatarFileCacheService.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ensureCacheDir(): void {
|
||||||
|
// 同步确保目录存在(构造函数调用)
|
||||||
|
try {
|
||||||
|
fs.mkdir(this.cacheDir, { recursive: true }).catch(() => {});
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ensureCacheDirAsync(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await fs.mkdir(this.cacheDir, { recursive: true });
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getFilePath(url: string): string {
|
||||||
|
// 使用URL的hash作为文件名,避免特殊字符问题
|
||||||
|
const hash = this.hashString(url);
|
||||||
|
return join(this.cacheDir, `avatar_${hash}.png`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private hashString(str: string): string {
|
||||||
|
let hash = 0;
|
||||||
|
for (let i = 0; i < str.length; i++) {
|
||||||
|
const char = str.charCodeAt(i);
|
||||||
|
hash = (hash << 5) - hash + char;
|
||||||
|
hash = hash & hash; // 转换为32位整数
|
||||||
|
}
|
||||||
|
return Math.abs(hash).toString(16);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadLruOrder(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const entries = await fs.readdir(this.cacheDir);
|
||||||
|
// 按修改时间排序(旧的在前)
|
||||||
|
const filesWithTime: { file: string; mtime: number }[] = [];
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (!entry.startsWith("avatar_") || !entry.endsWith(".png")) continue;
|
||||||
|
try {
|
||||||
|
const stat = await fs.stat(join(this.cacheDir, entry));
|
||||||
|
filesWithTime.push({ file: entry, mtime: stat.mtimeMs });
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
filesWithTime.sort((a, b) => a.mtime - b.mtime);
|
||||||
|
this.lruOrder.length = 0;
|
||||||
|
this.lruOrder.push(...filesWithTime.map((f) => f.file));
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateLru(fileName: string): void {
|
||||||
|
const index = this.lruOrder.indexOf(fileName);
|
||||||
|
if (index > -1) {
|
||||||
|
this.lruOrder.splice(index, 1);
|
||||||
|
}
|
||||||
|
this.lruOrder.push(fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async evictIfNeeded(): Promise<void> {
|
||||||
|
while (this.lruOrder.length >= this.maxCacheFiles) {
|
||||||
|
const oldest = this.lruOrder.shift();
|
||||||
|
if (oldest) {
|
||||||
|
try {
|
||||||
|
await fs.rm(join(this.cacheDir, oldest));
|
||||||
|
console.log(`[AvatarFileCache] Evicted: ${oldest}`);
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async downloadAvatar(url: string): Promise<string | null> {
|
||||||
|
const localPath = this.getFilePath(url);
|
||||||
|
|
||||||
|
// 检查文件是否已存在
|
||||||
|
try {
|
||||||
|
await fs.access(localPath);
|
||||||
|
const fileName = localPath.split("/").pop()!;
|
||||||
|
this.updateLru(fileName);
|
||||||
|
return localPath;
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
await this.ensureCacheDirAsync();
|
||||||
|
await this.evictIfNeeded();
|
||||||
|
|
||||||
|
return new Promise<string | null>((resolve) => {
|
||||||
|
const options = {
|
||||||
|
headers: {
|
||||||
|
"User-Agent":
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 MicroMessenger/7.0.20.1781(0x6700143B) WindowsWechat(0x63090719) XWEB/8351",
|
||||||
|
Referer: "https://servicewechat.com/",
|
||||||
|
Accept:
|
||||||
|
"image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8",
|
||||||
|
"Accept-Encoding": "gzip, deflate, br",
|
||||||
|
"Accept-Language": "zh-CN,zh;q=0.9",
|
||||||
|
Connection: "keep-alive",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const callback = (res: IncomingMessage) => {
|
||||||
|
if (res.statusCode !== 200) {
|
||||||
|
resolve(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
res.on("data", (chunk: Buffer) => chunks.push(chunk));
|
||||||
|
res.on("end", async () => {
|
||||||
|
try {
|
||||||
|
const buffer = Buffer.concat(chunks);
|
||||||
|
await fs.writeFile(localPath, buffer);
|
||||||
|
const fileName = localPath.split("/").pop()!;
|
||||||
|
this.updateLru(fileName);
|
||||||
|
console.log(
|
||||||
|
`[AvatarFileCache] Downloaded: ${url.substring(0, 50)}... -> ${localPath}`,
|
||||||
|
);
|
||||||
|
resolve(localPath);
|
||||||
|
} catch {
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
res.on("error", () => resolve(null));
|
||||||
|
};
|
||||||
|
|
||||||
|
const req = url.startsWith("https")
|
||||||
|
? https.get(url, options, callback)
|
||||||
|
: http.get(url, options, callback);
|
||||||
|
|
||||||
|
req.on("error", () => resolve(null));
|
||||||
|
req.setTimeout(10000, () => {
|
||||||
|
req.destroy();
|
||||||
|
resolve(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取头像本地文件路径,如果需要会下载
|
||||||
|
* 同一URL并发调用会复用同一个下载任务
|
||||||
|
*/
|
||||||
|
async getAvatarPath(url: string): Promise<string | null> {
|
||||||
|
if (!url) return null;
|
||||||
|
|
||||||
|
// 检查是否有正在进行的下载
|
||||||
|
const pending = this.pendingDownloads.get(url);
|
||||||
|
if (pending) {
|
||||||
|
return pending;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发起新下载
|
||||||
|
const downloadPromise = this.downloadAvatar(url);
|
||||||
|
this.pendingDownloads.set(url, downloadPromise);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await downloadPromise;
|
||||||
|
return result;
|
||||||
|
} finally {
|
||||||
|
this.pendingDownloads.delete(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理所有缓存文件(App退出时调用)
|
||||||
|
async clearCache(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const entries = await fs.readdir(this.cacheDir);
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.startsWith("avatar_") && entry.endsWith(".png")) {
|
||||||
|
try {
|
||||||
|
await fs.rm(join(this.cacheDir, entry));
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.lruOrder.length = 0;
|
||||||
|
console.log("[AvatarFileCache] Cache cleared");
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前缓存的文件数量
|
||||||
|
async getCacheCount(): Promise<number> {
|
||||||
|
try {
|
||||||
|
const entries = await fs.readdir(this.cacheDir);
|
||||||
|
return entries.filter(
|
||||||
|
(e) => e.startsWith("avatar_") && e.endsWith(".png"),
|
||||||
|
).length;
|
||||||
|
} catch {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const avatarFileCache = AvatarFileCacheService.getInstance();
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
import https from "https";
|
import { Notification } from "electron";
|
||||||
import http, { IncomingMessage } from "http";
|
import { avatarFileCache, AvatarFileCacheService } from "./avatarFileCacheService";
|
||||||
import { promises as fs } from "fs";
|
|
||||||
import { join } from "path";
|
|
||||||
import { app, Notification } from "electron";
|
|
||||||
|
|
||||||
export interface LinuxNotificationData {
|
export interface LinuxNotificationData {
|
||||||
sessionId?: string;
|
sessionId?: string;
|
||||||
@@ -19,11 +16,6 @@ let notificationCounter = 1;
|
|||||||
const activeNotifications: Map<number, Notification> = new Map();
|
const activeNotifications: Map<number, Notification> = new Map();
|
||||||
const closeTimers: Map<number, NodeJS.Timeout> = new Map();
|
const closeTimers: Map<number, NodeJS.Timeout> = new Map();
|
||||||
|
|
||||||
// 头像缓存:url->localFilePath
|
|
||||||
const avatarCache: Map<string, string> = new Map();
|
|
||||||
// 缓存目录
|
|
||||||
let avatarCacheDir: string | null = null;
|
|
||||||
|
|
||||||
function nextNotificationId(): number {
|
function nextNotificationId(): number {
|
||||||
const id = notificationCounter;
|
const id = notificationCounter;
|
||||||
notificationCounter += 1;
|
notificationCounter += 1;
|
||||||
@@ -39,91 +31,6 @@ function clearNotificationState(notificationId: number): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确保缓存目录存在
|
|
||||||
async function ensureCacheDir(): Promise<string> {
|
|
||||||
if (!avatarCacheDir) {
|
|
||||||
avatarCacheDir = join(app.getPath("temp"), "weflow-avatars");
|
|
||||||
try {
|
|
||||||
await fs.mkdir(avatarCacheDir, { recursive: true });
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
"[LinuxNotification] Failed to create avatar cache dir:",
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return avatarCacheDir;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 下载头像到本地临时文件
|
|
||||||
async function downloadAvatarToLocal(url: string): Promise<string | null> {
|
|
||||||
// 检查缓存
|
|
||||||
if (avatarCache.has(url)) {
|
|
||||||
return avatarCache.get(url) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const cacheDir = await ensureCacheDir();
|
|
||||||
// 生成唯一文件名
|
|
||||||
const fileName = `avatar_${Date.now()}_${Math.random().toString(36).substring(2, 8)}.png`;
|
|
||||||
const localPath = join(cacheDir, fileName);
|
|
||||||
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
|
||||||
// 微信 CDN 需要特殊的请求头才能下载图片
|
|
||||||
const options = {
|
|
||||||
headers: {
|
|
||||||
"User-Agent":
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 MicroMessenger/7.0.20.1781(0x6700143B) WindowsWechat(0x63090719) XWEB/8351",
|
|
||||||
Referer: "https://servicewechat.com/",
|
|
||||||
Accept:
|
|
||||||
"image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8",
|
|
||||||
"Accept-Encoding": "gzip, deflate, br",
|
|
||||||
"Accept-Language": "zh-CN,zh;q=0.9",
|
|
||||||
Connection: "keep-alive",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const callback = (res: IncomingMessage) => {
|
|
||||||
if (res.statusCode !== 200) {
|
|
||||||
reject(new Error(`HTTP ${res.statusCode}`));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const chunks: Buffer[] = [];
|
|
||||||
res.on("data", (chunk: Buffer) => chunks.push(chunk));
|
|
||||||
res.on("end", async () => {
|
|
||||||
try {
|
|
||||||
const buffer = Buffer.concat(chunks);
|
|
||||||
await fs.writeFile(localPath, buffer);
|
|
||||||
avatarCache.set(url, localPath);
|
|
||||||
resolve();
|
|
||||||
} catch (err) {
|
|
||||||
reject(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
res.on("error", reject);
|
|
||||||
};
|
|
||||||
|
|
||||||
const req = url.startsWith("https")
|
|
||||||
? https.get(url, options, callback)
|
|
||||||
: http.get(url, options, callback);
|
|
||||||
|
|
||||||
req.on("error", reject);
|
|
||||||
req.setTimeout(10000, () => {
|
|
||||||
req.destroy();
|
|
||||||
reject(new Error("Download timeout"));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`[LinuxNotification] Avatar downloaded: ${url} -> ${localPath}`,
|
|
||||||
);
|
|
||||||
return localPath;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[LinuxNotification] Failed to download avatar:", error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function triggerNotificationCallback(sessionId: string): void {
|
function triggerNotificationCallback(sessionId: string): void {
|
||||||
for (const callback of notificationCallbacks) {
|
for (const callback of notificationCallbacks) {
|
||||||
try {
|
try {
|
||||||
@@ -149,7 +56,7 @@ export async function showLinuxNotification(
|
|||||||
try {
|
try {
|
||||||
let iconPath: string | undefined;
|
let iconPath: string | undefined;
|
||||||
if (data.avatarUrl) {
|
if (data.avatarUrl) {
|
||||||
iconPath = (await downloadAvatarToLocal(data.avatarUrl)) || undefined;
|
iconPath = (await avatarFileCache.getAvatarPath(data.avatarUrl)) || undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const notification = new Notification({
|
const notification = new Notification({
|
||||||
@@ -248,3 +155,20 @@ export async function initLinuxNotificationService(): Promise<void> {
|
|||||||
const caps = await getCapabilities();
|
const caps = await getCapabilities();
|
||||||
console.log("[LinuxNotification] Service initialized with native API:", caps);
|
console.log("[LinuxNotification] Service initialized with native API:", caps);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function shutdownLinuxNotificationService(): Promise<void> {
|
||||||
|
// 清理所有活动的通知
|
||||||
|
for (const [id, notification] of activeNotifications) {
|
||||||
|
try {
|
||||||
|
notification.close();
|
||||||
|
} catch {}
|
||||||
|
clearNotificationState(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理头像文件缓存
|
||||||
|
try {
|
||||||
|
await avatarFileCache.clearCache();
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
console.log("[LinuxNotification] Service shutdown complete");
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,6 +27,14 @@ export function destroyNotificationWindow() {
|
|||||||
}
|
}
|
||||||
lastNotificationData = null;
|
lastNotificationData = null;
|
||||||
|
|
||||||
|
// Linux:关闭通知服务并清理缓存(fire-and-forget,不阻塞退出)
|
||||||
|
if (isLinux && linuxNotificationService) {
|
||||||
|
linuxNotificationService.shutdownLinuxNotificationService().catch((error) => {
|
||||||
|
console.warn("[NotificationWindow] Failed to shutdown Linux notification service:", error);
|
||||||
|
});
|
||||||
|
linuxNotificationService = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (!notificationWindow || notificationWindow.isDestroyed()) {
|
if (!notificationWindow || notificationWindow.isDestroyed()) {
|
||||||
notificationWindow = null;
|
notificationWindow = null;
|
||||||
return;
|
return;
|
||||||
|
|||||||
15
src/App.tsx
15
src/App.tsx
@@ -339,6 +339,21 @@ function App() {
|
|||||||
}
|
}
|
||||||
}, [setUpdateInfo, setDownloadProgress, setShowUpdateDialog, isNotificationWindow])
|
}, [setUpdateInfo, setDownloadProgress, setShowUpdateDialog, isNotificationWindow])
|
||||||
|
|
||||||
|
// 监听通知点击导航事件
|
||||||
|
useEffect(() => {
|
||||||
|
if (isNotificationWindow) return
|
||||||
|
|
||||||
|
const removeListener = window.electronAPI?.notification?.onNavigateToSession?.((sessionId: string) => {
|
||||||
|
if (!sessionId) return
|
||||||
|
// 导航到聊天页面,通过URL参数让ChatPage接收sessionId
|
||||||
|
navigate(`/chat?sessionId=${encodeURIComponent(sessionId)}`, { replace: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
removeListener?.()
|
||||||
|
}
|
||||||
|
}, [navigate, isNotificationWindow])
|
||||||
|
|
||||||
// 解锁后显示暂存的更新弹窗
|
// 解锁后显示暂存的更新弹窗
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isLocked && updateInfo?.hasUpdate && !showUpdateDialog && !isDownloading) {
|
if (!isLocked && updateInfo?.hasUpdate && !showUpdateDialog && !isDownloading) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react'
|
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react'
|
||||||
import { Search, MessageSquare, AlertCircle, Loader2, RefreshCw, X, ChevronDown, ChevronLeft, Info, Calendar, Database, Hash, Play, Pause, Image as ImageIcon, Mic, CheckCircle, Copy, Check, CheckSquare, Download, BarChart3, Edit2, Trash2, BellOff, Users, FolderClosed, UserCheck, Crown, Aperture, Newspaper } from 'lucide-react'
|
import { Search, MessageSquare, AlertCircle, Loader2, RefreshCw, X, ChevronDown, ChevronLeft, Info, Calendar, Database, Hash, Play, Pause, Image as ImageIcon, Mic, CheckCircle, Copy, Check, CheckSquare, Download, BarChart3, Edit2, Trash2, BellOff, Users, FolderClosed, UserCheck, Crown, Aperture, Newspaper } from 'lucide-react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate, useLocation } from 'react-router-dom'
|
||||||
import { createPortal } from 'react-dom'
|
import { createPortal } from 'react-dom'
|
||||||
import { Virtuoso, type VirtuosoHandle } from 'react-virtuoso'
|
import { Virtuoso, type VirtuosoHandle } from 'react-virtuoso'
|
||||||
import { useShallow } from 'zustand/react/shallow'
|
import { useShallow } from 'zustand/react/shallow'
|
||||||
@@ -1142,6 +1142,7 @@ function ChatPage(props: ChatPageProps) {
|
|||||||
const normalizedStandaloneInitialContactType = useMemo(() => String(standaloneInitialContactType || '').trim().toLowerCase(), [standaloneInitialContactType])
|
const normalizedStandaloneInitialContactType = useMemo(() => String(standaloneInitialContactType || '').trim().toLowerCase(), [standaloneInitialContactType])
|
||||||
const shouldHideStandaloneDetailButton = standaloneSessionWindow && normalizedStandaloneSource === 'export'
|
const shouldHideStandaloneDetailButton = standaloneSessionWindow && normalizedStandaloneSource === 'export'
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const location = useLocation()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isConnected,
|
isConnected,
|
||||||
@@ -5350,6 +5351,19 @@ function ChatPage(props: ChatPageProps) {
|
|||||||
selectSessionById
|
selectSessionById
|
||||||
])
|
])
|
||||||
|
|
||||||
|
// 监听URL参数中的sessionId,用于通知点击导航
|
||||||
|
useEffect(() => {
|
||||||
|
if (standaloneSessionWindow) return // standalone模式由上面的useEffect处理
|
||||||
|
const params = new URLSearchParams(location.search)
|
||||||
|
const urlSessionId = params.get('sessionId')
|
||||||
|
if (!urlSessionId) return
|
||||||
|
if (!isConnected || isConnecting) return
|
||||||
|
if (currentSessionId === urlSessionId) return
|
||||||
|
selectSessionById(urlSessionId)
|
||||||
|
// 选中后清除URL参数,避免影响后续用户手动切换会话
|
||||||
|
navigate('/chat', { replace: true })
|
||||||
|
}, [standaloneSessionWindow, location.search, isConnected, isConnecting, currentSessionId, selectSessionById, navigate])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!standaloneSessionWindow || !normalizedInitialSessionId) return
|
if (!standaloneSessionWindow || !normalizedInitialSessionId) return
|
||||||
if (!isConnected || isConnecting) {
|
if (!isConnected || isConnecting) {
|
||||||
|
|||||||
1
src/types/electron.d.ts
vendored
1
src/types/electron.d.ts
vendored
@@ -78,6 +78,7 @@ export interface ElectronAPI {
|
|||||||
ready: () => void
|
ready: () => void
|
||||||
resize: (width: number, height: number) => void
|
resize: (width: number, height: number) => void
|
||||||
onShow: (callback: (event: any, data: any) => void) => () => void
|
onShow: (callback: (event: any, data: any) => void) => () => void
|
||||||
|
onNavigateToSession: (callback: (sessionId: string) => void) => () => void
|
||||||
}
|
}
|
||||||
log: {
|
log: {
|
||||||
getPath: () => Promise<string>
|
getPath: () => Promise<string>
|
||||||
|
|||||||
Reference in New Issue
Block a user