Coverage for cli/app.py: 12%

1"""

2主程序入口模块

3

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

5

6 Import 顺序说明：

7 触发大量子模块 import 的代码（tools、agent 等）必须在 setup_logger 之后延迟导入，

8 否则各模块顶层的 get_logger() 会在日志系统初始化前执行，导致日志写入错误的文件。

9"""

10 import argparse

11 from rich.console import Console

12 from rich.panel import Panel

13 from qrclaw.logger import setup_logger

14 from qrclaw.workspace import Workspace

15 from qrclaw.config import LOG_LEVEL, LOG_MAX_DAYS, LOG_TO_FILE, LOG_TO_CONSOLE, LOG_CONSOLE_LEVEL

16 from qrclaw.cli.input import get_input

17 from qrclaw.cli.display import show_context_usage, show_plan_progress

18 from qrclaw.cli.commands import session as session_cmd

19 from qrclaw.cli.commands import skill as skill_cmd

20 from qrclaw.cli.commands import agent as agent_cmd

21

22 console = Console()

23

24

25 def 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

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