forked from zhouruizhe/gmTouringMiniApp
Update AI planner integration, docs, and config
AI Code Review / review (pull_request) Successful in 11m45s
AI Code Review / review (pull_request) Successful in 11m45s
This commit is contained in:
@@ -254,6 +254,9 @@ function selectQuickPois(
|
||||
cursor = next
|
||||
}
|
||||
|
||||
if (selected.length === 0 && remaining.length > 0)
|
||||
selected.push(rankedCandidates(remaining, preferences, [], origin)[0])
|
||||
|
||||
return selected
|
||||
}
|
||||
|
||||
@@ -312,7 +315,9 @@ function selectRecommendedPois(
|
||||
: recommendedPois
|
||||
}
|
||||
|
||||
const remaining = [...recommendedPois]
|
||||
const remaining = origin
|
||||
? orderPoisFromOrigin(recommendedPois, origin)
|
||||
: [...recommendedPois]
|
||||
const selected: PoiResolved[] = []
|
||||
let usedMinutes = 0
|
||||
let cursor: PlanningOrigin | PoiResolved | null = origin
|
||||
@@ -326,6 +331,8 @@ function selectRecommendedPois(
|
||||
usedMinutes += transfer + visit
|
||||
cursor = next
|
||||
}
|
||||
if (selected.length === 0 && recommendedPois.length > 0)
|
||||
selected.push(remaining[0] ?? recommendedPois[0])
|
||||
return selected
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -61,7 +61,7 @@
|
||||
"startLocationUpdate",
|
||||
"onLocationChange"
|
||||
],
|
||||
"projectname": "【dev】光明文旅地图"
|
||||
"projectname": "【prod】光明文旅地图"
|
||||
},
|
||||
"mp-alipay": {
|
||||
"usingComponents": true
|
||||
|
||||
+1
-1
@@ -55,7 +55,7 @@
|
||||
"type": "page",
|
||||
"layout": "map",
|
||||
"style": {
|
||||
"navigationBarTitleText": "AI 路线规划",
|
||||
"navigationBarTitleText": "智能路线规划",
|
||||
"navigationBarBackgroundColor": "#ffffff",
|
||||
"navigationBarTextStyle": "black",
|
||||
"backgroundColor": "#f4f8f6"
|
||||
|
||||
@@ -110,6 +110,13 @@ const routeTags = computed(() => {
|
||||
})
|
||||
|
||||
const planningModeLabel = computed(() => plan.value?.itinerary.planningMode === 'custom' ? '自选规划' : '快速规划')
|
||||
const planningSourceLabel = computed(() => {
|
||||
if (plan.value?.source === 'remote_ai')
|
||||
return '真实 AI 推荐'
|
||||
if (plan.value?.source === 'remote_mock')
|
||||
return '远端演示规划'
|
||||
return '本地智能规划'
|
||||
})
|
||||
const durationFitNotice = computed(() => plan.value ? getDurationFitNotice(plan.value.itinerary.durationFitStatus) : null)
|
||||
|
||||
function loadStoredPlan() {
|
||||
@@ -294,7 +301,7 @@ onReady(initializeMap)
|
||||
<view class="route-hero__top">
|
||||
<view class="route-hero__label">
|
||||
<text class="route-hero__spark">✦</text>
|
||||
{{ planningModeLabel }}
|
||||
{{ planningModeLabel }} · {{ planningSourceLabel }}
|
||||
</view>
|
||||
<button @click="openPlanner">重新规划</button>
|
||||
</view>
|
||||
|
||||
+16
-11
@@ -2,7 +2,7 @@
|
||||
import type { PoiSummary } from '@/domain/poi'
|
||||
import type { BudgetLevel, Interest, Pace, PlanningRequest, Theme, Transport, TravelPreferences } from '@/domain/travel'
|
||||
import { getPoiRepository } from '@/data/poi'
|
||||
import { createPlan, isRemoteTravelAssistantEnabled, loadPreferences, savePlan, savePreferences } from '@/services/travel-assistant'
|
||||
import { createPlan, isRemoteTravelAssistantEnabled, loadPlanningRequest, loadPreferences, savePlan, savePreferences } from '@/services/travel-assistant'
|
||||
import { useLocationStore } from '@/stores'
|
||||
|
||||
interface PlannerPreset {
|
||||
@@ -74,8 +74,8 @@ const durationText = computed(() => {
|
||||
})
|
||||
const usesCurrentLocation = computed(() => locationAvailable.value
|
||||
&& (planningMode.value === 'quick' || customStartsFromCurrentLocation.value))
|
||||
const planningEngineLabel = computed(() => remoteEnabled.value && planningMode.value === 'quick'
|
||||
? '在线 AI 服务'
|
||||
const planningEngineLabel = computed(() => remoteEnabled.value
|
||||
? '在线智能服务'
|
||||
: '本地智能规划')
|
||||
const locationActionText = computed(() => {
|
||||
if (locationStore.status === 'locating')
|
||||
@@ -151,9 +151,6 @@ function toggleInterest(interest: Interest) {
|
||||
|
||||
function setPlanningMode(mode: PlanningRequest['mode']) {
|
||||
planningMode.value = mode
|
||||
if (mode === 'custom' && remoteEnabled.value) {
|
||||
uni.showToast({ title: '自选点位将使用本地智能规划', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
function togglePoi(poiId: string) {
|
||||
@@ -333,10 +330,18 @@ async function submit() {
|
||||
}
|
||||
|
||||
onShow(() => {
|
||||
const stored = loadPreferences()
|
||||
if (stored) {
|
||||
assignPreferences(stored)
|
||||
durationMinutes.value = stored.duration === 'half_day' ? 4 * 60 : 8 * 60
|
||||
const storedRequest = loadPlanningRequest()
|
||||
const storedPreferences = storedRequest?.preferences ?? loadPreferences()
|
||||
if (storedPreferences)
|
||||
assignPreferences(storedPreferences)
|
||||
if (storedRequest) {
|
||||
planningMode.value = storedRequest.mode
|
||||
durationMinutes.value = storedRequest.durationMinutes
|
||||
selectedPoiIds.value = [...storedRequest.selectedPoiIds]
|
||||
customStartsFromCurrentLocation.value = false
|
||||
}
|
||||
else if (storedPreferences) {
|
||||
durationMinutes.value = storedPreferences.duration === 'half_day' ? 4 * 60 : 8 * 60
|
||||
}
|
||||
applyPreset()
|
||||
})
|
||||
@@ -390,7 +395,7 @@ onShow(() => {
|
||||
</button>
|
||||
</view>
|
||||
<text v-if="remoteEnabled && planningMode === 'custom'" class="planning-mode-tip">
|
||||
自选点位不依赖在线服务,将使用本地智能规划。
|
||||
在线 AI 将在你选择的点位内建议顺序,本机负责定位、交通与时长核算。
|
||||
</text>
|
||||
</view>
|
||||
|
||||
|
||||
@@ -2,10 +2,13 @@ export { isRemoteTravelAssistantEnabled } from './remote'
|
||||
export { adjustPlan, createPlan } from './service'
|
||||
export {
|
||||
loadPlan,
|
||||
loadPlanningRequest,
|
||||
loadPreferences,
|
||||
PLAN_STORAGE_KEY,
|
||||
PLANNING_REQUEST_STORAGE_KEY,
|
||||
PREFERENCES_STORAGE_KEY,
|
||||
savePlan,
|
||||
savePlanningRequest,
|
||||
savePreferences,
|
||||
} from './storage'
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
cloneTravelPreferences,
|
||||
createLocalPlan,
|
||||
createRecommendedPlan,
|
||||
normalizePlanResponse,
|
||||
normalizePlanningRequest,
|
||||
normalizeTravelPreferences,
|
||||
type PlanningOrigin,
|
||||
@@ -15,7 +14,7 @@ import {
|
||||
TravelValidationError,
|
||||
} from '@/domain/travel'
|
||||
import { createRemoteRecommendations, isRemoteTravelAssistantEnabled } from './remote'
|
||||
import { loadPlan, loadPreferences, savePreferences } from './storage'
|
||||
import { loadPlan, loadPlanningRequest, loadPreferences, savePlanningRequest, savePreferences } from './storage'
|
||||
|
||||
interface LocalSession {
|
||||
plan: PlanResponse
|
||||
@@ -55,7 +54,8 @@ function restoreLocalSession(conversationId: string): LocalSession | null {
|
||||
const preferences = loadPreferences()
|
||||
if (!plan || !preferences || plan.conversationId !== conversationId)
|
||||
return null
|
||||
rememberLocalSession(plan, {
|
||||
const storedRequest = loadPlanningRequest()
|
||||
rememberLocalSession(plan, storedRequest ?? {
|
||||
mode: plan.itinerary.planningMode,
|
||||
preferences,
|
||||
durationMinutes: plan.itinerary.requestedMinutes,
|
||||
@@ -118,11 +118,13 @@ export async function createPlan(
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -153,6 +155,7 @@ export async function adjustPlan(conversationId: string, message: string): Promi
|
||||
adjusted.plan.source = session.plan.source
|
||||
}
|
||||
rememberLocalSession(adjusted.plan, adjusted.request)
|
||||
savePlanningRequest(adjusted.request)
|
||||
savePreferences(adjusted.preferences)
|
||||
return clonePlanResponse(adjusted.plan, repository)
|
||||
}
|
||||
|
||||
@@ -4,12 +4,14 @@ import {
|
||||
cloneTravelPreferences,
|
||||
normalizePlanResponse,
|
||||
normalizeTravelPreferences,
|
||||
type PlanningRequest,
|
||||
type PlanResponse,
|
||||
type TravelPreferences,
|
||||
} from '@/domain/travel'
|
||||
|
||||
export const PLAN_STORAGE_KEY = 'guangming:last-plan'
|
||||
export const PREFERENCES_STORAGE_KEY = 'guangming:last-preferences'
|
||||
export const PLANNING_REQUEST_STORAGE_KEY = 'guangming:last-planning-request'
|
||||
|
||||
const STORAGE_SCHEMA_VERSION = 1
|
||||
|
||||
@@ -99,3 +101,68 @@ export function loadPreferences(): TravelPreferences | null {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function savePlanningRequest(request: PlanningRequest): void {
|
||||
const repository = getPoiRepository()
|
||||
const sanitized: PlanningRequest = {
|
||||
mode: request.mode,
|
||||
preferences: normalizeTravelPreferences(request.preferences),
|
||||
durationMinutes: request.durationMinutes,
|
||||
selectedPoiIds: [...request.selectedPoiIds],
|
||||
}
|
||||
if (sanitized.mode !== 'quick' && sanitized.mode !== 'custom')
|
||||
throw new Error('无法保存规划条件:规划模式无效')
|
||||
if (!Number.isInteger(sanitized.durationMinutes)
|
||||
|| sanitized.durationMinutes < 120
|
||||
|| sanitized.durationMinutes > 600
|
||||
|| sanitized.durationMinutes % 30 !== 0) {
|
||||
throw new Error('无法保存规划条件:规划时长无效')
|
||||
}
|
||||
if (sanitized.mode === 'quick' && sanitized.selectedPoiIds.length > 0)
|
||||
throw new Error('无法保存规划条件:快速规划不能预选点位')
|
||||
if (sanitized.mode === 'custom'
|
||||
&& (sanitized.selectedPoiIds.length < 2 || sanitized.selectedPoiIds.length > 8)) {
|
||||
throw new Error('无法保存规划条件:自选点位数量无效')
|
||||
}
|
||||
if (new Set(sanitized.selectedPoiIds).size !== sanitized.selectedPoiIds.length
|
||||
|| sanitized.selectedPoiIds.some(poiId => !repository.getPoiById(poiId))) {
|
||||
throw new Error('无法保存规划条件:自选点位无效')
|
||||
}
|
||||
writeStorage(PLANNING_REQUEST_STORAGE_KEY, createEnvelope(sanitized))
|
||||
}
|
||||
|
||||
export function loadPlanningRequest(): PlanningRequest | null {
|
||||
const repository = getPoiRepository()
|
||||
const envelope = readEnvelope(readStorage(PLANNING_REQUEST_STORAGE_KEY))
|
||||
if (!envelope || envelope.datasetVersion !== repository.getDatasetMeta().datasetVersion || !isRecord(envelope.payload))
|
||||
return null
|
||||
try {
|
||||
const payload = envelope.payload
|
||||
const request: PlanningRequest = {
|
||||
mode: payload.mode as PlanningRequest['mode'],
|
||||
preferences: normalizeTravelPreferences(payload.preferences),
|
||||
durationMinutes: Number(payload.durationMinutes),
|
||||
selectedPoiIds: Array.isArray(payload.selectedPoiIds) ? payload.selectedPoiIds.map(String) : [],
|
||||
}
|
||||
if (request.mode !== 'quick' && request.mode !== 'custom')
|
||||
return null
|
||||
if (!Number.isInteger(request.durationMinutes)
|
||||
|| request.durationMinutes < 120
|
||||
|| request.durationMinutes > 600
|
||||
|| request.durationMinutes % 30 !== 0) {
|
||||
return null
|
||||
}
|
||||
if (request.mode === 'quick' && request.selectedPoiIds.length > 0)
|
||||
return null
|
||||
if (request.mode === 'custom' && (request.selectedPoiIds.length < 2 || request.selectedPoiIds.length > 8))
|
||||
return null
|
||||
if (new Set(request.selectedPoiIds).size !== request.selectedPoiIds.length
|
||||
|| request.selectedPoiIds.some(poiId => !repository.getPoiById(poiId))) {
|
||||
return null
|
||||
}
|
||||
return request
|
||||
}
|
||||
catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user