Coverage for cli / app.py: 12%
113 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-29 02:55 +0800
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-29 02:55 +0800
1"""
2主程序入口模块
4负责参数解析、初始化和主循环。
6Import 顺序说明:
7 触发大量子模块 import 的代码(tools、agent 等)必须在 setup_logger 之后延迟导入,
8 否则各模块顶层的 get_logger() 会在日志系统初始化前执行,导致日志写入错误的文件。
9"""
10import argparse
11from rich.console import Console
12from rich.panel import Panel
13from qrclaw.logger import setup_logger
14from qrclaw.workspace import Workspace
15from qrclaw.config import LOG_LEVEL, LOG_MAX_DAYS, LOG_TO_FILE, LOG_TO_CONSOLE, LOG_CONSOLE_LEVEL
16from qrclaw.cli.input import get_input
17from qrclaw.cli.display import show_context_usage, show_plan_progress
18from qrclaw.cli.commands import session as session_cmd
19from qrclaw.cli.commands import skill as skill_cmd
20from qrclaw.cli.commands import agent as agent_cmd
22console = Console()
25def main() -> None:
26 parser = argparse.ArgumentParser(prog="qrclaw", add_help=False)
27 parser.add_argument("-a", "--agent", default="default", metavar="ID",
28 help="指定 agent ID(默认: default)")
29 parser.add_argument("--no-heartbeat", action="store_true",
30 help="禁用心跳机制")
31 parser.add_argument("-n", "--new-session", action="store_true",
32 help="创建新会话,不恢复历史")
33 parser.add_argument("-h", "--help", action="store_true")
34 args, _ = parser.parse_known_args()
36 if args.help:
37 _print_help()
38 return
40 # 1. 初始化工作空间(确定所有路径)
41 workspace = Workspace(agent_id=args.agent)
43 # 2. 延迟导入各子模块(在 setup_logger 之前不能触发 get_logger)
44 import qrclaw.tools # noqa: E402 触发所有工具注册
45 from qrclaw.tools.spawn_agent import set_console as set_spawn_console
46 set_spawn_console(console) # 注入 console,子 agent 完成时直接打印
47 from qrclaw.agent import run
48 from qrclaw.memory.session import Session
49 from qrclaw.heartbeat import get_default_heartbeat_content, execute_heartbeat_tasks
51 # 3. 创建 Session(路径由 workspace 决定)
52 # resume=True 且不传 session_id 时,自动恢复最近的会话
53 session = Session(
54 sessions_dir=workspace.sessions_dir,
55 resume=not args.new_session
56 )
58 # 4. 初始化日志(路径由 workspace 决定)
59 setup_logger(
60 session_id=session.session_id,
61 log_level=LOG_LEVEL,
62 log_to_file=LOG_TO_FILE,
63 log_to_console=LOG_TO_CONSOLE,
64 log_max_days=LOG_MAX_DAYS,
65 console_level=LOG_CONSOLE_LEVEL,
66 log_dir=workspace.logs_dir,
67 )
69 # 5. 初始化 HEARTBEAT.md(如果不存在)
70 if not workspace.heartbeat_file.exists():
71 workspace.heartbeat_file.write_text(get_default_heartbeat_content(), encoding="utf-8")
72 console.print("[dim]已创建默认心跳任务配置: HEARTBEAT.md[/dim]\n")
74 # 6. 启动心跳(可选)
75 if not args.no_heartbeat:
76 from qrclaw.heartbeat import start_heartbeat
77 from qrclaw.config import HEARTBEAT_ENABLED, HEARTBEAT_INTERVAL
79 if HEARTBEAT_ENABLED:
80 def on_heartbeat():
81 """心跳触发时,后台执行维护任务"""
82 console.print("\n[bold yellow]⏰ 心跳触发[/bold yellow] - 后台执行维护任务...\n")
83 try:
84 result = execute_heartbeat_tasks(workspace)
85 console.print(f"\n[bold green]✅ 心跳任务完成[/bold green]\n[dim]{result[:200]}{'...' if len(result) > 200 else ''}[/dim]\n")
86 except Exception as e:
87 console.print(f"\n[bold red]❌ 心跳任务失败[/bold red]: {e}\n")
89 start_heartbeat(interval=HEARTBEAT_INTERVAL, on_trigger=on_heartbeat)
91 # 启动提示
92 is_resumed = len(session.messages) > 0
93 session_status = "[dim]恢复会话[/dim]" if is_resumed else "[dim]新会话[/dim]"
95 console.print(
96 f"[bold cyan]QRClaw Agent[/bold cyan] 启动 "
97 f"[dim]agent: [/dim][bold cyan]{workspace.agent_id}[/bold cyan] "
98 f"[dim]会话: [/dim][bold cyan]{session.session_id}[/bold cyan] {session_status}\n"
99 "[dim]Enter 发送 · Alt+Enter 换行 · exit 退出 · clear 清除 · "
100 "/agent 管理agent · /session 管理会话 · /skill 管理技能[/dim]\n"
101 )
103 if session.messages:
104 console.print(f"[dim]已加载历史会话,共 {len(session.messages)} 条消息[/dim]\n")
106 while True:
107 try:
108 show_plan_progress(console, session)
109 show_context_usage(console, session)
110 user_input = get_input(session.session_id)
111 except (KeyboardInterrupt, EOFError):
112 console.print("\n[dim]再见![/dim]")
113 break
115 user_input = user_input.strip()
117 if not user_input:
118 continue
120 if user_input.startswith("/agent"):
121 sub = user_input[6:].strip()
122 workspace, new_session = agent_cmd.handle(sub, workspace, session, console)
123 if new_session is not None:
124 session = new_session
125 continue
127 if user_input.startswith("/session"):
128 sub = user_input[8:].strip()
129 session = session_cmd.handle(sub, session, console, workspace)
130 continue
132 if user_input.startswith("/skill"):
133 sub = user_input[6:].strip()
134 skill_cmd.handle(sub, console, workspace)
135 continue
137 if user_input == "exit":
138 console.print("[dim]再见![/dim]")
139 break
141 if user_input == "clear":
142 session.clear()
143 console.print("[dim]会话已清除[/dim]\n")
144 continue
146 # 清除 prompt_toolkit 留下的输入行,再打印带框版本
147 lines = user_input.count("\n") + 1
148 print(f"\033[{lines}A\033[J", end="")
150 console.print(Panel(
151 user_input,
152 title="[bold magenta]你[/bold magenta]",
153 border_style="magenta",
154 expand=False,
155 width=min(len(user_input) + 6, console.width),
156 ))
158 try:
159 run(user_input, session, console, workspace, auto_confirm=True)
160 except Exception:
161 console.print_exception()
164def _print_help() -> None:
165 console.print("[bold cyan]QRClaw Agent[/bold cyan]")
166 console.print()
167 console.print("用法: qrclaw [-a <agentID>] [--no-heartbeat] [-n]")
168 console.print()
169 console.print("选项:")
170 console.print(" -a, --agent <ID> 指定 agent ID(默认: default)")
171 console.print(" --no-heartbeat 禁用心跳机制")
172 console.print(" -n, --new-session 创建新会话,不恢复历史")
173 console.print()
174 console.print("运行时命令:")
175 console.print(" /agent list 列出所有 agent")
176 console.print(" /agent new <id> 新建 agent")
177 console.print(" /agent switch <id> 切换 agent")
178 console.print(" /agent delete <id> 删除 agent")
179 console.print(" /session list 列出所有会话")
180 console.print(" /session new [id] 新建会话")
181 console.print(" /session switch <id> 切换会话")
182 console.print(" /session delete <id> 删除会话")
183 console.print(" /skill list 列出技能")
184 console.print(" /skill import <name> 导入技能")
185 console.print(" clear 清除当前会话")
186 console.print(" exit 退出")