Files
WechatExplorer/src/shared/scheduled-report-error.ts
T

313 lines
11 KiB
TypeScript

import type { ScheduledReportExecutionStage } from './scheduled-report'
export const SCHEDULED_REPORT_ERROR_CODES = [
'AI_CONTEXT_LIMIT',
'AI_QUOTA_EXCEEDED',
'AI_AUTH_INVALID',
'AI_PROVIDER_UNAVAILABLE',
'AI_TIMEOUT',
'DATABASE_UNAVAILABLE',
'CHAT_NOT_FOUND',
'NO_MESSAGES',
'REPORT_GENERATION_FAILED',
'REPORT_HISTORY_SAVE_FAILED',
'WECHAT_SEND_UNAVAILABLE',
'WECHAT_SEND_FAILED',
'WECHAT_AUTH_EXPIRED',
'UNKNOWN'
] as const
export type ScheduledReportErrorCode = (typeof SCHEDULED_REPORT_ERROR_CODES)[number]
export type ScheduledReportErrorSeverity = 'info' | 'warning' | 'error'
export interface ScheduledReportErrorDetails {
code: ScheduledReportErrorCode
stage: ScheduledReportExecutionStage
severity: ScheduledReportErrorSeverity
technicalMessage: string
userTitle: string
userMessage: string
suggestedAction: string
retryable: boolean
}
export interface ScheduledReportErrorInput {
error?: unknown
code?: unknown
status?: unknown
type?: unknown
stage?: ScheduledReportExecutionStage
}
const knownCodes = new Set<string>(SCHEDULED_REPORT_ERROR_CODES)
const asRecord = (value: unknown): Record<string, unknown> | undefined =>
value && typeof value === 'object' ? (value as Record<string, unknown>) : undefined
const safeString = (value: unknown): string => {
if (value instanceof Error) return value.message
if (typeof value === 'string') return value
if (value === undefined || value === null) return ''
try {
return JSON.stringify(value)
} catch {
return String(value)
}
}
const redact = (value: string): string =>
value
.replace(/sk-[a-z0-9_-]+/gi, '***')
.replace(/Bearer\s+[A-Za-z0-9._~-]+/gi, 'Bearer [已隐藏]')
.slice(0, 1_000)
const legacyCode = (value: string): ScheduledReportErrorCode | undefined => {
const match = value.match(/^([a-z_]+):/i)?.[1]
switch (match) {
case 'wechat_not_ready':
return 'WECHAT_SEND_UNAVAILABLE'
case 'wechat_send_failed':
return 'WECHAT_SEND_FAILED'
case 'report_generation_failed':
return 'REPORT_GENERATION_FAILED'
case 'report_history_save_failed':
return 'REPORT_HISTORY_SAVE_FAILED'
default:
return undefined
}
}
const stageForCode = (code: ScheduledReportErrorCode): ScheduledReportExecutionStage => {
if (code.startsWith('AI_')) return 'ai'
if (code === 'DATABASE_UNAVAILABLE' || code === 'CHAT_NOT_FOUND' || code === 'NO_MESSAGES') {
return 'data'
}
if (code === 'REPORT_HISTORY_SAVE_FAILED') return 'persist'
if (code.startsWith('WECHAT_')) return 'send'
return 'report'
}
const messagesFor = (
code: ScheduledReportErrorCode
): Omit<ScheduledReportErrorDetails, 'code' | 'stage' | 'technicalMessage'> => {
switch (code) {
case 'AI_CONTEXT_LIMIT':
return {
severity: 'error',
userTitle: '日报生成失败',
userMessage: '今日的聊天内容较多,超过当前 AI 模型一次可以处理的内容长度。',
suggestedAction: '可以缩短日报范围,或更换支持更长上下文的模型。',
retryable: true
}
case 'AI_QUOTA_EXCEEDED':
return {
severity: 'error',
userTitle: '日报生成失败',
userMessage: '当前 AI 服务额度不足,因此暂时无法生成日报。',
suggestedAction: '请检查 AI Provider 的账户额度,或切换其他可用模型。',
retryable: true
}
case 'AI_AUTH_INVALID':
return {
severity: 'error',
userTitle: 'AI 服务不可用',
userMessage: '当前 AI 服务的认证信息无效。',
suggestedAction: '请检查 API Key 或重新配置模型。',
retryable: true
}
case 'AI_PROVIDER_UNAVAILABLE':
return {
severity: 'error',
userTitle: 'AI 服务暂时不可用',
userMessage: 'AI 服务暂时无法响应,本次日报没有生成。',
suggestedAction: '稍后重新执行。',
retryable: true
}
case 'AI_TIMEOUT':
return {
severity: 'error',
userTitle: '日报生成超时',
userMessage: 'AI 服务在限定时间内没有完成响应,本次日报没有生成。',
suggestedAction: '稍后重新执行,或适当增加日报生成超时时间。',
retryable: true
}
case 'DATABASE_UNAVAILABLE':
return {
severity: 'error',
userTitle: '日报生成失败',
userMessage: 'TraceMemo 当前无法读取微信数据库,本次日报没有生成。',
suggestedAction: '请连接微信数据后重新执行。',
retryable: true
}
case 'CHAT_NOT_FOUND':
return {
severity: 'error',
userTitle: '日报生成失败',
userMessage: '没有找到任务配置的目标群聊。',
suggestedAction: '请检查群聊配置后重新执行。',
retryable: false
}
case 'NO_MESSAGES':
return {
severity: 'warning',
userTitle: '没有可生成的日报',
userMessage: '所选时间范围内没有可总结的聊天消息。',
suggestedAction: '可以扩大日报范围,或稍后重新执行。',
retryable: true
}
case 'REPORT_HISTORY_SAVE_FAILED':
return {
severity: 'error',
userTitle: '日报保存失败',
userMessage: '日报已经生成,但保存到日报历史时失败。',
suggestedAction: '请检查磁盘空间和文件权限后重新执行。',
retryable: true
}
case 'WECHAT_SEND_UNAVAILABLE':
return {
severity: 'warning',
userTitle: '日报已生成,但未发送',
userMessage: '日报已经生成并保存,但当前个人微信发送能力不可用。',
suggestedAction: '恢复微信消息能力后,可以直接重新发送这份日报。',
retryable: true
}
case 'WECHAT_AUTH_EXPIRED':
return {
severity: 'warning',
userTitle: '日报已生成,微信登录已失效',
userMessage: '日报已经生成并保存,但个人微信登录凭证已失效。',
suggestedAction: '重新登录微信后,可以直接重新发送这份日报。',
retryable: true
}
case 'WECHAT_SEND_FAILED':
return {
severity: 'warning',
userTitle: '日报已生成,发送失败',
userMessage: '日报已经生成并保存,但发送到微信时失败。',
suggestedAction: '检查微信连接后重新发送。',
retryable: true
}
case 'REPORT_GENERATION_FAILED':
return {
severity: 'error',
userTitle: '日报生成失败',
userMessage: '日报生成过程中发生异常,本次日报没有生成。',
suggestedAction: '稍后重新执行;如果持续失败,请检查模型配置。',
retryable: true
}
case 'UNKNOWN':
return {
severity: 'error',
userTitle: '定时日报执行失败',
userMessage: '本次定时日报执行失败。',
suggestedAction: '查看详情并稍后重新执行。',
retryable: true
}
}
}
const inferCode = (
message: string,
stage: ScheduledReportExecutionStage,
input: ScheduledReportErrorInput
): ScheduledReportErrorCode => {
const explicit = safeString(input.code).trim().toUpperCase()
if (knownCodes.has(explicit)) return explicit as ScheduledReportErrorCode
const legacy = legacyCode(message)
if (legacy) return legacy
const status = Number(input.status)
const type = safeString(input.type).toLowerCase()
const lower = message.toLowerCase()
if (
/context.{0,30}(length|limit|window)|maximum.{0,20}(context|token)|too many tokens|prompt.{0,20}long|input.{0,20}token|上下文|超出.*长度|token.*超限/.test(
lower
)
)
return 'AI_CONTEXT_LIMIT'
if (
/quota|insufficient[_ -]?quota|billing|credit|额度|余额|付费|rate.?limit|too many requests/.test(
lower
) ||
status === 402
)
return 'AI_QUOTA_EXCEEDED'
if (
status === 401 ||
status === 403 ||
/invalid api key|incorrect api key|authentication|unauthorized|api key.*invalid|认证信息|密钥无效/.test(
lower
)
)
return stage === 'send' ? 'WECHAT_AUTH_EXPIRED' : 'AI_AUTH_INVALID'
if (/timeout|timed out|etimedout|超时/.test(lower)) {
return stage === 'send' ? 'WECHAT_SEND_FAILED' : 'AI_TIMEOUT'
}
if (/database|db unavailable|数据库|数据库未|wcdb/.test(lower)) return 'DATABASE_UNAVAILABLE'
if (/no messages|没有.*消息|没有可总结|消息为空/.test(lower)) return 'NO_MESSAGES'
if (/chat.*not found|group.*not found|没有找到群|未找到群|不是群聊/.test(lower))
return 'CHAT_NOT_FOUND'
if (stage === 'persist' || /history|日报历史|保存.*日报/.test(lower))
return 'REPORT_HISTORY_SAVE_FAILED'
if (stage === 'send' || /wechat|微信|connector|发送/.test(lower)) return 'WECHAT_SEND_FAILED'
if (
stage === 'ai' &&
(status >= 500 || /fetch failed|econnrefused|enotfound|network|unavailable|service/.test(lower))
)
return 'AI_PROVIDER_UNAVAILABLE'
if (stage === 'ai') return 'REPORT_GENERATION_FAILED'
if (type.includes('provider') || status >= 500) return 'AI_PROVIDER_UNAVAILABLE'
return 'UNKNOWN'
}
export function normalizeScheduledReportError(
input: unknown,
fallbackStage: ScheduledReportExecutionStage = 'report'
): ScheduledReportErrorDetails {
const record = asRecord(input)
const source = (record && 'error' in record ? record.error : input) as unknown
const sourceRecord = asRecord(source)
const message = safeString(
sourceRecord?.message || sourceRecord?.error || sourceRecord?.detail || source
)
const details: ScheduledReportErrorInput = {
error: source,
code: record?.code || sourceRecord?.code,
status: record?.status || sourceRecord?.status,
type: record?.type || sourceRecord?.type,
stage: (record?.stage || fallbackStage) as ScheduledReportExecutionStage
}
const stage = details.stage || fallbackStage
const code = inferCode(message, stage, details)
const copy = messagesFor(code)
return {
code,
stage: code === 'UNKNOWN' ? stage : stageForCode(code),
technicalMessage: redact(message || '未知错误'),
...copy
}
}
export function legacyScheduledReportError(
code: ScheduledReportErrorCode,
message: string
): string {
const prefix: Record<ScheduledReportErrorCode, string> = {
AI_CONTEXT_LIMIT: 'report_generation_failed',
AI_QUOTA_EXCEEDED: 'report_generation_failed',
AI_AUTH_INVALID: 'report_generation_failed',
AI_PROVIDER_UNAVAILABLE: 'report_generation_failed',
AI_TIMEOUT: 'report_generation_failed',
DATABASE_UNAVAILABLE: 'report_generation_failed',
CHAT_NOT_FOUND: 'report_generation_failed',
NO_MESSAGES: 'report_generation_failed',
REPORT_GENERATION_FAILED: 'report_generation_failed',
REPORT_HISTORY_SAVE_FAILED: 'report_history_save_failed',
WECHAT_SEND_UNAVAILABLE: 'wechat_not_ready',
WECHAT_SEND_FAILED: 'wechat_send_failed',
WECHAT_AUTH_EXPIRED: 'wechat_send_failed',
UNKNOWN: 'scheduled_report_failed'
}
return `${prefix[code]}:${message}`
}