fix: 修复导出日期、语音和联系人名字问题

- 导出的 HTML:点月份先看日期,选中日期后才跳到对应消息。
- 语音转文字:不同账号、不同聊天里的语音不会再串到一起。
- 联系人名字:正常昵称显示不变;昵称异常时使用可见的备注或账号,导出文件名也能正常使用。
This commit is contained in:
Wxw-Gu
2026-09-13 11:15:30 +08:00
parent ab3b3731b7
commit add16885fe
19 changed files with 702 additions and 150 deletions
@@ -1,5 +1,6 @@
import React, { useState } from 'react'
import { Contact } from '../../../../shared/types'
import { displayContactName } from '../../../../shared/contact-name'
interface ConversationItemProps {
contact: Contact
@@ -12,10 +13,9 @@ export function ConversationItem({
active,
onSelect
}: ConversationItemProps): React.ReactElement {
const nickname = contact.m_nsNickName?.trim()
const avatarUsername = contact.m_nsUsrName
const internalWxid = contact.wxid || avatarUsername
const displayName = nickname || internalWxid || '未命名会话'
const displayName = displayContactName(contact, '未命名会话')
const initial = (displayName || internalWxid || '?').charAt(0)
const [repairedAvatar, setRepairedAvatar] = useState<{
username: string
@@ -1,6 +1,7 @@
import type { Contact, Message } from '../../../../shared/types'
import type { ExportFormat, GroupMemberName } from './exportTypes'
import { resolveMemberName } from '../../../../shared/member-names'
import { displayContactName } from '../../../../shared/contact-name'
export const messageKinds = [
['text', '文字'],
@@ -24,7 +25,7 @@ export const formatLabels: Record<ExportFormat, { label: string; hint?: string }
export const formatOrder: ExportFormat[] = ['csv', 'html', 'json', 'markdown']
export function displayName(contact: Contact | null): string {
return contact?.m_nsNickName || contact?.m_nsUsrName || '未选择会话'
return displayContactName(contact, '未选择会话')
}
export function formatPreviewTime(message: Message): string {
@@ -598,6 +598,8 @@ export function ScheduledReportsWorkspace({
const [editingTask, setEditingTask] = React.useState<ScheduledReportTask | null>(null)
const [saving, setSaving] = React.useState(false)
const [deletingTask, setDeletingTask] = React.useState<ScheduledReportTask | null>(null)
const [retryingExecution, setRetryingExecution] =
React.useState<ScheduledReportExecution | null>(null)
const [busyTaskId, setBusyTaskId] = React.useState<string | null>(null)
const load = React.useCallback(async (): Promise<void> => {
@@ -784,6 +786,13 @@ export function ScheduledReportsWorkspace({
}
}
const confirmRetrySend = async (): Promise<void> => {
if (!retryingExecution || busyTaskId) return
const execution = retryingExecution
setRetryingExecution(null)
await retrySend(execution)
}
const testErrorNotification = async (task: ScheduledReportTask): Promise<void> => {
setBusyTaskId(task.id)
try {
@@ -1090,7 +1099,7 @@ export function ScheduledReportsWorkspace({
size="sm"
variant="outline"
disabled={busy}
onClick={() => void retrySend(execution)}
onClick={() => setRetryingExecution(execution)}
>
{busy ? '发送中…' : '重新发送'}
</Button>
@@ -1177,6 +1186,44 @@ export function ScheduledReportsWorkspace({
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<AlertDialog
open={Boolean(retryingExecution)}
onOpenChange={(open) => !open && setRetryingExecution(null)}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>将重新发送这份日报?</AlertDialogTitle>
<AlertDialogDescription asChild>
<div className="grid gap-1">
<span>
目标:
{retryingExecution?.sendTarget ||
tasks.find((item) => item.id === retryingExecution?.taskId)?.target ||
tasks.find((item) => item.id === retryingExecution?.taskId)?.group ||
'未指定'}
</span>
<span>
日报:
{tasks.find((item) => item.id === retryingExecution?.taskId)?.name || '定时日报'}
</span>
<span>日期:{formatDateTime(retryingExecution?.startedAt)}</span>
</div>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={Boolean(busyTaskId)}>取消</AlertDialogCancel>
<AlertDialogAction
onClick={(event) => {
event.preventDefault()
void confirmRetrySend()
}}
disabled={Boolean(busyTaskId)}
>
{busyTaskId ? '发送中…' : '确认重新发送'}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
)
}
+2 -1
View File
@@ -1,4 +1,5 @@
import { Contact } from '../../../../shared/types'
import { displayContactName } from '../../../../shared/contact-name'
export type {
GeneratedReportRecord,
ReportAssetStatus,
@@ -12,4 +13,4 @@ export type {
export type ReportWorkspaceView = 'configure' | 'result'
export const contactDisplayName = (contact: Contact | null): string =>
contact?.m_nsNickName?.trim() || contact?.m_nsUsrName || '未命名群聊'
displayContactName(contact, '未命名群聊')
@@ -1000,37 +1000,43 @@ export function AISearchWorkspace({
<div className="ai-search-insufficient-icon">!</div>
<span className="ai-search-kicker">检索反馈</span>
<h2>{analysisError || '当前范围没有足够证据'}</h2>
<p>可以扩大时间范围、切换群聊,或换一个更具体的问题。</p>
<Button
size="sm"
className="mt-5"
onClick={() => {
const expandToAll = range === '30d' || range === 'all'
setRange(expandToAll ? 'all' : '30d')
setTimeRangeOverride(
expandToAll
? {
label: '全部历史',
reason: '用户主动扩大到全部历史',
source: 'user_retry'
}
: undefined
)
skipNextCache()
void runAnalysis(undefined, {
range: expandToAll ? 'all' : '30d',
timeRangeOverride: expandToAll
? {
label: '全部历史',
reason: '用户主动扩大到全部历史',
source: 'user_retry'
}
: undefined
})
}}
>
{range === '30d' || range === 'all' ? '搜索全部历史' : '扩大到近 30 天'}
</Button>
{queryAgentEnabled ? (
<p>可以切换聊天范围,或换一个更具体的问题。</p>
) : (
<>
<p>可以扩大时间范围、切换群聊,或换一个更具体的问题。</p>
<Button
size="sm"
className="mt-5"
onClick={() => {
const expandToAll = range === '30d' || range === 'all'
setRange(expandToAll ? 'all' : '30d')
setTimeRangeOverride(
expandToAll
? {
label: '全部历史',
reason: '用户主动扩大到全部历史',
source: 'user_retry'
}
: undefined
)
skipNextCache()
void runAnalysis(undefined, {
range: expandToAll ? 'all' : '30d',
timeRangeOverride: expandToAll
? {
label: '全部历史',
reason: '用户主动扩大到全部历史',
source: 'user_retry'
}
: undefined
})
}}
>
{range === '30d' || range === 'all' ? '搜索全部历史' : '扩大到近 30 天'}
</Button>
</>
)}
</div>
)
@@ -1400,7 +1406,7 @@ export function AISearchWorkspace({
<AISearchComposer
query={query}
sourceLabel={sourceLabel}
rangeLabel={RANGE_LABELS[range]}
rangeLabel={queryAgentEnabled ? '时间由问题决定' : RANGE_LABELS[range]}
history={history}
historyOpen={historyOpen}
loading={stage === 'loading'}
@@ -1,6 +1,7 @@
import type { KnowledgeRuntimeStatus } from '../../../../shared/knowledge'
import { isKnowledgeFresh } from '../../../../shared/knowledge'
import type { Contact } from '../../../../shared/types'
import { displayContactName } from '../../../../shared/contact-name'
import type { SearchTrace } from './searchTypes'
export const formatBytes = (bytes: number): string => {
@@ -83,11 +84,7 @@ export const formatKnowledgeProcessed = (status: KnowledgeRuntimeStatus): string
}
export const contactLabel = (contact: Contact | null | undefined): string =>
contact?.m_nsNickName ||
contact?.remark ||
contact?.wechatNickname ||
contact?.m_nsUsrName ||
'未选择会话'
displayContactName(contact, '未选择会话')
export const formatSearchTraceOverview = (
trace: SearchTrace
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useMemo, useState } from 'react'
import type { Contact } from '../../../../../shared/types'
import { displayContactName } from '../../../../../shared/contact-name'
import type {
VoiceBatchConversationSummary,
VoiceBatchProgress,
@@ -68,7 +69,7 @@ const hasBatchApi = (): boolean =>
const BATCH_PAGE_SIZE = 12
function contactName(contact: Contact): string {
return contact.m_nsNickName || contact.remark || contact.wechatNickname || contact.m_nsUsrName
return displayContactName(contact)
}
export function VoiceRecognitionPage({
@@ -640,7 +641,7 @@ export function VoiceRecognitionPage({
{' · '}剩余 {formatTaskDuration(batchProgress.estimatedRemainingMs)}
</small>
{currentBatchConversation && (
<small>当前会话:{currentBatchConversation.m_nsNickName}</small>
<small>当前会话:{contactName(currentBatchConversation)}</small>
)}
</div>
<progress value={batchProgress.processed} max={Math.max(1, batchProgress.total)} />