新增说明

This commit is contained in:
2025-09-21 01:16:33 +08:00
parent 8e333ac03f
commit fd89745950
18 changed files with 525 additions and 19 deletions

View File

@@ -0,0 +1,230 @@
好的我现在需要处理用户的任务指令生成一个符合要求的行为树JSON。首先我得仔细阅读用户的指令和参考知识确保理解所有要求。
用户的要求是:无人机起飞后,先移动至学生宿舍,搜索并锁定任一红色的球,在对其环绕三十两圈后进行打击操作。参考知识中提到学生宿舍的坐标是(x:5, y:3, z:2)。不过,这里有个问题,用户提到的“红色的球”可能不在学生宿舍区域,但根据参考知识,学生宿舍的坐标是(5,3,2)而其他地点如跷跷板和A地的坐标不同。不过用户可能希望无人机先飞到学生宿舍然后在附近搜索球。
首先我需要确保所有坐标符合物理约束。学生宿舍的坐标是x=5y=3z=2这在允许的范围内。接下来无人机需要起飞然后飞往学生宿舍。这里可能需要使用fly_to_waypoint动作将坐标设置为学生宿舍的坐标。
接下来是搜索红色球。用户提到“搜索并锁定任一红色的球”这里可能需要使用search_pattern或者object_detect。不过用户可能希望先飞到学生宿舍区域然后进行搜索。根据参考知识学生宿舍的坐标是(5,3,2)所以可能需要先飞到那里然后进行搜索。不过用户可能希望在学生宿舍附近搜索所以可能需要使用search_pattern或者先悬停再检测。
不过用户提到“搜索并锁定”可能需要先使用search_pattern来搜索或者直接使用object_detect。但object_detect需要无人机在目标附近悬停而search_pattern是移动搜索。根据任务描述可能先飞到学生宿舍然后进行搜索所以可能需要使用search_pattern或者先飞到学生宿舍然后使用object_detect。
不过用户可能希望先飞到学生宿舍然后在该区域搜索红色球。所以可能需要先fly_to_waypoint到学生宿舍然后使用search_pattern或者object_detect。但根据参考知识学生宿舍的坐标是(5,3,2)而红色球可能在附近所以可能需要使用search_pattern或者直接使用object_detect但需要确认位置。
不过用户可能希望先飞到学生宿舍然后在该区域进行搜索。因此可能需要先fly_to_waypoint到学生宿舍然后使用search_pattern来搜索红色球。或者如果红色球的位置已知可能直接使用object_detect。但根据用户指令可能需要先搜索所以使用search_pattern更合适。
接下来找到红色球后需要环绕三十两圈。这里的“三十两圈”可能是笔误应该是“三十圈”或者“三圈”但用户写的是“三十两圈”可能是指半径30米绕两圈或者可能是“三十圈”需要根据中文习惯判断。通常“三十两圈”可能是指半径30米绕两圈或者可能是“三十圈”这里可能需要进一步确认但根据用户给出的参考知识可能需要将半径设为30米绕两圈。或者用户可能想表达的是“三十圈”但中文中“三十两圈”不太常见可能是“三十圈”或“三圈”。不过根据用户给出的参考知识中的“环绕三十两圈”可能是指半径30米绕两圈。因此在orbit_around_target中设置radius=30laps=2。
然后进行打击操作使用strike_target动作。
同时必须包含安全监控使用Parallel节点包含电池和GPS监控以及紧急处理流程。
现在,构建行为树的结构:
根节点是Parallel包含主任务和安全监控。主任务是一个Sequence包含起飞、飞往学生宿舍、搜索球、环绕、打击、降落。安全监控是Selector包含电池、GPS条件以及紧急处理Sequence。
在飞往学生宿舍时使用fly_to_waypoint参数x=5y=3z=2。然后进行搜索使用search_pattern可能以学生宿舍为中心半径适当比如50米搜索红色球。或者可能需要先悬停使用object_detect。但用户可能希望先搜索所以使用search_pattern参数center_x=5center_y=3radius=50target_class=balldescription=红色球。不过,用户提到的是红色球,可能需要调整参数。
接下来环绕目标使用orbit_around_target参数center_x=5假设球在学生宿舍附近但需要确认。或者可能在搜索后锁定球的位置然后环绕。但根据用户指令可能需要先飞到学生宿舍然后搜索球所以search_pattern的中心是学生宿舍的坐标半径50米搜索红色球。然后环绕该球使用orbit_around_targetradius=30laps=2。然后进行打击。
现在,检查所有参数是否符合约束:
fly_to_waypoint的坐标是学生宿舍的(5,3,2)z=2符合约束。
search_pattern的参数pattern_type=spiralcenter_x=5center_y=3center_z=2学生宿舍的坐标radius=50target_class=balldescription=红色球。这符合约束。
然后环绕该球使用orbit_around_target参数center_x=5假设球在学生宿舍附近但需要确认。或者可能在搜索后球的位置被确认所以使用orbit_around_target中心是搜索到的球的位置。但根据用户指令可能需要在搜索后锁定球的位置然后环绕。因此在search_pattern后可能需要使用object_detect来确认球的位置但用户可能希望直接环绕。
不过用户可能希望在搜索后锁定球的位置然后环绕。因此在search_pattern后可能需要使用object_detect来确认球的位置但根据任务描述可能需要直接环绕。
因此在search_pattern后可能需要使用orbit_around_target参数center_x=5假设球在学生宿舍附近但需要确认。或者可能在搜索后球的位置被确认所以使用orbit_around_target中心是球的位置。
但根据用户指令可能需要先飞到学生宿舍然后在该区域搜索球所以search_pattern的中心是学生宿舍的坐标半径50米搜索红色球。然后环绕该球使用orbit_around_target参数center_x=5假设球在学生宿舍附近radius=30laps=2。
然后进行打击操作。
现在,构建行为树的结构:
根节点是Parallel包含主任务Sequence和安全监控Selector。
主任务Sequence的步骤
1. preflight_checks检查级别为comprehensive。
2. takeoffaltitude=2.0。
3. fly_to_waypoint到学生宿舍的坐标(5,3,2)acceptance_radius=2.0。
4. search_patternpattern_type=spiralcenter_x=5center_y=3center_z=2radius=50target_class=balldescription=红色球。
5. orbit_around_target参数center_x=5假设球在学生宿舍附近但需要确认。或者可能在搜索后球的位置被确认所以使用orbit_around_target中心是球的位置。但根据用户指令可能需要在搜索后锁定球的位置然后环绕。因此在search_pattern后可能需要使用object_detect来确认球的位置但用户可能希望直接环绕。
因此在search_pattern后可能需要使用orbit_around_target参数center_x=5假设球在学生宿舍附近radius=30laps=2。
然后进行strike_target操作。
最后landmode=home。
同时安全监控Selector包含电池条件threshold=0.35和GPS条件min_satellites=8以及紧急处理Sequence。
现在,检查所有参数是否符合约束:
- fly_to_waypoint的坐标是学生宿舍的(5,3,2)z=2符合约束。
- search_pattern的参数center_x=5center_y=3center_z=2radius=50符合约束。
- orbit_around_target的参数center_x=5假设球在学生宿舍附近但需要确认。或者可能在搜索后球的位置被确认所以使用orbit_around_target中心是球的位置。但根据用户指令可能需要在搜索后锁定球的位置然后环绕。因此在search_pattern后可能需要使用object_detect来确认球的位置但用户可能希望直接环绕。
因此在search_pattern后可能需要使用orbit_around_target参数center_x=5假设球在学生宿舍附近radius=30laps=2。
然后进行strike_target操作。
现在构建JSON
根节点是Parallel包含主任务Sequence和安全监控Selector。
主任务Sequence的步骤
1. preflight_checks参数check_level=comprehensive。
2. takeoff参数altitude=2.0。
3. fly_to_waypoint到学生宿舍的坐标(5,3,2)acceptance_radius=2.0。
4. search_pattern参数pattern_type=spiralcenter_x=5center_y=3center_z=2radius=50target_class=balldescription=红色球。
5. orbit_around_target参数center_x=5假设球在学生宿舍附近但需要确认。或者可能在搜索后球的位置被确认所以使用orbit_around_target中心是球的位置。但根据用户指令可能需要在搜索后锁定球的位置然后环绕。因此在search_pattern后可能需要使用object_detect来确认球的位置但用户可能希望直接环绕。
因此在search_pattern后可能需要使用orbit_around_target参数center_x=5假设球在学生宿舍附近radius=30laps=2。
然后进行strike_target操作。
最后landmode=home。
同时安全监控Selector包含电池条件threshold=0.35和GPS条件min_satellites=8以及紧急处理Sequence。
现在,检查所有参数是否符合约束:
- orbit_around_target的参数center_x=5但需要确认球的位置是否在学生宿舍附近。根据用户指令可能需要在搜索后锁定球的位置所以使用orbit_around_target中心是球的位置。但根据参考知识学生宿舍的坐标是(5,3,2)而红色球可能在附近所以使用orbit_around_target参数center_x=5学生宿舍的坐标radius=30laps=2。
这样,参数符合约束。
现在构建JSON
{
"root": {
"type": "Parallel",
"name": "CompleteStrikeMission",
"params": {
"policy": "all_success"
},
"children": [
{
"type": "Sequence",
"name": "MainStrikeSequence",
"children": [
{
"type": "action",
"name": "preflight_checks",
"params": {
"check_level": "comprehensive"
}
},
{
"type": "action",
"name": "takeoff",
"params": {
"altitude": 2.0
}
},
{
"type": "action",
"name": "fly_to_waypoint",
"params": {
"x": 5.0,
"y": 3.0,
"z": 2.0,
"acceptance_radius": 2.0
}
},
{
"type": "action",
"name": "search_pattern",
"params": {
"pattern_type": "spiral",
"center_x": 5.0,
"center_y": 3.0,
"center_z": 2.0,
"radius": 50.0,
"target_class": "ball",
"description": "红色球",
"count": 1
}
},
{
"type": "action",
"name": "orbit_around_target",
"params": {
"center_x": 5.0,
"center_y": 3.0,
"center_z": 2.0,
"radius": 30.0,
"laps": 2,
"clockwise": true,
"gimbal_lock": true
}
},
{
"type": "action",
"name": "strike_target",
"params": {
"target_class": "ball",
"description": "红色球",
"count": 1
}
},
{
"type": "action",
"name": "land",
"params": {
"mode": "home"
}
}
]
},
{
"type": "Selector",
"name": "SafetyMonitor",
"params": {
"memory": true
},
"children": [
{
"type": "condition",
"name": "battery_above",
"params": {
"threshold": 0.35
}
},
{
"type": "condition",
"name": "gps_status",
"params": {
"min_satellites": 8
}
},
{
"type": "Sequence",
"name": "EmergencyHandler",
"children": [
{
"type": "action",
"name": "emergency_return",
"params": {
"reason": "safety_breach"
}
}
,
{
"type": "action",
"name": "land",
"params": {
"mode": "home"
}
}
]
}
]
}
]
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

After

Width:  |  Height:  |  Size: 208 KiB

View File

@@ -1,11 +1,10 @@
你是一个无人机简单指令执行规划器。你的任务:当用户给出“简单指令”(单一原子动作即可完成)时,输出一个严格的JSON对象。
你是一个无人机简单指令执行规划器。你的任务输出一个严格的JSON对象。
输出要求(必须遵守):
- 只输出一个JSON对象不要任何解释或多余文本。
- JSON结构
{"mode":"simple","action":{"name":"<action_name>","params":{...}}}
- <action_name> 与参数定义、取值范围必须与“复杂模式”提示词system_prompt.txt中的定义完全一致
- 简单模式下不包含任何行为树结构与安全监控并行,仅输出单一原子动作。
- 不包含任何行为树结构与安全监控并行,仅输出单一原子动作
示例:
- “起飞到10米” → {"mode":"simple","action":{"name":"takeoff","params":{"altitude":10.0}}}
@@ -21,13 +20,13 @@
{"name": "fly_to_waypoint", "description": "导航至一个指定坐标点。使用相对坐标系x,y,z单位为米。", "params": {"x": "float", "y": "float", "z": "float", "acceptance_radius": "float, 可选默认2.0"}},
{"name": "move_direction", "description": "按指定方向直线移动。方向可为绝对方位或相对机体朝向。", "params": {"direction": "string: north|south|east|west|forward|backward|left|right", "distance": "float[1,10000], 可选, 不指定则持续移动"}},
{"name": "orbit_around_point", "description": "以给定中心点为中心,等速圆周飞行指定圈数。", "params": {"center_x": "float", "center_y": "float", "center_z": "float", "radius": "float[5,1000]", "laps": "int[1,20]", "clockwise": "boolean, 可选, 默认true", "speed_mps": "float[0.5,15], 可选", "gimbal_lock": "boolean, 可选, 默认true"}},
{"name": "orbit_around_target", "description": "以目标为中心,等速圆周飞行指定圈数(需已有目标)。", "params": {"target_class": "string, 见复杂模式定义列表", "description": "string, 可选", "radius": "float[5,1000]", "laps": "int[1,20]", "clockwise": "boolean, 可选, 默认true", "speed_mps": "float[0.5,15], 可选", "gimbal_lock": "boolean, 可选, 默认true"}},
{"name": "orbit_around_target", "description": "以目标为中心,等速圆周飞行指定圈数(需已有目标)。", "params": {"target_class": "string, 取值同object_detect列表", "description": "string, 可选", "radius": "float[5,1000]", "laps": "int[1,20]", "clockwise": "boolean, 可选, 默认true", "speed_mps": "float[0.5,15], 可选", "gimbal_lock": "boolean, 可选, 默认true"}},
{"name": "loiter", "description": "在当前位置上空悬停一段时间或直到条件触发。", "params": {"duration": "float, 可选[1,600]", "until_condition": "string, 可选"}},
{"name": "object_detect", "description": "识别特定目标对象。", "params": {"target_class": "string, 见复杂模式定义列表", "description": "string, 可选", "count": "int, 可选, 默认1"}},
{"name": "object_detect", "description": "识别特定目标对象。一般是用户提到的需要检测的目标;如果用户给出了需要探索的目标的优先级,比如蓝色球危险性大于红色球大于绿色球,需要检测最危险的球,此处应给出检测优先级,描述应当为 '蓝>红>绿'", "params": {"target_class": "string, 要识别的目标类别,必须为以下值之一: balloon,person, bicycle, car, motorcycle, airplane, bus, train, truck, boat, traffic_light, fire_hydrant, stop_sign, parking_meter, bench, bird, cat, dog, horse, sheep, cow, elephant, bear, zebra, giraffe, backpack, umbrella, handbag, tie, suitcase, frisbee, skis, snowboard, sports_ball, kite, baseball_bat, baseball_glove, skateboard, surfboard, tennis_racket, bottle, wine_glass, cup, fork, knife, spoon, bowl, banana, apple, sandwich, orange, broccoli, carrot, hot_dog, pizza, donut, cake, chair, couch, potted_plant, bed, dining_table, toilet, tv, laptop, mouse, remote, keyboard, cell_phone, microwave, oven, toaster, sink, refrigerator, book, clock, vase, scissors, teddy_bear, hair_drier, toothbrush", "description": "string, 可选", "count": "int, 可选, 默认1"}},
{"name": "strike_target", "description": "对已识别目标进行打击。", "params": {"target_class": "string", "description": "string, 可选", "count": "int, 可选, 默认1"}},
{"name": "battle_damage_assessment", "description": "战损评估。", "params": {"target_class": "string", "assessment_time": "float[5-60], 默认15.0"}},
{"name": "search_pattern", "description": "按模式搜索。", "params": {"pattern_type": "string: spiral|grid", "center_x": "float", "center_y": "float", "center_z": "float", "radius": "float[5,1000]", "target_class": "string", "description": "string, 可选", "count": "int, 可选, 默认1"}},
{"name": "track_object", "description": "持续跟踪目标。", "params": {"target_class": "string, 见复杂模式定义列表", "description": "string, 可选", "track_time": "float[1,600], 默认30.0", "min_confidence": "float[0.5-1.0], 默认0.7", "safe_distance": "float[2-50], 默认10.0"}},
{"name": "track_object", "description": "持续跟踪目标。", "params": {"target_class": "string, 取值同object_detect列表", "description": "string, 可选", "track_time": "float[1,600], 默认30.0", "min_confidence": "float[0.5-1.0], 默认0.7", "safe_distance": "float[2-50], 默认10.0"}},
{"name": "deliver_payload", "description": "投放物资。", "params": {"payload_type": "string", "release_altitude": "float[2,100], 默认5.0"}},
{"name": "preflight_checks", "description": "飞行前系统自检。", "params": {"check_level": "string: basic|comprehensive"}},
{"name": "emergency_return", "description": "执行紧急返航程序。", "params": {"reason": "string"}}
@@ -53,7 +52,6 @@
- orbit_around_target.radius: [5, 1000]
- orbit_around_point/target.laps: [1, 20]
- orbit_around_point/target.speed_mps: [0.5, 15]
- 电池阈值等同复杂模式(如需涉及)
- 若参考知识提供坐标,必须使用并裁剪到约束范围内
—— 口令转化规则(环绕类)——

View File

@@ -85,8 +85,8 @@
"name": "object_detect",
"description": "在当前视野范围内识别特定目标对象。适用于定点检测,无人机应在目标大致位置悬停或保持稳定姿态。",
"params": {
"target_class": "string, 要识别的目标类别,必须为以下值之一: person, bicycle, car, motorcycle, airplane, bus, train, truck, boat, traffic_light, fire_hydrant, stop_sign, parking_meter, bench, bird, cat, dog, horse, sheep, cow, elephant, bear, zebra, giraffe, backpack, umbrella, handbag, tie, suitcase, frisbee, skis, snowboard, sports_ball, kite, baseball_bat, baseball_glove, skateboard, surfboard, tennis_racket, bottle, wine_glass, cup, fork, knife, spoon, bowl, banana, apple, sandwich, orange, broccoli, carrot, hot_dog, pizza, donut, cake, chair, couch, potted_plant, bed, dining_table, toilet, tv, laptop, mouse, remote, keyboard, cell_phone, microwave, oven, toaster, sink, refrigerator, book, clock, vase, scissors, teddy_bear, hair_drier, toothbrush",
"description": "string, 可选,目标属性描述(如颜色、状态等)",
"target_class": "string, 要识别的目标类别,必须为以下值之一: balloon,person, bicycle, car, motorcycle, airplane, bus, train, truck, boat, traffic_light, fire_hydrant, stop_sign, parking_meter, bench, bird, cat, dog, horse, sheep, cow, elephant, bear, zebra, giraffe, backpack, umbrella, handbag, tie, suitcase, frisbee, skis, snowboard, sports_ball, kite, baseball_bat, baseball_glove, skateboard, surfboard, tennis_racket, bottle, wine_glass, cup, fork, knife, spoon, bowl, banana, apple, sandwich, orange, broccoli, carrot, hot_dog, pizza, donut, cake, chair, couch, potted_plant, bed, dining_table, toilet, tv, laptop, mouse, remote, keyboard, cell_phone, microwave, oven, toaster, sink, refrigerator, book, clock, vase, scissors, teddy_bear, hair_drier, toothbrush",
"description": "string, 可选,目标属性描述(如颜色、状态等),一般是用户提到的需要检测的目标;如果用户给出了需要探索的目标的优先级,比如蓝色球危险性大于红色球大于绿色球,需要检测最危险的球,此处应给出检测优先级,描述应当为 '蓝>红>绿'",
"count": "int, 可选需要检测的目标个数默认1"
}
},

View File

@@ -201,7 +201,7 @@ def _generate_pytree_schema(allowed_actions: set, allowed_conditions: set) -> di
"sandwich", "orange", "broccoli", "carrot", "hot_dog", "pizza", "donut", "cake", "chair",
"couch", "potted_plant", "bed", "dining_table", "toilet", "tv", "laptop", "mouse", "remote",
"keyboard", "cell_phone", "microwave", "oven", "toaster", "sink", "refrigerator", "book",
"clock", "vase", "scissors", "teddy_bear", "hair_drier", "toothbrush"
"clock", "vase", "scissors", "teddy_bear", "hair_drier", "toothbrush","balloon"
]
# 递归节点定义
@@ -548,6 +548,16 @@ class PyTreeGenerator:
# Updated output directory for visualizations
self.vis_dir = os.path.abspath(os.path.join(self.base_dir, '..', 'generated_visualizations'))
os.makedirs(self.vis_dir, exist_ok=True)
# Reasoning content output directory (Markdown files)
self.reasoning_dir = os.path.abspath(os.path.join(self.base_dir, '..', 'generated_reasoning_content'))
os.makedirs(self.reasoning_dir, exist_ok=True)
# 控制是否允许模型返回含 <think> 的原文不强制JSON以便提取推理链
self.enable_reasoning_capture = os.getenv("ENABLE_REASONING_CAPTURE", "true").lower() in ("1", "true", "yes")
# 终端预览的最大行数
try:
self.reasoning_preview_lines = int(os.getenv("REASONING_PREVIEW_LINES", "20"))
except Exception:
self.reasoning_preview_lines = 20
# 加载提示词:复杂模式复用现有 system_prompt.txt简单模式与分类器独立提示词
self.complex_prompt = self._load_prompt("system_prompt.txt")
self.simple_prompt = self._load_prompt("simple_mode_prompt.txt")
@@ -661,16 +671,46 @@ class PyTreeGenerator:
# 简单/复杂分流到不同模型与提示词
client = self.simple_llm_client if mode == "simple" else self.complex_llm_client
model_name = self.simple_model if mode == "simple" else self.complex_model
response = client.chat.completions.create(
model=model_name,
messages=[
# 根据是否捕获推理链来决定是否强制JSON响应
response_kwargs = {
"model": model_name,
"messages": [
{"role": "system", "content": use_prompt},
{"role": "user", "content": final_user_prompt}
],
temperature=0.1 if mode == "complex" else 0.0,
response_format={"type": "json_object"}
)
pytree_str = response.choices[0].message.content
"temperature": 0.1 if mode == "complex" else 0.0,
}
if not self.enable_reasoning_capture:
response_kwargs["response_format"] = {"type": "json_object"}
response = client.chat.completions.create(**response_kwargs)
# 兼容可能存在的 reasoning_content 字段
try:
msg = response.choices[0].message
msg_content = getattr(msg, "content", None)
msg_reasoning = getattr(msg, "reasoning_content", None)
except Exception:
msg = response.choices[0]["message"] if isinstance(response.choices[0], dict) else None
msg_content = (msg or {}).get("content") if isinstance(msg, dict) else None
msg_reasoning = (msg or {}).get("reasoning_content") if isinstance(msg, dict) else None
combined_text = ""
if isinstance(msg_reasoning, str) and msg_reasoning.strip():
# 将 reasoning_content 包装为 <think>,便于统一解析
combined_text += f"<think>\n{msg_reasoning}\n</think>\n"
if isinstance(msg_content, str) and msg_content.strip():
combined_text += msg_content
pytree_str = combined_text if combined_text else (msg_content or "")
# 提取 <think> 推理链内容(若存在)
reasoning_text = None
try:
think_match = re.search(r"<think>([\s\S]*?)</think>", pytree_str)
if think_match:
reasoning_text = think_match.group(1).strip()
# 去除推理文本后再尝试解析JSON
pytree_str = re.sub(r"<think>[\s\S]*?</think>", "", pytree_str).strip()
except Exception:
reasoning_text = None
# 单独捕获JSON解析错误并打印原始响应
try:
pytree_dict = json.loads(pytree_str)
@@ -702,6 +742,25 @@ class PyTreeGenerator:
pytree_dict['visualization_url'] = f"/static/{vis_filename}"
except Exception as e:
logging.warning(f"简单模式可视化失败: {e}")
# 保存推理链(若有)
try:
if reasoning_text:
reasoning_path = os.path.join(self.reasoning_dir, f"{plan_id}.md")
with open(reasoning_path, 'w', encoding='utf-8') as rf:
rf.write(reasoning_text)
logging.info(f"📝 推理链已保存: {reasoning_path}")
# 终端预览最多N行
try:
lines = reasoning_text.splitlines()
preview = "\n".join(lines[: self.reasoning_preview_lines])
logging.info("🧠 推理链预览(前%d行)\n%s", self.reasoning_preview_lines, preview)
except Exception:
pass
else:
logging.info("未在模型输出中发现 <think> 推理链片段。若需捕获,请设置 ENABLE_REASONING_CAPTURE=true 以放宽JSON强制格式。")
except Exception as e:
logging.warning(f"保存推理链Markdown失败: {e}")
return pytree_dict
# 复杂模式回退:若模型误返回简单结构,则自动包装为含安全监控的行为树
@@ -754,6 +813,25 @@ class PyTreeGenerator:
vis_path = os.path.join(self.vis_dir, vis_filename)
_visualize_pytree(pytree_dict['root'], os.path.splitext(vis_path)[0])
pytree_dict['visualization_url'] = f"/static/{vis_filename}"
# 保存推理链(若有)
try:
if reasoning_text:
reasoning_path = os.path.join(self.reasoning_dir, f"{plan_id}.md")
with open(reasoning_path, 'w', encoding='utf-8') as rf:
rf.write(reasoning_text)
logging.info(f"📝 推理链已保存: {reasoning_path}")
# 终端预览最多N行
try:
lines = reasoning_text.splitlines()
preview = "\n".join(lines[: self.reasoning_preview_lines])
logging.info("🧠 推理链预览(前%d行)\n%s", self.reasoning_preview_lines, preview)
except Exception:
pass
else:
logging.info("未在模型输出中发现 <think> 推理链片段。若需捕获,请设置 ENABLE_REASONING_CAPTURE=true 以放宽JSON强制格式。")
except Exception as e:
logging.warning(f"保存推理链Markdown失败: {e}")
return pytree_dict
else:
# 打印未通过验证的Pytree以便排查