feat: 行程卡片间显示交通路线描述和时间

- get_travel_time 解析 Amap segments 提取线路名和站数
- PlanItem 新增 transitToNext / transitDescription 字段
- finalize_plan schema 加入 transit_to_next_description
- 修复 Turbopack 中文引号解析报错
- UI 连接器改为两行布局,路线描述与时长分行显示
This commit is contained in:
2026-03-02 14:20:53 +08:00
parent 7b6ce22f63
commit e5a255a49e
4 changed files with 82 additions and 7 deletions
+14
View File
@@ -469,6 +469,20 @@ export default function BlindboxPlan({
setDraft({ ...item }); setDraft({ ...item });
}} }}
/> />
{/* Transit connector to next activity */}
{item.transitToNext != null && i < currentDay.items.length - 1 && (
<div className="flex items-start gap-1.5 py-2 pl-1">
<Navigation size={9} className="mt-0.5 shrink-0 text-purple-400/40" />
<div className="flex flex-col gap-0.5">
{item.transitDescription && (
<span className="text-[10px] leading-snug text-dim">
{item.transitDescription}
</span>
)}
<span className="text-[10px] text-dim/70"> {item.transitToNext} </span>
</div>
</div>
)}
</motion.div> </motion.div>
))} ))}
</SortableContext> </SortableContext>
+12 -1
View File
@@ -53,12 +53,15 @@ const SCHEDULE_SYSTEM_PROMPT = `你是一个周末行程规划师。根据用户
"lat": 31.2, "lat": 31.2,
"lng": 121.5, "lng": 121.5,
"duration": 120, "duration": 120,
"reason": "选择这个时间和地点的简短理由" "reason": "选择这个时间和地点的简短理由",
"transitToNext": 20,
"transitDescription": "地铁2号线(6站) → 地铁9号线(3站)"
} }
], ],
"summary": "一句话总结这个行程的亮点" "summary": "一句话总结这个行程的亮点"
} }
transitToNext 为从本活动结束到下一活动的预计交通时间(分钟),根据两点地理距离估算;transitDescription 为交通方式描述(如"地铁X号线"、"公交XXX路"、"步行");最后一项不填这两个字段。
按时间顺序排列。只返回 JSON。`; 按时间顺序排列。只返回 JSON。`;
export interface ScheduleContext { export interface ScheduleContext {
@@ -222,6 +225,14 @@ ${Object.entries(ctx.candidates)
lng: Number(item.lng) || 0, lng: Number(item.lng) || 0,
duration: Number(item.duration) || 60, duration: Number(item.duration) || 60,
reason: String(item.reason ?? ""), reason: String(item.reason ?? ""),
...(item.transitToNext != null && Number(item.transitToNext) > 0
? {
transitToNext: Math.round(Number(item.transitToNext)),
...(item.transitDescription
? { transitDescription: String(item.transitDescription) }
: {}),
}
: {}),
})), })),
summary: String(parsed.summary ?? ""), summary: String(parsed.summary ?? ""),
}; };
+52 -6
View File
@@ -110,6 +110,34 @@ function selectIdeasForSlots(ideas: TaggedIdea[], availableHours: number): Tagge
return selected; return selected;
} }
// ---------------------------------------------------------------------------
// Transit segment parser
// ---------------------------------------------------------------------------
function parseTransitSegments(
segments: Record<string, unknown>[],
): { description: string; mode: string } {
const parts: string[] = [];
let hasSubway = false;
let hasBus = false;
for (const seg of segments) {
const bus = seg.bus as { buslines?: Record<string, unknown>[] } | undefined;
if (!bus?.buslines?.length) continue;
for (const line of bus.buslines) {
const name = String(line.name ?? "");
const viaNum = Number(line.via_num ?? 0);
const isSubway = String(line.type ?? "").includes("地铁") || String(line.type ?? "").includes("轨道");
if (isSubway) hasSubway = true; else hasBus = true;
parts.push(viaNum > 0 ? `${name}(${viaNum}站)` : name);
}
}
const mode = hasSubway ? "地铁" : hasBus ? "公交" : "步行";
const description = parts.length > 0 ? parts.join(" → ") : mode;
return { description, mode };
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// POI search (shared by both paths) // POI search (shared by both paths)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -205,7 +233,7 @@ const AGENT_SYSTEM_PROMPT = `你是一个周末行程规划 Agent。你有以下
4. 如果搜索结果不理想(0结果或不相关),尝试换关键词重搜 4. 如果搜索结果不理想(0结果或不相关),尝试换关键词重搜
5. 用 get_travel_time 检查关键路段的公共交通时间,如果某段超过 45 分钟考虑换更近的地点 5. 用 get_travel_time 检查关键路段的公共交通时间,如果某段超过 45 分钟考虑换更近的地点
6. 综合地理位置、时间安排、活动特点,规划最优路线 6. 综合地理位置、时间安排、活动特点,规划最优路线
7. 确认无误后 finalize_plan 提交 7. 确认无误后 finalize_plan 提交,每个活动填写 transit_to_next_minutes 和 transit_to_next_description(直接复制 get_travel_time 返回的 durationMin 和 description,最后一项不填)
规划原则: 规划原则:
- 地理位置相近,最小化移动距离 - 地理位置相近,最小化移动距离
@@ -229,6 +257,8 @@ interface FinalizePlanDay {
lng: number; lng: number;
duration: number; duration: number;
reason: string; reason: string;
transit_to_next_minutes?: number;
transit_to_next_description?: string;
}[]; }[];
summary: string; summary: string;
} }
@@ -333,10 +363,10 @@ function buildAgentTools(
return JSON.stringify({ error: "未找到公交路线" }); return JSON.stringify({ error: "未找到公交路线" });
} }
const transit = data.route.transits[0]; const transit = data.route.transits[0];
return JSON.stringify({ const durationMin = Math.ceil(Number(transit.duration) / 60);
distanceKm: Math.round(Number(data.route.distance) / 100) / 10, const distanceKm = Math.round(Number(data.route.distance) / 100) / 10;
durationMin: Math.ceil(Number(transit.duration) / 60), const { description, mode } = parseTransitSegments(transit.segments ?? []);
}); return JSON.stringify({ durationMin, distanceKm, description, mode });
} catch { } catch {
return JSON.stringify({ error: "路线查询失败" }); return JSON.stringify({ error: "路线查询失败" });
} }
@@ -345,7 +375,7 @@ function buildAgentTools(
progressAfter: (_args, result) => { progressAfter: (_args, result) => {
const r = JSON.parse(result); const r = JSON.parse(result);
if (r.error) return `路线查询失败`; if (r.error) return `路线查询失败`;
return `公共交通${r.durationMin} 分钟(${r.distanceKm}km`; return `${r.description} ${r.durationMin} 分钟(${r.distanceKm}km`;
}, },
}; };
@@ -374,6 +404,14 @@ function buildAgentTools(
lng: { type: "number" }, lng: { type: "number" },
duration: { type: "number", description: "时长(分钟)" }, duration: { type: "number", description: "时长(分钟)" },
reason: { type: "string", description: "选择理由" }, reason: { type: "string", description: "选择理由" },
transit_to_next_minutes: {
type: "number",
description: "从本活动结束后到下一活动的预计交通时间(分钟)。最后一个活动不填",
},
transit_to_next_description: {
type: "string",
description: "到下一活动的交通方式描述,直接使用 get_travel_time 返回的 description 字段值,例如:地铁2号线(6站) → 地铁9号线(3站)。最后一个活动不填",
},
}, },
required: ["time", "activity", "poi", "address", "lat", "lng", "duration", "reason"], required: ["time", "activity", "poi", "address", "lat", "lng", "duration", "reason"],
}, },
@@ -440,6 +478,14 @@ async function runAgentPlanGeneration(
lng: Number(item.lng) || 0, lng: Number(item.lng) || 0,
duration: Number(item.duration) || 60, duration: Number(item.duration) || 60,
reason: String(item.reason ?? ""), reason: String(item.reason ?? ""),
...(item.transit_to_next_minutes != null && Number(item.transit_to_next_minutes) > 0
? {
transitToNext: Math.round(Number(item.transit_to_next_minutes)),
...(item.transit_to_next_description
? { transitDescription: String(item.transit_to_next_description) }
: {}),
}
: {}),
})) }))
: [], : [],
summary: String(day.summary ?? ""), summary: String(day.summary ?? ""),
+4
View File
@@ -113,6 +113,10 @@ export interface PlanItem {
lng: number; lng: number;
duration: number; duration: number;
reason: string; reason: string;
/** 到下一个活动的估计交通时间(分钟),最后一项无此字段 */
transitToNext?: number;
/** 到下一个活动的交通方式描述,如"地铁2号线→9号线" */
transitDescription?: string;
} }
export interface WeekendPlanData { export interface WeekendPlanData {