Files
gmTouringMiniApp/src/services/travel-assistant/service.ts
T
周瑞哲 ac4179086c
AI Code Review / review (pull_request) Successful in 11m45s
Update AI planner integration, docs, and config
2026-07-30 16:33:39 +08:00

162 lines
5.7 KiB
TypeScript

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<string, LocalSession>()
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<PlanResponse>
export function createPlan(input: TravelPreferences, planningOrigin?: PlanningOrigin): Promise<PlanResponse>
export async function createPlan(
input: TravelPreferences | PlanningRequest,
planningOrigin?: PlanningOrigin,
): Promise<PlanResponse> {
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<PlanResponse> {
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)
}