mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-05-28 15:11:25 +00:00
fix: Group Chat Summary
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>
|
||||
|
||||
@@ -3595,7 +3595,8 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
input {
|
||||
input,
|
||||
.group-summary-date-trigger {
|
||||
height: 32px;
|
||||
min-width: 0;
|
||||
border: 1px solid var(--border-color);
|
||||
@@ -3607,6 +3608,40 @@
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
@@ -3656,23 +3691,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.group-summary-custom-range {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 6px;
|
||||
|
||||
input {
|
||||
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-generate-btn {
|
||||
height: 34px;
|
||||
border: none;
|
||||
|
||||
@@ -69,7 +69,7 @@ interface QuotedMessageJumpTarget {
|
||||
|
||||
type GlobalMsgSearchPhase = 'idle' | 'seed' | 'backfill' | 'done'
|
||||
type GlobalMsgSearchResult = Message & { sessionId: string }
|
||||
type GroupSummaryRangeMode = 1 | 2 | 4 | 8 | 12 | 24 | 'custom'
|
||||
type GroupSummaryRangeMode = 1 | 2 | 4 | 8 | 12 | 24
|
||||
|
||||
interface GlobalMsgPrefixCacheEntry {
|
||||
keyword: string
|
||||
@@ -80,7 +80,6 @@ interface GlobalMsgPrefixCacheEntry {
|
||||
|
||||
|
||||
const GLOBAL_MSG_PER_SESSION_LIMIT = 10
|
||||
const GROUP_SUMMARY_MAX_RANGE_HOURS = 48
|
||||
const GLOBAL_MSG_SEED_LIMIT = 120
|
||||
const GLOBAL_MSG_BACKFILL_CONCURRENCY = 3
|
||||
const GLOBAL_MSG_LEGACY_CONCURRENCY = 6
|
||||
@@ -772,21 +771,6 @@ function formatDateInputLocal(date: Date): string {
|
||||
return `${y}-${m}-${day}`
|
||||
}
|
||||
|
||||
function formatDateTimeLocal(date: Date): string {
|
||||
const y = date.getFullYear()
|
||||
const m = `${date.getMonth() + 1}`.padStart(2, '0')
|
||||
const day = `${date.getDate()}`.padStart(2, '0')
|
||||
const h = `${date.getHours()}`.padStart(2, '0')
|
||||
const min = `${date.getMinutes()}`.padStart(2, '0')
|
||||
return `${y}-${m}-${day}T${h}:${min}`
|
||||
}
|
||||
|
||||
function parseDateTimeLocalSeconds(value: string): number {
|
||||
const parsed = new Date(value)
|
||||
const time = parsed.getTime()
|
||||
return Number.isFinite(time) ? Math.floor(time / 1000) : 0
|
||||
}
|
||||
|
||||
function formatSummaryPeriod(start: number, end: number): string {
|
||||
return `${formatYmdHmDateTime(start * 1000)} - ${formatYmdHmDateTime(end * 1000)}`
|
||||
}
|
||||
@@ -1505,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)
|
||||
@@ -1574,8 +1559,7 @@ function ChatPage(props: ChatPageProps) {
|
||||
const [groupSummaryError, setGroupSummaryError] = useState<string | null>(null)
|
||||
const [groupSummaryDateFilter, setGroupSummaryDateFilter] = useState(() => formatDateInputLocal(new Date()))
|
||||
const [groupSummaryRangeMode, setGroupSummaryRangeMode] = useState<GroupSummaryRangeMode>(4)
|
||||
const [groupSummaryCustomStart, setGroupSummaryCustomStart] = useState(() => formatDateTimeLocal(new Date(Date.now() - 4 * 60 * 60 * 1000)))
|
||||
const [groupSummaryCustomEnd, setGroupSummaryCustomEnd] = useState(() => formatDateTimeLocal(new Date()))
|
||||
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)
|
||||
@@ -1903,25 +1887,29 @@ function ChatPage(props: ChatPageProps) {
|
||||
})
|
||||
}, [])
|
||||
|
||||
const getGroupSummaryDateRangeMs = useCallback(() => {
|
||||
const date = groupSummaryDateFilter || formatDateInputLocal(new Date())
|
||||
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: fallback.getTime(), endTime: fallbackEnd.getTime() }
|
||||
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: start.getTime(), endTime: end.getTime() }
|
||||
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 } = getGroupSummaryDateRangeMs()
|
||||
const { startTime, endTime } = getGroupSummaryDateRangeSeconds()
|
||||
setGroupSummaryLoading(true)
|
||||
setGroupSummaryError(null)
|
||||
try {
|
||||
@@ -1950,60 +1938,65 @@ function ChatPage(props: ChatPageProps) {
|
||||
setGroupSummaryLoading(false)
|
||||
}
|
||||
}
|
||||
}, [getGroupSummaryDateRangeMs])
|
||||
}, [getGroupSummaryDateRangeSeconds])
|
||||
|
||||
const resolveGroupSummaryManualRange = useCallback(() => {
|
||||
const resolveTodayGroupSummaryManualRange = useCallback(() => {
|
||||
const nowSeconds = Math.floor(Date.now() / 1000)
|
||||
if (groupSummaryRangeMode !== 'custom') {
|
||||
const hours = Number(groupSummaryRangeMode)
|
||||
return { startTime: nowSeconds - hours * 60 * 60, endTime: nowSeconds }
|
||||
}
|
||||
return {
|
||||
startTime: parseDateTimeLocalSeconds(groupSummaryCustomStart),
|
||||
endTime: parseDateTimeLocalSeconds(groupSummaryCustomEnd)
|
||||
}
|
||||
}, [groupSummaryCustomEnd, groupSummaryCustomStart, groupSummaryRangeMode])
|
||||
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 { startTime, endTime } = resolveGroupSummaryManualRange()
|
||||
if (startTime <= 0 || endTime <= startTime) {
|
||||
setGroupSummaryHint({ success: false, message: '请选择有效的总结时段' })
|
||||
return
|
||||
}
|
||||
if (endTime - startTime > GROUP_SUMMARY_MAX_RANGE_HOURS * 60 * 60) {
|
||||
setGroupSummaryHint({ success: false, message: '手动总结时段不能超过 48 小时' })
|
||||
return
|
||||
}
|
||||
const selectedDate = groupSummaryDateFilter || formatDateInputLocal(new Date())
|
||||
const today = formatDateInputLocal(new Date())
|
||||
|
||||
setIsTriggeringGroupSummary(true)
|
||||
setGroupSummaryHint({ success: true, message: '正在生成群聊总结...' })
|
||||
try {
|
||||
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) {
|
||||
const dateForFilter = formatDateInputLocal(new Date(startTime * 1000))
|
||||
setGroupSummaryDateFilter(dateForFilter)
|
||||
await loadGroupSummaryRecords(sessionId)
|
||||
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 {
|
||||
setGroupSummaryHint({ success: false, message: result.message || '群聊总结生成失败' })
|
||||
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, loadGroupSummaryRecords, resolveGroupSummaryManualRange])
|
||||
}, [currentSessionId, groupSummaryDateFilter, loadGroupSummaryRecords, resolveTodayGroupSummaryManualRange])
|
||||
|
||||
const openGroupSummaryLog = useCallback(async (recordId: string) => {
|
||||
try {
|
||||
@@ -5698,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 = () => {
|
||||
@@ -7917,11 +7924,24 @@ function ChatPage(props: ChatPageProps) {
|
||||
<div className="group-summary-controls">
|
||||
<div className="group-summary-date-row">
|
||||
<label>日期</label>
|
||||
<input
|
||||
type="date"
|
||||
value={groupSummaryDateFilter}
|
||||
onChange={(event) => setGroupSummaryDateFilter(event.target.value || formatDateInputLocal(new Date()))}
|
||||
/>
|
||||
<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"
|
||||
@@ -7932,39 +7952,21 @@ function ChatPage(props: ChatPageProps) {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
className={groupSummaryRangeMode === 'custom' ? 'active' : ''}
|
||||
onClick={() => setGroupSummaryRangeMode('custom')}
|
||||
>
|
||||
自定义
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{groupSummaryRangeMode === 'custom' && (
|
||||
<div className="group-summary-custom-range">
|
||||
<input
|
||||
type="datetime-local"
|
||||
value={groupSummaryCustomStart}
|
||||
onChange={(event) => setGroupSummaryCustomStart(event.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="datetime-local"
|
||||
value={groupSummaryCustomEnd}
|
||||
onChange={(event) => setGroupSummaryCustomEnd(event.target.value)}
|
||||
/>
|
||||
{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
|
||||
@@ -7976,7 +7978,9 @@ function ChatPage(props: ChatPageProps) {
|
||||
{isTriggeringGroupSummary ? <Loader2 size={14} className="spin" /> : <Sparkles size={14} />}
|
||||
<span>生成总结</span>
|
||||
</button>
|
||||
<div className="group-summary-rule-hint">少于 5 条可总结消息会自动跳过。</div>
|
||||
<div className="group-summary-rule-hint">
|
||||
{isGroupSummaryToday ? '少于 5 条可总结消息会自动跳过。' : '每个切片少于 5 条可总结消息会自动跳过。'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="group-summary-list">
|
||||
|
||||
@@ -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,
|
||||
@@ -70,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
|
||||
@@ -334,9 +336,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
||||
const [aiGroupSummaryEnabled, setAiGroupSummaryEnabled] = useState(false)
|
||||
const [aiGroupSummaryIntervalHours, setAiGroupSummaryIntervalHours] = useState(4)
|
||||
const [aiGroupSummarySystemPrompt, setAiGroupSummarySystemPrompt] = useState('')
|
||||
const [aiGroupSummaryFilterMode, setAiGroupSummaryFilterMode] = useState<configService.AiGroupSummaryFilterMode>('whitelist')
|
||||
const [aiGroupSummaryFilterList, setAiGroupSummaryFilterList] = useState<string[]>([])
|
||||
const [aiGroupSummaryFilterDropdownOpen, setAiGroupSummaryFilterDropdownOpen] = useState(false)
|
||||
const [aiGroupSummaryFilterSearchKeyword, setAiGroupSummaryFilterSearchKeyword] = useState('')
|
||||
const [aiMessageInsightEnabled, setAiMessageInsightEnabled] = useState(false)
|
||||
const [aiMessageInsightContextCount, setAiMessageInsightContextCount] = useState(50)
|
||||
@@ -607,7 +607,6 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
||||
const savedAiGroupSummaryEnabled = await configService.getAiGroupSummaryEnabled()
|
||||
const savedAiGroupSummaryIntervalHours = await configService.getAiGroupSummaryIntervalHours()
|
||||
const savedAiGroupSummarySystemPrompt = await configService.getAiGroupSummarySystemPrompt()
|
||||
const savedAiGroupSummaryFilterMode = await configService.getAiGroupSummaryFilterMode()
|
||||
const savedAiGroupSummaryFilterList = await configService.getAiGroupSummaryFilterList()
|
||||
const savedAiMessageInsightEnabled = await configService.getAiMessageInsightEnabled()
|
||||
const savedAiMessageInsightContextCount = await configService.getAiMessageInsightContextCount()
|
||||
@@ -641,7 +640,6 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
||||
setAiGroupSummaryEnabled(savedAiGroupSummaryEnabled)
|
||||
setAiGroupSummaryIntervalHours(savedAiGroupSummaryIntervalHours)
|
||||
setAiGroupSummarySystemPrompt(savedAiGroupSummarySystemPrompt)
|
||||
setAiGroupSummaryFilterMode(savedAiGroupSummaryFilterMode)
|
||||
setAiGroupSummaryFilterList(savedAiGroupSummaryFilterList)
|
||||
setAiMessageInsightEnabled(savedAiMessageInsightEnabled)
|
||||
setAiMessageInsightContextCount(savedAiMessageInsightContextCount)
|
||||
@@ -4061,19 +4059,21 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
||||
)
|
||||
|
||||
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)
|
||||
showMessage('已添加到群聊总结作用域', true)
|
||||
}
|
||||
|
||||
const removeFromFilterList = async (username: string) => {
|
||||
const next = aiGroupSummaryFilterList.filter((item) => item !== username)
|
||||
setAiGroupSummaryFilterList(next)
|
||||
await configService.setAiGroupSummaryFilterList(next)
|
||||
showMessage('已从群聊总结名单移除', true)
|
||||
showMessage('已从群聊总结作用域移除', true)
|
||||
}
|
||||
|
||||
const addAllFiltered = async () => {
|
||||
@@ -4089,7 +4089,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
||||
if (aiGroupSummaryFilterList.length === 0) return
|
||||
setAiGroupSummaryFilterList([])
|
||||
await configService.setAiGroupSummaryFilterList([])
|
||||
showMessage('已清空群聊总结名单', true)
|
||||
showMessage('已清空群聊总结作用域', true)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -4097,7 +4097,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
||||
<div className="form-group">
|
||||
<label>AI 群聊总结</label>
|
||||
<span className="form-hint">
|
||||
开启后,群聊页顶部会显示 AI 总结按钮;自动总结只对下面黑白名单命中的群聊生效。默认白名单且不选择任何群聊,不会自动消耗 token。
|
||||
开启后,群聊页顶部会显示 AI 总结按钮;自动总结只对下面作用域内的群聊生效。未选择任何群聊时不会自动消耗 token。
|
||||
</span>
|
||||
<div className="log-toggle-line">
|
||||
<span className="log-status">{aiGroupSummaryEnabled ? '已开启' : '已关闭'}</span>
|
||||
@@ -4143,81 +4143,49 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
||||
|
||||
<div className="form-group">
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12 }}>
|
||||
<label style={{ marginBottom: 0 }}>自定义提示词</label>
|
||||
{aiGroupSummarySystemPrompt && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary btn-sm"
|
||||
onClick={async () => {
|
||||
setAiGroupSummarySystemPrompt('')
|
||||
await configService.setAiGroupSummarySystemPrompt('')
|
||||
}}
|
||||
>
|
||||
恢复默认
|
||||
</button>
|
||||
)}
|
||||
<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"
|
||||
rows={5}
|
||||
className="field-input ai-prompt-textarea"
|
||||
rows={10}
|
||||
style={{ width: '100%', resize: 'vertical', marginTop: 8 }}
|
||||
value={aiGroupSummarySystemPrompt}
|
||||
placeholder="例如:优先识别待办事项和负责人,忽略纯闲聊。"
|
||||
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>
|
||||
<label>自动总结作用域群聊</label>
|
||||
<span className="form-hint">
|
||||
仅控制自动总结范围。手动点击群聊页 AI 总结按钮不受名单限制;白名单为空时自动总结不会运行。
|
||||
仅控制自动总结范围。手动点击群聊页 AI 总结按钮不受作用域限制;未选择任何群聊时自动总结不会运行。
|
||||
</span>
|
||||
<div className="custom-select" style={{ maxWidth: 240, marginTop: 8 }}>
|
||||
<div
|
||||
className={`custom-select-trigger ${aiGroupSummaryFilterDropdownOpen ? 'open' : ''}`}
|
||||
onClick={() => setAiGroupSummaryFilterDropdownOpen(!aiGroupSummaryFilterDropdownOpen)}
|
||||
>
|
||||
<span className="custom-select-value">
|
||||
{aiGroupSummaryFilterMode === 'whitelist' ? '白名单模式' : '黑名单模式'}
|
||||
</span>
|
||||
<ChevronDown size={14} className={`custom-select-arrow ${aiGroupSummaryFilterDropdownOpen ? 'rotate' : ''}`} />
|
||||
</div>
|
||||
<div className={`custom-select-dropdown ${aiGroupSummaryFilterDropdownOpen ? 'open' : ''}`}>
|
||||
{[
|
||||
{ value: 'whitelist', label: '白名单模式' },
|
||||
{ value: 'blacklist', label: '黑名单模式' }
|
||||
].map(option => (
|
||||
<div
|
||||
key={option.value}
|
||||
className={`custom-select-option ${aiGroupSummaryFilterMode === option.value ? 'selected' : ''}`}
|
||||
onClick={async () => {
|
||||
const mode = option.value as configService.AiGroupSummaryFilterMode
|
||||
setAiGroupSummaryFilterMode(mode)
|
||||
setAiGroupSummaryFilterDropdownOpen(false)
|
||||
await configService.setAiGroupSummaryFilterMode(mode)
|
||||
showMessage(mode === 'whitelist' ? '已切换为白名单模式' : '已切换为黑名单模式', true)
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
{aiGroupSummaryFilterMode === option.value && <Check size={14} />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{aiGroupSummaryFilterMode === 'whitelist' && aiGroupSummaryFilterList.length === 0 && (
|
||||
{aiGroupSummaryFilterList.length === 0 && (
|
||||
<div className="api-docs" style={{ marginTop: 12 }}>
|
||||
<div className="api-item">
|
||||
<p className="api-desc">当前为白名单空列表,自动群聊总结不会触发。</p>
|
||||
<p className="api-desc">当前未选择作用域群聊,自动群聊总结不会触发。</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -4265,7 +4233,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
||||
|
||||
<div className="filter-panel">
|
||||
<div className="filter-panel-header">
|
||||
<span>{aiGroupSummaryFilterMode === 'whitelist' ? '白名单' : '黑名单'}</span>
|
||||
<span>作用域群聊</span>
|
||||
{aiGroupSummaryFilterList.length > 0 && (
|
||||
<span className="filter-panel-count">{aiGroupSummaryFilterList.length}</span>
|
||||
)}
|
||||
|
||||
@@ -2183,6 +2183,7 @@ 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])
|
||||
|
||||
8
src/types/electron.d.ts
vendored
8
src/types/electron.d.ts
vendored
@@ -1456,7 +1456,13 @@ export interface ElectronAPI {
|
||||
avatarUrl?: string
|
||||
startTime: number
|
||||
endTime: number
|
||||
}) => Promise<{ success: boolean; message: string; recordId?: string; record?: GroupSummaryRecord; skipped?: boolean; skippedReason?: string }>
|
||||
}) => 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[] }>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user