import { getPoiRepository } from '@/data/poi' import { adjustLocalPlan, clonePlanResponse, cloneTravelPreferences, createLocalPlan, createRecommendedPlan, normalizePlanningRequest, normalizeTravelPreferences, type PlanningOrigin, type PlanningRequest, type PlanResponse, type TravelPreferences, TravelValidationError, } from '@/domain/travel' import { createRemoteRecommendations, isRemoteTravelAssistantEnabled } from './remote' import { loadPlan, loadPlanningRequest, loadPreferences, savePlanningRequest, savePreferences } from './storage' interface LocalSession { plan: PlanResponse preferences: TravelPreferences request: PlanningRequest planningOrigin: PlanningOrigin | null } const localSessions = new Map() function clonePlanningRequest(request: PlanningRequest, includeOrigin = true): PlanningRequest { return { mode: request.mode, preferences: cloneTravelPreferences(request.preferences), durationMinutes: request.durationMinutes, selectedPoiIds: [...request.selectedPoiIds], ...(includeOrigin && request.origin ? { origin: { ...request.origin } } : {}), } } function rememberLocalSession(plan: PlanResponse, request: PlanningRequest): void { const repository = getPoiRepository() localSessions.set(plan.conversationId, { plan: clonePlanResponse(plan, repository), preferences: cloneTravelPreferences(request.preferences), request: clonePlanningRequest(request), planningOrigin: request.origin ? { ...request.origin } : null, }) } function restoreLocalSession(conversationId: string): LocalSession | null { const remembered = localSessions.get(conversationId) if (remembered) return remembered const plan = loadPlan() const preferences = loadPreferences() if (!plan || !preferences || plan.conversationId !== conversationId) return null const storedRequest = loadPlanningRequest() rememberLocalSession(plan, storedRequest ?? { mode: plan.itinerary.planningMode, preferences, durationMinutes: plan.itinerary.requestedMinutes, selectedPoiIds: plan.itinerary.planningMode === 'custom' ? plan.itinerary.items.map(item => item.placeId) : [], }) return localSessions.get(conversationId) ?? null } function normalizePlanningOrigin(origin?: PlanningOrigin): PlanningOrigin | null { if (!origin || origin.coordinateSystem !== 'GCJ02' || !Number.isFinite(origin.longitude) || !Number.isFinite(origin.latitude) || origin.longitude < -180 || origin.longitude > 180 || origin.latitude < -90 || origin.latitude > 90) { return null } return { ...origin } } function isPlanningRequest(input: TravelPreferences | PlanningRequest): input is PlanningRequest { return typeof input === 'object' && input !== null && 'mode' in input && 'preferences' in input } export function createPlan(input: PlanningRequest): Promise export function createPlan(input: TravelPreferences, planningOrigin?: PlanningOrigin): Promise export async function createPlan( input: TravelPreferences | PlanningRequest, planningOrigin?: PlanningOrigin, ): Promise { const repository = getPoiRepository() const request = normalizePlanningRequest(isPlanningRequest(input) ? input : { mode: 'quick', preferences: normalizeTravelPreferences(input), durationMinutes: input.duration === 'half_day' ? 240 : 480, selectedPoiIds: [], ...(normalizePlanningOrigin(planningOrigin) ? { origin: normalizePlanningOrigin(planningOrigin)! } : {}), }, repository) if (isRemoteTravelAssistantEnabled()) { const candidates = repository.getPublishedPois().map(poi => ({ id: poi.id, name: poi.name, categoryCode: poi.categoryCode, tagCodes: poi.tagCodes, summary: poi.summary, recommendationIndex: poi.recommendationIndex, })) const remote = await createRemoteRecommendations(request, candidates) const plan = createRecommendedPlan( request, remote.recommendations, repository, remote.assistantMessage, remote.generationMode === 'real' ? 'remote_ai' : 'remote_mock', ) rememberLocalSession(plan, request) savePlanningRequest(request) return clonePlanResponse(plan, repository) } const plan = createLocalPlan(request, repository) rememberLocalSession(plan, request) savePlanningRequest(request) return clonePlanResponse(plan, repository) } export async function adjustPlan(conversationId: string, message: string): Promise { const normalizedConversationId = conversationId.trim() const normalizedMessage = message.trim() if (!normalizedConversationId) throw new TravelValidationError('会话 ID 不能为空') if (!normalizedMessage) throw new TravelValidationError('请填写需要调整的内容') if (normalizedMessage.length > 500) throw new TravelValidationError('调整内容不能超过 500 字') const repository = getPoiRepository() const session = restoreLocalSession(normalizedConversationId) if (!session) throw new Error('本机行程会话不存在,请重新生成行程') const adjusted = adjustLocalPlan( session.plan, session.preferences, normalizedMessage, repository, session.planningOrigin ?? undefined, session.request, ) if (session.plan.source !== 'local_poc') { adjusted.plan.assistantMessage = '已在本机按你的补充要求重新核算路线;如需 AI 重新选点,请返回规划页重新生成。' adjusted.plan.source = session.plan.source } rememberLocalSession(adjusted.plan, adjusted.request) savePlanningRequest(adjusted.request) savePreferences(adjusted.preferences) return clonePlanResponse(adjusted.plan, repository) }