Coverage for cli / app.py: 12%

113 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-29 02:55 +0800

1""" 

2主程序入口模块 

3 

4负责参数解析、初始化和主循环。 

5 

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 

21 

22console = Console() 

23 

24 

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() 

35 

36 if args.help: 

37 _print_help() 

38 return 

39 

40 # 1. 初始化工作空间(确定所有路径) 

41 workspace = Workspace(agent_id=args.agent) 

42 

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 

50 

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 ) 

57 

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 ) 

68 

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") 

73 

74 # 6. 启动心跳(可选) 

75 if not args.no_heartbeat: 

76 from qrclaw.heartbeat import start_heartbeat 

77 from qrclaw.config import HEARTBEAT_ENABLED, HEARTBEAT_INTERVAL 

78 

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") 

88 

89 start_heartbeat(interval=HEARTBEAT_INTERVAL, on_trigger=on_heartbeat) 

90 

91 # 启动提示 

92 is_resumed = len(session.messages) > 0 

93 session_status = "[dim]恢复会话[/dim]" if is_resumed else "[dim]新会话[/dim]" 

94 

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 ) 

102 

103 if session.messages: 

104 console.print(f"[dim]已加载历史会话,共 {len(session.messages)} 条消息[/dim]\n") 

105 

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 

114 

115 user_input = user_input.strip() 

116 

117 if not user_input: 

118 continue 

119 

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 

126 

127 if user_input.startswith("/session"): 

128 sub = user_input[8:].strip() 

129 session = session_cmd.handle(sub, session, console, workspace) 

130 continue 

131 

132 if user_input.startswith("/skill"): 

133 sub = user_input[6:].strip() 

134 skill_cmd.handle(sub, console, workspace) 

135 continue 

136 

137 if user_input == "exit": 

138 console.print("[dim]再见![/dim]") 

139 break 

140 

141 if user_input == "clear": 

142 session.clear() 

143 console.print("[dim]会话已清除[/dim]\n") 

144 continue 

145 

146 # 清除 prompt_toolkit 留下的输入行,再打印带框版本 

147 lines = user_input.count("\n") + 1 

148 print(f"\033[{lines}A\033[J", end="") 

149 

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 )) 

157 

158 try: 

159 run(user_input, session, console, workspace, auto_confirm=True) 

160 except Exception: 

161 console.print_exception() 

162 

163 

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 退出")