full-stack-coding-assistant-agent 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,686 @@
1
+ """
2
+ 全栈代码助手智能体 - 主入口
3
+ 支持 Python 3.13+
4
+ """
5
+
6
+ import argparse
7
+ import signal
8
+ import sys
9
+ from pathlib import Path
10
+ from typing import Dict, List, Optional
11
+
12
+ from coordinator.coordinator import Coordinator
13
+ from utils.agent_selector import AgentSelector
14
+ from utils.config_validator import validate_config
15
+ from utils.logger import debug, error, info, warning
16
+ from utils.output_manager import OutputManager
17
+ from utils.pdf_reader import read_document
18
+
19
+ # 退出命令集合
20
+ EXIT_COMMANDS = {"exit", "quit", "bye", "q"}
21
+
22
+ # Ctrl+C 退出标志
23
+ _interrupt_received = False
24
+
25
+
26
+ def _signal_handler(sig, frame):
27
+ """处理 Ctrl+C 信号"""
28
+ global _interrupt_received
29
+ _interrupt_received = True
30
+ info("\n\n收到退出信号,正在退出...")
31
+
32
+
33
+ def _is_exit_command(user_input: str) -> bool:
34
+ """判断用户输入是否为退出命令"""
35
+ return user_input.strip().lower() in EXIT_COMMANDS
36
+
37
+
38
+ def _print_welcome():
39
+ """打印欢迎信息"""
40
+ info("=" * 60)
41
+ info("全栈代码助手智能体 (Full-Stack Coding Assistant Agent)")
42
+ info("=" * 60)
43
+ info("")
44
+ info("交互模式已启动。输入需求开始开发,输入 exit/quit/bye 退出。")
45
+ info("")
46
+
47
+
48
+ def _print_help():
49
+ """打印帮助信息"""
50
+ info("")
51
+ info("可用命令:")
52
+ info(" help, h, ? - 显示此帮助")
53
+ info(" status, s - 显示当前项目状态")
54
+ info(" agents, a - 显示可用 Agent 列表")
55
+ info(" load <文件> - 加载 PDF/TXT/MD 文档作为需求上下文")
56
+ info(" exit, quit, bye - 退出程序")
57
+ info("")
58
+ info("输入任意文本即可作为新需求进行迭代开发。")
59
+ info("")
60
+
61
+
62
+ def _print_agent_list(selector: AgentSelector):
63
+ """打印可用 Agent 列表"""
64
+ info("")
65
+ info("可用 Agent:")
66
+ for name, desc in selector.AGENT_DESCRIPTIONS.items():
67
+ info(f" - {name}: {desc}")
68
+ info("")
69
+
70
+
71
+ def _print_project_status(output_dir: Optional[Path]):
72
+ """打印当前项目状态"""
73
+ info("")
74
+ if output_dir is None or not output_dir.exists():
75
+ info("当前状态: 未创建项目(请先输入项目描述)")
76
+ info("")
77
+ return
78
+
79
+ info(f"输出目录: {output_dir}")
80
+ info("")
81
+
82
+ # 读取元数据
83
+ meta_file = output_dir / ".meta.json"
84
+ if meta_file.exists():
85
+ import json
86
+
87
+ meta = json.loads(meta_file.read_text(encoding="utf-8"))
88
+ info(f"项目描述: {meta.get('task_description', 'N/A')}")
89
+ info(f"创建时间: {meta.get('created_at', 'N/A')}")
90
+ info(f"迭代次数: {meta.get('iteration_count', 0)}")
91
+ info(f"最后任务: {meta.get('last_task_id', 'N/A')}")
92
+ info(f"状态: {meta.get('status', 'N/A')}")
93
+ else:
94
+ info("未找到项目元数据")
95
+
96
+ info("")
97
+
98
+ # 统计文件数量
99
+ for sub_dir in ["backend", "frontend", "tests", "audits"]:
100
+ target = output_dir / sub_dir
101
+ if target.exists():
102
+ files = list(target.iterdir())
103
+ info(f" {sub_dir}: {len(files)} 个文件")
104
+ else:
105
+ info(f" {sub_dir}: 未创建")
106
+
107
+ # 统计 trace 文件
108
+ traces_dir = output_dir / "traces"
109
+ if traces_dir.exists():
110
+ trace_count = sum(1 for _ in traces_dir.rglob("*.md"))
111
+ if trace_count > 0:
112
+ info(
113
+ f" traces: {trace_count} 个审计 trace 文件(Agent 每一步的输入输出记录)"
114
+ )
115
+
116
+ info("")
117
+
118
+
119
+ def _get_user_input(prompt: str = ">>> ") -> Optional[str]:
120
+ """
121
+ 获取用户输入,处理 Ctrl+C
122
+
123
+ Returns:
124
+ 用户输入字符串,如果收到中断则返回 None
125
+ """
126
+ global _interrupt_received
127
+
128
+ if _interrupt_received:
129
+ return None
130
+
131
+ try:
132
+ user_input = input(prompt).strip()
133
+ if _interrupt_received:
134
+ return None
135
+ return user_input
136
+ except (EOFError, KeyboardInterrupt):
137
+ return None
138
+
139
+
140
+ def _get_project_description() -> Optional[str]:
141
+ """
142
+ 首次启动时,引导用户输入项目描述
143
+
144
+ 支持:
145
+ - 纯文本描述
146
+ - file:path/to/file.pdf 从 PDF 读取描述
147
+ - file:path/to/file.txt 从 TXT 读取描述
148
+
149
+ Returns:
150
+ 项目描述字符串,如果取消则返回 None
151
+ """
152
+ info("")
153
+ info("欢迎使用全栈代码助手!")
154
+ info("请描述你要开发的项目,或者指定文档文件:")
155
+ info(" 直接输入 - 文本描述项目")
156
+ info(" file:xxx - 从 PDF/TXT 文件读取描述")
157
+ info(" exit - 退出")
158
+ info("")
159
+
160
+ while True:
161
+ user_input = _get_user_input("项目描述> ")
162
+ if user_input is None:
163
+ return None
164
+ if _is_exit_command(user_input):
165
+ return None
166
+ if not user_input:
167
+ warning("项目描述不能为空,请重新输入。")
168
+ continue
169
+
170
+ # 检查是否为 file: 指令
171
+ if user_input.startswith("file:"):
172
+ file_path = user_input[5:].strip()
173
+ description = _load_document_description(file_path)
174
+ if description is None:
175
+ continue # 加载失败,让用户重新输入
176
+ return description
177
+
178
+ return user_input
179
+
180
+
181
+ def _load_document_description(file_path: str) -> Optional[str]:
182
+ """
183
+ 从文档文件加载原始文本内容
184
+
185
+ 支持 .pdf / .txt / .md 格式。
186
+ 成功时返回文档的原始全文,失败时打印错误并返回 None。
187
+
188
+ Args:
189
+ file_path: 文档文件路径
190
+
191
+ Returns:
192
+ 文档的原始全文,或 None
193
+ """
194
+ try:
195
+ summary, full_text = read_document(file_path)
196
+ info(f" 已从文件加载: {file_path}")
197
+ info(f" 文档摘要: {summary}")
198
+ info(f" 文档长度: {len(full_text)} 字符")
199
+ return full_text
200
+ except FileNotFoundError:
201
+ error(f"文件不存在: {file_path}")
202
+ return None
203
+ except ValueError as e:
204
+ error(str(e))
205
+ return None
206
+ except ImportError as e:
207
+ error(str(e))
208
+ error("请运行: pip install PyPDF2")
209
+ return None
210
+ except Exception as e:
211
+ error(f"读取文档失败: {str(e)}")
212
+ return None
213
+
214
+
215
+ def _merge_description(pdf_path: str, text_desc: str = "") -> str:
216
+ """
217
+ 将从 PDF 加载的内容和文本描述合并为一个完整描述
218
+
219
+ Args:
220
+ pdf_path: PDF 文件路径
221
+ text_desc: 用户输入的文本描述(可选)
222
+
223
+ Returns:
224
+ 合并后的描述文本
225
+ """
226
+ summary, full_text = read_document(pdf_path)
227
+ if text_desc:
228
+ # 文本描述作为摘要,PDF 内容作为详细需求
229
+ description = (
230
+ f"{text_desc}\n\n"
231
+ f"---\n"
232
+ f"以下为详细需求文档(来自 {pdf_path}):\n\n"
233
+ f"{full_text}"
234
+ )
235
+ else:
236
+ # 纯 PDF,用摘要作为描述
237
+ description = f"{summary}\n\n---\n以下来自文档 {pdf_path}:\n\n{full_text}"
238
+ return description
239
+
240
+
241
+ def _select_all_agents() -> List[str]:
242
+ """返回全部 Agent 列表"""
243
+ return ["backend", "frontend", "test", "audit"]
244
+
245
+
246
+ def _run_task(
247
+ coordinator: Coordinator,
248
+ output_mgr: OutputManager,
249
+ output_dir: Path,
250
+ description: str,
251
+ requirements: str,
252
+ agent_types: List[str],
253
+ is_first_run: bool,
254
+ ) -> Dict:
255
+ """
256
+ 执行一次任务(可能只运行选中的 Agent)
257
+
258
+ Args:
259
+ coordinator: 协调器实例
260
+ output_mgr: 输出管理器实例
261
+ output_dir: 输出目录
262
+ description: 任务描述
263
+ requirements: 详细需求
264
+ agent_types: 选中的 Agent 类型列表
265
+ is_first_run: 是否为首次运行
266
+
267
+ Returns:
268
+ 执行结果字典
269
+ """
270
+ # 重置 DAG
271
+ coordinator.reset_dag()
272
+
273
+ # 提交任务
274
+ if is_first_run:
275
+ # 首次运行:使用 submit_task(完整 DAG)
276
+ main_task_id = coordinator.submit_task(
277
+ description=description,
278
+ requirements=requirements,
279
+ is_iteration=False,
280
+ )
281
+ else:
282
+ # 迭代运行:使用 submit_iteration_task(只创建选中的 Agent)
283
+ main_task_id = coordinator.submit_iteration_task(
284
+ description=description,
285
+ requirements=requirements,
286
+ agent_types=agent_types,
287
+ )
288
+
289
+ info(f" 主任务 ID: {main_task_id}")
290
+ info(f" 选中 Agent: {', '.join(agent_types)}")
291
+ debug("任务已提交到 DAG 调度器")
292
+
293
+ # 执行任务
294
+ info("-" * 60)
295
+ results = coordinator.run(main_task_id)
296
+ info("-" * 60)
297
+ debug(f"任务执行完成,结果: {len(results)} 个任务")
298
+
299
+ # 保存迭代记录
300
+ if output_dir is not None:
301
+ try:
302
+ output_mgr.save_iteration(
303
+ output_path=output_dir,
304
+ prompt=description + (" " + requirements if requirements else ""),
305
+ result={
306
+ "task_id": main_task_id,
307
+ "results": {
308
+ k: v.get("status")
309
+ for k, v in results.items()
310
+ if not k.startswith("_")
311
+ },
312
+ },
313
+ )
314
+ # 更新元数据
315
+ output_mgr.update_meta(
316
+ output_path=output_dir,
317
+ last_task_id=main_task_id,
318
+ status="completed",
319
+ )
320
+ except Exception as e:
321
+ error(f"保存迭代记录失败: {str(e)}")
322
+
323
+ return results
324
+
325
+
326
+ def _print_results(
327
+ results: Dict,
328
+ output_dir: Optional[Path],
329
+ coordinator: Coordinator,
330
+ main_task_id: str,
331
+ ):
332
+ """打印任务执行结果"""
333
+ info("")
334
+ info("执行结果:")
335
+ info("=" * 60)
336
+ for task_suffix, result in results.items():
337
+ if task_suffix.startswith("_"):
338
+ continue
339
+ status = result.get("status", "unknown")
340
+ info(f"\n[{task_suffix}]:")
341
+ info(f" 状态: {status}")
342
+ if result.get("error"):
343
+ error(f" 错误: {result['error']}")
344
+ if result.get("traceback"):
345
+ debug(f" 堆栈: {result['traceback']}")
346
+ files = result.get("files", [])
347
+ if files:
348
+ info(f" 生成文件: {len(files)} 个")
349
+
350
+ # 获取完整状态
351
+ status = coordinator.get_task_status(main_task_id)
352
+ info("")
353
+ info("任务状态汇总:")
354
+ info("=" * 60)
355
+ for agent, agent_info in status["tasks"].items():
356
+ info(f" {agent}: {agent_info['status']}")
357
+
358
+ info("")
359
+ if output_dir:
360
+ info(f"输出目录: {output_dir}")
361
+ # 提示 trace 文件位置
362
+ traces_dir = output_dir / "traces"
363
+ if traces_dir.exists():
364
+ trace_count = sum(1 for _ in traces_dir.rglob("*.md"))
365
+ if trace_count > 0:
366
+ info(f"Agent 执行追溯: {traces_dir} ({trace_count} 个 trace 文件)")
367
+ info("完成!")
368
+ info("=" * 60)
369
+
370
+
371
+ def interactive_mode(
372
+ coordinator: Coordinator,
373
+ output_mgr: OutputManager,
374
+ output_dir: Optional[Path],
375
+ selector: AgentSelector,
376
+ first_description: Optional[str] = None,
377
+ ):
378
+ """
379
+ 交互模式主循环
380
+
381
+ Args:
382
+ coordinator: 协调器实例(持久化,跨迭代复用)
383
+ output_mgr: 输出管理器实例
384
+ output_dir: 输出目录(首次可能为 None)
385
+ selector: Agent 选择器实例
386
+ first_description: 首次运行的项目描述(命令行参数传入)
387
+ """
388
+ global _interrupt_received
389
+
390
+ _print_welcome()
391
+
392
+ # 情况 1: 命令行已传入描述,先执行一次
393
+ if first_description:
394
+ info(f"[首次运行] 项目描述: {first_description}")
395
+ info("")
396
+
397
+ # 创建输出目录
398
+ output_dir = output_mgr.create_output_dir(first_description)
399
+ coordinator.output_dir = output_dir
400
+ for agent in coordinator.agents.values():
401
+ agent._set_output_dir(output_dir)
402
+
403
+ info("[1/3] 智能选择 Agent...")
404
+ # 首次运行,无已有代码,使用全部 Agent
405
+ agent_types = _select_all_agents()
406
+ info(f" 选中: {', '.join(agent_types)}")
407
+ info("")
408
+
409
+ info("[2/3] 提交并执行任务...")
410
+ results = _run_task(
411
+ coordinator=coordinator,
412
+ output_mgr=output_mgr,
413
+ output_dir=output_dir,
414
+ description=first_description,
415
+ requirements="",
416
+ agent_types=agent_types,
417
+ is_first_run=True,
418
+ )
419
+
420
+ info("[3/3] 输出结果...")
421
+ # 获取 main_task_id(从 coordinator)
422
+ main_task_id = coordinator._main_task_id
423
+ _print_results(results, output_dir, coordinator, main_task_id)
424
+
425
+ # 自动生成 README.md
426
+ output_mgr.generate_readme(output_dir)
427
+ info(f" 已生成执行说明: {output_dir / 'README.md'}")
428
+
429
+ # 情况 2: 无参数启动,引导用户输入项目描述
430
+ if output_dir is None:
431
+ project_desc = _get_project_description()
432
+ if project_desc is None or _interrupt_received:
433
+ info("退出程序。")
434
+ return
435
+
436
+ # 创建输出目录
437
+ output_dir = output_mgr.create_output_dir(project_desc)
438
+ coordinator.output_dir = output_dir
439
+ for agent in coordinator.agents.values():
440
+ agent._set_output_dir(output_dir)
441
+
442
+ info("[1/3] 首次运行,使用全部 Agent...")
443
+ agent_types = _select_all_agents()
444
+
445
+ info("[2/3] 提交并执行任务...")
446
+ results = _run_task(
447
+ coordinator=coordinator,
448
+ output_mgr=output_mgr,
449
+ output_dir=output_dir,
450
+ description=project_desc,
451
+ requirements="",
452
+ agent_types=agent_types,
453
+ is_first_run=True,
454
+ )
455
+
456
+ main_task_id = coordinator._main_task_id
457
+ info("[3/3] 输出结果...")
458
+ _print_results(results, output_dir, coordinator, main_task_id)
459
+
460
+ # 自动生成 README.md
461
+ output_mgr.generate_readme(output_dir)
462
+ info(f" 已生成执行说明: {output_dir / 'README.md'}")
463
+
464
+ # 进入主循环
465
+ info("")
466
+ info("进入交互模式。输入 help 查看可用命令。")
467
+ info("")
468
+
469
+ while not _interrupt_received:
470
+ user_input = _get_user_input("\n>>> ")
471
+ if user_input is None:
472
+ break
473
+ if not user_input:
474
+ continue
475
+
476
+ # 处理命令
477
+ cmd = user_input.strip().lower()
478
+
479
+ if _is_exit_command(cmd):
480
+ info("再见!")
481
+ break
482
+
483
+ if cmd in ("help", "h", "?"):
484
+ _print_help()
485
+ continue
486
+
487
+ if cmd in ("status", "s"):
488
+ _print_project_status(output_dir)
489
+ continue
490
+
491
+ if cmd in ("agents", "a"):
492
+ _print_agent_list(selector)
493
+ continue
494
+
495
+ if cmd.startswith("load "):
496
+ file_path = user_input.strip()[5:].strip()
497
+ doc_text = _load_document_description(file_path)
498
+ if doc_text is None:
499
+ continue
500
+ info(" 文档已加载,接下来输入的需求将附带此文档内容。")
501
+ info(" 请输入具体的开发需求(基于以上文档):")
502
+ user_input = _get_user_input("\n>>> ")
503
+ if user_input is None or _is_exit_command(user_input.strip().lower()):
504
+ break
505
+ if not user_input:
506
+ continue
507
+ # 将文档内容拼接到用户需求后面,不改变 user_input 用于 Agent 选择(取第一行)
508
+ user_input = f"{user_input}\n\n---\n以下来自文档 {file_path}:\n\n{doc_text}"
509
+ # 继续执行下面的任务流程(不 continue)
510
+ cmd = user_input.strip().lower()
511
+
512
+ # 用户需求:智能选择 Agent 并执行
513
+ info("[1/3] 智能选择 Agent...")
514
+
515
+ # 收集已有代码摘要
516
+ code_summary = ""
517
+ if output_dir is not None:
518
+ code_summary = AgentSelector.format_existing_code_summary(str(output_dir))
519
+
520
+ try:
521
+ selection = selector.select_agents(user_input, code_summary)
522
+ agent_types = selection["agents"]
523
+ reason = selection["reason"]
524
+ info(f" 选中: {', '.join(agent_types)}")
525
+ debug(f" 原因: {reason}")
526
+ except Exception as e:
527
+ warning(f"Agent 选择失败,使用全部 Agent: {e}")
528
+ agent_types = _select_all_agents()
529
+
530
+ info("")
531
+ info("[2/3] 提交并执行任务...")
532
+
533
+ results = _run_task(
534
+ coordinator=coordinator,
535
+ output_mgr=output_mgr,
536
+ output_dir=output_dir,
537
+ description=user_input,
538
+ requirements="",
539
+ agent_types=agent_types,
540
+ is_first_run=False,
541
+ )
542
+
543
+ main_task_id = coordinator._main_task_id
544
+ info("[3/3] 输出结果...")
545
+ _print_results(results, output_dir, coordinator, main_task_id)
546
+
547
+ # 自动生成/更新 README.md
548
+ output_mgr.generate_readme(output_dir)
549
+ info(f" 已更新执行说明: {output_dir / 'README.md'}")
550
+
551
+
552
+ def main():
553
+ """主函数"""
554
+ info("=" * 60)
555
+ info("全栈代码助手智能体 (Full-Stack Coding Assistant Agent)")
556
+ info("=" * 60)
557
+ info("")
558
+
559
+ # 0. 验证配置
560
+ info("[0/5] 验证配置...")
561
+ if not validate_config(exit_on_error=False):
562
+ error("配置验证失败,请检查 .env 文件")
563
+ error("运行: python utils/config_validator.py 查看详细配置")
564
+ sys.exit(1)
565
+
566
+ # 解析命令行参数
567
+ parser = argparse.ArgumentParser(
568
+ description="全栈代码助手智能体(交互模式)",
569
+ formatter_class=argparse.RawDescriptionHelpFormatter,
570
+ epilog="""
571
+ 示例:
572
+ # 无参数启动:进入交互模式,引导创建项目
573
+ python main.py
574
+
575
+ # 带描述启动:先执行一次,然后进入交互模式
576
+ python main.py "开发一个用户登录功能"
577
+
578
+ # 从 PDF 文件加载项目描述
579
+ python main.py --pdf spec.pdf "开发一个后台管理系统"
580
+
581
+ # 纯 PDF 文件作为描述(自动提取摘要)
582
+ python main.py --pdf spec.pdf
583
+
584
+ # 继续已有项目
585
+ python main.py --continue output/20260530_230000_xxx
586
+
587
+ 交互模式命令:
588
+ help, h, ? - 显示帮助
589
+ status, s - 显示项目状态
590
+ agents, a - 显示可用 Agent
591
+ load <文件> - 加载 PDF/TXT/MD 文档作为需求
592
+ exit, quit, bye - 退出
593
+ """,
594
+ )
595
+ parser.add_argument(
596
+ "description", nargs="?", help="项目描述(可选,可与 --pdf 组合使用)"
597
+ )
598
+ parser.add_argument("-r", "--requirements", default="", help="详细需求")
599
+ parser.add_argument("-p", "--pdf", dest="pdf_path", help="PDF 需求文档路径")
600
+ parser.add_argument(
601
+ "-c",
602
+ "--continue",
603
+ dest="continue_dir",
604
+ help="已有项目的输出目录路径(继续开发)",
605
+ )
606
+ args = parser.parse_args()
607
+
608
+ # 注册 Ctrl+C 信号处理
609
+ signal.signal(signal.SIGINT, _signal_handler)
610
+
611
+ # 初始化协调器和输出管理器
612
+ info("[1/5] 初始化协调器...")
613
+ try:
614
+ coordinator = Coordinator()
615
+ debug("协调器初始化成功")
616
+ except Exception as e:
617
+ error(f"协调器初始化失败: {str(e)}")
618
+ sys.exit(1)
619
+
620
+ output_mgr = OutputManager()
621
+ selector = AgentSelector(coordinator.model_router)
622
+
623
+ output_dir = None
624
+ first_description = None
625
+
626
+ # 合并 PDF + 文本描述
627
+ _pdf_content = "" # PDF 原始文本(用于 --continue 模式注入到需求)
628
+ if args.pdf_path:
629
+ info(f"[2/5] 加载 PDF 文档: {args.pdf_path}")
630
+ try:
631
+ summary, _pdf_content = read_document(args.pdf_path)
632
+ if args.description:
633
+ first_description = _merge_description(args.pdf_path, args.description)
634
+ else:
635
+ first_description = _merge_description(args.pdf_path)
636
+ info(f" 文档已加载,摘要: {summary[:60]}...")
637
+ except Exception as e:
638
+ error(f"加载 PDF 失败: {str(e)}")
639
+ sys.exit(1)
640
+
641
+ # 处理 --continue 模式
642
+ if args.continue_dir is not None:
643
+ info(f"[2/5] 加载已有项目: {args.continue_dir}")
644
+ try:
645
+ existing_context = output_mgr.load_output_dir(args.continue_dir)
646
+ output_dir = Path(args.continue_dir).resolve()
647
+ coordinator.output_dir = output_dir
648
+ for agent in coordinator.agents.values():
649
+ agent._set_output_dir(output_dir)
650
+
651
+ info(f" 项目描述: {existing_context['meta'].get('task_description', '')}")
652
+ info(f" 迭代次数: {existing_context['meta'].get('iteration_count', 0)}")
653
+ except FileNotFoundError as e:
654
+ error(str(e))
655
+ sys.exit(1)
656
+
657
+ # --continue + --pdf: PDF 内容作为本次迭代的额外需求
658
+ if _pdf_content:
659
+ first_description = f"{args.description or '请基于已有代码继续开发'}\n\n---\n以下来自文档 {args.pdf_path}:\n\n{_pdf_content}"
660
+ else:
661
+ first_description = args.description or ""
662
+
663
+ elif args.description or _pdf_content:
664
+ # 有描述或 PDF:先执行一次,然后进入交互模式
665
+ if not first_description:
666
+ first_description = args.description
667
+ info(f"[2/5] 准备首次运行: {first_description[:100]}...")
668
+ else:
669
+ # 无参数:进入交互模式,由交互模式引导创建项目
670
+ info("[2/5] 无参数启动,将进入交互模式")
671
+
672
+ info("[3] 启动交互模式...")
673
+ info("=" * 60)
674
+
675
+ # 进入交互模式
676
+ interactive_mode(
677
+ coordinator=coordinator,
678
+ output_mgr=output_mgr,
679
+ output_dir=output_dir,
680
+ selector=selector,
681
+ first_description=first_description,
682
+ )
683
+
684
+
685
+ if __name__ == "__main__":
686
+ main()