From 1c8bbae1f0a41abddd8e3089950299894b1c9ad8 Mon Sep 17 00:00:00 2001 From: Wxw-Gu Date: Tue, 1 Sep 2026 11:40:14 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=A2=9E=E5=8A=A0thinking=20disabled=20?= =?UTF-8?q?=E4=BF=AE=E6=94=B9=E9=80=80=E7=BE=A4=E7=9B=91=E6=8E=A7=E6=A8=A1?= =?UTF-8?q?=E6=9D=BF=20=E5=AE=9A=E6=97=B6=E6=97=A5=E6=8A=A5=E4=BF=9D?= =?UTF-8?q?=E5=AD=98=E6=97=A5=E6=8A=A5=E5=8E=86=E5=8F=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/report-history-service.ts | 2 +- .../services/agent-group-report-service.ts | 43 ++++++++++++++++- src/main/services/ai-provider-service.ts | 27 +++++++++-- src/main/services/scheduled-report-service.ts | 47 +++++++++++++++++++ src/renderer/src/App.tsx | 14 ++++++ .../reports/ReportHistorySidebar.tsx | 15 +++--- .../GroupExitMonitorWorkspace.tsx | 3 +- .../settings/ai-model/AIProviderEditor.tsx | 23 +++++++++ .../src/features/settings/ai-model/presets.ts | 1 + src/renderer/src/utils/group-report.ts | 29 ++++++++---- src/shared/ai-provider.ts | 2 + src/shared/group-exit-monitor.ts | 30 ++++++++++-- src/shared/report-history.ts | 2 + tests/component/ai-model-settings.test.tsx | 1 + tests/e2e/support/electron-main.cjs | 21 ++++++--- tests/unit/ai-provider-vision-routing.test.ts | 38 +++++++++++++++ tests/unit/group-exit-monitor-service.test.ts | 14 +++++- tests/unit/group-exit-monitor.test.ts | 5 +- tests/unit/group-report.test.ts | 22 +++++++++ tests/unit/report-history-service.test.ts | 2 + tests/unit/scheduled-report-service.test.ts | 36 ++++++++++++++ 21 files changed, 343 insertions(+), 34 deletions(-) diff --git a/src/main/report-history-service.ts b/src/main/report-history-service.ts index 4835f3e..411c9c4 100644 --- a/src/main/report-history-service.ts +++ b/src/main/report-history-service.ts @@ -174,7 +174,7 @@ export async function saveGeneratedReport( dateRange: request.dateRange, messageCount: request.messageCount, generatedAt: timestamp.toISOString(), - reportDate: `${year}-${month}-${pad2(timestamp.getDate())}`, + reportDate: request.reportDate?.trim() || `${year}-${month}-${pad2(timestamp.getDate())}`, htmlPath: savedHtmlPath, pngPath: savedPngPath, jsonPath, diff --git a/src/main/services/agent-group-report-service.ts b/src/main/services/agent-group-report-service.ts index 5416d02..b9142b5 100644 --- a/src/main/services/agent-group-report-service.ts +++ b/src/main/services/agent-group-report-service.ts @@ -1,4 +1,5 @@ import type { Contact, Message } from '../../shared/types' +import type { GroupDailyReport, GroupReportMetadata } from '../../shared/group-report' import { exportGroupReport } from '../group-report-service' import { getGroupSnapshot, listMessages, resolveMd5 } from './chat-service' import { AIProviderService } from './ai-provider-service' @@ -32,14 +33,26 @@ export interface AgentGroupReportRequest { export interface AgentGroupReportResult { success: boolean groupName?: string + htmlPath?: string pngPath?: string messageCount?: number + reportSnapshot?: GroupDailyReport + reportMetadata?: GroupReportMetadata + modelName?: string + tokenUsage?: { + input?: number + output?: number + total?: number + estimated?: boolean + } + duration?: number error?: string } export async function generateAgentGroupReport( request: AgentGroupReportRequest ): Promise { + const startedAt = Date.now() const query = String(request.group || '') .trim() .replace(/群聊?$/, '') @@ -54,7 +67,10 @@ export async function generateAgentGroupReport( const range = request.range === 'yesterday' || request.range === '7days' ? request.range : 'today' const { startTime, endTime } = request.range === 'recent24h' - ? { startTime: Math.floor(Date.now() / 1000) - 86400, endTime: Math.floor(Date.now() / 1000) } + ? (() => { + const now = Math.floor(Date.now() / 1000) + return { startTime: now - 86400, endTime: now } + })() : getSummaryDateRange(range) let messages = listMessages(contact.md5, startTime, endTime) as Message[] if (!messages.length) return { success: false, error: '所选时间范围没有可总结的消息' } @@ -98,6 +114,7 @@ export async function generateAgentGroupReport( } const input = await buildGroupReportInput(messages, contact as Contact, true, 'full') + const runtime = aiProvider.getRuntimeConfig() const ai = await aiProvider.chat( [ { role: 'system', content: GROUP_REPORT_SYSTEM_PROMPT }, @@ -106,6 +123,7 @@ export async function generateAgentGroupReport( { timeoutMs: Math.max(30, Math.min(1800, request.timeoutSeconds || 300)) * 1000 } ) if (!ai.success || !ai.data) return { success: false, error: ai.error || 'AI 总结失败' } + let tokenUsage = ai.usage const parseReport = (raw: string): ReturnType => parseGroupDailyReport( raw, @@ -133,6 +151,7 @@ export async function generateAgentGroupReport( error: `${repaired.error || 'AI 修复日报 JSON 失败'}(原始错误:${cause})` } } + tokenUsage = mergeTokenUsage(tokenUsage, repaired.usage) try { report = parseReport(repaired.data) } catch (repairError) { @@ -153,7 +172,27 @@ export async function generateAgentGroupReport( return { success: true, groupName: input.metadata.groupName, + htmlPath: exported.htmlPath, pngPath: exported.pngPath, - messageCount: messages.length + messageCount: messages.length, + reportSnapshot: report, + reportMetadata: input.metadata, + modelName: runtime.modelName || runtime.model, + tokenUsage, + duration: Date.now() - startedAt + } +} + +function mergeTokenUsage( + first: AgentGroupReportResult['tokenUsage'], + second: AgentGroupReportResult['tokenUsage'] +): AgentGroupReportResult['tokenUsage'] { + if (!first) return second + if (!second) return first + return { + input: (first.input || 0) + (second.input || 0), + output: (first.output || 0) + (second.output || 0), + total: (first.total || 0) + (second.total || 0), + estimated: Boolean(first.estimated || second.estimated) } } diff --git a/src/main/services/ai-provider-service.ts b/src/main/services/ai-provider-service.ts index 4c7201e..853dcb6 100644 --- a/src/main/services/ai-provider-service.ts +++ b/src/main/services/ai-provider-service.ts @@ -32,7 +32,13 @@ type AIRequestResult = { } interface OpenAIResponsePayload { error?: { message?: string } - choices?: Array<{ message?: { content?: string }; finish_reason?: string }> + choices?: Array<{ + message?: { + content?: string | Array<{ type?: string; text?: string }> | null + reasoning_content?: string + } + finish_reason?: string + }> usage?: { prompt_tokens?: number; completion_tokens?: number; total_tokens?: number } } interface OpenAIStreamPayload { @@ -263,6 +269,7 @@ export class AIProviderService { ): Promise<{ success: boolean data?: string + finishReason?: string usage?: { input?: number; output?: number; total?: number; estimated?: boolean } error?: string }> { @@ -352,6 +359,7 @@ export class AIProviderService { onDelta?: AIChatDeltaHandler ): Promise<{ data: string + finishReason?: string usage?: { input?: number; output?: number; total?: number; estimated?: boolean } }> { if (options?.apiKey) return this.requestLegacy(messages, options, signal, onDelta) @@ -634,6 +642,18 @@ function toOpenAIMessages(messages: AIMessage[]): Array<{ role: string; content: })) } +function openAIMessageText(content: unknown): string { + if (typeof content === 'string') return content + if (!Array.isArray(content)) return '' + return content + .map((part) => { + if (!part || typeof part !== 'object') return '' + const text = (part as { text?: unknown }).text + return typeof text === 'string' ? text : '' + }) + .join('') +} + function toOpenAIResponsesRequest(messages: AIMessage[]): { instructions?: string input: Array<{ role: string; content: unknown[] }> @@ -723,6 +743,7 @@ async function requestOpenAICompatible( messages: toOpenAIMessages(messages), temperature: provider.advanced.temperature, max_tokens: testing ? 8 : modelMaxTokens(provider, model), + ...(provider.advanced.thinking === 'disabled' ? { thinking: { type: 'disabled' } } : {}), ...(provider.advanced.stream ? { stream: true } : {}) }) }, @@ -741,7 +762,7 @@ async function requestOpenAICompatible( } const payload = await parseJsonResponse(response) return { - data: String(payload.choices?.[0]?.message?.content || ''), + data: openAIMessageText(payload.choices?.[0]?.message?.content), finishReason: String(payload.choices?.[0]?.finish_reason || 'unknown'), usage: toOpenAIUsage(payload.usage) } @@ -935,7 +956,7 @@ async function parseOpenAIStream( throw new Error('模型服务返回了无法解析的流式数据') } if (payload.error?.message) throw new Error(payload.error.message) - const content = String(payload.choices?.[0]?.message?.content || '') + const content = openAIMessageText(payload.choices?.[0]?.message?.content) if (content) onDelta?.(content) return { data: content, diff --git a/src/main/services/scheduled-report-service.ts b/src/main/services/scheduled-report-service.ts index 56f2425..7c61e17 100644 --- a/src/main/services/scheduled-report-service.ts +++ b/src/main/services/scheduled-report-service.ts @@ -16,6 +16,8 @@ import type { ScheduledReportTask, ScheduledReportUpdateInput } from '../../shared/scheduled-report' +import type { SaveGeneratedReportRequest } from '../../shared/report-history' +import { saveGeneratedReport } from '../report-history-service' import { generateAgentGroupReport } from './agent-group-report-service' import { personalWechatSendService } from './personal-wechat-send-service' import { personalWechatCapabilityService } from './personal-wechat-capability-service' @@ -29,6 +31,9 @@ const TICK_MS = 15_000 export interface ScheduledReportDependencies { getCapability: () => Promise generateReport: typeof generateAgentGroupReport + saveGeneratedReport: ( + request: SaveGeneratedReportRequest + ) => ReturnType send: (request: PersonalWechatSendRequest) => ReturnType storageDir: string isDatabaseReady: () => boolean @@ -38,6 +43,7 @@ export interface ScheduledReportDependencies { const defaultDependencies = (): ScheduledReportDependencies => ({ getCapability: () => personalWechatCapabilityService.getPersonalWechatSendCapability(), generateReport: generateAgentGroupReport, + saveGeneratedReport, send: (request) => personalWechatSendService.send(request), storageDir: path.join(app.getPath('userData'), STORAGE_DIR), isDatabaseReady: () => isChatReady() @@ -45,6 +51,15 @@ const defaultDependencies = (): ScheduledReportDependencies => ({ const rangeValues = new Set(['today', 'yesterday', '7days', 'recent24h']) +const reportRangeLabel = (range: ScheduledReportRange): string => + range === 'recent24h' + ? '最近 24 小时' + : range === '7days' + ? '近 7 天' + : range === 'today' + ? '今天' + : '昨日' + export function validateScheduleTime(value: string): boolean { return /^(?:[01]\d|2[0-3]):[0-5]\d$/.test(String(value || '').trim()) } @@ -309,6 +324,38 @@ export class ScheduledReportService { if (!generated.success || !generated.pngPath) { return finish('failed', `report_generation_failed:${generated.error || '日报生成失败'}`) } + const reportContact = resolveMd5(task.group) + let savedHistory: Awaited> + try { + savedHistory = await this.deps.saveGeneratedReport({ + contactId: reportContact?.md5 || task.group, + contactName: generated.groupName || reportContact?.m_nsNickName || task.group, + contactAvatar: reportContact?.avatar, + dateRange: generated.reportMetadata?.dateRange || reportRangeLabel(task.reportRange), + reportDate: generated.reportMetadata?.reportDate, + messageCount: generated.messageCount || generated.reportMetadata?.messageCount || 0, + generatedAt: new Date().toISOString(), + htmlPath: generated.htmlPath, + pngPath: generated.pngPath, + duration: generated.duration, + modelName: generated.modelName, + tokenUsage: generated.tokenUsage, + reportSnapshot: generated.reportSnapshot, + reportMetadata: generated.reportMetadata, + templateId: task.templateId + }) + } catch (error) { + return finish( + 'failed', + `report_history_save_failed:${error instanceof Error ? error.message : String(error)}` + ) + } + if (!savedHistory.success) { + return finish( + 'failed', + `report_history_save_failed:${savedHistory.error || '日报历史保存失败'}` + ) + } const target = this.resolveTarget(task) if (!target) return finish('failed', 'wechat_send_failed:未找到指定微信群') let sent: Awaited> diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 0b42a0d..5f7844b 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -437,6 +437,7 @@ function App(): React.ReactElement { modelConfig: reportTextModelConfig, visionModelConfig: aiVisionModelConfig }) + const reportHistoryRequestRef = React.useRef(0) const lastCapturedReportKeyRef = React.useRef('') React.useEffect(() => { @@ -531,8 +532,10 @@ function App(): React.ReactElement { } const loadGeneratedReports = React.useCallback(async (): Promise => { + const requestId = ++reportHistoryRequestRef.current try { const result = await window.api.listGeneratedReports() + if (requestId !== reportHistoryRequestRef.current) return if (!result.success) { setReportNotice(result.error || '日报历史加载失败') return @@ -543,6 +546,7 @@ function App(): React.ReactElement { current && reports.some((report) => report.id === current) ? current : null ) } catch (error) { + if (requestId !== reportHistoryRequestRef.current) return setReportNotice(error instanceof Error ? error.message : String(error)) } }, []) @@ -817,6 +821,15 @@ function App(): React.ReactElement { void loadGeneratedReports() }, [isAuthenticated, loadGeneratedReports]) + React.useEffect(() => { + if (!isAuthenticated || activePage !== 'report') return + void loadGeneratedReports() + const refreshTimer = window.setInterval(() => { + void loadGeneratedReports() + }, 15_000) + return () => window.clearInterval(refreshTimer) + }, [activePage, isAuthenticated, loadGeneratedReports, reportSection, reportWorkspaceView]) + const handleLogin = async (keyInput?: string, accountRootInput?: string): Promise => { const keyToUse = keyInput || dbKey let accountRoot = accountRootInput || selectedAccount?.accountRoot @@ -1597,6 +1610,7 @@ function App(): React.ReactElement { : summaryDateRange === '7days' ? '近 7 天' : '今天', + reportDate: reportMetadata.reportDate, messageCount: reportGeneration.reportMessages.length, generatedAt: new Date().toISOString(), generatedImage: reportGeneration.generatedImage || undefined, diff --git a/src/renderer/src/components/reports/ReportHistorySidebar.tsx b/src/renderer/src/components/reports/ReportHistorySidebar.tsx index ba90d68..157ee68 100644 --- a/src/renderer/src/components/reports/ReportHistorySidebar.tsx +++ b/src/renderer/src/components/reports/ReportHistorySidebar.tsx @@ -37,8 +37,6 @@ interface ReportGroup { reports: GeneratedReportRecord[] } -const DAY_MS = 86400000 - const dateKey = (value: string): number => { const parsed = new Date(value).getTime() return Number.isFinite(parsed) ? parsed : 0 @@ -62,12 +60,15 @@ const groupLabelFor = (value: string): string => { if (!Number.isFinite(date.getTime())) return '更早' const now = new Date() - const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime() - const startOfYesterday = startOfToday - DAY_MS - const time = new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime() + const startOfToday = new Date(now) + startOfToday.setHours(0, 0, 0, 0) + const startOfYesterday = new Date(startOfToday) + startOfYesterday.setDate(startOfYesterday.getDate() - 1) + const time = new Date(date) + time.setHours(0, 0, 0, 0) - if (time >= startOfToday) return '今天' - if (time >= startOfYesterday) return '昨天' + if (time.getTime() >= startOfToday.getTime()) return '今天' + if (time.getTime() >= startOfYesterday.getTime()) return '昨天' if (date.getFullYear() === now.getFullYear()) return `${date.getMonth() + 1}月${date.getDate()}日` return `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日` } diff --git a/src/renderer/src/features/group-exit-monitor/GroupExitMonitorWorkspace.tsx b/src/renderer/src/features/group-exit-monitor/GroupExitMonitorWorkspace.tsx index 968eec4..a61bb50 100644 --- a/src/renderer/src/features/group-exit-monitor/GroupExitMonitorWorkspace.tsx +++ b/src/renderer/src/features/group-exit-monitor/GroupExitMonitorWorkspace.tsx @@ -167,7 +167,8 @@ function GroupExitNotificationTemplate({ />
- 支持 {`{user}`}、{`{groupRemark}`}、{`{wxid}`}、{`{time}`} + 支持 {`{user}`}、{`{groupRemark}`}、{`{wxid}`}、{`{previousCount}`}、{`{currentCount}`} + 、{`{time}`} {draft.length}/{GROUP_EXIT_NOTIFICATION_TEMPLATE_MAX_LENGTH} diff --git a/src/renderer/src/features/settings/ai-model/AIProviderEditor.tsx b/src/renderer/src/features/settings/ai-model/AIProviderEditor.tsx index f8ab050..1bff9f6 100644 --- a/src/renderer/src/features/settings/ai-model/AIProviderEditor.tsx +++ b/src/renderer/src/features/settings/ai-model/AIProviderEditor.tsx @@ -357,6 +357,29 @@ export function AIProviderEditor({ Codex 或仅支持 /responses 的中转服务请选择 Responses API。 +