mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-05-27 23:26:45 +00:00
Merge pull request #1008 from Jasonzhu1207/main
feat: AI Summaries for Group Chats
This commit is contained in:
@@ -15,6 +15,7 @@ interface JumpToDatePopoverProps {
|
||||
messageDateCounts?: Record<string, number>
|
||||
loadingDates?: boolean
|
||||
loadingDateCounts?: boolean
|
||||
maxDate?: Date
|
||||
}
|
||||
|
||||
const JumpToDatePopover: React.FC<JumpToDatePopoverProps> = ({
|
||||
@@ -29,7 +30,8 @@ const JumpToDatePopover: React.FC<JumpToDatePopoverProps> = ({
|
||||
hasLoadedMessageDates = false,
|
||||
messageDateCounts,
|
||||
loadingDates = false,
|
||||
loadingDateCounts = false
|
||||
loadingDateCounts = false,
|
||||
maxDate
|
||||
}) => {
|
||||
type CalendarViewMode = 'day' | 'month' | 'year'
|
||||
const getYearPageStart = (year: number): number => Math.floor(year / 12) * 12
|
||||
@@ -73,6 +75,14 @@ const JumpToDatePopover: React.FC<JumpToDatePopoverProps> = ({
|
||||
return messageDates.has(toDateKey(day))
|
||||
}
|
||||
|
||||
const isAfterMaxDate = (day: number): boolean => {
|
||||
if (!maxDate) return false
|
||||
const max = new Date(maxDate)
|
||||
max.setHours(23, 59, 59, 999)
|
||||
const candidate = new Date(calendarDate.getFullYear(), calendarDate.getMonth(), day, 0, 0, 0, 0)
|
||||
return candidate.getTime() > max.getTime()
|
||||
}
|
||||
|
||||
const isToday = (day: number): boolean => {
|
||||
const today = new Date()
|
||||
return day === today.getDate()
|
||||
@@ -102,6 +112,7 @@ const JumpToDatePopover: React.FC<JumpToDatePopoverProps> = ({
|
||||
|
||||
const handleDateClick = (day: number) => {
|
||||
if (hasLoadedMessageDates && !hasMessage(day)) return
|
||||
if (isAfterMaxDate(day)) return
|
||||
const targetDate = new Date(calendarDate.getFullYear(), calendarDate.getMonth(), day)
|
||||
setSelectedDate(targetDate)
|
||||
onSelect(targetDate)
|
||||
@@ -113,7 +124,7 @@ const JumpToDatePopover: React.FC<JumpToDatePopoverProps> = ({
|
||||
const classes = ['day-cell']
|
||||
if (isToday(day)) classes.push('today')
|
||||
if (isSelected(day)) classes.push('selected')
|
||||
if (hasLoadedMessageDates && !hasMessage(day)) classes.push('no-message')
|
||||
if ((hasLoadedMessageDates && !hasMessage(day)) || isAfterMaxDate(day)) classes.push('no-message')
|
||||
return classes.join(' ')
|
||||
}
|
||||
|
||||
@@ -225,6 +236,7 @@ const JumpToDatePopover: React.FC<JumpToDatePopoverProps> = ({
|
||||
if (day === null) return <div key={index} className="day-cell empty" />
|
||||
const dateKey = toDateKey(day)
|
||||
const hasMessageOnDay = hasMessage(day)
|
||||
const isDisabled = (hasLoadedMessageDates && !hasMessageOnDay) || isAfterMaxDate(day)
|
||||
const count = Number(messageDateCounts?.[dateKey] || 0)
|
||||
const showCount = count > 0
|
||||
const showCountLoading = hasMessageOnDay && loadingDateCounts && !showCount
|
||||
@@ -233,7 +245,7 @@ const JumpToDatePopover: React.FC<JumpToDatePopoverProps> = ({
|
||||
key={index}
|
||||
className={getDayClassName(day)}
|
||||
onClick={() => handleDateClick(day)}
|
||||
disabled={hasLoadedMessageDates && !hasMessageOnDay}
|
||||
disabled={isDisabled}
|
||||
type="button"
|
||||
>
|
||||
<span className="day-number">{day}</span>
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
Info,
|
||||
Loader2,
|
||||
Mic,
|
||||
Newspaper,
|
||||
RefreshCw,
|
||||
Search,
|
||||
Sparkles,
|
||||
@@ -22,9 +23,11 @@ export interface ChatHeaderProps {
|
||||
isGroupChat: boolean
|
||||
standaloneSessionWindow: boolean
|
||||
showGroupMembersPanel: boolean
|
||||
showGroupSummaryPanel: boolean
|
||||
showJumpPopover: boolean
|
||||
showInSessionSearch: boolean
|
||||
showDetailPanel: boolean
|
||||
aiGroupSummaryEnabled: boolean
|
||||
shouldHideStandaloneDetailButton: boolean
|
||||
isPrivateSnsSupported: boolean
|
||||
isExportActionBusy: boolean
|
||||
@@ -39,6 +42,7 @@ export interface ChatHeaderProps {
|
||||
currentSessionId?: string | null
|
||||
jumpCalendarWrapRef: React.RefObject<HTMLDivElement | null>
|
||||
onTriggerSessionInsight: () => void
|
||||
onToggleGroupSummaryPanel: () => void
|
||||
onGroupAnalytics: () => void
|
||||
onToggleGroupMembersPanel: () => void
|
||||
onExportCurrentSession: () => void
|
||||
@@ -56,9 +60,11 @@ function ChatHeader({
|
||||
isGroupChat,
|
||||
standaloneSessionWindow,
|
||||
showGroupMembersPanel,
|
||||
showGroupSummaryPanel,
|
||||
showJumpPopover,
|
||||
showInSessionSearch,
|
||||
showDetailPanel,
|
||||
aiGroupSummaryEnabled,
|
||||
shouldHideStandaloneDetailButton,
|
||||
isPrivateSnsSupported,
|
||||
isExportActionBusy,
|
||||
@@ -73,6 +79,7 @@ function ChatHeader({
|
||||
currentSessionId,
|
||||
jumpCalendarWrapRef,
|
||||
onTriggerSessionInsight,
|
||||
onToggleGroupSummaryPanel,
|
||||
onGroupAnalytics,
|
||||
onToggleGroupMembersPanel,
|
||||
onExportCurrentSession,
|
||||
@@ -116,6 +123,17 @@ function ChatHeader({
|
||||
>
|
||||
{isTriggeringSessionInsight ? <Loader2 size={18} className="spin" /> : <Sparkles size={18} />}
|
||||
</button>
|
||||
{isGroupChat && aiGroupSummaryEnabled && (
|
||||
<button
|
||||
className={`icon-btn group-summary-btn ${showGroupSummaryPanel ? 'active' : ''}`}
|
||||
onClick={onToggleGroupSummaryPanel}
|
||||
disabled={!currentSessionId}
|
||||
title="AI 群聊总结"
|
||||
aria-label="AI 群聊总结"
|
||||
>
|
||||
<Newspaper size={18} />
|
||||
</button>
|
||||
)}
|
||||
{!standaloneSessionWindow && isGroupChat && (
|
||||
<button className="icon-btn group-analytics-btn" onClick={onGroupAnalytics} title="群聊分析">
|
||||
<BarChart3 size={18} />
|
||||
@@ -217,9 +235,11 @@ function areEqual(prev: ChatHeaderProps, next: ChatHeaderProps) {
|
||||
prev.isGroupChat === next.isGroupChat &&
|
||||
prev.standaloneSessionWindow === next.standaloneSessionWindow &&
|
||||
prev.showGroupMembersPanel === next.showGroupMembersPanel &&
|
||||
prev.showGroupSummaryPanel === next.showGroupSummaryPanel &&
|
||||
prev.showJumpPopover === next.showJumpPopover &&
|
||||
prev.showInSessionSearch === next.showInSessionSearch &&
|
||||
prev.showDetailPanel === next.showDetailPanel &&
|
||||
prev.aiGroupSummaryEnabled === next.aiGroupSummaryEnabled &&
|
||||
prev.shouldHideStandaloneDetailButton === next.shouldHideStandaloneDetailButton &&
|
||||
prev.isPrivateSnsSupported === next.isPrivateSnsSupported &&
|
||||
prev.isExportActionBusy === next.isExportActionBusy &&
|
||||
@@ -234,6 +254,7 @@ function areEqual(prev: ChatHeaderProps, next: ChatHeaderProps) {
|
||||
prev.currentSessionId === next.currentSessionId &&
|
||||
prev.jumpCalendarWrapRef === next.jumpCalendarWrapRef &&
|
||||
prev.onTriggerSessionInsight === next.onTriggerSessionInsight &&
|
||||
prev.onToggleGroupSummaryPanel === next.onToggleGroupSummaryPanel &&
|
||||
prev.onGroupAnalytics === next.onGroupAnalytics &&
|
||||
prev.onToggleGroupMembersPanel === next.onToggleGroupMembersPanel &&
|
||||
prev.onExportCurrentSession === next.onExportCurrentSession &&
|
||||
|
||||
@@ -3569,6 +3569,271 @@
|
||||
}
|
||||
}
|
||||
|
||||
.group-summary-panel {
|
||||
width: clamp(320px, 30vw, 420px);
|
||||
min-width: 320px;
|
||||
max-width: 420px;
|
||||
|
||||
.group-summary-controls {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.group-summary-date-row {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
label {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
input,
|
||||
.group-summary-date-trigger {
|
||||
height: 32px;
|
||||
min-width: 0;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
background: var(--card-bg);
|
||||
color: var(--text-primary);
|
||||
padding: 0 9px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.group-summary-date-picker {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.group-summary-date-trigger {
|
||||
width: 100%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.16s ease;
|
||||
|
||||
svg {
|
||||
color: var(--text-secondary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.open {
|
||||
border-color: color-mix(in srgb, var(--primary) 36%, var(--border-color));
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.group-summary-calendar-popover {
|
||||
right: auto;
|
||||
left: 0;
|
||||
top: calc(100% + 8px);
|
||||
width: min(312px, calc(100vw - 32px));
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.group-summary-icon-btn,
|
||||
.group-summary-code-btn {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: 1px solid color-mix(in srgb, var(--border-color) 78%, transparent);
|
||||
border-radius: 8px;
|
||||
background: var(--card-bg);
|
||||
color: var(--text-secondary);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.16s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--primary);
|
||||
background: var(--bg-hover);
|
||||
border-color: color-mix(in srgb, var(--primary) 34%, var(--border-color));
|
||||
}
|
||||
|
||||
.spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.group-summary-range-tabs {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 6px;
|
||||
|
||||
button {
|
||||
height: 30px;
|
||||
border: 1px solid color-mix(in srgb, var(--border-color) 78%, transparent);
|
||||
border-radius: 8px;
|
||||
background: var(--card-bg);
|
||||
color: var(--text-secondary);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.16s ease;
|
||||
|
||||
&.active {
|
||||
color: var(--primary);
|
||||
border-color: color-mix(in srgb, var(--primary) 42%, var(--border-color));
|
||||
background: color-mix(in srgb, var(--primary) 10%, var(--card-bg));
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.group-summary-generate-btn {
|
||||
height: 34px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 7px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: transform 0.16s ease, opacity 0.16s ease;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.group-summary-rule-hint,
|
||||
.group-summary-count {
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.group-summary-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.group-summary-record {
|
||||
border: 1px solid color-mix(in srgb, var(--border-color) 74%, transparent);
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.group-summary-record-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.group-summary-period {
|
||||
display: block;
|
||||
color: var(--text-primary);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.group-summary-meta {
|
||||
display: block;
|
||||
margin-top: 3px;
|
||||
color: var(--text-tertiary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.group-summary-topic-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.group-summary-topic {
|
||||
border-radius: 8px;
|
||||
border: 1px solid color-mix(in srgb, var(--border-color) 70%, transparent);
|
||||
background: color-mix(in srgb, var(--card-bg) 88%, transparent);
|
||||
padding: 9px;
|
||||
|
||||
h5 {
|
||||
margin: 0 0 7px;
|
||||
font-size: 13px;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.35;
|
||||
}
|
||||
}
|
||||
|
||||
.group-summary-topic-row {
|
||||
display: grid;
|
||||
grid-template-columns: 72px 1fr;
|
||||
gap: 8px;
|
||||
padding: 5px 0;
|
||||
border-top: 1px dashed color-mix(in srgb, var(--border-color) 72%, transparent);
|
||||
|
||||
span {
|
||||
color: var(--text-tertiary);
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: var(--text-secondary);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.group-summary-log-modal {
|
||||
width: min(860px, calc(100vw - 32px));
|
||||
max-height: min(760px, calc(100vh - 32px));
|
||||
|
||||
.detail-content {
|
||||
max-height: calc(100vh - 120px);
|
||||
}
|
||||
}
|
||||
|
||||
.group-summary-log-pre {
|
||||
margin: 0;
|
||||
max-height: 260px;
|
||||
overflow: auto;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
background: color-mix(in srgb, var(--card-bg) 86%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--border-color) 72%, transparent);
|
||||
color: var(--text-secondary);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
@keyframes detailCardEnter {
|
||||
from {
|
||||
opacity: 0;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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, Star, Sparkles } 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, Star, Sparkles, Code2 } from 'lucide-react'
|
||||
import { useNavigate, useLocation } from 'react-router-dom'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { Virtuoso, type VirtuosoHandle } from 'react-virtuoso'
|
||||
@@ -8,6 +8,7 @@ import { useChatStore } from '../stores/chatStore'
|
||||
import { useBatchTranscribeStore, type BatchVoiceTaskType } from '../stores/batchTranscribeStore'
|
||||
import { useBatchImageDecryptStore } from '../stores/batchImageDecryptStore'
|
||||
import type { ChatRecordItem, ChatSession, Message } from '../types/models'
|
||||
import type { GroupSummaryRecord, GroupSummaryRecordSummary } from '../types/electron'
|
||||
import { getEmojiPath } from 'wechat-emojis'
|
||||
import { VoiceTranscribeDialog } from '../components/VoiceTranscribeDialog'
|
||||
import { LivePhotoIcon } from '../components/LivePhotoIcon'
|
||||
@@ -68,6 +69,7 @@ interface QuotedMessageJumpTarget {
|
||||
|
||||
type GlobalMsgSearchPhase = 'idle' | 'seed' | 'backfill' | 'done'
|
||||
type GlobalMsgSearchResult = Message & { sessionId: string }
|
||||
type GroupSummaryRangeMode = 1 | 2 | 4 | 8 | 12 | 24
|
||||
|
||||
interface GlobalMsgPrefixCacheEntry {
|
||||
keyword: string
|
||||
@@ -762,6 +764,17 @@ function formatYmdHmDateTime(timestamp?: number): string {
|
||||
return `${y}-${m}-${day} ${h}:${min}`
|
||||
}
|
||||
|
||||
function formatDateInputLocal(date: Date): string {
|
||||
const y = date.getFullYear()
|
||||
const m = `${date.getMonth() + 1}`.padStart(2, '0')
|
||||
const day = `${date.getDate()}`.padStart(2, '0')
|
||||
return `${y}-${m}-${day}`
|
||||
}
|
||||
|
||||
function formatSummaryPeriod(start: number, end: number): string {
|
||||
return `${formatYmdHmDateTime(start * 1000)} - ${formatYmdHmDateTime(end * 1000)}`
|
||||
}
|
||||
|
||||
interface ChatPageProps {
|
||||
standaloneSessionWindow?: boolean
|
||||
initialSessionId?: string | null
|
||||
@@ -1476,6 +1489,7 @@ function ChatPage(props: ChatPageProps) {
|
||||
const sessionListRef = useRef<HTMLDivElement>(null)
|
||||
const jumpCalendarWrapRef = useRef<HTMLDivElement>(null)
|
||||
const jumpPopoverPortalRef = useRef<HTMLDivElement>(null)
|
||||
const groupSummaryDateWrapRef = useRef<HTMLDivElement>(null)
|
||||
const [currentOffset, setCurrentOffset] = useState(0)
|
||||
const [jumpStartTime, setJumpStartTime] = useState(0)
|
||||
const [jumpEndTime, setJumpEndTime] = useState(0)
|
||||
@@ -1537,6 +1551,18 @@ function ChatPage(props: ChatPageProps) {
|
||||
const [contextMenu, setContextMenu] = useState<{ x: number, y: number, message: Message } | null>(null)
|
||||
const [showMessageInfo, setShowMessageInfo] = useState<Message | null>(null)
|
||||
const [editingMessage, setEditingMessage] = useState<{ message: Message, content: string } | null>(null)
|
||||
const [aiGroupSummaryEnabled, setAiGroupSummaryEnabled] = useState(false)
|
||||
const [showGroupSummaryPanel, setShowGroupSummaryPanel] = useState(false)
|
||||
const [groupSummaryRecords, setGroupSummaryRecords] = useState<GroupSummaryRecordSummary[]>([])
|
||||
const [groupSummaryTotal, setGroupSummaryTotal] = useState(0)
|
||||
const [groupSummaryLoading, setGroupSummaryLoading] = useState(false)
|
||||
const [groupSummaryError, setGroupSummaryError] = useState<string | null>(null)
|
||||
const [groupSummaryDateFilter, setGroupSummaryDateFilter] = useState(() => formatDateInputLocal(new Date()))
|
||||
const [groupSummaryRangeMode, setGroupSummaryRangeMode] = useState<GroupSummaryRangeMode>(4)
|
||||
const [showGroupSummaryDatePopover, setShowGroupSummaryDatePopover] = useState(false)
|
||||
const [isTriggeringGroupSummary, setIsTriggeringGroupSummary] = useState(false)
|
||||
const [groupSummaryHint, setGroupSummaryHint] = useState<{ success: boolean; message: string } | null>(null)
|
||||
const [groupSummaryLogRecord, setGroupSummaryLogRecord] = useState<GroupSummaryRecord | null>(null)
|
||||
|
||||
// 多选模式
|
||||
const [isSelectionMode, setIsSelectionMode] = useState(false)
|
||||
@@ -1861,6 +1887,130 @@ function ChatPage(props: ChatPageProps) {
|
||||
})
|
||||
}, [])
|
||||
|
||||
const getGroupSummaryDateRangeSeconds = useCallback((dateValue = groupSummaryDateFilter) => {
|
||||
const date = dateValue || formatDateInputLocal(new Date())
|
||||
const start = new Date(`${date}T00:00:00`)
|
||||
if (!Number.isFinite(start.getTime())) {
|
||||
const fallback = new Date()
|
||||
fallback.setHours(0, 0, 0, 0)
|
||||
const fallbackEnd = new Date(fallback)
|
||||
fallbackEnd.setHours(23, 59, 59, 999)
|
||||
return { startTime: Math.floor(fallback.getTime() / 1000), endTime: Math.floor(fallbackEnd.getTime() / 1000) }
|
||||
}
|
||||
const end = new Date(start)
|
||||
end.setHours(23, 59, 59, 999)
|
||||
return { startTime: Math.floor(start.getTime() / 1000), endTime: Math.floor(end.getTime() / 1000) }
|
||||
}, [groupSummaryDateFilter])
|
||||
|
||||
const isGroupSummaryToday = useMemo(() => {
|
||||
return (groupSummaryDateFilter || formatDateInputLocal(new Date())) === formatDateInputLocal(new Date())
|
||||
}, [groupSummaryDateFilter])
|
||||
|
||||
const loadGroupSummaryRecords = useCallback(async (sessionId?: string) => {
|
||||
const targetSessionId = String(sessionId || currentSessionRef.current || '').trim()
|
||||
if (!targetSessionId || !targetSessionId.endsWith('@chatroom')) return
|
||||
const { startTime, endTime } = getGroupSummaryDateRangeSeconds()
|
||||
setGroupSummaryLoading(true)
|
||||
setGroupSummaryError(null)
|
||||
try {
|
||||
const result = await window.electronAPI.groupSummary.listRecords({
|
||||
sessionId: targetSessionId,
|
||||
startTime,
|
||||
endTime,
|
||||
limit: 100
|
||||
})
|
||||
if (currentSessionRef.current !== targetSessionId) return
|
||||
if (!result.success) {
|
||||
setGroupSummaryRecords([])
|
||||
setGroupSummaryTotal(0)
|
||||
setGroupSummaryError(result.error || '读取群聊总结失败')
|
||||
return
|
||||
}
|
||||
setGroupSummaryRecords(result.records || [])
|
||||
setGroupSummaryTotal(result.total || 0)
|
||||
} catch (error) {
|
||||
if (currentSessionRef.current !== targetSessionId) return
|
||||
setGroupSummaryRecords([])
|
||||
setGroupSummaryTotal(0)
|
||||
setGroupSummaryError((error as Error).message || String(error))
|
||||
} finally {
|
||||
if (currentSessionRef.current === targetSessionId) {
|
||||
setGroupSummaryLoading(false)
|
||||
}
|
||||
}
|
||||
}, [getGroupSummaryDateRangeSeconds])
|
||||
|
||||
const resolveTodayGroupSummaryManualRange = useCallback(() => {
|
||||
const nowSeconds = Math.floor(Date.now() / 1000)
|
||||
const hours = Number(groupSummaryRangeMode)
|
||||
return { startTime: nowSeconds - hours * 60 * 60, endTime: nowSeconds }
|
||||
}, [groupSummaryRangeMode])
|
||||
|
||||
const triggerManualGroupSummary = useCallback(async () => {
|
||||
const sessionId = String(currentSessionId || '').trim()
|
||||
if (!sessionId || !sessionId.endsWith('@chatroom')) return
|
||||
const sessionInfo = sessionMapRef.current.get(sessionId)
|
||||
const selectedDate = groupSummaryDateFilter || formatDateInputLocal(new Date())
|
||||
const today = formatDateInputLocal(new Date())
|
||||
|
||||
setIsTriggeringGroupSummary(true)
|
||||
setGroupSummaryHint({ success: true, message: '正在生成群聊总结...' })
|
||||
try {
|
||||
if (selectedDate === today) {
|
||||
const { startTime, endTime } = resolveTodayGroupSummaryManualRange()
|
||||
if (startTime <= 0 || endTime <= startTime) {
|
||||
setGroupSummaryHint({ success: false, message: '请选择有效的总结时段' })
|
||||
return
|
||||
}
|
||||
const result = await window.electronAPI.groupSummary.triggerManual({
|
||||
sessionId,
|
||||
displayName: sessionInfo?.displayName || sessionId,
|
||||
avatarUrl: sessionInfo?.avatarUrl,
|
||||
startTime,
|
||||
endTime
|
||||
})
|
||||
if (result.success) {
|
||||
setGroupSummaryHint({ success: true, message: result.message || '群聊总结已生成' })
|
||||
if (!result.skipped) {
|
||||
await loadGroupSummaryRecords(sessionId)
|
||||
}
|
||||
} else {
|
||||
setGroupSummaryHint({ success: false, message: result.message || '群聊总结生成失败' })
|
||||
}
|
||||
} else {
|
||||
const result = await window.electronAPI.groupSummary.triggerDay({
|
||||
sessionId,
|
||||
displayName: sessionInfo?.displayName || sessionId,
|
||||
avatarUrl: sessionInfo?.avatarUrl,
|
||||
date: selectedDate
|
||||
})
|
||||
if (result.success) {
|
||||
setGroupSummaryHint({ success: true, message: result.message || '群聊总结已生成' })
|
||||
await loadGroupSummaryRecords(sessionId)
|
||||
} else {
|
||||
setGroupSummaryHint({ success: false, message: result.message || '群聊总结生成失败' })
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
setGroupSummaryHint({ success: false, message: (error as Error).message || String(error) })
|
||||
} finally {
|
||||
setIsTriggeringGroupSummary(false)
|
||||
}
|
||||
}, [currentSessionId, groupSummaryDateFilter, loadGroupSummaryRecords, resolveTodayGroupSummaryManualRange])
|
||||
|
||||
const openGroupSummaryLog = useCallback(async (recordId: string) => {
|
||||
try {
|
||||
const result = await window.electronAPI.groupSummary.getRecord(recordId)
|
||||
if (!result.success || !result.record) {
|
||||
setGroupSummaryHint({ success: false, message: result.error || '读取总结日志失败' })
|
||||
return
|
||||
}
|
||||
setGroupSummaryLogRecord(result.record)
|
||||
} catch (error) {
|
||||
setGroupSummaryHint({ success: false, message: (error as Error).message || String(error) })
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleToggleJumpPopover = useCallback(() => {
|
||||
if (!currentSessionId) return
|
||||
if (showJumpPopover) {
|
||||
@@ -2903,9 +3053,21 @@ function ChatPage(props: ChatPageProps) {
|
||||
return
|
||||
}
|
||||
setShowDetailPanel(false)
|
||||
setShowGroupSummaryPanel(false)
|
||||
setShowGroupMembersPanel(true)
|
||||
}, [currentSessionId, showGroupMembersPanel, isGroupChatSession])
|
||||
|
||||
const toggleGroupSummaryPanel = useCallback(() => {
|
||||
if (!currentSessionId || !isGroupChatSession(currentSessionId) || !aiGroupSummaryEnabled) return
|
||||
if (showGroupSummaryPanel) {
|
||||
setShowGroupSummaryPanel(false)
|
||||
return
|
||||
}
|
||||
setShowDetailPanel(false)
|
||||
setShowGroupMembersPanel(false)
|
||||
setShowGroupSummaryPanel(true)
|
||||
}, [aiGroupSummaryEnabled, currentSessionId, showGroupSummaryPanel, isGroupChatSession])
|
||||
|
||||
// 切换详情面板
|
||||
const toggleDetailPanel = useCallback(() => {
|
||||
if (showDetailPanel) {
|
||||
@@ -2913,6 +3075,7 @@ function ChatPage(props: ChatPageProps) {
|
||||
return
|
||||
}
|
||||
setShowGroupMembersPanel(false)
|
||||
setShowGroupSummaryPanel(false)
|
||||
setShowDetailPanel(true)
|
||||
if (currentSessionId) {
|
||||
void loadSessionDetail(currentSessionId)
|
||||
@@ -2929,6 +3092,15 @@ function ChatPage(props: ChatPageProps) {
|
||||
void loadGroupMembersPanel(currentSessionId)
|
||||
}, [showGroupMembersPanel, currentSessionId, loadGroupMembersPanel, isGroupChatSession])
|
||||
|
||||
useEffect(() => {
|
||||
if (!showGroupSummaryPanel) return
|
||||
if (!currentSessionId || !isGroupChatSession(currentSessionId) || !aiGroupSummaryEnabled) {
|
||||
setShowGroupSummaryPanel(false)
|
||||
return
|
||||
}
|
||||
void loadGroupSummaryRecords(currentSessionId)
|
||||
}, [aiGroupSummaryEnabled, currentSessionId, groupSummaryDateFilter, loadGroupSummaryRecords, showGroupSummaryPanel, isGroupChatSession])
|
||||
|
||||
useEffect(() => {
|
||||
const chatroomId = String(sessionDetail?.wxid || '').trim()
|
||||
if (!chatroomId || !chatroomId.includes('@chatroom')) return
|
||||
@@ -3010,6 +3182,10 @@ function ChatPage(props: ChatPageProps) {
|
||||
setIsLoadingRelationStats(false)
|
||||
setShowDetailPanel(false)
|
||||
setShowGroupMembersPanel(false)
|
||||
setShowGroupSummaryPanel(false)
|
||||
setGroupSummaryRecords([])
|
||||
setGroupSummaryError(null)
|
||||
setGroupSummaryHint(null)
|
||||
setGroupPanelMembers([])
|
||||
setGroupMembersError(null)
|
||||
setGroupMembersLoadingHint('')
|
||||
@@ -4405,6 +4581,9 @@ function ChatPage(props: ChatPageProps) {
|
||||
setShowJumpPopover(false)
|
||||
setShowDetailPanel(false)
|
||||
setShowGroupMembersPanel(false)
|
||||
setShowGroupSummaryPanel(false)
|
||||
setGroupSummaryError(null)
|
||||
setGroupSummaryHint(null)
|
||||
setGroupMemberSearchKeyword('')
|
||||
setGroupMembersError(null)
|
||||
setGroupMembersLoadingHint('')
|
||||
@@ -5512,6 +5691,20 @@ function ChatPage(props: ChatPageProps) {
|
||||
}
|
||||
}, [showJumpPopover])
|
||||
|
||||
useEffect(() => {
|
||||
if (!showGroupSummaryDatePopover) return
|
||||
const handleGlobalPointerDown = (event: MouseEvent) => {
|
||||
const target = event.target as Node | null
|
||||
if (!target) return
|
||||
if (groupSummaryDateWrapRef.current?.contains(target)) return
|
||||
setShowGroupSummaryDatePopover(false)
|
||||
}
|
||||
document.addEventListener('mousedown', handleGlobalPointerDown)
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleGlobalPointerDown)
|
||||
}
|
||||
}, [showGroupSummaryDatePopover])
|
||||
|
||||
useEffect(() => {
|
||||
if (!showJumpPopover) return
|
||||
const syncPosition = () => {
|
||||
@@ -5870,6 +6063,29 @@ function ChatPage(props: ChatPageProps) {
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
let canceled = false
|
||||
|
||||
const loadGroupSummaryConfig = () => {
|
||||
void configService.getAiGroupSummaryEnabled()
|
||||
.then((enabled) => {
|
||||
if (!canceled) setAiGroupSummaryEnabled(enabled)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn('加载群聊总结配置失败:', error)
|
||||
if (!canceled) setAiGroupSummaryEnabled(false)
|
||||
})
|
||||
}
|
||||
|
||||
loadGroupSummaryConfig()
|
||||
const handleFocus = () => loadGroupSummaryConfig()
|
||||
window.addEventListener('focus', handleFocus)
|
||||
return () => {
|
||||
canceled = true
|
||||
window.removeEventListener('focus', handleFocus)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!standaloneSessionWindow) return
|
||||
setStandaloneInitialLoadRequested(false)
|
||||
@@ -7375,9 +7591,11 @@ function ChatPage(props: ChatPageProps) {
|
||||
isGroupChat={isCurrentSessionGroup}
|
||||
standaloneSessionWindow={standaloneSessionWindow}
|
||||
showGroupMembersPanel={showGroupMembersPanel}
|
||||
showGroupSummaryPanel={showGroupSummaryPanel}
|
||||
showJumpPopover={showJumpPopover}
|
||||
showInSessionSearch={showInSessionSearch}
|
||||
showDetailPanel={showDetailPanel}
|
||||
aiGroupSummaryEnabled={aiGroupSummaryEnabled}
|
||||
shouldHideStandaloneDetailButton={shouldHideStandaloneDetailButton}
|
||||
isPrivateSnsSupported={isCurrentSessionPrivateSnsSupported}
|
||||
isExportActionBusy={isExportActionBusy}
|
||||
@@ -7392,6 +7610,7 @@ function ChatPage(props: ChatPageProps) {
|
||||
currentSessionId={currentSessionId}
|
||||
jumpCalendarWrapRef={jumpCalendarWrapRef}
|
||||
onTriggerSessionInsight={handleTriggerSessionInsight}
|
||||
onToggleGroupSummaryPanel={toggleGroupSummaryPanel}
|
||||
onGroupAnalytics={handleGroupAnalytics}
|
||||
onToggleGroupMembersPanel={toggleGroupMembersPanel}
|
||||
onExportCurrentSession={handleExportCurrentSession}
|
||||
@@ -7443,6 +7662,13 @@ function ChatPage(props: ChatPageProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{groupSummaryHint && (
|
||||
<div className={`session-insight-hint ${groupSummaryHint.success ? 'success' : 'error'}`} role="status" aria-live="polite">
|
||||
{isTriggeringGroupSummary ? <Loader2 size={14} className="spin" /> : <Newspaper size={14} />}
|
||||
<span>{groupSummaryHint.message}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ContactSnsTimelineDialog
|
||||
target={chatSnsTimelineTarget}
|
||||
onClose={() => setChatSnsTimelineTarget(null)}
|
||||
@@ -7683,6 +7909,138 @@ function ChatPage(props: ChatPageProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showGroupSummaryPanel && isCurrentSessionGroup && (
|
||||
<div className="detail-panel group-summary-panel">
|
||||
<div className="detail-header">
|
||||
<div className="detail-title-wrap">
|
||||
<h4>AI 群聊总结</h4>
|
||||
<span className="detail-title-sub">{currentSession?.displayName || currentSessionId}</span>
|
||||
</div>
|
||||
<button className="close-btn" onClick={() => setShowGroupSummaryPanel(false)}>
|
||||
<X size={16} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="group-summary-controls">
|
||||
<div className="group-summary-date-row">
|
||||
<label>日期</label>
|
||||
<div className="group-summary-date-picker" ref={groupSummaryDateWrapRef}>
|
||||
<button
|
||||
type="button"
|
||||
className={`group-summary-date-trigger ${showGroupSummaryDatePopover ? 'open' : ''}`}
|
||||
onClick={() => setShowGroupSummaryDatePopover((open) => !open)}
|
||||
>
|
||||
<span>{groupSummaryDateFilter}</span>
|
||||
<Calendar size={14} />
|
||||
</button>
|
||||
<JumpToDatePopover
|
||||
isOpen={showGroupSummaryDatePopover}
|
||||
onClose={() => setShowGroupSummaryDatePopover(false)}
|
||||
currentDate={new Date(`${groupSummaryDateFilter || formatDateInputLocal(new Date())}T00:00:00`)}
|
||||
onSelect={(date) => setGroupSummaryDateFilter(formatDateInputLocal(date))}
|
||||
className="group-summary-calendar-popover"
|
||||
maxDate={new Date()}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="group-summary-icon-btn"
|
||||
onClick={() => void loadGroupSummaryRecords(currentSessionId || undefined)}
|
||||
title="刷新总结"
|
||||
>
|
||||
<RefreshCw size={14} className={groupSummaryLoading ? 'spin' : ''} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isGroupSummaryToday ? (
|
||||
<div className="group-summary-range-tabs">
|
||||
{([1, 2, 4, 8, 12, 24] as const).map((hours) => (
|
||||
<button
|
||||
key={hours}
|
||||
type="button"
|
||||
className={groupSummaryRangeMode === hours ? 'active' : ''}
|
||||
onClick={() => setGroupSummaryRangeMode(hours)}
|
||||
>
|
||||
{hours}h
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="group-summary-rule-hint">将按设置里的自动总结间隔切分选中日期的完整聊天记录。</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="group-summary-generate-btn"
|
||||
onClick={() => void triggerManualGroupSummary()}
|
||||
disabled={isTriggeringGroupSummary}
|
||||
>
|
||||
{isTriggeringGroupSummary ? <Loader2 size={14} className="spin" /> : <Sparkles size={14} />}
|
||||
<span>生成总结</span>
|
||||
</button>
|
||||
<div className="group-summary-rule-hint">
|
||||
{isGroupSummaryToday ? '少于 5 条可总结消息会自动跳过。' : '每个切片少于 5 条可总结消息会自动跳过。'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="group-summary-list">
|
||||
{groupSummaryLoading ? (
|
||||
<div className="detail-loading">
|
||||
<Loader2 size={20} className="spin" />
|
||||
<span>加载总结中...</span>
|
||||
</div>
|
||||
) : groupSummaryError ? (
|
||||
<div className="detail-empty">{groupSummaryError}</div>
|
||||
) : groupSummaryRecords.length === 0 ? (
|
||||
<div className="detail-empty">当前日期暂无群聊总结</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="group-summary-count">共 {groupSummaryTotal} 条总结</div>
|
||||
{groupSummaryRecords.map((record) => (
|
||||
<div key={record.id} className="group-summary-record">
|
||||
<div className="group-summary-record-head">
|
||||
<div>
|
||||
<span className="group-summary-period">{formatSummaryPeriod(record.periodStart, record.periodEnd)}</span>
|
||||
<span className="group-summary-meta">
|
||||
{record.triggerType === 'manual' ? '手动' : '自动'} · {record.readableMessageCount} 条消息
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="group-summary-code-btn"
|
||||
onClick={() => void openGroupSummaryLog(record.id)}
|
||||
title="查看完整日志"
|
||||
>
|
||||
<Code2 size={14} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="group-summary-topic-list">
|
||||
{record.topics.map((topic, topicIndex) => (
|
||||
<div key={`${record.id}-${topicIndex}`} className="group-summary-topic">
|
||||
<h5>{topic.title}</h5>
|
||||
<div className="group-summary-topic-row">
|
||||
<span>参与者</span>
|
||||
<p>{topic.participants.length > 0 ? topic.participants.join('、') : '未明确'}</p>
|
||||
</div>
|
||||
<div className="group-summary-topic-row">
|
||||
<span>关键/矛盾点</span>
|
||||
<p>{topic.keyPoints.length > 0 ? topic.keyPoints.join(';') : '无'}</p>
|
||||
</div>
|
||||
<div className="group-summary-topic-row">
|
||||
<span>结论</span>
|
||||
<p>{topic.conclusion || '暂无明确结论'}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 会话详情面板 */}
|
||||
{showDetailPanel && (
|
||||
<div className="detail-panel session-detail-panel">
|
||||
@@ -8339,6 +8697,79 @@ function ChatPage(props: ChatPageProps) {
|
||||
document.body
|
||||
)}
|
||||
|
||||
{groupSummaryLogRecord && createPortal(
|
||||
<div className="message-info-overlay" onClick={() => setGroupSummaryLogRecord(null)}>
|
||||
<div className="message-info-modal group-summary-log-modal" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="detail-header">
|
||||
<h4>群聊总结日志</h4>
|
||||
<button className="close-btn" onClick={() => setGroupSummaryLogRecord(null)}>
|
||||
<X size={16} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="detail-content">
|
||||
<div className="detail-section">
|
||||
<div className="detail-item">
|
||||
<span className="label">群聊</span>
|
||||
<span className="value">{groupSummaryLogRecord.displayName}</span>
|
||||
</div>
|
||||
<div className="detail-item">
|
||||
<span className="label">时段</span>
|
||||
<span className="value">{formatSummaryPeriod(groupSummaryLogRecord.periodStart, groupSummaryLogRecord.periodEnd)}</span>
|
||||
</div>
|
||||
<div className="detail-item">
|
||||
<span className="label">触发</span>
|
||||
<span className="value">{groupSummaryLogRecord.triggerType === 'manual' ? '手动' : '自动'}</span>
|
||||
</div>
|
||||
<div className="detail-item">
|
||||
<span className="label">模型</span>
|
||||
<span className="value">{groupSummaryLogRecord.log.model}</span>
|
||||
</div>
|
||||
<div className="detail-item">
|
||||
<span className="label">消息数</span>
|
||||
<span className="value">{groupSummaryLogRecord.log.readableMessageCount} / {groupSummaryLogRecord.log.messageCount}</span>
|
||||
</div>
|
||||
<div className="detail-item">
|
||||
<span className="label">JSON Mode</span>
|
||||
<span className="value">
|
||||
{groupSummaryLogRecord.log.responseFormatJson ? '启用' : '未启用'}
|
||||
{groupSummaryLogRecord.log.responseFormatFallback ? `,已降级:${groupSummaryLogRecord.log.responseFormatFallbackReason || '未知原因'}` : ''}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="detail-section">
|
||||
<div className="section-title">
|
||||
<Code2 size={14} />
|
||||
<span>系统提示词</span>
|
||||
</div>
|
||||
<pre className="group-summary-log-pre">{groupSummaryLogRecord.log.systemPrompt}</pre>
|
||||
</div>
|
||||
<div className="detail-section">
|
||||
<div className="section-title">
|
||||
<Code2 size={14} />
|
||||
<span>用户提示词与完整记录</span>
|
||||
</div>
|
||||
<pre className="group-summary-log-pre">{groupSummaryLogRecord.log.userPrompt}</pre>
|
||||
</div>
|
||||
<div className="detail-section">
|
||||
<div className="section-title">
|
||||
<Code2 size={14} />
|
||||
<span>模型输出原文</span>
|
||||
</div>
|
||||
<pre className="group-summary-log-pre">{groupSummaryLogRecord.log.rawOutput}</pre>
|
||||
</div>
|
||||
<div className="detail-section">
|
||||
<div className="section-title">
|
||||
<Newspaper size={14} />
|
||||
<span>最终总结</span>
|
||||
</div>
|
||||
<pre className="group-summary-log-pre">{groupSummaryLogRecord.log.finalSummary}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
|
||||
{/* 修改消息弹窗 */}
|
||||
{editingMessage && createPortal(
|
||||
<div className="modal-overlay">
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useThemeStore, themes } from '../stores/themeStore'
|
||||
import { useAnalyticsStore } from '../stores/analyticsStore'
|
||||
import { dialog } from '../services/ipc'
|
||||
import * as configService from '../services/config'
|
||||
import groupSummaryPrompt from '../../shared/groupSummaryPrompt.json'
|
||||
import type { ChatSession, ContactInfo } from '../types/models'
|
||||
import {
|
||||
Eye, EyeOff, FolderSearch, FolderOpen, Search, Copy,
|
||||
@@ -32,6 +33,7 @@ type SettingsTab =
|
||||
| 'aiCommon'
|
||||
| 'insight'
|
||||
| 'aiFootprint'
|
||||
| 'aiGroupSummary'
|
||||
| 'aiMessageInsight'
|
||||
| 'autoDownload'
|
||||
|
||||
@@ -57,10 +59,11 @@ const filteredTabs = tabs.filter(tab => {
|
||||
return true
|
||||
})
|
||||
|
||||
const aiTabs: Array<{ id: Extract<SettingsTab, 'aiCommon' | 'insight' | 'aiFootprint' | 'aiMessageInsight'>; label: string }> = [
|
||||
const aiTabs: Array<{ id: Extract<SettingsTab, 'aiCommon' | 'insight' | 'aiFootprint' | 'aiGroupSummary' | 'aiMessageInsight'>; label: string }> = [
|
||||
{ id: 'aiCommon', label: '基础配置' },
|
||||
{ id: 'insight', label: 'AI 见解' },
|
||||
{ id: 'aiFootprint', label: 'AI 足迹' },
|
||||
{ id: 'aiGroupSummary', label: '群聊总结' },
|
||||
{ id: 'aiMessageInsight', label: '消息解析' }
|
||||
]
|
||||
|
||||
@@ -68,6 +71,7 @@ const isMac = navigator.userAgent.toLowerCase().includes('mac')
|
||||
const isLinux = navigator.userAgent.toLowerCase().includes('linux')
|
||||
const isWindows = !isMac && !isLinux
|
||||
const MAC_KEY_FAQ_URL = 'https://github.com/hicccc77/WeFlow/blob/main/docs/MAC-KEY-FAQ.md'
|
||||
const DEFAULT_GROUP_SUMMARY_SYSTEM_PROMPT = String(groupSummaryPrompt.defaultSystemPrompt || '').trim()
|
||||
|
||||
const dbDirName = isMac ? '2.0b4.0.9 目录' : 'xwechat_files 目录'
|
||||
const dbPathPlaceholder = isMac
|
||||
@@ -329,6 +333,11 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
||||
const [weiboBindingLoadingSessionId, setWeiboBindingLoadingSessionId] = useState<string | null>(null)
|
||||
const [aiFootprintEnabled, setAiFootprintEnabled] = useState(false)
|
||||
const [aiFootprintSystemPrompt, setAiFootprintSystemPrompt] = useState('')
|
||||
const [aiGroupSummaryEnabled, setAiGroupSummaryEnabled] = useState(false)
|
||||
const [aiGroupSummaryIntervalHours, setAiGroupSummaryIntervalHours] = useState(4)
|
||||
const [aiGroupSummarySystemPrompt, setAiGroupSummarySystemPrompt] = useState('')
|
||||
const [aiGroupSummaryFilterList, setAiGroupSummaryFilterList] = useState<string[]>([])
|
||||
const [aiGroupSummaryFilterSearchKeyword, setAiGroupSummaryFilterSearchKeyword] = useState('')
|
||||
const [aiMessageInsightEnabled, setAiMessageInsightEnabled] = useState(false)
|
||||
const [aiMessageInsightContextCount, setAiMessageInsightContextCount] = useState(50)
|
||||
const [aiMessageInsightSystemPrompt, setAiMessageInsightSystemPrompt] = useState('')
|
||||
@@ -377,7 +386,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
||||
}, [location.state])
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab === 'aiCommon' || activeTab === 'insight' || activeTab === 'aiFootprint' || activeTab === 'aiMessageInsight') {
|
||||
if (activeTab === 'aiCommon' || activeTab === 'insight' || activeTab === 'aiFootprint' || activeTab === 'aiGroupSummary' || activeTab === 'aiMessageInsight') {
|
||||
setAiGroupExpanded(true)
|
||||
}
|
||||
}, [activeTab])
|
||||
@@ -595,6 +604,10 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
||||
const savedAiInsightWeiboBindings = await configService.getAiInsightWeiboBindings()
|
||||
const savedAiFootprintEnabled = await configService.getAiFootprintEnabled()
|
||||
const savedAiFootprintSystemPrompt = await configService.getAiFootprintSystemPrompt()
|
||||
const savedAiGroupSummaryEnabled = await configService.getAiGroupSummaryEnabled()
|
||||
const savedAiGroupSummaryIntervalHours = await configService.getAiGroupSummaryIntervalHours()
|
||||
const savedAiGroupSummarySystemPrompt = await configService.getAiGroupSummarySystemPrompt()
|
||||
const savedAiGroupSummaryFilterList = await configService.getAiGroupSummaryFilterList()
|
||||
const savedAiMessageInsightEnabled = await configService.getAiMessageInsightEnabled()
|
||||
const savedAiMessageInsightContextCount = await configService.getAiMessageInsightContextCount()
|
||||
const savedAiMessageInsightSystemPrompt = await configService.getAiMessageInsightSystemPrompt()
|
||||
@@ -624,6 +637,10 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
||||
setAiInsightWeiboBindings(savedAiInsightWeiboBindings)
|
||||
setAiFootprintEnabled(savedAiFootprintEnabled)
|
||||
setAiFootprintSystemPrompt(savedAiFootprintSystemPrompt)
|
||||
setAiGroupSummaryEnabled(savedAiGroupSummaryEnabled)
|
||||
setAiGroupSummaryIntervalHours(savedAiGroupSummaryIntervalHours)
|
||||
setAiGroupSummarySystemPrompt(savedAiGroupSummarySystemPrompt)
|
||||
setAiGroupSummaryFilterList(savedAiGroupSummaryFilterList)
|
||||
setAiMessageInsightEnabled(savedAiMessageInsightEnabled)
|
||||
setAiMessageInsightContextCount(savedAiMessageInsightContextCount)
|
||||
setAiMessageInsightSystemPrompt(savedAiMessageInsightSystemPrompt)
|
||||
@@ -2914,6 +2931,15 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
||||
messagePushFilterSearchKeyword
|
||||
)
|
||||
|
||||
const groupSummaryFilterOptions = sessionFilterOptions.filter((session) => session.type === 'group')
|
||||
const groupSummaryAvailableSessions = groupSummaryFilterOptions.filter((session) => {
|
||||
const keyword = aiGroupSummaryFilterSearchKeyword.trim().toLowerCase()
|
||||
if (aiGroupSummaryFilterList.includes(session.username)) return false
|
||||
if (!keyword) return true
|
||||
return String(session.displayName || '').toLowerCase().includes(keyword) ||
|
||||
session.username.toLowerCase().includes(keyword)
|
||||
})
|
||||
|
||||
const handleAddAllNotificationFilterSessions = async () => {
|
||||
const usernames = notificationAvailableSessions.map(session => session.username)
|
||||
if (usernames.length === 0) return
|
||||
@@ -4032,6 +4058,219 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
||||
</div>
|
||||
)
|
||||
|
||||
const renderAiGroupSummaryTab = () => {
|
||||
const groupSummaryPromptDisplayValue = aiGroupSummarySystemPrompt || DEFAULT_GROUP_SUMMARY_SYSTEM_PROMPT
|
||||
|
||||
const addToFilterList = async (username: string) => {
|
||||
if (!username.endsWith('@chatroom') || aiGroupSummaryFilterList.includes(username)) return
|
||||
const next = [...aiGroupSummaryFilterList, username]
|
||||
setAiGroupSummaryFilterList(next)
|
||||
await configService.setAiGroupSummaryFilterList(next)
|
||||
showMessage('已添加到群聊总结作用域', true)
|
||||
}
|
||||
|
||||
const removeFromFilterList = async (username: string) => {
|
||||
const next = aiGroupSummaryFilterList.filter((item) => item !== username)
|
||||
setAiGroupSummaryFilterList(next)
|
||||
await configService.setAiGroupSummaryFilterList(next)
|
||||
showMessage('已从群聊总结作用域移除', true)
|
||||
}
|
||||
|
||||
const addAllFiltered = async () => {
|
||||
const usernames = groupSummaryAvailableSessions.map((session) => session.username)
|
||||
if (usernames.length === 0) return
|
||||
const next = Array.from(new Set([...aiGroupSummaryFilterList, ...usernames]))
|
||||
setAiGroupSummaryFilterList(next)
|
||||
await configService.setAiGroupSummaryFilterList(next)
|
||||
showMessage(`已添加 ${usernames.length} 个群聊`, true)
|
||||
}
|
||||
|
||||
const clearFilterList = async () => {
|
||||
if (aiGroupSummaryFilterList.length === 0) return
|
||||
setAiGroupSummaryFilterList([])
|
||||
await configService.setAiGroupSummaryFilterList([])
|
||||
showMessage('已清空群聊总结作用域', true)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="tab-content">
|
||||
<div className="form-group">
|
||||
<label>AI 群聊总结</label>
|
||||
<span className="form-hint">
|
||||
开启后,群聊页顶部会显示 AI 总结按钮;自动总结只对下面作用域内的群聊生效。未选择任何群聊时不会自动消耗 token。
|
||||
</span>
|
||||
<div className="log-toggle-line">
|
||||
<span className="log-status">{aiGroupSummaryEnabled ? '已开启' : '已关闭'}</span>
|
||||
<label className="switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={aiGroupSummaryEnabled}
|
||||
onChange={async (e) => {
|
||||
const val = e.target.checked
|
||||
setAiGroupSummaryEnabled(val)
|
||||
await configService.setAiGroupSummaryEnabled(val)
|
||||
showMessage(val ? 'AI 群聊总结已开启' : 'AI 群聊总结已关闭', true)
|
||||
}}
|
||||
/>
|
||||
<span className="switch-slider" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="divider" />
|
||||
|
||||
<div className="form-group">
|
||||
<label>自动总结间隔</label>
|
||||
<span className="form-hint">
|
||||
按本地系统时间从当天 00:00 开始切分完整时间段,到点总结上一段。时段内可总结消息少于 5 条时会跳过。
|
||||
</span>
|
||||
<div className="push-filter-type-tabs" style={{ marginTop: 10 }}>
|
||||
{[1, 2, 4, 8, 12, 24].map((hours) => (
|
||||
<button
|
||||
key={hours}
|
||||
type="button"
|
||||
className={`push-filter-type-tab ${aiGroupSummaryIntervalHours === hours ? 'active' : ''}`}
|
||||
onClick={() => {
|
||||
setAiGroupSummaryIntervalHours(hours)
|
||||
scheduleConfigSave('aiGroupSummaryIntervalHours', () => configService.setAiGroupSummaryIntervalHours(hours))
|
||||
}}
|
||||
>
|
||||
{hours} 小时
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12 }}>
|
||||
<label style={{ marginBottom: 0 }}>群聊总结提示词</label>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary btn-sm"
|
||||
onClick={async () => {
|
||||
setAiGroupSummarySystemPrompt('')
|
||||
await configService.setAiGroupSummarySystemPrompt('')
|
||||
}}
|
||||
>
|
||||
恢复默认
|
||||
</button>
|
||||
</div>
|
||||
<span className="form-hint">
|
||||
群聊总结专用提示词。留空时使用内置默认提示词。
|
||||
</span>
|
||||
<textarea
|
||||
className="field-input ai-prompt-textarea"
|
||||
rows={10}
|
||||
style={{ width: '100%', resize: 'vertical', marginTop: 8 }}
|
||||
value={groupSummaryPromptDisplayValue}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value
|
||||
setAiGroupSummarySystemPrompt(val)
|
||||
scheduleConfigSave('aiGroupSummarySystemPrompt', () => configService.setAiGroupSummarySystemPrompt(val))
|
||||
}}
|
||||
/>
|
||||
<span className="form-hint" style={{ color: 'var(--danger, #ef4444)', marginTop: 8, display: 'block' }}>
|
||||
该提示词控制 JSON 输出结构和总结解析路径,不建议随意修改,否则可能导致总结失败或内容错位。
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="divider" />
|
||||
|
||||
<div className="form-group">
|
||||
<label>自动总结作用域群聊</label>
|
||||
<span className="form-hint">
|
||||
仅控制自动总结范围。手动点击群聊页 AI 总结按钮不受作用域限制;未选择任何群聊时自动总结不会运行。
|
||||
</span>
|
||||
|
||||
{aiGroupSummaryFilterList.length === 0 && (
|
||||
<div className="api-docs" style={{ marginTop: 12 }}>
|
||||
<div className="api-item">
|
||||
<p className="api-desc">当前未选择作用域群聊,自动群聊总结不会触发。</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="notification-filter-container" style={{ marginTop: 12 }}>
|
||||
<div className="filter-panel">
|
||||
<div className="filter-panel-header">
|
||||
<span>可选群聊</span>
|
||||
{groupSummaryAvailableSessions.length > 0 && (
|
||||
<button type="button" className="filter-panel-action" onClick={() => { void addAllFiltered() }}>
|
||||
全选当前
|
||||
</button>
|
||||
)}
|
||||
<div className="filter-search-box">
|
||||
<Search size={14} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索群聊..."
|
||||
value={aiGroupSummaryFilterSearchKeyword}
|
||||
onChange={(e) => setAiGroupSummaryFilterSearchKeyword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="filter-panel-list">
|
||||
{groupSummaryAvailableSessions.length > 0 ? (
|
||||
groupSummaryAvailableSessions.map(session => (
|
||||
<div
|
||||
key={session.username}
|
||||
className="filter-panel-item"
|
||||
onClick={() => { void addToFilterList(session.username) }}
|
||||
>
|
||||
<Avatar src={session.avatarUrl} name={session.displayName || session.username} size={28} />
|
||||
<span className="filter-item-name">{session.displayName || session.username}</span>
|
||||
<span className="filter-item-type">群聊</span>
|
||||
<span className="filter-item-action">+</span>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="filter-panel-empty">
|
||||
{aiGroupSummaryFilterSearchKeyword ? '没有匹配的群聊' : '暂无可添加的群聊'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="filter-panel">
|
||||
<div className="filter-panel-header">
|
||||
<span>作用域群聊</span>
|
||||
{aiGroupSummaryFilterList.length > 0 && (
|
||||
<span className="filter-panel-count">{aiGroupSummaryFilterList.length}</span>
|
||||
)}
|
||||
{aiGroupSummaryFilterList.length > 0 && (
|
||||
<button type="button" className="filter-panel-action" onClick={() => { void clearFilterList() }}>
|
||||
全不选
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="filter-panel-list">
|
||||
{aiGroupSummaryFilterList.length > 0 ? (
|
||||
aiGroupSummaryFilterList.map(username => {
|
||||
const info = getSessionFilterOptionInfo(username)
|
||||
return (
|
||||
<div
|
||||
key={username}
|
||||
className="filter-panel-item selected"
|
||||
onClick={() => { void removeFromFilterList(username) }}
|
||||
>
|
||||
<Avatar src={info.avatarUrl} name={info.displayName} size={28} />
|
||||
<span className="filter-item-name">{info.displayName}</span>
|
||||
<span className="filter-item-type">群聊</span>
|
||||
<span className="filter-item-action">×</span>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<div className="filter-panel-empty">尚未添加任何群聊</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const renderAiMessageInsightTab = () => (
|
||||
<div className="tab-content">
|
||||
{(() => {
|
||||
@@ -5161,7 +5400,7 @@ JSON 输出格式:
|
||||
row.push(
|
||||
<div key="ai-settings-group" className={`tab-group ${aiGroupExpanded ? 'expanded' : ''}`}>
|
||||
<button
|
||||
className={`tab-btn tab-group-trigger ${(activeTab === 'aiCommon' || activeTab === 'insight' || activeTab === 'aiFootprint' || activeTab === 'aiMessageInsight') ? 'active' : ''}`}
|
||||
className={`tab-btn tab-group-trigger ${(activeTab === 'aiCommon' || activeTab === 'insight' || activeTab === 'aiFootprint' || activeTab === 'aiGroupSummary' || activeTab === 'aiMessageInsight') ? 'active' : ''}`}
|
||||
onClick={() => setAiGroupExpanded((prev) => !prev)}
|
||||
aria-expanded={aiGroupExpanded}
|
||||
>
|
||||
@@ -5203,6 +5442,7 @@ JSON 输出格式:
|
||||
{activeTab === 'aiCommon' && renderAiCommonTab()}
|
||||
{activeTab === 'insight' && renderInsightTab()}
|
||||
{activeTab === 'aiFootprint' && renderAiFootprintTab()}
|
||||
{activeTab === 'aiGroupSummary' && renderAiGroupSummaryTab()}
|
||||
{activeTab === 'aiMessageInsight' && renderAiMessageInsightTab()}
|
||||
{activeTab === 'autoDownload' && renderAutoDownloadTab()}
|
||||
{activeTab === 'updates' && renderUpdatesTab()}
|
||||
|
||||
@@ -120,6 +120,11 @@ export const CONFIG_KEYS = {
|
||||
// AI 足迹
|
||||
AI_FOOTPRINT_ENABLED: 'aiFootprintEnabled',
|
||||
AI_FOOTPRINT_SYSTEM_PROMPT: 'aiFootprintSystemPrompt',
|
||||
AI_GROUP_SUMMARY_ENABLED: 'aiGroupSummaryEnabled',
|
||||
AI_GROUP_SUMMARY_INTERVAL_HOURS: 'aiGroupSummaryIntervalHours',
|
||||
AI_GROUP_SUMMARY_SYSTEM_PROMPT: 'aiGroupSummarySystemPrompt',
|
||||
AI_GROUP_SUMMARY_FILTER_MODE: 'aiGroupSummaryFilterMode',
|
||||
AI_GROUP_SUMMARY_FILTER_LIST: 'aiGroupSummaryFilterList',
|
||||
AI_MESSAGE_INSIGHT_ENABLED: 'aiMessageInsightEnabled',
|
||||
AI_MESSAGE_INSIGHT_CONTEXT_COUNT: 'aiMessageInsightContextCount',
|
||||
AI_MESSAGE_INSIGHT_SYSTEM_PROMPT: 'aiMessageInsightSystemPrompt',
|
||||
@@ -2178,6 +2183,67 @@ export async function setAiFootprintSystemPrompt(prompt: string): Promise<void>
|
||||
await config.set(CONFIG_KEYS.AI_FOOTPRINT_SYSTEM_PROMPT, prompt)
|
||||
}
|
||||
|
||||
// Legacy only: 群聊总结现在只使用 aiGroupSummaryFilterList 作为作用域白名单。
|
||||
export type AiGroupSummaryFilterMode = 'whitelist' | 'blacklist'
|
||||
|
||||
const AI_GROUP_SUMMARY_INTERVALS = new Set([1, 2, 4, 8, 12, 24])
|
||||
|
||||
const normalizeAiGroupSummaryFilterList = (value: unknown): string[] => {
|
||||
if (!Array.isArray(value)) return []
|
||||
return Array.from(new Set(
|
||||
value
|
||||
.map((item) => String(item || '').trim())
|
||||
.filter((item) => item.endsWith('@chatroom'))
|
||||
))
|
||||
}
|
||||
|
||||
export async function getAiGroupSummaryEnabled(): Promise<boolean> {
|
||||
const value = await config.get(CONFIG_KEYS.AI_GROUP_SUMMARY_ENABLED)
|
||||
return value === true
|
||||
}
|
||||
|
||||
export async function setAiGroupSummaryEnabled(enabled: boolean): Promise<void> {
|
||||
await config.set(CONFIG_KEYS.AI_GROUP_SUMMARY_ENABLED, enabled)
|
||||
}
|
||||
|
||||
export async function getAiGroupSummaryIntervalHours(): Promise<number> {
|
||||
const value = Number(await config.get(CONFIG_KEYS.AI_GROUP_SUMMARY_INTERVAL_HOURS))
|
||||
const normalized = Number.isFinite(value) ? Math.floor(value) : 4
|
||||
return AI_GROUP_SUMMARY_INTERVALS.has(normalized) ? normalized : 4
|
||||
}
|
||||
|
||||
export async function setAiGroupSummaryIntervalHours(hours: number): Promise<void> {
|
||||
const normalized = Math.floor(Number(hours) || 4)
|
||||
await config.set(CONFIG_KEYS.AI_GROUP_SUMMARY_INTERVAL_HOURS, AI_GROUP_SUMMARY_INTERVALS.has(normalized) ? normalized : 4)
|
||||
}
|
||||
|
||||
export async function getAiGroupSummarySystemPrompt(): Promise<string> {
|
||||
const value = await config.get(CONFIG_KEYS.AI_GROUP_SUMMARY_SYSTEM_PROMPT)
|
||||
return typeof value === 'string' ? value : ''
|
||||
}
|
||||
|
||||
export async function setAiGroupSummarySystemPrompt(prompt: string): Promise<void> {
|
||||
await config.set(CONFIG_KEYS.AI_GROUP_SUMMARY_SYSTEM_PROMPT, prompt)
|
||||
}
|
||||
|
||||
export async function getAiGroupSummaryFilterMode(): Promise<AiGroupSummaryFilterMode> {
|
||||
const value = await config.get(CONFIG_KEYS.AI_GROUP_SUMMARY_FILTER_MODE)
|
||||
return value === 'blacklist' ? 'blacklist' : 'whitelist'
|
||||
}
|
||||
|
||||
export async function setAiGroupSummaryFilterMode(mode: AiGroupSummaryFilterMode): Promise<void> {
|
||||
await config.set(CONFIG_KEYS.AI_GROUP_SUMMARY_FILTER_MODE, mode === 'blacklist' ? 'blacklist' : 'whitelist')
|
||||
}
|
||||
|
||||
export async function getAiGroupSummaryFilterList(): Promise<string[]> {
|
||||
const value = await config.get(CONFIG_KEYS.AI_GROUP_SUMMARY_FILTER_LIST)
|
||||
return normalizeAiGroupSummaryFilterList(value)
|
||||
}
|
||||
|
||||
export async function setAiGroupSummaryFilterList(list: string[]): Promise<void> {
|
||||
await config.set(CONFIG_KEYS.AI_GROUP_SUMMARY_FILTER_LIST, normalizeAiGroupSummaryFilterList(list))
|
||||
}
|
||||
|
||||
export async function getAiMessageInsightEnabled(): Promise<boolean> {
|
||||
const value = await config.get(CONFIG_KEYS.AI_MESSAGE_INSIGHT_ENABLED)
|
||||
return value === true
|
||||
|
||||
89
src/types/electron.d.ts
vendored
89
src/types/electron.d.ts
vendored
@@ -124,6 +124,78 @@ export interface InsightRecordResult {
|
||||
error?: string
|
||||
}
|
||||
|
||||
export type GroupSummaryTriggerType = 'auto' | 'manual'
|
||||
|
||||
export interface GroupSummaryTopic {
|
||||
title: string
|
||||
participants: string[]
|
||||
keyPoints: string[]
|
||||
conclusion: string
|
||||
}
|
||||
|
||||
export interface GroupSummaryLog {
|
||||
endpoint: string
|
||||
model: string
|
||||
temperature: number
|
||||
triggerType: GroupSummaryTriggerType
|
||||
periodStart: number
|
||||
periodEnd: number
|
||||
messageCount: number
|
||||
readableMessageCount: number
|
||||
systemPrompt: string
|
||||
userPrompt: string
|
||||
rawOutput: string
|
||||
finalSummary: string
|
||||
durationMs: number
|
||||
createdAt: number
|
||||
responseFormatJson?: boolean
|
||||
responseFormatFallback?: boolean
|
||||
responseFormatFallbackReason?: string
|
||||
parsedTopics?: GroupSummaryTopic[]
|
||||
}
|
||||
|
||||
export interface GroupSummaryRecordSummary {
|
||||
id: string
|
||||
createdAt: number
|
||||
sessionId: string
|
||||
displayName: string
|
||||
avatarUrl?: string
|
||||
triggerType: GroupSummaryTriggerType
|
||||
periodStart: number
|
||||
periodEnd: number
|
||||
messageCount: number
|
||||
readableMessageCount: number
|
||||
topics: GroupSummaryTopic[]
|
||||
summaryText: string
|
||||
}
|
||||
|
||||
export interface GroupSummaryRecord extends GroupSummaryRecordSummary {
|
||||
accountScope: string
|
||||
rawOutput: string
|
||||
log: GroupSummaryLog
|
||||
}
|
||||
|
||||
export interface GroupSummaryRecordFilters {
|
||||
sessionId?: string
|
||||
startTime?: number
|
||||
endTime?: number
|
||||
limit?: number
|
||||
offset?: number
|
||||
}
|
||||
|
||||
export interface GroupSummaryRecordListResult {
|
||||
success: boolean
|
||||
records: GroupSummaryRecordSummary[]
|
||||
total: number
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface GroupSummaryRecordResult {
|
||||
success: boolean
|
||||
record?: GroupSummaryRecord
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface BackupProgress {
|
||||
phase: 'preparing' | 'scanning' | 'exporting' | 'packing' | 'inspecting' | 'restoring' | 'done' | 'failed'
|
||||
message: string
|
||||
@@ -1375,6 +1447,23 @@ export interface ElectronAPI {
|
||||
forceRefresh?: boolean
|
||||
}) => Promise<{ success: boolean; message: string; cached?: boolean; recordId?: string; data?: MessageInsightAnalysis }>
|
||||
}
|
||||
groupSummary: {
|
||||
listRecords: (filters?: GroupSummaryRecordFilters) => Promise<GroupSummaryRecordListResult>
|
||||
getRecord: (id: string) => Promise<GroupSummaryRecordResult>
|
||||
triggerManual: (payload: {
|
||||
sessionId: string
|
||||
displayName?: string
|
||||
avatarUrl?: string
|
||||
startTime: number
|
||||
endTime: number
|
||||
}) => Promise<{ success: boolean; message: string; recordId?: string; record?: GroupSummaryRecordSummary; skipped?: boolean; skippedReason?: string }>
|
||||
triggerDay: (payload: {
|
||||
sessionId: string
|
||||
displayName?: string
|
||||
avatarUrl?: string
|
||||
date: string
|
||||
}) => Promise<{ success: boolean; message: string; generated: number; skipped: number; records: GroupSummaryRecordSummary[] }>
|
||||
}
|
||||
}
|
||||
|
||||
export interface ExportOptions {
|
||||
|
||||
Reference in New Issue
Block a user