feat(travel): 接入远端 AI 推荐服务,支持 GLM real 模式与本地静默回退
- createPlan 在配置 VITE_TRAVEL_ASSISTANT_API_BASE_URL 时调用 FastAPI 推荐服务 做无坐标选点,路线/交通/时长仍由本机核算;远端不可用即静默回退本地规划 - 放开 loadPlan 对远端行程的回读限制(原 AI 冻结期禁用),行程页跳转不再丢行程 - planner/assistant 文案按是否启用远端如实切换,不再谎称「本期不调用 AI」 - 新增远端接通/回退/候选载荷单测;server/README 更新联调与 real 模式说明 - .env.development 默认指向已部署推荐服务;server/.env.example 补 GLM 示例 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,17 +1,25 @@
|
||||
import type { PoiRepository } from '@/domain/poi'
|
||||
import { getPoiRepository } from '@/data/poi'
|
||||
import {
|
||||
adjustLocalPlan,
|
||||
clonePlanResponse,
|
||||
cloneTravelPreferences,
|
||||
createLocalPlan,
|
||||
createRecommendedPlan,
|
||||
normalizePlanningRequest,
|
||||
normalizeTravelPreferences,
|
||||
type PlanningOrigin,
|
||||
type PlanningRecommendation,
|
||||
type PlanningRequest,
|
||||
type PlanResponse,
|
||||
type TravelPreferences,
|
||||
TravelValidationError,
|
||||
} from '@/domain/travel'
|
||||
import {
|
||||
createRemoteRecommendations,
|
||||
isRemoteTravelAssistantEnabled,
|
||||
type RemotePoiCandidateInput,
|
||||
} from './remote'
|
||||
import { loadPlan, loadPlanningRequest, loadPreferences, savePlanningRequest, savePreferences } from './storage'
|
||||
|
||||
interface LocalSession {
|
||||
@@ -82,6 +90,38 @@ function isPlanningRequest(input: TravelPreferences | PlanningRequest): input is
|
||||
return typeof input === 'object' && input !== null && 'mode' in input && 'preferences' in input
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the coordinate-free POI candidate whitelist sent to the remote service.
|
||||
* Quick mode offers every published POI; custom mode offers only the user's picks.
|
||||
* No coordinates ever leave the client.
|
||||
*/
|
||||
function buildRemoteCandidates(repository: PoiRepository, request: PlanningRequest): RemotePoiCandidateInput[] {
|
||||
const pois = request.mode === 'custom'
|
||||
? request.selectedPoiIds.flatMap(poiId => repository.getPoiById(poiId) ?? [])
|
||||
: repository.getPublishedPois()
|
||||
return pois.map(poi => ({
|
||||
id: poi.id,
|
||||
name: poi.name,
|
||||
categoryCode: poi.categoryCode,
|
||||
tagCodes: poi.tagCodes,
|
||||
summary: poi.summary,
|
||||
recommendationIndex: poi.recommendationIndex,
|
||||
}))
|
||||
}
|
||||
|
||||
/** Asks the remote service for POI recommendations, then schedules them locally. */
|
||||
async function createRemotePlan(
|
||||
request: PlanningRequest,
|
||||
repository: PoiRepository,
|
||||
): Promise<PlanResponse> {
|
||||
const candidates = buildRemoteCandidates(repository, request)
|
||||
const remote = await createRemoteRecommendations(request, candidates)
|
||||
// Array order is the suggested visit order; reasons travel with each POI.
|
||||
const recommendations: PlanningRecommendation[] = remote.recommendations.map(({ poiId, reason }) => ({ poiId, reason }))
|
||||
const source = remote.generationMode === 'real' ? 'remote_ai' : 'remote_mock'
|
||||
return createRecommendedPlan(request, recommendations, repository, remote.assistantMessage, source)
|
||||
}
|
||||
|
||||
export function createPlan(input: PlanningRequest): Promise<PlanResponse>
|
||||
export function createPlan(input: TravelPreferences, planningOrigin?: PlanningOrigin): Promise<PlanResponse>
|
||||
export async function createPlan(
|
||||
@@ -98,7 +138,23 @@ export async function createPlan(
|
||||
selectedPoiIds: [],
|
||||
...(normalizePlanningOrigin(planningOrigin) ? { origin: normalizePlanningOrigin(planningOrigin)! } : {}),
|
||||
}, repository)
|
||||
|
||||
if (isRemoteTravelAssistantEnabled()) {
|
||||
try {
|
||||
const plan = await createRemotePlan(request, repository)
|
||||
rememberLocalSession(plan, request)
|
||||
savePlanningRequest(request)
|
||||
return clonePlanResponse(plan, repository)
|
||||
}
|
||||
catch (error) {
|
||||
// Resilient POC default: never block the demo on a remote hiccup.
|
||||
console.warn('Remote travel assistant unavailable, falling back to local planner', error)
|
||||
}
|
||||
}
|
||||
|
||||
const plan = createLocalPlan(request, repository)
|
||||
if (isRemoteTravelAssistantEnabled())
|
||||
plan.assistantMessage = '远程行程服务暂不可用,已改用本机确定性规划生成路线。'
|
||||
rememberLocalSession(plan, request)
|
||||
savePlanningRequest(request)
|
||||
return clonePlanResponse(plan, repository)
|
||||
|
||||
Reference in New Issue
Block a user