jarvis-ai-assistant 0.3.26__py3-none-any.whl → 0.3.27__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.
Files changed (33) hide show
  1. jarvis/__init__.py +1 -1
  2. jarvis/jarvis_agent/__init__.py +290 -173
  3. jarvis/jarvis_agent/config.py +92 -0
  4. jarvis/jarvis_agent/event_bus.py +48 -0
  5. jarvis/jarvis_agent/jarvis.py +69 -35
  6. jarvis/jarvis_agent/memory_manager.py +70 -2
  7. jarvis/jarvis_agent/prompt_manager.py +82 -0
  8. jarvis/jarvis_agent/run_loop.py +130 -0
  9. jarvis/jarvis_agent/task_analyzer.py +88 -9
  10. jarvis/jarvis_agent/task_manager.py +26 -0
  11. jarvis/jarvis_agent/user_interaction.py +42 -0
  12. jarvis/jarvis_code_agent/code_agent.py +18 -3
  13. jarvis/jarvis_code_agent/lint.py +5 -5
  14. jarvis/jarvis_data/config_schema.json +7 -6
  15. jarvis/jarvis_git_squash/main.py +6 -1
  16. jarvis/jarvis_git_utils/git_commiter.py +38 -12
  17. jarvis/jarvis_platform/base.py +4 -5
  18. jarvis/jarvis_platform_manager/main.py +28 -11
  19. jarvis/jarvis_stats/cli.py +13 -32
  20. jarvis/jarvis_stats/stats.py +179 -51
  21. jarvis/jarvis_tools/registry.py +15 -0
  22. jarvis/jarvis_tools/sub_agent.py +94 -84
  23. jarvis/jarvis_tools/sub_code_agent.py +12 -6
  24. jarvis/jarvis_utils/config.py +14 -0
  25. jarvis/jarvis_utils/fzf.py +56 -0
  26. jarvis/jarvis_utils/input.py +0 -3
  27. jarvis/jarvis_utils/utils.py +56 -8
  28. {jarvis_ai_assistant-0.3.26.dist-info → jarvis_ai_assistant-0.3.27.dist-info}/METADATA +2 -3
  29. {jarvis_ai_assistant-0.3.26.dist-info → jarvis_ai_assistant-0.3.27.dist-info}/RECORD +33 -27
  30. {jarvis_ai_assistant-0.3.26.dist-info → jarvis_ai_assistant-0.3.27.dist-info}/WHEEL +0 -0
  31. {jarvis_ai_assistant-0.3.26.dist-info → jarvis_ai_assistant-0.3.27.dist-info}/entry_points.txt +0 -0
  32. {jarvis_ai_assistant-0.3.26.dist-info → jarvis_ai_assistant-0.3.27.dist-info}/licenses/LICENSE +0 -0
  33. {jarvis_ai_assistant-0.3.26.dist-info → jarvis_ai_assistant-0.3.27.dist-info}/top_level.txt +0 -0
@@ -6,7 +6,7 @@
6
6
  from typing import Optional
7
7
 
8
8
  from jarvis.jarvis_utils.globals import get_interrupt, set_interrupt
9
- from jarvis.jarvis_utils.input import user_confirm
9
+
10
10
  from jarvis.jarvis_agent.prompts import TASK_ANALYSIS_PROMPT
11
11
  from jarvis.jarvis_utils.output import OutputType, PrettyOutput
12
12
 
@@ -22,6 +22,12 @@ class TaskAnalyzer:
22
22
  agent: Agent实例
23
23
  """
24
24
  self.agent = agent
25
+ self._analysis_done = False
26
+ # 旁路集成事件订阅,失败不影响主流程
27
+ try:
28
+ self._subscribe_events()
29
+ except Exception:
30
+ pass
25
31
 
26
32
  def analysis_task(self, satisfaction_feedback: str = ""):
27
33
  """分析任务并生成方法论"""
@@ -40,6 +46,13 @@ class TaskAnalyzer:
40
46
 
41
47
  except Exception as e:
42
48
  PrettyOutput.print("分析失败", OutputType.ERROR)
49
+ finally:
50
+ # 标记已完成一次分析,避免事件回调重复执行
51
+ self._analysis_done = True
52
+ try:
53
+ self.agent.set_user_data("__task_analysis_done__", True)
54
+ except Exception:
55
+ pass
43
56
 
44
57
  def _prepare_analysis_prompt(self, satisfaction_feedback: str) -> str:
45
58
  """准备分析提示"""
@@ -59,8 +72,27 @@ class TaskAnalyzer:
59
72
  if not self._handle_analysis_interrupt(response):
60
73
  break
61
74
 
62
- # 执行工具调用
63
- need_return, self.agent.session.prompt = self.agent._call_tools(response)
75
+ # 执行工具调用(补充事件:before_tool_call/after_tool_call)
76
+ try:
77
+ self.agent.event_bus.emit(
78
+ "before_tool_call",
79
+ agent=self.agent,
80
+ current_response=response,
81
+ )
82
+ except Exception:
83
+ pass
84
+ need_return, tool_prompt = self.agent._call_tools(response)
85
+ self.agent.session.prompt = tool_prompt
86
+ try:
87
+ self.agent.event_bus.emit(
88
+ "after_tool_call",
89
+ agent=self.agent,
90
+ current_response=response,
91
+ need_return=need_return,
92
+ tool_prompt=tool_prompt,
93
+ )
94
+ except Exception:
95
+ pass
64
96
 
65
97
  # 如果没有工具调用或者没有新的提示,退出循环
66
98
  if not self.agent.session.prompt:
@@ -73,8 +105,8 @@ class TaskAnalyzer:
73
105
  bool: True 继续分析,False 退出分析
74
106
  """
75
107
  set_interrupt(False)
76
- user_input = self.agent.multiline_inputer(
77
- f"分析任务期间被中断,请输入用户干预信息:"
108
+ user_input = self.agent._multiline_input(
109
+ "分析任务期间被中断,请输入用户干预信息:", False
78
110
  )
79
111
 
80
112
  if not user_input:
@@ -98,7 +130,7 @@ class TaskAnalyzer:
98
130
 
99
131
  def _handle_interrupt_with_tool_calls(self, user_input: str) -> str:
100
132
  """处理有工具调用时的中断"""
101
- if user_confirm("检测到有工具调用,是否继续处理工具调用?", True):
133
+ if self.agent.user_confirm("检测到有工具调用,是否继续处理工具调用?", True):
102
134
  return f"被用户中断,用户补充信息为:{user_input}\n\n用户同意继续工具调用。"
103
135
  else:
104
136
  return f"被用户中断,用户补充信息为:{user_input}\n\n检测到有工具调用,但被用户拒绝执行。请根据用户的补充信息重新考虑下一步操作。"
@@ -108,11 +140,11 @@ class TaskAnalyzer:
108
140
  satisfaction_feedback = ""
109
141
 
110
142
  if not auto_completed and self.agent.use_analysis:
111
- if user_confirm("您对本次任务的完成是否满意?", True):
143
+ if self.agent.user_confirm("您对本次任务的完成是否满意?", True):
112
144
  satisfaction_feedback = "\n\n用户对本次任务的完成表示满意。"
113
145
  else:
114
- feedback = self.agent.multiline_inputer(
115
- "请提供您的反馈意见(可留空直接回车):"
146
+ feedback = self.agent._multiline_input(
147
+ "请提供您的反馈意见(可留空直接回车):", False
116
148
  )
117
149
  if feedback:
118
150
  satisfaction_feedback = (
@@ -124,3 +156,50 @@ class TaskAnalyzer:
124
156
  )
125
157
 
126
158
  return satisfaction_feedback
159
+
160
+ # -----------------------
161
+ # 事件订阅与处理(旁路)
162
+ # -----------------------
163
+ def _subscribe_events(self) -> None:
164
+ bus = self.agent.get_event_bus() # type: ignore[attr-defined]
165
+ # 在生成总结前触发(保持与原顺序一致)
166
+ bus.subscribe("before_summary", self._on_before_summary)
167
+ # 当无需总结时,作为兜底触发分析
168
+ bus.subscribe("task_completed", self._on_task_completed)
169
+
170
+ def _on_before_summary(self, **payload) -> None:
171
+ if self._analysis_done:
172
+ return
173
+ # 避免与直接调用重复
174
+ try:
175
+ if bool(self.agent.get_user_data("__task_analysis_done__")):
176
+ self._analysis_done = True
177
+ return
178
+ except Exception:
179
+ pass
180
+ auto_completed = bool(payload.get("auto_completed", False))
181
+ try:
182
+ feedback = self.collect_satisfaction_feedback(auto_completed)
183
+ if getattr(self.agent, "use_analysis", False):
184
+ self.analysis_task(feedback)
185
+ except Exception:
186
+ # 忽略事件处理异常,保证主流程
187
+ self._analysis_done = True
188
+
189
+ def _on_task_completed(self, **payload) -> None:
190
+ # 当未在 before_summary 阶段执行过时,作为兜底
191
+ if self._analysis_done:
192
+ return
193
+ try:
194
+ if bool(self.agent.get_user_data("__task_analysis_done__")):
195
+ self._analysis_done = True
196
+ return
197
+ except Exception:
198
+ pass
199
+ auto_completed = bool(payload.get("auto_completed", False))
200
+ try:
201
+ feedback = self.collect_satisfaction_feedback(auto_completed)
202
+ if getattr(self.agent, "use_analysis", False):
203
+ self.analysis_task(feedback)
204
+ except Exception:
205
+ self._analysis_done = True
@@ -15,6 +15,7 @@ from jarvis.jarvis_agent import (
15
15
  user_confirm,
16
16
  )
17
17
  from jarvis.jarvis_utils.config import get_data_dir
18
+ from jarvis.jarvis_utils.fzf import fzf_select
18
19
 
19
20
 
20
21
  class TaskManager:
@@ -89,6 +90,31 @@ class TaskManager:
89
90
  Console().print(table)
90
91
  PrettyOutput.print("[0] 跳过预定义任务", OutputType.INFO)
91
92
 
93
+ # Try fzf selection first (with numbered options and a skip option)
94
+ fzf_list = [f"{0:>3} | 跳过预定义任务"] + [
95
+ f"{i:>3} | {name}" for i, name in enumerate(task_names, 1)
96
+ ]
97
+ selected_str = fzf_select(fzf_list, prompt="选择一个任务编号 (ESC跳过) > ")
98
+ if selected_str:
99
+ try:
100
+ num_part = selected_str.split("|", 1)[0].strip()
101
+ idx = int(num_part)
102
+ if idx == 0:
103
+ return ""
104
+ if 1 <= idx <= len(task_names):
105
+ selected_task = tasks[task_names[idx - 1]]
106
+ PrettyOutput.print(f"将要执行任务:\n {selected_task}", OutputType.INFO)
107
+ # 询问是否需要补充信息
108
+ need_additional = user_confirm("需要为此任务添加补充信息吗?", default=False)
109
+ if need_additional:
110
+ additional_input = get_multiline_input("请输入补充信息:")
111
+ if additional_input:
112
+ selected_task = f"{selected_task}\n\n补充信息:\n{additional_input}"
113
+ return selected_task
114
+ except Exception:
115
+ # 如果解析失败,则回退到手动输入
116
+ pass
117
+
92
118
  while True:
93
119
  try:
94
120
  choice_str = prompt(
@@ -0,0 +1,42 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ UserInteractionHandler: 抽象用户交互(多行输入与确认)逻辑,便于将来替换为 TUI/WebUI。
4
+
5
+ 阶段一(最小变更):
6
+ - 仅提供封装,不直接修改 Agent 的现有调用
7
+ - 后续步骤在 Agent 中以旁路方式接入,保持向后兼容
8
+ """
9
+ from typing import Callable, Optional
10
+
11
+
12
+
13
+ class UserInteractionHandler:
14
+ def __init__(
15
+ self,
16
+ multiline_inputer: Callable[..., str],
17
+ confirm_func: Callable[[str, bool], bool],
18
+ ) -> None:
19
+ """
20
+ 参数:
21
+ - multiline_inputer: 提供多行输入的函数,优先支持 (tip, print_on_empty=bool),兼容仅接受 (tip) 的实现
22
+ - confirm_func: 用户确认函数 (tip: str, default: bool) -> bool
23
+ """
24
+ self._multiline_inputer = multiline_inputer
25
+ self._confirm = confirm_func
26
+
27
+ def multiline_input(self, tip: str, print_on_empty: bool) -> str:
28
+ """
29
+ 多行输入封装:兼容两类签名
30
+ 1) func(tip, print_on_empty=True/False)
31
+ 2) func(tip)
32
+ """
33
+ try:
34
+ return self._multiline_inputer(tip, print_on_empty=print_on_empty) # type: ignore[call-arg]
35
+ except TypeError:
36
+ return self._multiline_inputer(tip) # type: ignore[misc]
37
+
38
+ def confirm(self, tip: str, default: bool = True) -> bool:
39
+ """
40
+ 用户确认封装,直接委派
41
+ """
42
+ return self._confirm(tip, default)
@@ -21,6 +21,7 @@ from jarvis.jarvis_tools.registry import ToolRegistry
21
21
  from jarvis.jarvis_utils.config import (
22
22
  is_confirm_before_apply_patch,
23
23
  is_enable_static_analysis,
24
+ get_git_check_mode,
24
25
  )
25
26
  from jarvis.jarvis_utils.git_utils import (
26
27
  confirm_add_new_files,
@@ -197,6 +198,17 @@ class CodeAgent:
197
198
  missing_configs
198
199
  )
199
200
  PrettyOutput.print(message, OutputType.WARNING)
201
+ # 通过配置控制严格校验模式(JARVIS_GIT_CHECK_MODE):
202
+ # - warn: 仅告警并继续,后续提交可能失败
203
+ # - strict: 严格模式(默认),直接退出
204
+ mode = get_git_check_mode().lower()
205
+ if mode == "warn":
206
+ PrettyOutput.print(
207
+ "已启用 Git 校验警告模式(JARVIS_GIT_CHECK_MODE=warn),将继续运行。"
208
+ "注意:后续提交可能失败,请尽快配置 git user.name 与 user.email。",
209
+ OutputType.INFO,
210
+ )
211
+ return
200
212
  sys.exit(1)
201
213
 
202
214
  except FileNotFoundError:
@@ -250,7 +262,7 @@ class CodeAgent:
250
262
  if has_uncommitted_changes():
251
263
 
252
264
  git_commiter = GitCommitTool()
253
- git_commiter.execute({"prefix": prefix, "suffix": suffix})
265
+ git_commiter.execute({"prefix": prefix, "suffix": suffix, "agent": self.agent})
254
266
 
255
267
  def _init_env(self, prefix: str, suffix: str) -> None:
256
268
  """初始化环境,组合以下功能:
@@ -510,7 +522,7 @@ class CodeAgent:
510
522
  check=True,
511
523
  )
512
524
  git_commiter = GitCommitTool()
513
- git_commiter.execute({"prefix": prefix, "suffix": suffix})
525
+ git_commiter.execute({"prefix": prefix, "suffix": suffix, "agent": self.agent})
514
526
 
515
527
  # 在用户接受commit后,根据配置决定是否保存记忆
516
528
  if self.agent.force_save_memory:
@@ -544,7 +556,7 @@ class CodeAgent:
544
556
  f"提交 {i+1}: {commit['hash'][:7]} - {commit['message']} ({len(commit['files'])}个文件)\n"
545
557
  + "\n".join(f" - {file}" for file in commit["files"][:5])
546
558
  + ("\n ..." if len(commit["files"]) > 5 else "")
547
- for i, commit in enumerate(commits_info)
559
+ for i, commit in enumerate(commits_info[:5])
548
560
  )
549
561
  project_info.append(f"最近提交:\n{commits_str}")
550
562
 
@@ -575,6 +587,8 @@ class CodeAgent:
575
587
  PrettyOutput.print(f"执行失败: {str(e)}", OutputType.WARNING)
576
588
  return str(e)
577
589
 
590
+
591
+
578
592
  self._handle_uncommitted_changes()
579
593
  end_commit = get_latest_commit_hash()
580
594
  commits = self._show_commit_history(start_commit, end_commit)
@@ -615,6 +629,7 @@ class CodeAgent:
615
629
 
616
630
  StatsManager.increment("code_modifications", group="code_agent")
617
631
 
632
+
618
633
  # 获取提交信息
619
634
  end_hash = get_latest_commit_hash()
620
635
  commits = get_commits_between(start_hash, end_hash)
@@ -27,11 +27,11 @@ LINT_TOOLS = {
27
27
  # Go
28
28
  ".go": ["go vet"],
29
29
  # Python
30
- ".py": ["black", "pylint", "mypy"],
31
- ".pyw": ["black", "pylint", "mypy"],
32
- ".pyi": ["black", "pylint", "mypy"],
33
- ".pyx": ["black", "pylint", "mypy"],
34
- ".pxd": ["black", "pylint", "mypy"],
30
+ ".py": ["ruff", "mypy"],
31
+ ".pyw": ["ruff", "mypy"],
32
+ ".pyi": ["ruff", "mypy"],
33
+ ".pyx": ["ruff", "mypy"],
34
+ ".pxd": ["ruff", "mypy"],
35
35
  # Rust
36
36
  ".rs": ["cargo clippy", "rustfmt"],
37
37
  ".rlib": ["cargo clippy", "rustfmt"],
@@ -126,8 +126,6 @@
126
126
  "description": "常规操作模型名称",
127
127
  "default": "deep_seek_v3"
128
128
  },
129
-
130
-
131
129
  "JARVIS_WEB_SEARCH_PLATFORM": {
132
130
  "type": "string",
133
131
  "description": "Web搜索使用的平台名称",
@@ -160,8 +158,6 @@
160
158
  "type": "string",
161
159
  "default": "deep_seek_v3"
162
160
  },
163
-
164
-
165
161
  "JARVIS_MAX_INPUT_TOKEN_COUNT": {
166
162
  "type": "number",
167
163
  "default": 32000
@@ -196,7 +192,6 @@
196
192
  "description": "Jarvis数据存储目录路径",
197
193
  "default": "~/.jarvis"
198
194
  },
199
-
200
195
  "JARVIS_PRETTY_OUTPUT": {
201
196
  "type": "boolean",
202
197
  "description": "是否启用美化输出",
@@ -292,6 +287,12 @@
292
287
  "description": "是否启用立即中断:在对话迭代中检测到中断信号时立即返回",
293
288
  "default": false
294
289
  },
290
+ "JARVIS_GIT_CHECK_MODE": {
291
+ "type": "string",
292
+ "enum": ["strict", "warn"],
293
+ "description": "Git 配置校验模式:strict 表示严格模式(默认),缺失配置时直接退出;warn 表示警告模式,仅提示并继续运行",
294
+ "default": "strict"
295
+ },
295
296
  "JARVIS_TOOL_GROUP": {
296
297
  "type": "string",
297
298
  "description": "选择一个预定义的工具配置组",
@@ -445,4 +446,4 @@
445
446
  }
446
447
  },
447
448
  "additionalProperties": true
448
- }
449
+ }
@@ -8,6 +8,7 @@ from jarvis.jarvis_git_utils.git_commiter import GitCommitTool
8
8
  from jarvis.jarvis_utils.output import OutputType, PrettyOutput
9
9
  from jarvis.jarvis_utils.utils import init_env
10
10
  from jarvis.jarvis_utils.input import user_confirm
11
+ from jarvis.jarvis_utils.globals import get_agent, current_agent_name
11
12
 
12
13
  app = typer.Typer(help="Git压缩工具")
13
14
 
@@ -46,7 +47,11 @@ class GitSquashTool:
46
47
 
47
48
  # Use existing GitCommitTool for new commit
48
49
  commit_tool = GitCommitTool()
49
- commit_tool.execute({"lang": args.get("lang", "Chinese")})
50
+ agent = get_agent(current_agent_name)
51
+ exec_args = {"lang": args.get("lang", "Chinese")}
52
+ if agent:
53
+ exec_args["agent"] = agent
54
+ commit_tool.execute(exec_args)
50
55
  except Exception as e:
51
56
  PrettyOutput.print(f"压缩提交失败: {str(e)}", OutputType.WARNING)
52
57
 
@@ -163,6 +163,9 @@ commit信息
163
163
  {ct("COMMIT_MESSAGE")}
164
164
  """
165
165
 
166
+ # 优先从调用方传入的 agent 获取平台与模型
167
+ agent_from_args = args.get("agent")
168
+
166
169
  # Get model_group from args
167
170
  model_group = args.get("model_group")
168
171
 
@@ -172,21 +175,44 @@ commit信息
172
175
  get_normal_model_name,
173
176
  )
174
177
 
175
- platform_name = get_normal_platform_name(model_group)
176
- model_name = get_normal_model_name(model_group)
178
+ platform_name = None
179
+ model_name = None
177
180
 
178
- # If no explicit parameters, try to get from existing agent
179
- agent = get_agent(current_agent_name)
180
181
  if (
181
- not platform_name
182
- and agent
183
- and hasattr(agent, "model")
184
- and agent.model
182
+ agent_from_args
183
+ and hasattr(agent_from_args, "model")
184
+ and getattr(agent_from_args, "model", None)
185
185
  ):
186
- platform_name = agent.model.platform_name()
187
- model_name = agent.model.name()
188
- if not model_group:
189
- model_group = agent.model.model_group
186
+ try:
187
+ platform_name = agent_from_args.model.platform_name()
188
+ model_name = agent_from_args.model.name()
189
+ if not model_group and hasattr(
190
+ agent_from_args.model, "model_group"
191
+ ):
192
+ model_group = agent_from_args.model.model_group
193
+ except Exception:
194
+ # 安全回退到后续逻辑
195
+ platform_name = None
196
+ model_name = None
197
+
198
+ # 如果未能从agent获取到,再根据 model_group 获取
199
+ if not platform_name:
200
+ platform_name = get_normal_platform_name(model_group)
201
+ if not model_name:
202
+ model_name = get_normal_model_name(model_group)
203
+
204
+ # If no explicit parameters, try to get from existing global agent
205
+ if not platform_name:
206
+ agent = get_agent(current_agent_name)
207
+ if (
208
+ agent
209
+ and hasattr(agent, "model")
210
+ and getattr(agent, "model", None)
211
+ ):
212
+ platform_name = agent.model.platform_name()
213
+ model_name = agent.model.name()
214
+ if not model_group and hasattr(agent.model, "model_group"):
215
+ model_group = agent.model.model_group
190
216
 
191
217
  # Create a new platform instance
192
218
  if platform_name:
@@ -188,12 +188,11 @@ class BasePlatform(ABC):
188
188
 
189
189
  # If content overflows, truncate to show only the last few lines
190
190
  if len(lines) > max_text_height:
191
- new_text = "\n".join(
192
- text_content.plain.splitlines()[
193
- -max_text_height:
194
- ]
191
+ # Rebuild the text from the wrapped lines to ensure visual consistency
192
+ # This correctly handles both wrapped long lines and explicit newlines
193
+ text_content.plain = "\n".join(
194
+ [line.plain for line in lines[-max_text_height:]]
195
195
  )
196
- text_content.plain = new_text
197
196
 
198
197
  panel.subtitle = (
199
198
  "[yellow]正在回答... (按 Ctrl+C 中断)[/yellow]"
@@ -18,6 +18,7 @@ from jarvis.jarvis_utils.input import get_multiline_input, get_single_line_input
18
18
  from jarvis.jarvis_utils.output import OutputType, PrettyOutput
19
19
  from jarvis.jarvis_utils.utils import init_env
20
20
  from jarvis.jarvis_platform_manager.service import start_service
21
+ from jarvis.jarvis_utils.fzf import fzf_select
21
22
 
22
23
  app = typer.Typer(help="Jarvis AI 平台")
23
24
 
@@ -451,17 +452,33 @@ def role_command(
451
452
  )
452
453
  PrettyOutput.print(output_str, OutputType.INFO)
453
454
 
454
- # 让用户选择角色
455
- raw_choice = get_single_line_input("请选择角色(输入编号,直接回车退出): ")
456
- if not raw_choice.strip():
457
- PrettyOutput.print("已取消,退出程序", OutputType.INFO)
458
- raise typer.Exit(code=0)
459
- try:
460
- choice = int(raw_choice)
461
- selected_role = config["roles"][choice - 1]
462
- except (ValueError, IndexError):
463
- PrettyOutput.print("无效的选择", OutputType.ERROR)
464
- return
455
+ # 让用户选择角色(优先 fzf,回退编号输入)
456
+ selected_role = None # type: ignore[var-annotated]
457
+ fzf_options = [
458
+ f"{i:>3} | {role['name']} - {role.get('description', '')}"
459
+ for i, role in enumerate(config["roles"], 1)
460
+ ]
461
+ selected_str = fzf_select(fzf_options, prompt="选择角色编号 (Enter退出) > ")
462
+ if selected_str:
463
+ try:
464
+ num_part = selected_str.split("|", 1)[0].strip()
465
+ idx = int(num_part)
466
+ if 1 <= idx <= len(config["roles"]):
467
+ selected_role = config["roles"][idx - 1]
468
+ except Exception:
469
+ selected_role = None
470
+
471
+ if selected_role is None:
472
+ raw_choice = get_single_line_input("请选择角色(输入编号,直接回车退出): ")
473
+ if not raw_choice.strip():
474
+ PrettyOutput.print("已取消,退出程序", OutputType.INFO)
475
+ raise typer.Exit(code=0)
476
+ try:
477
+ choice = int(raw_choice)
478
+ selected_role = config["roles"][choice - 1]
479
+ except (ValueError, IndexError):
480
+ PrettyOutput.print("无效的选择", OutputType.ERROR)
481
+ return
465
482
 
466
483
 
467
484
 
@@ -98,11 +98,6 @@ def inc(
98
98
  @app.command()
99
99
  def show(
100
100
  metric: Optional[str] = typer.Argument(None, help="指标名称,不指定则显示所有"),
101
- last_hours: Optional[int] = typer.Option(None, "--hours", "-h", help="最近N小时"),
102
- last_days: Optional[int] = typer.Option(None, "--days", "-d", help="最近N天"),
103
- format: str = typer.Option(
104
- "table", "--format", "-f", help="显示格式: table/chart/summary"
105
- ),
106
101
  aggregation: str = typer.Option(
107
102
  "hourly", "--agg", "-a", help="聚合方式: hourly/daily"
108
103
  ),
@@ -123,9 +118,7 @@ def show(
123
118
 
124
119
  stats.show(
125
120
  metric_name=metric,
126
- last_hours=last_hours,
127
- last_days=last_days,
128
- format=format,
121
+
129
122
  aggregation=aggregation,
130
123
  tags=tag_dict if tag_dict else None,
131
124
  )
@@ -136,8 +129,6 @@ def plot(
136
129
  metric: Optional[str] = typer.Argument(
137
130
  None, help="指标名称(可选,不指定则根据标签过滤所有匹配的指标)"
138
131
  ),
139
- last_hours: Optional[int] = typer.Option(None, "--hours", "-h", help="最近N小时"),
140
- last_days: Optional[int] = typer.Option(None, "--days", "-d", help="最近N天"),
141
132
  aggregation: str = typer.Option(
142
133
  "hourly", "--agg", "-a", help="聚合方式: hourly/daily"
143
134
  ),
@@ -160,8 +151,6 @@ def plot(
160
151
 
161
152
  stats.plot(
162
153
  metric_name=metric,
163
- last_hours=last_hours,
164
- last_days=last_days,
165
154
  aggregation=aggregation,
166
155
  width=width,
167
156
  height=height,
@@ -260,9 +249,9 @@ def clean(
260
249
  @app.command()
261
250
  def export(
262
251
  metric: str = typer.Argument(..., help="指标名称"),
263
- output: str = typer.Option("csv", "--format", "-f", help="输出格式: csv/json"),
264
- last_hours: Optional[int] = typer.Option(None, "--hours", "-h", help="最近N小时"),
265
- last_days: Optional[int] = typer.Option(None, "--days", "-d", help="最近N天"),
252
+
253
+
254
+
266
255
  tags: Optional[List[str]] = typer.Option(
267
256
  None, "--tag", "-t", help="标签过滤,格式: key=value"
268
257
  ),
@@ -285,27 +274,19 @@ def export(
285
274
  # 获取数据
286
275
  data = stats.get_stats(
287
276
  metric_name=metric,
288
- last_hours=last_hours,
289
- last_days=last_days,
290
277
  tags=tag_dict if tag_dict else None,
291
278
  )
292
279
 
293
- if output == "json":
294
- # JSON格式输出
295
- PrettyOutput.print(
296
- json.dumps(data, indent=2, ensure_ascii=False), OutputType.CODE, lang="json"
297
- )
280
+ # CSV格式输出(默认)
281
+ records = data.get("records", [])
282
+ if records:
283
+ writer = csv.writer(sys.stdout)
284
+ writer.writerow(["timestamp", "value", "tags"])
285
+ for record in records:
286
+ tags_str = json.dumps(record.get("tags", {}))
287
+ writer.writerow([record["timestamp"], record["value"], tags_str])
298
288
  else:
299
- # CSV格式输出
300
- records = data.get("records", [])
301
- if records:
302
- writer = csv.writer(sys.stdout)
303
- writer.writerow(["timestamp", "value", "tags"])
304
- for record in records:
305
- tags_str = json.dumps(record.get("tags", {}))
306
- writer.writerow([record["timestamp"], record["value"], tags_str])
307
- else:
308
- rprint("[yellow]没有找到数据[/yellow]", file=sys.stderr)
289
+ rprint("[yellow]没有找到数据[/yellow]", file=sys.stderr)
309
290
 
310
291
 
311
292
  @app.command()