用于本项目的单巡逻组巡逻路线求解。只要用户提到巡逻路线规划、巡点排序、班次内排路线、时间窗、配对组、工歇、起终点固定或开放、无解判断、冲突返回,就要优先使用这个 skill。即使用户没有点名 skill,只要需求本质是“给定一组巡点和约束,生成单组巡逻路线或判断无解”,也应触发。
这个 skill 用于处理单巡逻组、单条路线的巡逻路径求解。
它依赖现有 point-route-estimator skill 生成两点距离/时间矩阵,然后调用本目录下的启发式求解脚本:
scripts/solve_patrol_route.py这个 skill 的目标不是生成“看起来像对的路线”,而是做严格保守的可行性判断。首版实现采用接近 OR-Tools CP-SAT 求解风格的启发式流程:
在单巡逻组场景下,目标能力边界是约 100 个巡逻点。
它的输出规则仍然保持保守:
feasibleconflictunsupported在以下场景使用本 skill:
在以下场景不要硬套本 skill:
遇到超范围场景时,不要猜测路线,直接返回 unsupported 或先明确说明超出首版能力边界。
本 skill 明确分两层职责:
point-route-estimator:负责两点距离和耗时估算patrol-route-solver:负责矩阵编排、约束校验、启发式求解和结果封装不要在本 skill 里重写两点估算规则。需要矩阵时,逐对调用 point-route-estimator,收集:
distance_kmduration_min然后组装成:
{
"source": "point-route-estimator",
"time_minutes": {
"node_a": {"node_b": 12}
},
"distance_km": {
"node_a": {"node_b": 6.8}
}
}
优先把用户输入归一化成下面这些字段:
patrol_group_idmodeshift_startshift_endobjectivestart_point 可选end_point 可选break_windowsstopslicense_plate 可选vehicle_type 可选默认目标固定为:
{
"objective": "min_total_travel_time"
}
固定起点或终点时,使用:
{
"node_id": "station",
"name": "派出所"
}
四种模式都支持:
如果某一端开放,不要擅自补成派出所。
每个巡点至少包含:
{
"stop_id": "s1",
"name": "巡点1",
"lat": 39.9042,
"lng": 116.4074,
"dwell_min": 5,
"time_window": {
"start": "08:20",
"end": "08:40"
},
"pair_group_id": "pair-1"
}
其中:
lat / lng 用于调用 point-route-estimator 生成矩阵dwell_min 为必填time_window 可选pair_group_id 可选先做范围检查,再决定是否继续:
shift_start / shift_end / dwell_min / 巡点坐标,先追问unsupportedunsupported追问尽量短,例如:
“当前还缺班次时间、巡点坐标或停留时长,我没法可靠生成矩阵和路线。请补充这些字段后我再继续。”
严格按下面顺序执行:
point-route-estimator 逐对生成 time_minutes / distance_kmscripts/solve_patrol_route.py需要纳入矩阵的节点包括:
start_point,如果存在end_point,如果存在如果起点或终点开放,就不要为开放端额外造节点。
不要手工自己排配对组顺序。只需要把原始巡点的 pair_group_id 保留到求解请求里,脚本会:
优先使用本地脚本,而不是在对话里手工穷举。
脚本路径:
scripts/solve_patrol_route.py脚本输入是完整 JSON 请求,最少包含:
{
"patrol_group_id": "A01",
"mode": "车巡",
"shift_start": "08:00",
"shift_end": "12:00",
"objective": "min_total_travel_time",
"start_point": {"node_id": "station", "name": "派出所"},
"end_point": {"node_id": "station", "name": "派出所"},
"break_windows": [{"start": "10:00", "end": "10:20"}],
"stops": [
{"stop_id": "s1", "name": "巡点1", "dwell_min": 5, "time_window": {"start": "08:20", "end": "08:40"}},
{"stop_id": "s2", "name": "巡点2", "dwell_min": 5, "pair_group_id": "pair-1"}
],
"matrix": {
"source": "point-route-estimator",
"time_minutes": {},
"distance_km": {}
}
}
如果可以落临时文件,优先运行:
python .codex\skills\patrol-route-solver\scripts\solve_patrol_route.py --input-json <payload.json>
如果只能传内联 JSON,也可以使用:
python .codex\skills\patrol-route-solver\scripts\solve_patrol_route.py --json '<compact-json>'
默认返回 JSON。字段固定为:
statusobjectivematrix_sourceroutesummaryconflictinput_snapshotmatrix_snapshotassumptionsstatus = feasible{
"status": "feasible",
"objective": "min_total_travel_time",
"matrix_source": "point-route-estimator",
"route": [
{
"node_id": "station",
"name": "派出所",
"node_type": "start",
"arrival_time": "08:00",
"dwell_min": 0
},
{
"node_id": "s1",
"name": "巡点1",
"node_type": "stop",
"arrival_time": "08:12",
"dwell_min": 5,
"pair_group_id": "",
"time_window": {"start": "08:20", "end": "08:40"}
}
],
"summary": {
"start_time": "08:00",
"end_time": "09:10",
"total_distance_km": 12.4,
"total_travel_min": 34,
"total_dwell_min": 15,
"total_wait_min": 21,
"total_duration_min": 70,
"shift_overrun": false,
"shift_overrun_min": 0
},
"conflict": "",
"input_snapshot": {
"patrol_group_id": "A01",
"mode": "车巡",
"objective": "min_total_travel_time",
"shift_start": "08:00",
"shift_end": "12:00",
"start_point": {"node_id": "station", "name": "派出所", "coordinate": [116.4074, 39.9042]},
"end_point": {"node_id": "station", "name": "派出所", "coordinate": [116.4074, 39.9042]},
"break_windows": [{"start": "10:00", "end": "10:20"}],
"stops": [
{
"stop_id": "s1",
"name": "巡点1",
"coordinate": [116.4074, 39.9042],
"dwell_min": 5,
"time_window": {"start": "08:20", "end": "08:40"},
"pair_group_id": ""
}
]
},
"matrix_snapshot": {
"source": "point-route-estimator",
"nodes": [
{
"node_id": "station",
"name": "派出所",
"node_type": "start",
"coordinate": [116.4074, 39.9042]
},
{
"node_id": "s1",
"name": "巡点1",
"node_type": "stop",
"coordinate": [116.4174, 39.9142]
}
],
"edges": [
{
"from_node_id": "station",
"to_node_id": "s1",
"from_coordinate": [116.4074, 39.9042],
"to_coordinate": [116.4174, 39.9142],
"distance_km": 1.4,
"duration_min": 2.1,
"data_source": "point-route-estimator"
}
]
},
"assumptions": [
"默认优化目标为 min_total_travel_time"
]
}
input_snapshot 用于回显本次求解使用的输入关键信息,方便 demo 展示、调试比对和结果归档。它至少包含:
start_pointend_pointbreak_windowsstops其中 start_point、end_point 和 stops 的坐标统一按 demo 数据约定回显为 coordinate: [lng, lat]。stops 逐条保留原始巡逻点输入信息:stop_id、name、coordinate、dwell_min、time_window、pair_group_id。
matrix_snapshot 用于承接 point-route-estimator 的矩阵产物,方便后续地图渲染:
nodes:所有渲染点位,含起点、终点、巡点,统一使用 coordinate: [lng, lat]edges:两点估算边,至少包含 from_node_id、to_node_id、from_coordinate、to_coordinate、distance_km、duration_min、data_source输出 JSON 不允许出现 null。凡是缺省值、空对象位或无约束字段,统一回写为 ""。
route 中的巡点节点也要回显时间窗信息:
"time_window": {"start": "...", "end": "..."}"time_window": ""status = conflictconflict.type 只能从下面这些值里选:
time_window_conflictpair_time_window_conflictbreak_window_conflictshift_overrunmissing_required_datastatus = unsupportedconflict.type 固定为:
unsupported_scope始终遵守这些规则:
unsupported如果用户明确不要 JSON,可以在保持底层判断口径不变的前提下转成 Markdown,例如:
线路可行。巡逻组 A01 从 08:00 出发,按当前矩阵估算会先到巡点 3,再到巡点 7,最后于 11:55 返回派出所。总行驶时间 130 分钟,总停留时长 45 分钟,总耗时 175 分钟,未超班次。
但不要省略关键事实:
当用户说:
“给 A01 组排一条上午 8 点到 12 点的车巡路线,起终点都是派出所,4 个巡点都要覆盖。”
你应该:
point-route-estimator 生成矩阵solve_patrol_route.py当用户说:
“这个巡点必须 08:02 到 08:04 到达,起点在派出所,开车过去要 10 分钟。”
你应该返回 conflict,而不是给一条勉强路线。
当用户说:
“把 3 个巡逻组一起规划出来,再顺便帮我做组间点位分配。”
你应该明确说明这超出本 skill 的首版范围,并返回或描述 unsupported_scope。