forked from zhouruizhe/gmTouringMiniApp
feat(travel): 接入远端 AI 推荐服务,支持 GLM real 模式与本地静默回退
AI Code Review / review (pull_request) Successful in 1s
AI Code Review / review (pull_request) Successful in 1s
- 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,4 +1,4 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { getPoiRepository } from '../src/data/poi'
|
||||
import {
|
||||
adjustLocalPlan,
|
||||
@@ -550,12 +550,14 @@ describe('travel storage', () => {
|
||||
expect(loadPlan()).toBeNull()
|
||||
})
|
||||
|
||||
it('does not restore remote plans while AI planning is frozen', () => {
|
||||
it('restores remote plans so the itinerary survives navigation', () => {
|
||||
const plan = createLocalPlan(preferences(), getPoiRepository())
|
||||
const remotePlan = { ...plan, source: 'remote_ai' as const }
|
||||
values.set(PLAN_STORAGE_KEY, storageEnvelope(remotePlan))
|
||||
|
||||
expect(loadPlan()).toBeNull()
|
||||
const loaded = loadPlan()
|
||||
expect(loaded).not.toBeNull()
|
||||
expect(loaded?.source).toBe('remote_ai')
|
||||
})
|
||||
|
||||
it('contains storage read failures and reports write failures', () => {
|
||||
@@ -571,3 +573,88 @@ describe('travel storage', () => {
|
||||
expect(() => savePreferences(preferences())).toThrow('无法保存本机行程数据:quota exceeded')
|
||||
})
|
||||
})
|
||||
|
||||
describe('remote travel assistant integration', () => {
|
||||
const repository = getPoiRepository()
|
||||
const publishedIds = repository.getPublishedPois().map(poi => poi.id)
|
||||
|
||||
beforeEach(() => {
|
||||
// createPlan persists the planning request; keep storage harmless and isolated
|
||||
// from the "quota exceeded" implementation left behind by the storage suite.
|
||||
vi.mocked(uni.setStorageSync).mockImplementation(() => {})
|
||||
vi.mocked(uni.getStorageSync).mockImplementation(() => undefined)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs()
|
||||
vi.mocked(uni.request).mockReset()
|
||||
vi.mocked(uni.setStorageSync).mockReset()
|
||||
vi.mocked(uni.getStorageSync).mockReset()
|
||||
})
|
||||
|
||||
function quickRequest(origin = true): PlanningRequest {
|
||||
return {
|
||||
mode: 'quick',
|
||||
preferences: preferences(),
|
||||
durationMinutes: 240,
|
||||
selectedPoiIds: [],
|
||||
...(origin ? { origin: { longitude: 113.94, latitude: 22.76, coordinateSystem: 'GCJ02' } } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
it('routes createPlan through the remote service, maps generation mode, and leaks no coordinates', async () => {
|
||||
vi.stubEnv('VITE_TRAVEL_ASSISTANT_API_BASE_URL', 'http://localhost:8000')
|
||||
let postedBody: { candidates?: Array<Record<string, unknown>>, origin?: unknown }
|
||||
vi.mocked(uni.request).mockImplementation((options: { data?: unknown, success: (res: unknown) => void }) => {
|
||||
postedBody = options.data as typeof postedBody
|
||||
options.success({
|
||||
statusCode: 200,
|
||||
data: {
|
||||
requestId: 'req-remote-1',
|
||||
mode: 'quick',
|
||||
assistantMessage: '远端演示推荐完成',
|
||||
generationMode: 'mock',
|
||||
recommendations: publishedIds.slice(0, 3).map((poiId, index) => ({
|
||||
poiId,
|
||||
reason: `远端推荐理由 ${index + 1}`,
|
||||
order: index + 1,
|
||||
})),
|
||||
},
|
||||
})
|
||||
return {} as never
|
||||
})
|
||||
|
||||
const plan = await createPlan(quickRequest())
|
||||
|
||||
expect(plan.source).toBe('remote_mock')
|
||||
expect(plan.assistantMessage).toBe('远端演示推荐完成')
|
||||
expect(plan.itinerary.items.every(item => item.reason.includes('远端推荐理由'))).toBe(true)
|
||||
// The current location never leaves the client…
|
||||
expect(postedBody!.origin).toBeUndefined()
|
||||
expect(JSON.stringify(postedBody!)).not.toContain('113.94')
|
||||
expect(JSON.stringify(postedBody!)).not.toContain('22.76')
|
||||
// …candidates are coordinate-free and cover every published POI…
|
||||
expect(postedBody!.candidates).toHaveLength(publishedIds.length)
|
||||
const candidateKeys = new Set(postedBody!.candidates!.flatMap(candidate => Object.keys(candidate)))
|
||||
expect(candidateKeys.has('longitude')).toBe(false)
|
||||
expect(candidateKeys.has('latitude')).toBe(false)
|
||||
expect(candidateKeys.has('summary')).toBe(true)
|
||||
// …and the resulting plan carries no coordinates either.
|
||||
expect(JSON.stringify(plan)).not.toContain('113.94')
|
||||
expect(JSON.stringify(plan)).not.toContain('22.76')
|
||||
})
|
||||
|
||||
it('falls back to the local planner when the remote service is unreachable', async () => {
|
||||
vi.stubEnv('VITE_TRAVEL_ASSISTANT_API_BASE_URL', 'http://localhost:8000')
|
||||
vi.mocked(uni.request).mockImplementation((options: { fail: () => void }) => {
|
||||
options.fail()
|
||||
return {} as never
|
||||
})
|
||||
|
||||
const plan = await createPlan(quickRequest(false))
|
||||
|
||||
expect(vi.mocked(uni.request)).toHaveBeenCalled()
|
||||
expect(plan.source).toBe('local_poc')
|
||||
expect(plan.assistantMessage).toContain('已改用本机')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user