import { onUnmounted, ref, watch } from 'vue' import { ElMessage } from 'element-plus' import type { AiAdvice } from '../types' import { fetchDeepSeekAdvice } from '../api/deepseek/analyze' import type { AiAnalysisPayload } from '../api/deepseek/types' import { useQuotaStore, type KlinePeriod } from '../stores/quota' import { useContractStore } from '../stores/contract' import { useSettingsStore } from '../stores/settings' import { isWsTradingSession } from '../utils/tradingDate' import { useTitleBlink } from './useTitleBlink' /** 全局额外关键字 + 当前合约关键字(合约段追加在后) */ function mergeKeywords(globalKeywords: string, contractKeywords: string): string { const global = globalKeywords.trim() const extra = contractKeywords.trim() if (!extra) return global if (!global) return extra return `${global}\n${extra}` } const KLINE_PERIODS: KlinePeriod[] = ['day', 'week', 'month'] /** 调度轮询间隔:检查距上次执行是否已超过设定分钟 */ const SCHEDULE_TICK_MS = 15_000 function emptyAdvice(): AiAdvice { return { action: '—', direction: 'neutral', confidence: 0, summary: '点击「一键获取建议」获取 AI 分析', reasons: [], updatedAt: '', } } export function useAiAdvice() { const data = ref(emptyAdvice()) const loading = ref(false) const error = ref(null) /** 上次成功执行分析的时间戳(ms) */ const lastExecutedAt = ref(null) const quota = useQuotaStore() const contractStore = useContractStore() const settings = useSettingsStore() useTitleBlink(data) let scheduleTimer: ReturnType | null = null /** 切换合约时递增,丢弃进行中的旧分析结果 */ let analysisEpoch = 0 function clearAdvice() { data.value = emptyAdvice() error.value = null lastExecutedAt.value = null } watch( () => contractStore.selectedId, () => { analysisEpoch += 1 loading.value = false clearAdvice() }, ) /** 尽量补全日/周/月 K 线,失败不阻断主流程 */ async function ensureKlines() { await Promise.all( KLINE_PERIODS.map(async (period) => { try { await quota.loadKline(period) } catch { /* 已在 store 内打日志 */ } }), ) } /** * 一键获取建议:收集行情 / 新闻 / 持仓后提交 DeepSeek,返回真实 AI 建议。 * @param opts.silent 定时任务时静默:无 Key / 非开盘 / 进行中则跳过,成功不弹 toast */ async function refresh(opts?: { silent?: boolean }) { const silent = Boolean(opts?.silent) // 定时分析仅在开盘时段执行(与行情 WS 时段一致) if (silent && !isWsTradingSession()) return const apiKey = settings.apiKey.trim() if (!apiKey) { if (!silent) ElMessage.warning('请先在设置中配置 DeepSeek API Key') return } if (loading.value) return const epoch = analysisEpoch loading.value = true error.value = null try { await quota.fetchForAnalysis() if (epoch !== analysisEpoch) return await ensureKlines() if (epoch !== analysisEpoch) return const payload: AiAnalysisPayload = { keywords: mergeKeywords(settings.keywords, contractStore.current.keywords), snapshot: quota.getAnalysisSnapshot(), } console.log('[AI] 提交给 DeepSeek 的关键词', payload.keywords) console.log('[AI] 提交给 DeepSeek 的数据快照', payload.snapshot) const advice = await fetchDeepSeekAdvice(apiKey, payload) if (epoch !== analysisEpoch) return data.value = advice lastExecutedAt.value = Date.now() if (!silent) ElMessage.success('AI 建议已更新') } catch (e) { if (epoch !== analysisEpoch) return error.value = e const msg = formatError(e) console.error('[AI] 一键获取建议失败', e) if (!silent) ElMessage.error(msg) } finally { if (epoch === analysisEpoch) loading.value = false } } /** 距上次成功执行是否已满设定间隔(从未执行则视为应执行) */ function shouldRunScheduled(): boolean { if (!settings.scheduledAnalysisEnabled) return false if (!isWsTradingSession()) return false if (loading.value) return false if (!settings.apiKey.trim()) return false const minutes = Math.max(1, settings.scheduledAnalysisMinutes || 5) const last = lastExecutedAt.value if (last == null) return true return Date.now() - last >= minutes * 60_000 } function tickSchedule() { if (shouldRunScheduled()) { void refresh({ silent: true }) } } function clearSchedule() { if (scheduleTimer) { clearInterval(scheduleTimer) scheduleTimer = null } } function setupSchedule() { clearSchedule() if (!settings.scheduledAnalysisEnabled) return // 开启后立即检查一次,之后按短周期轮询「是否已超过间隔」 tickSchedule() scheduleTimer = setInterval(tickSchedule, SCHEDULE_TICK_MS) } watch( () => [settings.scheduledAnalysisEnabled, settings.scheduledAnalysisMinutes] as const, () => setupSchedule(), { immediate: true }, ) onUnmounted(clearSchedule) return { data, loading, error, lastExecutedAt, refresh } } function formatError(e: unknown): string { if (typeof e === 'object' && e && 'response' in e) { const resp = (e as { response?: { status?: number; data?: { error?: { message?: string } } } }) .response const apiMsg = resp?.data?.error?.message if (apiMsg) return `DeepSeek:${apiMsg}` if (resp?.status) return `DeepSeek 请求失败(HTTP ${resp.status})` } if (e instanceof Error) return e.message return '获取 AI 建议失败' }