mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-10-10 01:42:49 +08:00
chore: 统一日报时间逻辑
This commit is contained in:
@@ -129,7 +129,7 @@ WeFlow 帮我跨过了微信 4.x 数据访问这道门槛,我才有机会继
|
|||||||
| 找一句记得原文或关键词的聊天 | [档案](./docs/user-guide/chat-archive.md) | 连接微信数据,不需要 AI |
|
| 找一句记得原文或关键词的聊天 | [档案](./docs/user-guide/chat-archive.md) | 连接微信数据,不需要 AI |
|
||||||
| 找一件记得大意、但不知道在哪聊过的事 | [问问微信](./docs/user-guide/ai-search.md) | 配置 AI 服务,并选择会话和时间范围 |
|
| 找一件记得大意、但不知道在哪聊过的事 | [问问微信](./docs/user-guide/ai-search.md) | 配置 AI 服务,并选择会话和时间范围 |
|
||||||
| 让长期、跨群聊查找更稳定 | [问问微信 → 本地知识库](./docs/user-guide/knowledge.md) | 主动建立本地索引;不会自动创建 |
|
| 让长期、跨群聊查找更稳定 | [问问微信 → 本地知识库](./docs/user-guide/knowledge.md) | 主动建立本地索引;不会自动创建 |
|
||||||
| 快速了解一个群今天、昨天或近 7 天聊了什么 | [日报](./docs/user-guide/report.md) | 选择群聊并配置 AI 服务 |
|
| 快速了解一个群今日、昨日或近 7 天聊了什么 | [日报](./docs/user-guide/report.md) | 选择群聊并配置 AI 服务 |
|
||||||
| 把群聊日报生成微信分享卡片(实验性) | [微信分享卡片](./docs/deployment/experimental-wechat-share-card.md) | 自备 Cloudflare、域名和微信测试号 |
|
| 把群聊日报生成微信分享卡片(实验性) | [微信分享卡片](./docs/deployment/experimental-wechat-share-card.md) | 自备 Cloudflare、域名和微信测试号 |
|
||||||
| 把微信语音变成可搜索的文字 | [设置 → 语音转文字](./docs/user-guide/voice.md) | 准备本地语音模型 |
|
| 把微信语音变成可搜索的文字 | [设置 → 语音转文字](./docs/user-guide/voice.md) | 准备本地语音模型 |
|
||||||
| 把聊天保存成 HTML、Markdown、CSV 或 JSON | [导出](./docs/user-guide/export.md) | 选择聊天、时间和格式,不需要 AI |
|
| 把聊天保存成 HTML、Markdown、CSV 或 JSON | [导出](./docs/user-guide/export.md) | 选择聊天、时间和格式,不需要 AI |
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
|
|
||||||
典型场景包括:
|
典型场景包括:
|
||||||
|
|
||||||
- 整理今天工作群的讨论重点;
|
- 整理今日工作群的讨论重点;
|
||||||
- 回顾昨天错过的决定和资源;
|
- 回顾昨日错过的决定和资源;
|
||||||
- 汇总近 7 天的项目进展、待办和未解决问题;
|
- 汇总近 7 天的项目进展、待办和未解决问题;
|
||||||
- 把群里的图片、语音统计和重要消息放进一张长图或 HTML 页面。
|
- 把群里的图片、语音统计和重要消息放进一张长图或 HTML 页面。
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
你可以从两个入口开始:打开一级导航“日报”后新建报告,或者在“档案”中选中一个群聊并点击“生成 AI 日报”。
|
你可以从两个入口开始:打开一级导航“日报”后新建报告,或者在“档案”中选中一个群聊并点击“生成 AI 日报”。
|
||||||
|
|
||||||
1. 选择一个群聊。当前日报入口只支持群聊,不支持单聊。
|
1. 选择一个群聊。当前日报入口只支持群聊,不支持单聊。
|
||||||
2. 选择时间范围:今天、昨天或近 7 天。
|
2. 选择时间范围:今日、昨日或近 7 天。
|
||||||
3. 按需要选择参与总结的消息类型,先从文字开始最容易核对。
|
3. 按需要选择参与总结的消息类型,先从文字开始最容易核对。
|
||||||
4. 选择报告模板/内容模式并开始生成。
|
4. 选择报告模板/内容模式并开始生成。
|
||||||
5. 等待“整理输入 → AI 生成 → HTML/PNG 导出”完成。
|
5. 等待“整理输入 → AI 生成 → HTML/PNG 导出”完成。
|
||||||
@@ -37,6 +37,6 @@
|
|||||||
|
|
||||||
- 先选正确的群和时间范围;
|
- 先选正确的群和时间范围;
|
||||||
- 不确定时先只选择文字消息;
|
- 不确定时先只选择文字消息;
|
||||||
- 群太活跃时分成“今天”和“近 7 天”两次生成;
|
- 群太活跃时分成“今日”和“近 7 天”两次生成;
|
||||||
- 看到待办和结论后回到原消息核对上下文;
|
- 看到待办和结论后回到原消息核对上下文;
|
||||||
- AI Provider 不可用时先检查模型配置和网络/本地服务状态。
|
- AI Provider 不可用时先检查模型配置和网络/本地服务状态。
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ const activityLine = Array.from(document.querySelectorAll('.analytics > .card'))
|
|||||||
const values = {
|
const values = {
|
||||||
REPORT_TITLE: title,
|
REPORT_TITLE: title,
|
||||||
REPORT_DATE: reportDate,
|
REPORT_DATE: reportDate,
|
||||||
DATE_RANGE: '今天',
|
DATE_RANGE: '今日',
|
||||||
TIME_SPAN: statValues[2] || dateTimeRange,
|
TIME_SPAN: statValues[2] || dateTimeRange,
|
||||||
HERO_SUMMARY: overview,
|
HERO_SUMMARY: overview,
|
||||||
HERO_TAKEAWAY: '',
|
HERO_TAKEAWAY: '',
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ const reportRangeLabel = (range: ScheduledReportRange): string =>
|
|||||||
: range === '7days'
|
: range === '7days'
|
||||||
? '近 7 天'
|
? '近 7 天'
|
||||||
: range === 'today'
|
: range === 'today'
|
||||||
? '今天'
|
? '今日'
|
||||||
: '昨日'
|
: '昨日'
|
||||||
|
|
||||||
interface ScheduledReportNotificationPayload {
|
interface ScheduledReportNotificationPayload {
|
||||||
|
|||||||
@@ -1614,7 +1614,7 @@ function App(): React.ReactElement {
|
|||||||
? '昨日'
|
? '昨日'
|
||||||
: summaryDateRange === '7days'
|
: summaryDateRange === '7days'
|
||||||
? '近 7 天'
|
? '近 7 天'
|
||||||
: '今天',
|
: '今日',
|
||||||
reportDate: reportMetadata.reportDate,
|
reportDate: reportMetadata.reportDate,
|
||||||
messageCount: reportGeneration.reportMessages.length,
|
messageCount: reportGeneration.reportMessages.length,
|
||||||
generatedAt: new Date().toISOString(),
|
generatedAt: new Date().toISOString(),
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ interface AiReportWorkspaceProps {
|
|||||||
const rangeLabel = (range: SummaryDateRange): string => {
|
const rangeLabel = (range: SummaryDateRange): string => {
|
||||||
if (range === 'yesterday') return '昨日'
|
if (range === 'yesterday') return '昨日'
|
||||||
if (range === '7days') return '近 7 天'
|
if (range === '7days') return '近 7 天'
|
||||||
return '今天'
|
return '今日'
|
||||||
}
|
}
|
||||||
|
|
||||||
const modelLabel = (model: string): string => {
|
const modelLabel = (model: string): string => {
|
||||||
|
|||||||
@@ -69,8 +69,8 @@ const groupLabelFor = (value: string): string => {
|
|||||||
const time = new Date(date)
|
const time = new Date(date)
|
||||||
time.setHours(0, 0, 0, 0)
|
time.setHours(0, 0, 0, 0)
|
||||||
|
|
||||||
if (time.getTime() >= startOfToday.getTime()) return '今天'
|
if (time.getTime() >= startOfToday.getTime()) return '今日'
|
||||||
if (time.getTime() >= startOfYesterday.getTime()) return '昨天'
|
if (time.getTime() >= startOfYesterday.getTime()) return '昨日'
|
||||||
if (date.getFullYear() === now.getFullYear()) return `${date.getMonth() + 1}月${date.getDate()}日`
|
if (date.getFullYear() === now.getFullYear()) return `${date.getMonth() + 1}月${date.getDate()}日`
|
||||||
return `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日`
|
return `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,8 +66,8 @@ const rangeLabel = (range: ScheduledReportRange): string =>
|
|||||||
: range === '7days'
|
: range === '7days'
|
||||||
? '近 7 天'
|
? '近 7 天'
|
||||||
: range === 'today'
|
: range === 'today'
|
||||||
? '今天'
|
? '今日'
|
||||||
: '昨天'
|
: '昨日'
|
||||||
|
|
||||||
const isScheduledReportRange = (value: unknown): value is ScheduledReportRange =>
|
const isScheduledReportRange = (value: unknown): value is ScheduledReportRange =>
|
||||||
value === 'today' || value === 'yesterday' || value === '7days' || value === 'recent24h'
|
value === 'today' || value === 'yesterday' || value === '7days' || value === 'recent24h'
|
||||||
@@ -99,7 +99,7 @@ const formatDateTime = (value?: string): string => {
|
|||||||
if (Number.isNaN(date.getTime())) return '时间未知'
|
if (Number.isNaN(date.getTime())) return '时间未知'
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const dayLabel =
|
const dayLabel =
|
||||||
date.toDateString() === now.toDateString() ? '今天' : date.toLocaleDateString('zh-CN')
|
date.toDateString() === now.toDateString() ? '今日' : date.toLocaleDateString('zh-CN')
|
||||||
return `${dayLabel} ${date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}`
|
return `${dayLabel} ${date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -386,7 +386,7 @@ function ScheduledReportDialog({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<SegmentedControl
|
<SegmentedControl
|
||||||
className="w-full"
|
className="grid w-full grid-cols-2 gap-1 sm:grid-cols-5"
|
||||||
aria-label="总结范围"
|
aria-label="总结范围"
|
||||||
value={reportRange}
|
value={reportRange}
|
||||||
onValueChange={(value) => setReportRange(value as ScheduledReportRange)}
|
onValueChange={(value) => setReportRange(value as ScheduledReportRange)}
|
||||||
@@ -395,20 +395,24 @@ function ScheduledReportDialog({
|
|||||||
<SegmentedControlItem
|
<SegmentedControlItem
|
||||||
key={option.value}
|
key={option.value}
|
||||||
value={option.value}
|
value={option.value}
|
||||||
className="min-h-9 flex-1"
|
className="min-h-9 w-full min-w-0 whitespace-nowrap"
|
||||||
>
|
>
|
||||||
{option.label}
|
{option.label}
|
||||||
</SegmentedControlItem>
|
</SegmentedControlItem>
|
||||||
))}
|
))}
|
||||||
<SegmentedControlItem value="recent24h" className="min-h-9 flex-1">
|
<SegmentedControlItem
|
||||||
|
value="recent24h"
|
||||||
|
className="min-h-9 w-full min-w-0 whitespace-nowrap"
|
||||||
|
>
|
||||||
最近24小时
|
最近24小时
|
||||||
</SegmentedControlItem>
|
</SegmentedControlItem>
|
||||||
<span
|
<span
|
||||||
aria-disabled="true"
|
aria-disabled="true"
|
||||||
title="当前业务尚未支持自定义时间"
|
title="当前业务尚未支持自定义时间"
|
||||||
className="inline-flex min-h-8 min-w-0 flex-1 cursor-not-allowed items-center justify-center rounded-sm px-3 text-xs font-medium text-disabled-foreground opacity-100"
|
className="inline-flex min-h-9 w-full min-w-0 cursor-not-allowed flex-col items-center justify-center gap-0 rounded-sm px-2 py-1 text-xs font-medium leading-4 text-disabled-foreground opacity-100"
|
||||||
>
|
>
|
||||||
自定义 <small>即将支持</small>
|
<span className="whitespace-nowrap">自定义</span>
|
||||||
|
<small className="whitespace-nowrap text-[11px] leading-[14px]">即将支持</small>
|
||||||
</span>
|
</span>
|
||||||
</SegmentedControl>
|
</SegmentedControl>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export const API_ENDPOINTS: ApiEndpoint[] = [
|
|||||||
}),
|
}),
|
||||||
endpoint('agent-group-report', {
|
endpoint('agent-group-report', {
|
||||||
name: '生成群聊总结图片',
|
name: '生成群聊总结图片',
|
||||||
description: '读取指定群聊并生成今天、昨天或近 7 天的总结长图。',
|
description: '读取指定群聊并生成今日、昨日或近 7 天的总结长图。',
|
||||||
body: true
|
body: true
|
||||||
}),
|
}),
|
||||||
endpoint('agent-send', {
|
endpoint('agent-send', {
|
||||||
|
|||||||
@@ -586,7 +586,7 @@ const buildMediaSection = async (
|
|||||||
funBadges.push({
|
funBadges.push({
|
||||||
title: '高能输出王',
|
title: '高能输出王',
|
||||||
owner: topSpeaker[0],
|
owner: topSpeaker[0],
|
||||||
note: `今天一共发了 ${topSpeaker[1]} 条消息。`
|
note: `今日一共发了 ${topSpeaker[1]} 条消息。`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (visionGallery[0]) {
|
if (visionGallery[0]) {
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ JSON 结构必须为:
|
|||||||
"overview": "1至2句整体讨论风格与氛围",
|
"overview": "1至2句整体讨论风格与氛围",
|
||||||
"hero": {
|
"hero": {
|
||||||
"headline": "一句抓重点的日报标题",
|
"headline": "一句抓重点的日报标题",
|
||||||
"summary": "一句总结今天发生了什么,最多3行",
|
"summary": "一句总结今日发生了什么,最多3行",
|
||||||
"keyTakeaway": "最重要结论,没有可空",
|
"keyTakeaway": "最重要结论,没有可空",
|
||||||
"pendingNote": "最值得跟进的一项,没有可空",
|
"pendingNote": "最值得跟进的一项,没有可空",
|
||||||
"statusLine": "例如:今日形成 3 个结论 · 2 个待办 · 1 个问题尚未解决"
|
"statusLine": "例如:今日形成 3 个结论 · 2 个待办 · 1 个问题尚未解决"
|
||||||
@@ -645,7 +645,7 @@ const postProcessReport = (
|
|||||||
topics[0]?.title ||
|
topics[0]?.title ||
|
||||||
`${metadata.groupName}${mode === 'compact' ? '速览' : '日报'}`,
|
`${metadata.groupName}${mode === 'compact' ? '速览' : '日报'}`,
|
||||||
summary: truncate(
|
summary: truncate(
|
||||||
report.hero?.summary || report.overview || '今天群里有新的讨论进展。',
|
report.hero?.summary || report.overview || '今日群里有新的讨论进展。',
|
||||||
mode === 'compact' ? 84 : 120
|
mode === 'compact' ? 84 : 120
|
||||||
),
|
),
|
||||||
keyTakeaway: report.hero?.keyTakeaway
|
keyTakeaway: report.hero?.keyTakeaway
|
||||||
@@ -814,7 +814,7 @@ export const parseGroupDailyReport = (
|
|||||||
? {
|
? {
|
||||||
headline: asString(heroRoot.headline) || topics[0]?.title || '今日群聊速览',
|
headline: asString(heroRoot.headline) || topics[0]?.title || '今日群聊速览',
|
||||||
summary:
|
summary:
|
||||||
asString(heroRoot.summary) || asString(root.overview) || '今天群里有新的讨论进展。',
|
asString(heroRoot.summary) || asString(root.overview) || '今日群里有新的讨论进展。',
|
||||||
keyTakeaway: asString(heroRoot.keyTakeaway),
|
keyTakeaway: asString(heroRoot.keyTakeaway),
|
||||||
pendingNote: asString(heroRoot.pendingNote),
|
pendingNote: asString(heroRoot.pendingNote),
|
||||||
statusLine: asString(heroRoot.statusLine)
|
statusLine: asString(heroRoot.statusLine)
|
||||||
@@ -864,7 +864,7 @@ export type SummaryMessageType =
|
|||||||
| 'system'
|
| 'system'
|
||||||
|
|
||||||
export const SUMMARY_DATE_OPTIONS: { value: SummaryDateRange; label: string }[] = [
|
export const SUMMARY_DATE_OPTIONS: { value: SummaryDateRange; label: string }[] = [
|
||||||
{ value: 'today', label: '今天' },
|
{ value: 'today', label: '今日' },
|
||||||
{ value: 'yesterday', label: '昨日' },
|
{ value: 'yesterday', label: '昨日' },
|
||||||
{ value: '7days', label: '近 7 天' }
|
{ value: '7days', label: '近 7 天' }
|
||||||
]
|
]
|
||||||
@@ -923,6 +923,8 @@ export const getSummaryDateRangeAt = (
|
|||||||
range: SummaryDateRange,
|
range: SummaryDateRange,
|
||||||
now = new Date()
|
now = new Date()
|
||||||
): { startTime: number; endTime: number } => {
|
): { startTime: number; endTime: number } => {
|
||||||
|
// These ranges are local calendar dates. Only the separate recent24h mode
|
||||||
|
// uses a rolling 24-hour window in the scheduled-report service.
|
||||||
const startOfToday = new Date(now)
|
const startOfToday = new Date(now)
|
||||||
startOfToday.setHours(0, 0, 0, 0)
|
startOfToday.setHours(0, 0, 0, 0)
|
||||||
const startOfYesterday = new Date(startOfToday)
|
const startOfYesterday = new Date(startOfToday)
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ const messagesFor = (
|
|||||||
return {
|
return {
|
||||||
severity: 'error',
|
severity: 'error',
|
||||||
userTitle: '日报生成失败',
|
userTitle: '日报生成失败',
|
||||||
userMessage: '今天的聊天内容较多,超过当前 AI 模型一次可以处理的内容长度。',
|
userMessage: '今日的聊天内容较多,超过当前 AI 模型一次可以处理的内容长度。',
|
||||||
suggestedAction: '可以缩短日报范围,或更换支持更长上下文的模型。',
|
suggestedAction: '可以缩短日报范围,或更换支持更长上下文的模型。',
|
||||||
retryable: true
|
retryable: true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ describe('daily report controls', () => {
|
|||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(screen.getByRole('radio', { name: '今天' })).toBeChecked()
|
expect(screen.getByRole('radio', { name: '今日' })).toBeChecked()
|
||||||
await user.click(screen.getByRole('radio', { name: '近 7 天' }))
|
await user.click(screen.getByRole('radio', { name: '近 7 天' }))
|
||||||
expect(onRangeChange).toHaveBeenCalledWith('7days')
|
expect(onRangeChange).toHaveBeenCalledWith('7days')
|
||||||
expect(screen.getAllByRole('checkbox')).toHaveLength(SUMMARY_TYPE_OPTIONS.length)
|
expect(screen.getAllByRole('checkbox')).toHaveLength(SUMMARY_TYPE_OPTIONS.length)
|
||||||
@@ -382,7 +382,7 @@ describe('daily report controls', () => {
|
|||||||
id: 'report-1',
|
id: 'report-1',
|
||||||
contactId: 'group-md5',
|
contactId: 'group-md5',
|
||||||
contactName: '测试群',
|
contactName: '测试群',
|
||||||
dateRange: '今天',
|
dateRange: '今日',
|
||||||
messageCount: 10,
|
messageCount: 10,
|
||||||
generatedAt: '2026-08-12T10:00:00.000Z',
|
generatedAt: '2026-08-12T10:00:00.000Z',
|
||||||
reportDate: '2026-08-12',
|
reportDate: '2026-08-12',
|
||||||
@@ -443,7 +443,7 @@ describe('daily report controls', () => {
|
|||||||
id: 'temporary-result',
|
id: 'temporary-result',
|
||||||
contactId: 'group-md5',
|
contactId: 'group-md5',
|
||||||
contactName: '测试群',
|
contactName: '测试群',
|
||||||
dateRange: '今天',
|
dateRange: '今日',
|
||||||
messageCount: 10,
|
messageCount: 10,
|
||||||
generatedAt: '2026-08-13T10:00:00.000Z',
|
generatedAt: '2026-08-13T10:00:00.000Z',
|
||||||
reportDate: '2026-08-13',
|
reportDate: '2026-08-13',
|
||||||
@@ -514,7 +514,7 @@ describe('daily report controls', () => {
|
|||||||
id: 'model-info',
|
id: 'model-info',
|
||||||
contactId: 'group-md5',
|
contactId: 'group-md5',
|
||||||
contactName: '测试群',
|
contactName: '测试群',
|
||||||
dateRange: '今天',
|
dateRange: '今日',
|
||||||
messageCount: 10,
|
messageCount: 10,
|
||||||
generatedAt: '2026-08-13T10:00:00.000Z',
|
generatedAt: '2026-08-13T10:00:00.000Z',
|
||||||
reportDate: '2026-08-13',
|
reportDate: '2026-08-13',
|
||||||
@@ -582,7 +582,7 @@ describe('daily report controls', () => {
|
|||||||
id: 'report-send',
|
id: 'report-send',
|
||||||
contactId: groupContact.md5,
|
contactId: groupContact.md5,
|
||||||
contactName: groupContact.m_nsNickName,
|
contactName: groupContact.m_nsNickName,
|
||||||
dateRange: '今天',
|
dateRange: '今日',
|
||||||
messageCount: 10,
|
messageCount: 10,
|
||||||
generatedAt: '2026-08-17T10:00:00.000Z',
|
generatedAt: '2026-08-17T10:00:00.000Z',
|
||||||
reportDate: '2026-08-17',
|
reportDate: '2026-08-17',
|
||||||
@@ -621,7 +621,7 @@ describe('daily report controls', () => {
|
|||||||
id: 'report-send-disabled',
|
id: 'report-send-disabled',
|
||||||
contactId: groupContact.md5,
|
contactId: groupContact.md5,
|
||||||
contactName: groupContact.m_nsNickName,
|
contactName: groupContact.m_nsNickName,
|
||||||
dateRange: '今天',
|
dateRange: '今日',
|
||||||
messageCount: 10,
|
messageCount: 10,
|
||||||
generatedAt: '2026-08-17T10:00:00.000Z',
|
generatedAt: '2026-08-17T10:00:00.000Z',
|
||||||
reportDate: '2026-08-17',
|
reportDate: '2026-08-17',
|
||||||
@@ -657,7 +657,7 @@ describe('daily report controls', () => {
|
|||||||
id: 'report-switch',
|
id: 'report-switch',
|
||||||
contactId: 'group-md5',
|
contactId: 'group-md5',
|
||||||
contactName: '测试群',
|
contactName: '测试群',
|
||||||
dateRange: '今天',
|
dateRange: '今日',
|
||||||
messageCount: 10,
|
messageCount: 10,
|
||||||
generatedAt: '2026-08-12T10:00:00.000Z',
|
generatedAt: '2026-08-12T10:00:00.000Z',
|
||||||
reportDate: '2026-08-12',
|
reportDate: '2026-08-12',
|
||||||
@@ -753,7 +753,7 @@ describe('daily report controls', () => {
|
|||||||
id: 'report-delete-cancel',
|
id: 'report-delete-cancel',
|
||||||
contactId: 'group-md5',
|
contactId: 'group-md5',
|
||||||
contactName: '测试群',
|
contactName: '测试群',
|
||||||
dateRange: '今天',
|
dateRange: '今日',
|
||||||
messageCount: 10,
|
messageCount: 10,
|
||||||
generatedAt: '2026-08-17T10:00:00.000Z',
|
generatedAt: '2026-08-17T10:00:00.000Z',
|
||||||
reportDate: '2026-08-17',
|
reportDate: '2026-08-17',
|
||||||
@@ -823,7 +823,7 @@ describe('daily report controls', () => {
|
|||||||
id: 'report-delete-result',
|
id: 'report-delete-result',
|
||||||
contactId: 'group-md5',
|
contactId: 'group-md5',
|
||||||
contactName: '测试群',
|
contactName: '测试群',
|
||||||
dateRange: '今天',
|
dateRange: '今日',
|
||||||
messageCount: 10,
|
messageCount: 10,
|
||||||
generatedAt: '2026-08-17T10:00:00.000Z',
|
generatedAt: '2026-08-17T10:00:00.000Z',
|
||||||
reportDate: '2026-08-17',
|
reportDate: '2026-08-17',
|
||||||
|
|||||||
@@ -128,6 +128,26 @@ describe('ScheduledReportsWorkspace', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('keeps scheduled report range labels readable in compact layouts', async () => {
|
||||||
|
vi.mocked(window.api.listScheduledReports).mockResolvedValue([])
|
||||||
|
render(
|
||||||
|
<ScheduledReportsWorkspace
|
||||||
|
contacts={[group]}
|
||||||
|
onNotice={onNotice}
|
||||||
|
onOpenWechatSettings={onOpenWechatSettings}
|
||||||
|
onOpenAgentHub={onOpenAgentHub}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
await waitFor(() => expect(screen.getByText('还没有定时日报')).toBeVisible())
|
||||||
|
fireEvent.click(screen.getAllByRole('button', { name: /新建定时日报/ })[0])
|
||||||
|
|
||||||
|
const range = screen.getByRole('radiogroup', { name: '总结范围' })
|
||||||
|
expect(range).toHaveClass('grid-cols-2', 'sm:grid-cols-5')
|
||||||
|
expect(screen.getByText('自定义', { exact: true })).toHaveClass('whitespace-nowrap')
|
||||||
|
expect(screen.getByText('即将支持', { exact: true })).toHaveClass('whitespace-nowrap')
|
||||||
|
})
|
||||||
|
|
||||||
it('keeps the notification switch off by default and loads the persisted setting', async () => {
|
it('keeps the notification switch off by default and loads the persisted setting', async () => {
|
||||||
render(
|
render(
|
||||||
<ScheduledReportsWorkspace
|
<ScheduledReportsWorkspace
|
||||||
|
|||||||
@@ -32,6 +32,15 @@ test('SCHEDULED-REPORT-UI-01 opens the scheduled report dialog without viewport
|
|||||||
await expect(dialog.getByPlaceholder('搜索群聊')).toBeVisible()
|
await expect(dialog.getByPlaceholder('搜索群聊')).toBeVisible()
|
||||||
await expect(dialog.getByRole('button', { name: '创建定时日报' })).toBeVisible()
|
await expect(dialog.getByRole('button', { name: '创建定时日报' })).toBeVisible()
|
||||||
|
|
||||||
|
const customRange = dialog.locator('[aria-disabled="true"]')
|
||||||
|
await expect(customRange).toHaveCount(1)
|
||||||
|
const customLabel = customRange.getByText('自定义', { exact: true })
|
||||||
|
const customHint = customRange.getByText('即将支持', { exact: true })
|
||||||
|
await expect(customLabel).toHaveCSS('white-space', 'nowrap')
|
||||||
|
await expect(customHint).toHaveCSS('white-space', 'nowrap')
|
||||||
|
expect((await customLabel.boundingBox())?.height).toBeLessThanOrEqual(20)
|
||||||
|
expect((await customHint.boundingBox())?.height).toBeLessThanOrEqual(20)
|
||||||
|
|
||||||
const selects = dialog.getByRole('combobox')
|
const selects = dialog.getByRole('combobox')
|
||||||
await expect(selects).toHaveCount(2)
|
await expect(selects).toHaveCount(2)
|
||||||
await selects.nth(0).click()
|
await selects.nth(0).click()
|
||||||
@@ -55,11 +64,11 @@ test('SCHEDULED-REPORT-UI-01 opens the scheduled report dialog without viewport
|
|||||||
expect(bounds!.y + bounds!.height).toBeLessThanOrEqual(600)
|
expect(bounds!.y + bounds!.height).toBeLessThanOrEqual(600)
|
||||||
|
|
||||||
await dialog.getByPlaceholder('例如:技术交流 · 每日日报').fill('E2E 定时日报')
|
await dialog.getByPlaceholder('例如:技术交流 · 每日日报').fill('E2E 定时日报')
|
||||||
await dialog.getByRole('radio', { name: '今天' }).click()
|
await dialog.getByRole('radio', { name: '今日' }).click()
|
||||||
await dialog.getByRole('button', { name: '创建定时日报' }).click()
|
await dialog.getByRole('button', { name: '创建定时日报' }).click()
|
||||||
await expect(dialog).toHaveCount(0)
|
await expect(dialog).toHaveCount(0)
|
||||||
await expect(fixture.page.getByText('E2E 定时日报', { exact: true })).toBeVisible()
|
await expect(fixture.page.getByText('E2E 定时日报', { exact: true })).toBeVisible()
|
||||||
await expect(fixture.page.getByText('今天', { exact: true })).toBeVisible()
|
await expect(fixture.page.getByText('今日', { exact: true })).toBeVisible()
|
||||||
|
|
||||||
await fixture.page.getByRole('button', { name: '设置' }).click()
|
await fixture.page.getByRole('button', { name: '设置' }).click()
|
||||||
await fixture.page.getByRole('button', { name: '微信发送', exact: true }).click()
|
await fixture.page.getByRole('button', { name: '微信发送', exact: true }).click()
|
||||||
|
|||||||
@@ -345,4 +345,14 @@ describe('summary date ranges', () => {
|
|||||||
endTime: now.getTime() / 1000
|
endTime: now.getTime() / 1000
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('keeps a scheduled 20:00 today report on the current local calendar day', () => {
|
||||||
|
const now = new Date(2026, 7, 27, 20, 0, 0)
|
||||||
|
const startOfToday = new Date(2026, 7, 27, 0, 0, 0)
|
||||||
|
|
||||||
|
expect(getSummaryDateRangeAt('today', now)).toEqual({
|
||||||
|
startTime: startOfToday.getTime() / 1000,
|
||||||
|
endTime: now.getTime() / 1000
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ const reportSnapshot = {
|
|||||||
const reportMetadata = {
|
const reportMetadata = {
|
||||||
groupName: '测试群',
|
groupName: '测试群',
|
||||||
reportDate: '2026-08-12',
|
reportDate: '2026-08-12',
|
||||||
dateRange: '今天',
|
dateRange: '今日',
|
||||||
messageCount: 10,
|
messageCount: 10,
|
||||||
activeUsers: 3,
|
activeUsers: 3,
|
||||||
timeSpan: '09:00–18:00',
|
timeSpan: '09:00–18:00',
|
||||||
@@ -68,7 +68,7 @@ describe('generated report template history', () => {
|
|||||||
const saved = await saveGeneratedReport({
|
const saved = await saveGeneratedReport({
|
||||||
contactId: 'group-md5',
|
contactId: 'group-md5',
|
||||||
contactName: '测试群',
|
contactName: '测试群',
|
||||||
dateRange: '今天',
|
dateRange: '今日',
|
||||||
messageCount: 10,
|
messageCount: 10,
|
||||||
generatedAt: '2026-08-12T10:00:00.000Z',
|
generatedAt: '2026-08-12T10:00:00.000Z',
|
||||||
reportDate: '2026-08-11',
|
reportDate: '2026-08-11',
|
||||||
@@ -114,7 +114,7 @@ describe('generated report template history', () => {
|
|||||||
const saved = await saveGeneratedReport({
|
const saved = await saveGeneratedReport({
|
||||||
contactId: 'legacy-group',
|
contactId: 'legacy-group',
|
||||||
contactName: '旧报告',
|
contactName: '旧报告',
|
||||||
dateRange: '今天',
|
dateRange: '今日',
|
||||||
messageCount: 5,
|
messageCount: 5,
|
||||||
generatedAt: '2026-08-12T11:00:00.000Z',
|
generatedAt: '2026-08-12T11:00:00.000Z',
|
||||||
generatedImage: `data:image/png;base64,${Buffer.from('legacy').toString('base64')}`,
|
generatedImage: `data:image/png;base64,${Buffer.from('legacy').toString('base64')}`,
|
||||||
@@ -142,7 +142,7 @@ describe('generated report template history', () => {
|
|||||||
const saved = await saveGeneratedReport({
|
const saved = await saveGeneratedReport({
|
||||||
contactId: 'legacy-snapshot-group',
|
contactId: 'legacy-snapshot-group',
|
||||||
contactName: '旧报告快照',
|
contactName: '旧报告快照',
|
||||||
dateRange: '今天',
|
dateRange: '今日',
|
||||||
messageCount: 8,
|
messageCount: 8,
|
||||||
generatedAt: '2026-08-12T12:00:00.000Z',
|
generatedAt: '2026-08-12T12:00:00.000Z',
|
||||||
generatedImage: `data:image/png;base64,${Buffer.from('legacy-snapshot').toString('base64')}`,
|
generatedImage: `data:image/png;base64,${Buffer.from('legacy-snapshot').toString('base64')}`,
|
||||||
@@ -172,7 +172,7 @@ describe('generated report template history', () => {
|
|||||||
const saved = await saveGeneratedReport({
|
const saved = await saveGeneratedReport({
|
||||||
contactId: 'classic-group',
|
contactId: 'classic-group',
|
||||||
contactName: '经典日报群',
|
contactName: '经典日报群',
|
||||||
dateRange: '今天',
|
dateRange: '今日',
|
||||||
messageCount: 6,
|
messageCount: 6,
|
||||||
generatedAt: '2026-08-12T13:00:00.000Z',
|
generatedAt: '2026-08-12T13:00:00.000Z',
|
||||||
generatedImage: `data:image/png;base64,${Buffer.from('classic').toString('base64')}`,
|
generatedImage: `data:image/png;base64,${Buffer.from('classic').toString('base64')}`,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ const structuredReport = {
|
|||||||
id: 'report-1',
|
id: 'report-1',
|
||||||
contactId: 'group-1',
|
contactId: 'group-1',
|
||||||
contactName: '测试群',
|
contactName: '测试群',
|
||||||
dateRange: '今天',
|
dateRange: '今日',
|
||||||
messageCount: 10,
|
messageCount: 10,
|
||||||
generatedAt: '2026-08-12T10:00:00.000Z',
|
generatedAt: '2026-08-12T10:00:00.000Z',
|
||||||
reportDate: '2026-08-12',
|
reportDate: '2026-08-12',
|
||||||
|
|||||||
@@ -254,6 +254,42 @@ describe('scheduled report scheduling', () => {
|
|||||||
).toContain('微信发送成功')
|
).toContain('微信发送成功')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('passes a selected today range through a scheduled 20:00 execution', async () => {
|
||||||
|
const storageDir = await mkdtemp(join(tmpdir(), 'tracememo-scheduled-report-today-'))
|
||||||
|
let currentTime = new Date(2026, 7, 27, 19, 59, 0)
|
||||||
|
const generateReport = vi.fn(async () => ({
|
||||||
|
success: true,
|
||||||
|
groupName: '研发群',
|
||||||
|
pngPath: '/tmp/generated.png',
|
||||||
|
messageCount: 1
|
||||||
|
}))
|
||||||
|
const service = new ScheduledReportService({
|
||||||
|
storageDir,
|
||||||
|
now: () => currentTime,
|
||||||
|
getCapability: async () => capability,
|
||||||
|
generateReport,
|
||||||
|
saveGeneratedReport: async () => ({ success: true, record: reportRecord('/tmp/saved.png') }),
|
||||||
|
send: async () => ({ success: true, status: capability.senderStatus }),
|
||||||
|
isDatabaseReady: () => true
|
||||||
|
})
|
||||||
|
const created = await service.createTask({
|
||||||
|
name: '今日日报',
|
||||||
|
group: '研发群',
|
||||||
|
target: '研发群@chatroom',
|
||||||
|
scheduleTime: '20:00',
|
||||||
|
reportRange: 'today'
|
||||||
|
})
|
||||||
|
expect(created.success).toBe(true)
|
||||||
|
currentTime = new Date(2026, 7, 27, 20, 0, 0)
|
||||||
|
|
||||||
|
const execution = await runScheduled(service, created.data!.id)
|
||||||
|
|
||||||
|
expect(execution.status).toBe('success')
|
||||||
|
expect(generateReport).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ group: '研发群', range: 'today' })
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
it('does not send a generated report when history persistence fails', async () => {
|
it('does not send a generated report when history persistence fails', async () => {
|
||||||
const storageDir = await mkdtemp(join(tmpdir(), 'tracememo-scheduled-report-history-'))
|
const storageDir = await mkdtemp(join(tmpdir(), 'tracememo-scheduled-report-history-'))
|
||||||
const generatedPath = join(storageDir, 'report.png')
|
const generatedPath = join(storageDir, 'report.png')
|
||||||
|
|||||||
Reference in New Issue
Block a user