jarvis-ai-assistant 0.1.222__py3-none-any.whl → 0.7.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.
Files changed (162) hide show
  1. jarvis/__init__.py +1 -1
  2. jarvis/jarvis_agent/__init__.py +1143 -245
  3. jarvis/jarvis_agent/agent_manager.py +97 -0
  4. jarvis/jarvis_agent/builtin_input_handler.py +12 -10
  5. jarvis/jarvis_agent/config_editor.py +57 -0
  6. jarvis/jarvis_agent/edit_file_handler.py +392 -99
  7. jarvis/jarvis_agent/event_bus.py +48 -0
  8. jarvis/jarvis_agent/events.py +157 -0
  9. jarvis/jarvis_agent/file_context_handler.py +79 -0
  10. jarvis/jarvis_agent/file_methodology_manager.py +117 -0
  11. jarvis/jarvis_agent/jarvis.py +1117 -147
  12. jarvis/jarvis_agent/main.py +78 -34
  13. jarvis/jarvis_agent/memory_manager.py +195 -0
  14. jarvis/jarvis_agent/methodology_share_manager.py +174 -0
  15. jarvis/jarvis_agent/prompt_manager.py +82 -0
  16. jarvis/jarvis_agent/prompts.py +46 -9
  17. jarvis/jarvis_agent/protocols.py +4 -1
  18. jarvis/jarvis_agent/rewrite_file_handler.py +141 -0
  19. jarvis/jarvis_agent/run_loop.py +146 -0
  20. jarvis/jarvis_agent/session_manager.py +9 -9
  21. jarvis/jarvis_agent/share_manager.py +228 -0
  22. jarvis/jarvis_agent/shell_input_handler.py +23 -3
  23. jarvis/jarvis_agent/stdio_redirect.py +295 -0
  24. jarvis/jarvis_agent/task_analyzer.py +212 -0
  25. jarvis/jarvis_agent/task_manager.py +154 -0
  26. jarvis/jarvis_agent/task_planner.py +496 -0
  27. jarvis/jarvis_agent/tool_executor.py +8 -4
  28. jarvis/jarvis_agent/tool_share_manager.py +139 -0
  29. jarvis/jarvis_agent/user_interaction.py +42 -0
  30. jarvis/jarvis_agent/utils.py +54 -0
  31. jarvis/jarvis_agent/web_bridge.py +189 -0
  32. jarvis/jarvis_agent/web_output_sink.py +53 -0
  33. jarvis/jarvis_agent/web_server.py +751 -0
  34. jarvis/jarvis_c2rust/__init__.py +26 -0
  35. jarvis/jarvis_c2rust/cli.py +613 -0
  36. jarvis/jarvis_c2rust/collector.py +258 -0
  37. jarvis/jarvis_c2rust/library_replacer.py +1122 -0
  38. jarvis/jarvis_c2rust/llm_module_agent.py +1300 -0
  39. jarvis/jarvis_c2rust/optimizer.py +960 -0
  40. jarvis/jarvis_c2rust/scanner.py +1681 -0
  41. jarvis/jarvis_c2rust/transpiler.py +2325 -0
  42. jarvis/jarvis_code_agent/build_validation_config.py +133 -0
  43. jarvis/jarvis_code_agent/code_agent.py +1605 -178
  44. jarvis/jarvis_code_agent/code_analyzer/__init__.py +62 -0
  45. jarvis/jarvis_code_agent/code_analyzer/base_language.py +74 -0
  46. jarvis/jarvis_code_agent/code_analyzer/build_validator/__init__.py +44 -0
  47. jarvis/jarvis_code_agent/code_analyzer/build_validator/base.py +102 -0
  48. jarvis/jarvis_code_agent/code_analyzer/build_validator/cmake.py +59 -0
  49. jarvis/jarvis_code_agent/code_analyzer/build_validator/detector.py +125 -0
  50. jarvis/jarvis_code_agent/code_analyzer/build_validator/fallback.py +69 -0
  51. jarvis/jarvis_code_agent/code_analyzer/build_validator/go.py +38 -0
  52. jarvis/jarvis_code_agent/code_analyzer/build_validator/java_gradle.py +44 -0
  53. jarvis/jarvis_code_agent/code_analyzer/build_validator/java_maven.py +38 -0
  54. jarvis/jarvis_code_agent/code_analyzer/build_validator/makefile.py +50 -0
  55. jarvis/jarvis_code_agent/code_analyzer/build_validator/nodejs.py +93 -0
  56. jarvis/jarvis_code_agent/code_analyzer/build_validator/python.py +129 -0
  57. jarvis/jarvis_code_agent/code_analyzer/build_validator/rust.py +54 -0
  58. jarvis/jarvis_code_agent/code_analyzer/build_validator/validator.py +154 -0
  59. jarvis/jarvis_code_agent/code_analyzer/build_validator.py +43 -0
  60. jarvis/jarvis_code_agent/code_analyzer/context_manager.py +363 -0
  61. jarvis/jarvis_code_agent/code_analyzer/context_recommender.py +18 -0
  62. jarvis/jarvis_code_agent/code_analyzer/dependency_analyzer.py +132 -0
  63. jarvis/jarvis_code_agent/code_analyzer/file_ignore.py +330 -0
  64. jarvis/jarvis_code_agent/code_analyzer/impact_analyzer.py +781 -0
  65. jarvis/jarvis_code_agent/code_analyzer/language_registry.py +185 -0
  66. jarvis/jarvis_code_agent/code_analyzer/language_support.py +89 -0
  67. jarvis/jarvis_code_agent/code_analyzer/languages/__init__.py +31 -0
  68. jarvis/jarvis_code_agent/code_analyzer/languages/c_cpp_language.py +231 -0
  69. jarvis/jarvis_code_agent/code_analyzer/languages/go_language.py +183 -0
  70. jarvis/jarvis_code_agent/code_analyzer/languages/python_language.py +219 -0
  71. jarvis/jarvis_code_agent/code_analyzer/languages/rust_language.py +209 -0
  72. jarvis/jarvis_code_agent/code_analyzer/llm_context_recommender.py +451 -0
  73. jarvis/jarvis_code_agent/code_analyzer/symbol_extractor.py +77 -0
  74. jarvis/jarvis_code_agent/code_analyzer/tree_sitter_extractor.py +48 -0
  75. jarvis/jarvis_code_agent/lint.py +275 -13
  76. jarvis/jarvis_code_agent/utils.py +142 -0
  77. jarvis/jarvis_code_analysis/checklists/loader.py +20 -6
  78. jarvis/jarvis_code_analysis/code_review.py +583 -548
  79. jarvis/jarvis_data/config_schema.json +339 -28
  80. jarvis/jarvis_git_squash/main.py +22 -13
  81. jarvis/jarvis_git_utils/git_commiter.py +171 -55
  82. jarvis/jarvis_mcp/sse_mcp_client.py +22 -15
  83. jarvis/jarvis_mcp/stdio_mcp_client.py +4 -4
  84. jarvis/jarvis_mcp/streamable_mcp_client.py +36 -16
  85. jarvis/jarvis_memory_organizer/memory_organizer.py +753 -0
  86. jarvis/jarvis_methodology/main.py +48 -63
  87. jarvis/jarvis_multi_agent/__init__.py +302 -43
  88. jarvis/jarvis_multi_agent/main.py +70 -24
  89. jarvis/jarvis_platform/ai8.py +40 -23
  90. jarvis/jarvis_platform/base.py +210 -49
  91. jarvis/jarvis_platform/human.py +11 -1
  92. jarvis/jarvis_platform/kimi.py +82 -76
  93. jarvis/jarvis_platform/openai.py +73 -1
  94. jarvis/jarvis_platform/registry.py +8 -15
  95. jarvis/jarvis_platform/tongyi.py +115 -101
  96. jarvis/jarvis_platform/yuanbao.py +89 -63
  97. jarvis/jarvis_platform_manager/main.py +194 -132
  98. jarvis/jarvis_platform_manager/service.py +122 -86
  99. jarvis/jarvis_rag/cli.py +156 -53
  100. jarvis/jarvis_rag/embedding_manager.py +155 -12
  101. jarvis/jarvis_rag/llm_interface.py +10 -13
  102. jarvis/jarvis_rag/query_rewriter.py +63 -12
  103. jarvis/jarvis_rag/rag_pipeline.py +222 -40
  104. jarvis/jarvis_rag/reranker.py +26 -3
  105. jarvis/jarvis_rag/retriever.py +270 -14
  106. jarvis/jarvis_sec/__init__.py +3605 -0
  107. jarvis/jarvis_sec/checkers/__init__.py +32 -0
  108. jarvis/jarvis_sec/checkers/c_checker.py +2680 -0
  109. jarvis/jarvis_sec/checkers/rust_checker.py +1108 -0
  110. jarvis/jarvis_sec/cli.py +116 -0
  111. jarvis/jarvis_sec/report.py +257 -0
  112. jarvis/jarvis_sec/status.py +264 -0
  113. jarvis/jarvis_sec/types.py +20 -0
  114. jarvis/jarvis_sec/workflow.py +219 -0
  115. jarvis/jarvis_smart_shell/main.py +405 -137
  116. jarvis/jarvis_stats/__init__.py +13 -0
  117. jarvis/jarvis_stats/cli.py +387 -0
  118. jarvis/jarvis_stats/stats.py +711 -0
  119. jarvis/jarvis_stats/storage.py +612 -0
  120. jarvis/jarvis_stats/visualizer.py +282 -0
  121. jarvis/jarvis_tools/ask_user.py +1 -0
  122. jarvis/jarvis_tools/base.py +18 -2
  123. jarvis/jarvis_tools/clear_memory.py +239 -0
  124. jarvis/jarvis_tools/cli/main.py +220 -144
  125. jarvis/jarvis_tools/execute_script.py +52 -12
  126. jarvis/jarvis_tools/file_analyzer.py +17 -12
  127. jarvis/jarvis_tools/generate_new_tool.py +46 -24
  128. jarvis/jarvis_tools/read_code.py +277 -18
  129. jarvis/jarvis_tools/read_symbols.py +141 -0
  130. jarvis/jarvis_tools/read_webpage.py +86 -13
  131. jarvis/jarvis_tools/registry.py +294 -90
  132. jarvis/jarvis_tools/retrieve_memory.py +227 -0
  133. jarvis/jarvis_tools/save_memory.py +194 -0
  134. jarvis/jarvis_tools/search_web.py +62 -28
  135. jarvis/jarvis_tools/sub_agent.py +205 -0
  136. jarvis/jarvis_tools/sub_code_agent.py +217 -0
  137. jarvis/jarvis_tools/virtual_tty.py +330 -62
  138. jarvis/jarvis_utils/builtin_replace_map.py +4 -5
  139. jarvis/jarvis_utils/clipboard.py +90 -0
  140. jarvis/jarvis_utils/config.py +607 -50
  141. jarvis/jarvis_utils/embedding.py +3 -0
  142. jarvis/jarvis_utils/fzf.py +57 -0
  143. jarvis/jarvis_utils/git_utils.py +251 -29
  144. jarvis/jarvis_utils/globals.py +174 -17
  145. jarvis/jarvis_utils/http.py +58 -79
  146. jarvis/jarvis_utils/input.py +899 -153
  147. jarvis/jarvis_utils/methodology.py +210 -83
  148. jarvis/jarvis_utils/output.py +220 -137
  149. jarvis/jarvis_utils/utils.py +1906 -135
  150. jarvis_ai_assistant-0.7.0.dist-info/METADATA +465 -0
  151. jarvis_ai_assistant-0.7.0.dist-info/RECORD +192 -0
  152. {jarvis_ai_assistant-0.1.222.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/entry_points.txt +8 -2
  153. jarvis/jarvis_git_details/main.py +0 -265
  154. jarvis/jarvis_platform/oyi.py +0 -357
  155. jarvis/jarvis_tools/edit_file.py +0 -255
  156. jarvis/jarvis_tools/rewrite_file.py +0 -195
  157. jarvis_ai_assistant-0.1.222.dist-info/METADATA +0 -767
  158. jarvis_ai_assistant-0.1.222.dist-info/RECORD +0 -110
  159. /jarvis/{jarvis_git_details → jarvis_memory_organizer}/__init__.py +0 -0
  160. {jarvis_ai_assistant-0.1.222.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/WHEEL +0 -0
  161. {jarvis_ai_assistant-0.1.222.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/licenses/LICENSE +0 -0
  162. {jarvis_ai_assistant-0.1.222.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/top_level.txt +0 -0
@@ -1,175 +1,1145 @@
1
1
  # -*- coding: utf-8 -*-
2
- import argparse
2
+ """Jarvis AI 助手主入口模块"""
3
+ from typing import Optional, List
4
+ import shutil
5
+ from datetime import datetime
6
+
7
+ import typer
8
+
9
+ from jarvis.jarvis_agent import OutputType, PrettyOutput
10
+ from jarvis.jarvis_agent.agent_manager import AgentManager
11
+ from jarvis.jarvis_agent.config_editor import ConfigEditor
12
+ from jarvis.jarvis_agent.methodology_share_manager import MethodologyShareManager
13
+ from jarvis.jarvis_agent.tool_share_manager import ToolShareManager
14
+ from jarvis.jarvis_utils.utils import init_env
15
+ from jarvis.jarvis_utils.config import (
16
+ is_enable_git_repo_jca_switch,
17
+ is_enable_builtin_config_selector,
18
+ get_agent_definition_dirs,
19
+ get_multi_agent_dirs,
20
+ get_roles_dirs,
21
+ get_data_dir,
22
+ set_config,
23
+ is_non_interactive,
24
+ )
25
+ import jarvis.jarvis_utils.utils as jutils
26
+ from jarvis.jarvis_utils.input import user_confirm, get_single_line_input
27
+ from jarvis.jarvis_utils.fzf import fzf_select
3
28
  import os
29
+ import subprocess
30
+ from pathlib import Path
31
+ import signal
32
+ import yaml # type: ignore
33
+ from rich.table import Table
34
+ from rich.console import Console
35
+
4
36
  import sys
5
- from typing import Dict
6
37
 
7
- import yaml # type: ignore
8
- from prompt_toolkit import prompt # type: ignore
9
-
10
- from jarvis.jarvis_agent import (
11
- Agent,
12
- OutputType,
13
- PrettyOutput,
14
- get_multiline_input,
15
- origin_agent_system_prompt,
16
- user_confirm,
17
- )
18
- from jarvis.jarvis_agent.builtin_input_handler import builtin_input_handler
19
- from jarvis.jarvis_agent.shell_input_handler import shell_input_handler
20
- from jarvis.jarvis_tools.registry import ToolRegistry
21
- from jarvis.jarvis_utils.config import get_data_dir
22
- from jarvis.jarvis_utils.utils import init_env
23
38
 
39
+ def _normalize_backup_data_argv(argv: List[str]) -> None:
40
+ """
41
+ 兼容旧版 Click/Typer 对可选参数的解析差异:
42
+ 若用户仅提供 --backup-data 而不跟参数,则在解析前注入默认目录。
43
+ """
44
+ try:
45
+ i = 0
46
+ while i < len(argv):
47
+ tok = argv[i]
48
+ if tok == "--backup-data":
49
+ # 情况1:位于末尾,无参数
50
+ # 情况2:后续是下一个选项(以 '-' 开头),表示未提供参数
51
+ if i == len(argv) - 1 or (i + 1 < len(argv) and argv[i + 1].startswith("-")):
52
+ argv.insert(i + 1, "~/jarvis_backups")
53
+ i += 1 # 跳过我们插入的默认值,避免重复插入
54
+ i += 1
55
+ except Exception:
56
+ # 静默忽略任何异常,避免影响主流程
57
+ pass
24
58
 
25
- def _load_tasks() -> Dict[str, str]:
26
- """Load tasks from .jarvis files in user home and current directory."""
27
- tasks: Dict[str, str] = {}
28
59
 
29
- # Check pre-command in data directory
30
- data_dir = get_data_dir()
31
- pre_command_path = os.path.join(data_dir, "pre-command")
32
- if os.path.exists(pre_command_path):
33
- print(f"🔍 从{pre_command_path}加载预定义任务...")
34
- try:
35
- with open(pre_command_path, "r", encoding="utf-8", errors="ignore") as f:
36
- user_tasks = yaml.safe_load(f)
37
- if isinstance(user_tasks, dict):
38
- for name, desc in user_tasks.items():
39
- if desc:
40
- tasks[str(name)] = str(desc)
41
- print(f"✅ 预定义任务加载完成 {pre_command_path}")
42
- except (yaml.YAMLError, OSError):
43
- print(f" 预定义任务加载失败 {pre_command_path}")
44
-
45
- # Check .jarvis/pre-command in current directory
46
- pre_command_path = ".jarvis/pre-command"
47
- if os.path.exists(pre_command_path):
48
- abs_path = os.path.abspath(pre_command_path)
49
- print(f"🔍 从{abs_path}加载预定义任务...")
60
+ _normalize_backup_data_argv(sys.argv)
61
+
62
+ app = typer.Typer(help="Jarvis AI 助手")
63
+
64
+
65
+ def print_commands_overview() -> None:
66
+ """打印命令与快捷方式总览表。"""
67
+ try:
68
+ cmd_table = Table(show_header=True, header_style="bold magenta")
69
+ cmd_table.add_column("命令", style="bold")
70
+ cmd_table.add_column("快捷方式", style="cyan")
71
+ cmd_table.add_column("功能描述", style="white")
72
+
73
+ cmd_table.add_row("jarvis", "jvs", "通用AI代理,适用于多种任务")
74
+ cmd_table.add_row("jarvis-agent", "ja", "AI代理基础功能,处理会话和任务")
75
+ cmd_table.add_row(
76
+ "jarvis-code-agent",
77
+ "jca",
78
+ "专注于代码分析、修改和生成的代码代理",
79
+ )
80
+ cmd_table.add_row("jarvis-code-review", "jcr", "智能代码审查工具")
81
+ cmd_table.add_row(
82
+ "jarvis-git-commit",
83
+ "jgc",
84
+ "自动化分析代码变更并生成规范的Git提交信息",
85
+ )
86
+ cmd_table.add_row("jarvis-git-squash", "jgs", "Git提交历史整理工具")
87
+ cmd_table.add_row(
88
+ "jarvis-platform-manager",
89
+ "jpm",
90
+ "管理和测试不同的大语言模型平台",
91
+ )
92
+ cmd_table.add_row("jarvis-multi-agent", "jma", "多智能体协作系统")
93
+ cmd_table.add_row("jarvis-tool", "jt", "工具管理与调用系统")
94
+ cmd_table.add_row("jarvis-methodology", "jm", "方法论知识库管理")
95
+ cmd_table.add_row(
96
+ "jarvis-rag",
97
+ "jrg",
98
+ "构建和查询本地化的RAG知识库",
99
+ )
100
+ cmd_table.add_row("jarvis-smart-shell", "jss", "实验性的智能Shell功能")
101
+ cmd_table.add_row(
102
+ "jarvis-stats",
103
+ "jst",
104
+ "通用统计模块,支持记录和可视化任意指标数据",
105
+ )
106
+ cmd_table.add_row(
107
+ "jarvis-memory-organizer",
108
+ "jmo",
109
+ "记忆管理工具,支持整理、合并、导入导出记忆",
110
+ )
111
+
112
+ Console().print(cmd_table)
113
+ except Exception:
114
+ # 静默忽略渲染异常,避免影响主流程
115
+ pass
116
+
117
+
118
+ def handle_edit_option(edit: bool, config_file: Optional[str]) -> bool:
119
+ """处理配置文件编辑选项,返回是否已处理并需提前结束。"""
120
+ if edit:
121
+ ConfigEditor.edit_config(config_file)
122
+ return True
123
+ return False
124
+
125
+
126
+ def handle_share_methodology_option(
127
+ share_methodology: bool, config_file: Optional[str]
128
+ ) -> bool:
129
+ """处理方法论分享选项,返回是否已处理并需提前结束。"""
130
+ if share_methodology:
131
+ init_env("", config_file=config_file) # 初始化配置但不显示欢迎信息
132
+ methodology_manager = MethodologyShareManager()
133
+ methodology_manager.run()
134
+ return True
135
+ return False
136
+
137
+
138
+ def handle_share_tool_option(share_tool: bool, config_file: Optional[str]) -> bool:
139
+ """处理工具分享选项,返回是否已处理并需提前结束。"""
140
+ if share_tool:
141
+ init_env("", config_file=config_file) # 初始化配置但不显示欢迎信息
142
+ tool_manager = ToolShareManager()
143
+ tool_manager.run()
144
+ return True
145
+ return False
146
+
147
+
148
+ def handle_interactive_config_option(
149
+ interactive_config: bool, config_file: Optional[str]
150
+ ) -> bool:
151
+ """处理交互式配置选项,返回是否已处理并需提前结束。"""
152
+ if not interactive_config:
153
+ return False
154
+ try:
155
+ config_path = (
156
+ Path(config_file)
157
+ if config_file is not None
158
+ else Path(os.path.expanduser("~/.jarvis/config.yaml"))
159
+ )
160
+ if not config_path.exists():
161
+ # 无现有配置时,进入完整引导流程(该流程内会写入并退出)
162
+ jutils._interactive_config_setup(config_path)
163
+ return True
164
+
165
+ # 读取现有配置
166
+ _, config_data = jutils._load_config_file(str(config_path))
167
+
168
+ # 复用 utils 中的交互式配置逻辑,对所有项进行询问,默认值来自现有配置
169
+ changed = jutils._collect_optional_config_interactively(
170
+ config_data, ask_all=True
171
+ )
172
+ if not changed:
173
+ PrettyOutput.print("没有需要更新的配置项,保持现有配置。", OutputType.INFO)
174
+ return True
175
+
176
+ # 剔除与 schema 默认值一致的键,保持配置精简
50
177
  try:
51
- with open(pre_command_path, "r", encoding="utf-8", errors="ignore") as f:
52
- local_tasks = yaml.safe_load(f)
53
- if isinstance(local_tasks, dict):
54
- for name, desc in local_tasks.items():
55
- if desc:
56
- tasks[str(name)] = str(desc)
57
- print(f"✅ 预定义任务加载完成 {pre_command_path}")
58
- except (yaml.YAMLError, OSError):
59
- print(f"❌ 预定义任务加载失败 {pre_command_path}")
60
-
61
- return tasks
62
-
63
-
64
- def _select_task(tasks: Dict[str, str]) -> str:
65
- """Let user select a task from the list or skip. Returns task description if selected."""
66
- if not tasks:
67
- return ""
68
-
69
- task_names = list(tasks.keys())
70
- task_list = ["可用任务:"]
71
- for i, name in enumerate(task_names, 1):
72
- task_list.append(f"[{i}] {name}")
73
- task_list.append("[0] 跳过预定义任务")
74
- PrettyOutput.print("\n".join(task_list), OutputType.INFO)
75
-
76
- while True:
178
+ jutils._prune_defaults_with_schema(config_data)
179
+ except Exception:
180
+ pass
181
+
182
+ # 生成/保留 schema 头
183
+ header = ""
77
184
  try:
78
- choice_str = prompt("\n请选择一个任务编号(0 跳过预定义任务):").strip()
79
- if not choice_str:
80
- return ""
81
-
82
- choice = int(choice_str)
83
- if choice == 0:
84
- return ""
85
- if 1 <= choice <= len(task_names):
86
- selected_task = tasks[task_names[choice - 1]]
87
- PrettyOutput.print(f"将要执行任务:\n {selected_task}", OutputType.INFO)
88
- # 询问是否需要补充信息
89
- need_additional = user_confirm(
90
- "需要为此任务添加补充信息吗?", default=False
185
+ with open(config_path, "r", encoding="utf-8") as rf:
186
+ first_line = rf.readline()
187
+ if first_line.startswith("# yaml-language-server: $schema="):
188
+ header = first_line
189
+ except Exception:
190
+ header = ""
191
+
192
+ yaml_str = yaml.dump(config_data, allow_unicode=True, sort_keys=False)
193
+ if not header:
194
+ try:
195
+ schema_path = Path(
196
+ os.path.relpath(
197
+ Path(__file__).resolve().parents[1]
198
+ / "jarvis_data"
199
+ / "config_schema.json",
200
+ start=str(config_path.parent),
201
+ )
91
202
  )
92
- if need_additional:
93
- additional_input = get_multiline_input("请输入补充信息:")
94
- if additional_input:
95
- selected_task = (
96
- f"{selected_task}\n\n补充信息:\n{additional_input}"
97
- )
98
- return selected_task
99
- PrettyOutput.print(
100
- "无效的选择。请选择列表中的一个号码。", OutputType.WARNING
203
+ header = f"# yaml-language-server: $schema={schema_path}\n"
204
+ except Exception:
205
+ header = ""
206
+
207
+ with open(config_path, "w", encoding="utf-8") as wf:
208
+ if header:
209
+ wf.write(header)
210
+ wf.write(yaml_str)
211
+
212
+ PrettyOutput.print(f"配置已更新: {config_path}", OutputType.SUCCESS)
213
+ return True
214
+ except Exception as e:
215
+ PrettyOutput.print(f"交互式配置失败: {e}", OutputType.ERROR)
216
+ return True
217
+
218
+
219
+ def handle_backup_option(backup_dir_path: Optional[str]) -> bool:
220
+ """处理数据备份选项,返回是否已处理并需提前结束。"""
221
+ if backup_dir_path is None:
222
+ return False
223
+
224
+ init_env("", config_file=None)
225
+ data_dir = Path(get_data_dir())
226
+ if not data_dir.is_dir():
227
+ PrettyOutput.print(f"数据目录不存在: {data_dir}", OutputType.ERROR)
228
+ return True
229
+
230
+ backup_dir_str = backup_dir_path if backup_dir_path.strip() else "~/jarvis_backups"
231
+ backup_dir = Path(os.path.expanduser(backup_dir_str))
232
+ backup_dir.mkdir(exist_ok=True)
233
+
234
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
235
+ backup_file_base = backup_dir / f"jarvis_data_{timestamp}"
236
+
237
+ try:
238
+ archive_path = shutil.make_archive(
239
+ str(backup_file_base), "zip", root_dir=str(data_dir)
240
+ )
241
+ PrettyOutput.print(f"数据已成功备份到: {archive_path}", OutputType.SUCCESS)
242
+ except Exception as e:
243
+ PrettyOutput.print(f"数据备份失败: {e}", OutputType.ERROR)
244
+
245
+ return True
246
+
247
+
248
+ def handle_restore_option(restore_path: Optional[str], config_file: Optional[str]) -> bool:
249
+ """处理数据恢复选项,返回是否已处理并需提前结束。"""
250
+ if not restore_path:
251
+ return False
252
+
253
+ restore_file = Path(os.path.expanduser(os.path.expandvars(restore_path)))
254
+ # 兼容 ~ 与环境变量,避免用户输入未展开路径导致找不到文件
255
+ if not restore_file.is_file():
256
+ PrettyOutput.print(f"指定的恢复文件不存在: {restore_file}", OutputType.ERROR)
257
+ return True
258
+
259
+ # 在恢复数据时不要触发完整环境初始化,避免引导流程或网络请求
260
+ # 优先从配置文件解析 JARVIS_DATA_PATH,否则回退到默认数据目录
261
+ data_dir_str: Optional[str] = None
262
+ try:
263
+ if config_file:
264
+ cfg_path = Path(os.path.expanduser(os.path.expandvars(config_file)))
265
+ if cfg_path.is_file():
266
+ with open(cfg_path, "r", encoding="utf-8", errors="ignore") as cf:
267
+ cfg_data = yaml.safe_load(cf) or {}
268
+ if isinstance(cfg_data, dict):
269
+ val = cfg_data.get("JARVIS_DATA_PATH")
270
+ if isinstance(val, str) and val.strip():
271
+ data_dir_str = val.strip()
272
+ except Exception:
273
+ data_dir_str = None
274
+
275
+ if not data_dir_str:
276
+ data_dir_str = get_data_dir()
277
+
278
+ data_dir = Path(os.path.expanduser(os.path.expandvars(str(data_dir_str))))
279
+
280
+ if data_dir.exists():
281
+ if not user_confirm(
282
+ f"数据目录 '{data_dir}' 已存在,恢复操作将覆盖它。是否继续?", default=False
283
+ ):
284
+ PrettyOutput.print("恢复操作已取消。", OutputType.INFO)
285
+ return True
286
+ try:
287
+ shutil.rmtree(data_dir)
288
+ except Exception as e:
289
+ PrettyOutput.print(f"无法移除现有数据目录: {e}", OutputType.ERROR)
290
+ return True
291
+
292
+ try:
293
+ data_dir.mkdir(parents=True)
294
+ shutil.unpack_archive(str(restore_file), str(data_dir), "zip")
295
+ PrettyOutput.print(
296
+ f"数据已从 '{restore_path}' 成功恢复到 '{data_dir}'", OutputType.SUCCESS
297
+ )
298
+
299
+ except Exception as e:
300
+ PrettyOutput.print(f"数据恢复失败: {e}", OutputType.ERROR)
301
+
302
+ return True
303
+
304
+
305
+ def preload_config_for_flags(config_file: Optional[str]) -> None:
306
+ """预加载配置(仅用于读取功能开关),不会显示欢迎信息或影响后续 init_env。"""
307
+ try:
308
+ jutils.g_config_file = config_file
309
+ jutils.load_config()
310
+ except Exception:
311
+ # 静默忽略配置加载异常
312
+ pass
313
+
314
+
315
+ def try_switch_to_jca_if_git_repo(
316
+ model_group: Optional[str],
317
+ tool_group: Optional[str],
318
+ config_file: Optional[str],
319
+ restore_session: bool,
320
+ task: Optional[str],
321
+ ) -> None:
322
+ """在初始化环境前检测Git仓库,并可选择自动切换到代码开发模式(jca)。"""
323
+ # 非交互模式下跳过代码模式切换提示与相关输出
324
+ if is_non_interactive():
325
+ return
326
+ if is_enable_git_repo_jca_switch():
327
+ try:
328
+ res = subprocess.run(
329
+ ["git", "rev-parse", "--show-toplevel"],
330
+ capture_output=True,
331
+ text=True,
101
332
  )
333
+ if res.returncode == 0:
334
+ git_root = res.stdout.strip()
335
+ if git_root and os.path.isdir(git_root):
336
+ PrettyOutput.print(
337
+ f"检测到当前位于 Git 仓库: {git_root}", OutputType.INFO
338
+ )
339
+ if user_confirm(
340
+ "检测到Git仓库,是否切换到代码开发模式(jca)?", default=False
341
+ ):
342
+ # 构建并切换到 jarvis-code-agent 命令,传递兼容参数
343
+ args = ["jarvis-code-agent"]
344
+ if model_group:
345
+ args += ["-g", model_group]
346
+ if tool_group:
347
+ args += ["-G", tool_group]
348
+ if config_file:
349
+ args += ["-f", config_file]
350
+ if restore_session:
351
+ args += ["--restore-session"]
352
+ if task:
353
+ args += ["-r", task]
354
+ PrettyOutput.print(
355
+ "正在切换到 'jca'(jarvis-code-agent)以进入代码开发模式...",
356
+ OutputType.INFO,
357
+ )
358
+ os.execvp(args[0], args)
359
+ except Exception:
360
+ # 静默忽略检测异常,不影响主流程
361
+ pass
102
362
 
103
- except (KeyboardInterrupt, EOFError):
104
- return ""
105
- except ValueError as val_err:
106
- PrettyOutput.print(f"选择任务失败: {str(val_err)}", OutputType.ERROR)
107
363
 
364
+ def handle_builtin_config_selector(
365
+ model_group: Optional[str],
366
+ tool_group: Optional[str],
367
+ config_file: Optional[str],
368
+ task: Optional[str],
369
+ ) -> None:
370
+ """在进入默认通用代理前,列出内置配置供选择(agent/multi_agent/roles)。"""
371
+ if is_enable_builtin_config_selector():
372
+ try:
373
+ # 查找可用的 builtin 目录(支持多候选)
374
+ builtin_dirs: List[Path] = []
375
+ try:
376
+ ancestors = list(Path(__file__).resolve().parents)
377
+ for anc in ancestors[:8]:
378
+ p = anc / "builtin"
379
+ if p.exists():
380
+ builtin_dirs.append(p)
381
+ except Exception:
382
+ pass
383
+ # 去重,保留顺序
384
+ _seen = set()
385
+ _unique: List[Path] = []
386
+ for d in builtin_dirs:
387
+ try:
388
+ key = str(d.resolve())
389
+ except Exception:
390
+ key = str(d)
391
+ if key not in _seen:
392
+ _seen.add(key)
393
+ _unique.append(d)
394
+ builtin_dirs = _unique
395
+ # 向后兼容:保留第一个候选作为 builtin_root
396
+ builtin_root = builtin_dirs[0] if builtin_dirs else None # type: ignore[assignment]
108
397
 
109
- def main() -> None:
398
+ categories = [
399
+ ("agent", "jarvis-agent", "*.yaml"),
400
+ ("multi_agent", "jarvis-multi-agent", "*.yaml"),
401
+ ("roles", "jarvis-platform-manager", "*.yaml"),
402
+ ]
110
403
 
111
- parser = argparse.ArgumentParser(description="Jarvis AI assistant")
112
- parser.add_argument(
113
- "--llm_type",
114
- type=str,
115
- default="normal",
116
- choices=["normal", "thinking"],
117
- help="LLM type to use",
118
- )
119
- parser.add_argument(
120
- "-t",
121
- "--task",
122
- type=str,
123
- help="Directly input task content from command line",
124
- )
125
- parser.add_argument("-f", "--config", type=str, help="Path to custom config file")
126
- parser.add_argument(
404
+ options = []
405
+ for cat, cmd, pattern in categories:
406
+ # 构建待扫描目录列表:优先使用配置中的目录,其次回退到内置目录
407
+ search_dirs = []
408
+ try:
409
+ if cat == "agent":
410
+ search_dirs.extend(
411
+ [
412
+ Path(os.path.expanduser(os.path.expandvars(str(p))))
413
+ for p in get_agent_definition_dirs()
414
+ if p
415
+ ]
416
+ )
417
+ elif cat == "multi_agent":
418
+ search_dirs.extend(
419
+ [
420
+ Path(os.path.expanduser(os.path.expandvars(str(p))))
421
+ for p in get_multi_agent_dirs()
422
+ if p
423
+ ]
424
+ )
425
+ elif cat == "roles":
426
+ search_dirs.extend(
427
+ [
428
+ Path(os.path.expanduser(os.path.expandvars(str(p))))
429
+ for p in get_roles_dirs()
430
+ if p
431
+ ]
432
+ )
433
+ except Exception:
434
+ # 忽略配置读取异常
435
+ pass
436
+
437
+ # 追加内置目录(支持多个候选)
438
+ try:
439
+ candidates = builtin_dirs if isinstance(builtin_dirs, list) and builtin_dirs else ([builtin_root] if builtin_root else [])
440
+ except Exception:
441
+ candidates = ([builtin_root] if builtin_root else [])
442
+ for _bd in candidates:
443
+ if _bd:
444
+ search_dirs.append(Path(_bd) / cat)
445
+
446
+ # 去重并保留顺序
447
+ unique_dirs = []
448
+ seen = set()
449
+ for d in search_dirs:
450
+ try:
451
+ key = str(Path(d).resolve())
452
+ except Exception:
453
+ key = str(d)
454
+ if key not in seen:
455
+ seen.add(key)
456
+ unique_dirs.append(Path(d))
457
+
458
+ # 可选调试输出:查看每类的搜索目录
459
+ try:
460
+ if os.environ.get("JARVIS_DEBUG_BUILTIN_SELECTOR") == "1":
461
+ PrettyOutput.print(
462
+ f"DEBUG: category={cat} search_dirs=" + ", ".join(str(p) for p in unique_dirs),
463
+ OutputType.INFO,
464
+ )
465
+ except Exception:
466
+ pass
467
+
468
+ for dir_path in unique_dirs:
469
+ if not dir_path.exists():
470
+ continue
471
+ for fpath in sorted(dir_path.glob(pattern)):
472
+ # 解析YAML以获取可读名称/描述(失败时静默降级为文件名)
473
+ name = fpath.stem
474
+ desc = ""
475
+ roles_count = 0
476
+ try:
477
+ with open(
478
+ fpath, "r", encoding="utf-8", errors="ignore"
479
+ ) as fh:
480
+ data = yaml.safe_load(fh) or {}
481
+ if isinstance(data, dict):
482
+ name = data.get("name") or data.get("title") or name
483
+ desc = data.get("description") or data.get("desc") or ""
484
+ if cat == "roles" and isinstance(
485
+ data.get("roles"), list
486
+ ):
487
+ roles_count = len(data["roles"])
488
+ if not desc:
489
+ desc = f"{roles_count} 个角色"
490
+ except Exception:
491
+ # 忽略解析错误,使用默认显示
492
+ pass
493
+
494
+ # 为 roles 构建详细信息(每个角色的名称与描述)
495
+ details = ""
496
+ if cat == "roles":
497
+ roles = (data or {}).get("roles", [])
498
+ if isinstance(roles, list):
499
+ lines = []
500
+ for role in roles:
501
+ if isinstance(role, dict):
502
+ rname = str(role.get("name", "") or "")
503
+ rdesc = str(role.get("description", "") or "")
504
+ lines.append(
505
+ f"{rname} - {rdesc}" if rdesc else rname
506
+ )
507
+ details = "\n".join([ln for ln in lines if ln])
508
+ # 如果没有角色详情,退回到统计信息
509
+ if not details and isinstance(
510
+ (data or {}).get("roles"), list
511
+ ):
512
+ details = f"{len(data['roles'])} 个角色"
513
+
514
+ options.append(
515
+ {
516
+ "category": cat,
517
+ "cmd": cmd,
518
+ "file": str(fpath),
519
+ "name": str(name),
520
+ "desc": str(desc),
521
+ "details": str(details),
522
+ "roles_count": int(roles_count),
523
+ }
524
+ )
525
+
526
+ if options:
527
+ # Add a default option to skip selection
528
+ options.insert(
529
+ 0,
530
+ {
531
+ "category": "skip",
532
+ "cmd": "",
533
+ "file": "",
534
+ "name": "跳过选择 (使用默认通用代理)",
535
+ "desc": "直接按回车或ESC也可跳过",
536
+ "details": "",
537
+ "roles_count": 0,
538
+ },
539
+ )
540
+
541
+ PrettyOutput.section("可用的内置配置", OutputType.SUCCESS)
542
+ # 使用 rich Table 呈现
543
+ table = Table(show_header=True, header_style="bold magenta")
544
+ table.add_column("No.", style="cyan", no_wrap=True)
545
+ table.add_column("类型", style="green", no_wrap=True)
546
+ table.add_column("名称", style="bold")
547
+ table.add_column("文件", style="dim")
548
+ table.add_column("描述", style="white")
549
+
550
+ for idx, opt in enumerate(options, 1):
551
+ category = str(opt.get("category", ""))
552
+ name = str(opt.get("name", ""))
553
+ file_path = str(opt.get("file", ""))
554
+ # 描述列显示配置描述;若为 roles 同时显示角色数量与列表
555
+ if category == "roles":
556
+ count = opt.get("roles_count")
557
+ details_val = opt.get("details", "")
558
+ parts: List[str] = []
559
+ if isinstance(count, int) and count > 0:
560
+ parts.append(f"{count} 个角色")
561
+ if isinstance(details_val, str) and details_val:
562
+ parts.append(details_val)
563
+ desc_display = "\n".join(parts) if parts else ""
564
+ else:
565
+ desc_display = str(opt.get("desc", ""))
566
+ table.add_row(str(idx), category, name, file_path, desc_display)
567
+
568
+ Console().print(table)
569
+
570
+ # Try to use fzf for selection if available (include No. to support number-based filtering)
571
+ fzf_options = [
572
+ f"{idx:>3} | {opt['category']:<12} | {opt['name']:<30} | {opt.get('desc', '')}"
573
+ for idx, opt in enumerate(options, 1)
574
+ ]
575
+ selected_str = fzf_select(
576
+ fzf_options, prompt="选择要启动的配置编号 (ESC跳过) > "
577
+ )
578
+
579
+ choice_index = -1
580
+ if selected_str:
581
+ # Try to parse leading number before first '|'
582
+ try:
583
+ num_part = selected_str.split("|", 1)[0].strip()
584
+ selected_index = int(num_part)
585
+ if 1 <= selected_index <= len(options):
586
+ choice_index = selected_index - 1
587
+ except Exception:
588
+ # Fallback to equality matching if parsing fails
589
+ for i, fzf_opt in enumerate(fzf_options):
590
+ if fzf_opt == selected_str:
591
+ choice_index = i
592
+ break
593
+ else:
594
+ # Fallback to manual input if fzf is not used or available
595
+ choice = get_single_line_input(
596
+ "选择要启动的配置编号,直接回车使用默认通用代理(jvs): ", default=""
597
+ )
598
+ if choice.strip():
599
+ try:
600
+ selected_index = int(choice.strip())
601
+ if 1 <= selected_index <= len(options):
602
+ choice_index = selected_index - 1
603
+ except ValueError:
604
+ pass # Invalid input
605
+
606
+ if choice_index != -1:
607
+ try:
608
+ sel = options[choice_index]
609
+ # If the "skip" option is chosen, do nothing and proceed to default agent
610
+ if sel["category"] == "skip":
611
+ pass
612
+ else:
613
+ args: List[str] = []
614
+
615
+ if sel["category"] == "agent":
616
+ # jarvis-agent 支持 -f/--config(全局配置)与 -c/--agent-definition
617
+ args = [str(sel["cmd"]), "-c", str(sel["file"])]
618
+ if model_group:
619
+ args += ["-g", str(model_group)]
620
+ if config_file:
621
+ args += ["-f", str(config_file)]
622
+ if task:
623
+ args += ["--task", str(task)]
624
+
625
+ elif sel["category"] == "multi_agent":
626
+ # jarvis-multi-agent 需要 -c/--config,用户输入通过 -i/--input 传递
627
+ # 同时传递 -g/--llm-group 以继承 jvs 的模型组选择
628
+ args = [str(sel["cmd"]), "-c", str(sel["file"])]
629
+ if model_group:
630
+ args += ["-g", str(model_group)]
631
+ if task:
632
+ args += ["-i", str(task)]
633
+
634
+ elif sel["category"] == "roles":
635
+ # jarvis-platform-manager role 子命令,支持 -c/-t/-g
636
+ args = [
637
+ str(sel["cmd"]),
638
+ "role",
639
+ "-c",
640
+ str(sel["file"]),
641
+ ]
642
+ if model_group:
643
+ args += ["-g", str(model_group)]
644
+
645
+ if args:
646
+ PrettyOutput.print(
647
+ f"正在启动: {' '.join(args)}", OutputType.INFO
648
+ )
649
+ os.execvp(args[0], args)
650
+ except Exception:
651
+ # 任何异常都不影响默认流程
652
+ pass
653
+ else:
654
+ # User pressed Enter or provided invalid input
655
+ pass
656
+ except Exception:
657
+ # 静默忽略内置配置扫描错误,不影响主流程
658
+ pass
659
+
660
+
661
+ @app.callback(invoke_without_command=True)
662
+ def run_cli(
663
+ ctx: typer.Context,
664
+ task: Optional[str] = typer.Option(
665
+ None, "-T", "--task", help="从命令行直接输入任务内容"
666
+ ),
667
+ model_group: Optional[str] = typer.Option(
668
+ None, "-g", "--llm-group", help="使用的模型组,覆盖配置文件中的设置"
669
+ ),
670
+ tool_group: Optional[str] = typer.Option(
671
+ None, "-G", "--tool-group", help="使用的工具组,覆盖配置文件中的设置"
672
+ ),
673
+ config_file: Optional[str] = typer.Option(
674
+ None, "-f", "--config", help="自定义配置文件路径"
675
+ ),
676
+ restore_session: bool = typer.Option(
677
+ False,
127
678
  "--restore-session",
128
- action="store_true",
129
- help="Restore session from .jarvis/saved_session.json",
130
- default=False,
131
- )
132
- args = parser.parse_args()
679
+ help="从 .jarvis/saved_session.json 恢复会话",
680
+ ),
681
+ edit: bool = typer.Option(False, "-e", "--edit", help="编辑配置文件"),
682
+ share_methodology: bool = typer.Option(
683
+ False, "--share-methodology", help="分享本地方法论到中心方法论仓库"
684
+ ),
685
+ share_tool: bool = typer.Option(
686
+ False, "--share-tool", help="分享本地工具到中心工具仓库"
687
+ ),
688
+ interactive_config: bool = typer.Option(
689
+ False,
690
+ "-I",
691
+ "--interactive-config",
692
+ help="启动交互式配置向导(基于当前配置补充设置)",
693
+ ),
694
+ disable_methodology_analysis: bool = typer.Option(
695
+ False,
696
+ "-D",
697
+ "--disable-methodology-analysis",
698
+ help="禁用方法论和任务分析(覆盖配置文件设置)",
699
+ ),
700
+ backup_data: Optional[str] = typer.Option(
701
+ None,
702
+ "--backup-data",
703
+ help="备份 Jarvis 数据目录. 可选地传入备份目录. 默认为 '~/jarvis_backups'",
704
+ show_default=False,
705
+ flag_value="~/jarvis_backups",
706
+ ),
707
+ restore_data: Optional[str] = typer.Option(
708
+ None, "--restore-data", help="从指定的压缩包恢复 Jarvis 数据"
709
+ ),
710
+ non_interactive: bool = typer.Option(
711
+ False, "-n", "--non-interactive", help="启用非交互模式:用户无法与命令交互,脚本执行超时限制为5分钟"
712
+ ),
713
+ plan: Optional[bool] = typer.Option(None, "--plan/--no-plan", help="启用或禁用任务规划(不指定则从配置加载)"),
714
+ web: bool = typer.Option(False, "--web", help="以 Web 模式启动,通过浏览器 WebSocket 交互"),
715
+ web_host: str = typer.Option("127.0.0.1", "--web-host", help="Web 服务主机"),
716
+ web_port: int = typer.Option(8765, "--web-port", help="Web 服务端口"),
717
+ stop: bool = typer.Option(False, "--stop", help="停止后台 Web 服务(需与 --web 一起使用)"),
718
+ ) -> None:
719
+ """Jarvis AI assistant command-line interface."""
720
+ if ctx.invoked_subcommand is not None:
721
+ return
722
+
723
+ # 使用 rich 输出命令与快捷方式总览
724
+ print_commands_overview()
725
+
726
+ # CLI 标志:非交互模式(不依赖配置文件)
727
+ if non_interactive:
728
+ try:
729
+ os.environ["JARVIS_NON_INTERACTIVE"] = "true"
730
+ except Exception:
731
+ pass
732
+ # 注意:全局配置同步在 init_env 之后执行,避免被覆盖
733
+
734
+ # 同步其他 CLI 选项到全局配置,确保后续模块读取一致
735
+ try:
736
+ if model_group:
737
+ set_config("JARVIS_LLM_GROUP", str(model_group))
738
+ if tool_group:
739
+ set_config("JARVIS_TOOL_GROUP", str(tool_group))
740
+ if disable_methodology_analysis:
741
+ set_config("JARVIS_USE_METHODOLOGY", False)
742
+ set_config("JARVIS_USE_ANALYSIS", False)
743
+ if restore_session:
744
+ set_config("JARVIS_RESTORE_SESSION", True)
745
+ except Exception:
746
+ # 静默忽略同步异常,不影响主流程
747
+ pass
748
+
749
+ # 非交互模式要求从命令行传入任务
750
+ if non_interactive and not (task and str(task).strip()):
751
+ PrettyOutput.print(
752
+ "非交互模式已启用:必须使用 --task 传入任务内容,因多行输入不可用。",
753
+ OutputType.ERROR,
754
+ )
755
+ raise typer.Exit(code=2)
756
+
757
+ # 处理数据备份
758
+ if handle_backup_option(backup_data):
759
+ return
760
+
761
+ # 处理数据恢复
762
+ if handle_restore_option(restore_data, config_file):
763
+ return
764
+
765
+ # 处理配置文件编辑
766
+ if handle_edit_option(edit, config_file):
767
+ return
768
+
769
+ # 处理方法论分享
770
+ if handle_share_methodology_option(share_methodology, config_file):
771
+ return
772
+
773
+ # 处理工具分享
774
+ if handle_share_tool_option(share_tool, config_file):
775
+ return
776
+
777
+ # 交互式配置(基于现有配置补充设置)
778
+ if handle_interactive_config_option(interactive_config, config_file):
779
+ return
780
+
781
+ # 预加载配置(仅用于读取功能开关),不会显示欢迎信息或影响后续 init_env
782
+ preload_config_for_flags(config_file)
783
+ # Web 模式后台管理:支持 --web 后台启动与 --web --stop 停止
784
+ if web:
785
+ # PID 文件路径(按端口区分,便于多实例)
786
+ pidfile = Path(os.path.expanduser("~/.jarvis")) / f"jarvis_web_{web_port}.pid"
787
+ # 停止后台服务
788
+ if stop:
789
+ try:
790
+ pf = pidfile
791
+ if not pf.exists():
792
+ # 兼容旧版本:回退检查数据目录中的旧 PID 文件位置
793
+ try:
794
+ pf_alt = Path(os.path.expanduser(os.path.expandvars(get_data_dir()))) / f"jarvis_web_{web_port}.pid"
795
+ except Exception:
796
+ pf_alt = None # type: ignore[assignment]
797
+ if pf_alt and pf_alt.exists(): # type: ignore[truthy-bool]
798
+ pf = pf_alt
799
+ if not pf.exists():
800
+ # 进一步回退:尝试按端口查找并停止(无 PID 文件)
801
+ killed_any = False
802
+ try:
803
+ res = subprocess.run(
804
+ ["lsof", "-iTCP:%d" % web_port, "-sTCP:LISTEN", "-t"],
805
+ capture_output=True,
806
+ text=True,
807
+ )
808
+ if res.returncode == 0 and res.stdout.strip():
809
+ for ln in res.stdout.strip().splitlines():
810
+ try:
811
+ candidate_pid = int(ln.strip())
812
+ try:
813
+ os.kill(candidate_pid, signal.SIGTERM)
814
+ PrettyOutput.print(f"已按端口停止后台 Web 服务 (PID {candidate_pid})。", OutputType.SUCCESS)
815
+ killed_any = True
816
+ except Exception as e:
817
+ PrettyOutput.print(f"按端口停止失败: {e}", OutputType.WARNING)
818
+ except Exception:
819
+ continue
820
+ except Exception:
821
+ pass
822
+ if not killed_any:
823
+ try:
824
+ res2 = subprocess.run(["ss", "-ltpn"], capture_output=True, text=True)
825
+ if res2.returncode == 0 and res2.stdout:
826
+ for ln in res2.stdout.splitlines():
827
+ if f":{web_port} " in ln or f":{web_port}\n" in ln:
828
+ try:
829
+ idx = ln.find("pid=")
830
+ if idx != -1:
831
+ end = ln.find(",", idx)
832
+ pid_str2 = ln[idx+4:end if end != -1 else None]
833
+ candidate_pid = int(pid_str2)
834
+ try:
835
+ os.kill(candidate_pid, signal.SIGTERM)
836
+ PrettyOutput.print(f"已按端口停止后台 Web 服务 (PID {candidate_pid})。", OutputType.SUCCESS)
837
+ killed_any = True
838
+ except Exception as e:
839
+ PrettyOutput.print(f"按端口停止失败: {e}", OutputType.WARNING)
840
+ break
841
+ except Exception:
842
+ continue
843
+ except Exception:
844
+ pass
845
+ # 若仍未找到,扫描家目录下所有 Web PID 文件,尽力停止所有实例
846
+ if not killed_any:
847
+ try:
848
+ pid_dir = Path(os.path.expanduser("~/.jarvis"))
849
+ if pid_dir.is_dir():
850
+ for f in pid_dir.glob("jarvis_web_*.pid"):
851
+ try:
852
+ ptxt = f.read_text(encoding="utf-8").strip()
853
+ p = int(ptxt)
854
+ try:
855
+ os.kill(p, signal.SIGTERM)
856
+ PrettyOutput.print(f"已停止后台 Web 服务 (PID {p})。", OutputType.SUCCESS)
857
+ killed_any = True
858
+ except Exception as e:
859
+ PrettyOutput.print(f"停止 PID {p} 失败: {e}", OutputType.WARNING)
860
+ except Exception:
861
+ pass
862
+ try:
863
+ f.unlink(missing_ok=True)
864
+ except Exception:
865
+ pass
866
+ except Exception:
867
+ pass
868
+ if not killed_any:
869
+ PrettyOutput.print("未找到后台 Web 服务的 PID 文件,可能未启动或已停止。", OutputType.WARNING)
870
+ return
871
+ # 优先使用 PID 文件中的 PID
872
+ try:
873
+ pid_str = pf.read_text(encoding="utf-8").strip()
874
+ pid = int(pid_str)
875
+ except Exception:
876
+ pid = 0
877
+ killed = False
878
+ if pid > 0:
879
+ try:
880
+ os.kill(pid, signal.SIGTERM)
881
+ PrettyOutput.print(f"已向后台 Web 服务发送停止信号 (PID {pid})。", OutputType.SUCCESS)
882
+ killed = True
883
+ except Exception as e:
884
+ PrettyOutput.print(f"发送停止信号失败或进程不存在: {e}", OutputType.WARNING)
885
+ if not killed:
886
+ # 无 PID 文件或停止失败时,尝试按端口查找进程
887
+ candidate_pid = 0
888
+ try:
889
+ res = subprocess.run(
890
+ ["lsof", "-iTCP:%d" % web_port, "-sTCP:LISTEN", "-t"],
891
+ capture_output=True,
892
+ text=True,
893
+ )
894
+ if res.returncode == 0 and res.stdout.strip():
895
+ for ln in res.stdout.strip().splitlines():
896
+ try:
897
+ candidate_pid = int(ln.strip())
898
+ break
899
+ except Exception:
900
+ continue
901
+ except Exception:
902
+ pass
903
+ if not candidate_pid:
904
+ try:
905
+ res2 = subprocess.run(["ss", "-ltpn"], capture_output=True, text=True)
906
+ if res2.returncode == 0 and res2.stdout:
907
+ for ln in res2.stdout.splitlines():
908
+ if f":{web_port} " in ln or f":{web_port}\n" in ln:
909
+ # 格式示例: LISTEN ... users:(("uvicorn",pid=12345,fd=7))
910
+ try:
911
+ idx = ln.find("pid=")
912
+ if idx != -1:
913
+ end = ln.find(",", idx)
914
+ pid_str2 = ln[idx+4:end if end != -1 else None]
915
+ candidate_pid = int(pid_str2)
916
+ break
917
+ except Exception:
918
+ continue
919
+ except Exception:
920
+ pass
921
+ if candidate_pid:
922
+ try:
923
+ os.kill(candidate_pid, signal.SIGTERM)
924
+ PrettyOutput.print(f"已按端口停止后台 Web 服务 (PID {candidate_pid})。", OutputType.SUCCESS)
925
+ killed = True
926
+ except Exception as e:
927
+ PrettyOutput.print(f"按端口停止失败: {e}", OutputType.WARNING)
928
+ # 清理可能存在的 PID 文件(两个位置)
929
+ try:
930
+ pidfile.unlink(missing_ok=True) # 家目录位置
931
+ except Exception:
932
+ pass
933
+ try:
934
+ alt_pf = Path(os.path.expanduser(os.path.expandvars(get_data_dir()))) / f"jarvis_web_{web_port}.pid"
935
+ alt_pf.unlink(missing_ok=True)
936
+ except Exception:
937
+ pass
938
+ except Exception as e:
939
+ PrettyOutput.print(f"停止后台 Web 服务失败: {e}", OutputType.ERROR)
940
+ finally:
941
+ return
942
+ # 后台启动:父进程拉起子进程并记录 PID
943
+ is_daemon = False
944
+ try:
945
+ is_daemon = os.environ.get("JARVIS_WEB_DAEMON") == "1"
946
+ except Exception:
947
+ is_daemon = False
948
+ if not is_daemon:
949
+ try:
950
+ # 构建子进程参数,传递关键配置
951
+ args = [
952
+ sys.executable,
953
+ "-m",
954
+ "jarvis.jarvis_agent.jarvis",
955
+ "--web",
956
+ "--web-host",
957
+ str(web_host),
958
+ "--web-port",
959
+ str(web_port),
960
+ ]
961
+ if model_group:
962
+ args += ["-g", str(model_group)]
963
+ if tool_group:
964
+ args += ["-G", str(tool_group)]
965
+ if config_file:
966
+ args += ["-f", str(config_file)]
967
+ if restore_session:
968
+ args += ["--restore-session"]
969
+ if disable_methodology_analysis:
970
+ args += ["-D"]
971
+ if non_interactive:
972
+ args += ["-n"]
973
+ env = os.environ.copy()
974
+ env["JARVIS_WEB_DAEMON"] = "1"
975
+ # 启动子进程(后台运行)
976
+ proc = subprocess.Popen(
977
+ args,
978
+ env=env,
979
+ stdout=subprocess.DEVNULL,
980
+ stderr=subprocess.DEVNULL,
981
+ stdin=subprocess.DEVNULL,
982
+ close_fds=True,
983
+ )
984
+ # 记录 PID 到文件
985
+ try:
986
+ pidfile.parent.mkdir(parents=True, exist_ok=True)
987
+ except Exception:
988
+ pass
989
+ try:
990
+ pidfile.write_text(str(proc.pid), encoding="utf-8")
991
+ except Exception:
992
+ pass
993
+ PrettyOutput.print(
994
+ f"Web 服务已在后台启动 (PID {proc.pid}),地址: http://{web_host}:{web_port}",
995
+ OutputType.SUCCESS,
996
+ )
997
+ except Exception as e:
998
+ PrettyOutput.print(f"后台启动 Web 服务失败: {e}", OutputType.ERROR)
999
+ raise typer.Exit(code=1)
1000
+ return
1001
+
1002
+ # 在初始化环境前检测Git仓库,并可选择自动切换到代码开发模式(jca)
1003
+ if not non_interactive:
1004
+ try_switch_to_jca_if_git_repo(
1005
+ model_group, tool_group, config_file, restore_session, task
1006
+ )
1007
+
1008
+ # 在进入默认通用代理前,列出内置配置供选择(agent/multi_agent/roles)
1009
+ # 非交互模式下跳过内置角色/配置选择
1010
+ if not non_interactive:
1011
+ handle_builtin_config_selector(model_group, tool_group, config_file, task)
1012
+
1013
+ # 初始化环境
133
1014
  init_env(
134
- "欢迎使用 Jarvis AI 助手,您的智能助理已准备就绪!", config_file=args.config
1015
+ "欢迎使用 Jarvis AI 助手,您的智能助理已准备就绪!", config_file=config_file
135
1016
  )
136
1017
 
1018
+ # 在初始化环境后同步 CLI 选项到全局配置,避免被 init_env 覆盖
1019
+ try:
1020
+ if model_group:
1021
+ set_config("JARVIS_LLM_GROUP", str(model_group))
1022
+ if tool_group:
1023
+ set_config("JARVIS_TOOL_GROUP", str(tool_group))
1024
+ if disable_methodology_analysis:
1025
+ set_config("JARVIS_USE_METHODOLOGY", False)
1026
+ set_config("JARVIS_USE_ANALYSIS", False)
1027
+ if restore_session:
1028
+ set_config("JARVIS_RESTORE_SESSION", True)
1029
+ if non_interactive:
1030
+ # 保持运行期非交互标志
1031
+ set_config("JARVIS_NON_INTERACTIVE", True)
1032
+ except Exception:
1033
+ # 静默忽略同步异常,不影响主流程
1034
+ pass
1035
+
1036
+ # 运行主流程
137
1037
  try:
138
- agent = Agent(
139
- system_prompt=origin_agent_system_prompt,
140
- llm_type=args.llm_type,
141
- input_handler=[shell_input_handler, builtin_input_handler],
142
- output_handler=[ToolRegistry()],
143
- need_summary=False,
1038
+ # Web 模式下注入基于 WebSocket 的输入/确认回调
1039
+ extra_kwargs = {}
1040
+ if web:
1041
+ # 纯 xterm 交互模式:不注入 WebBridge 的输入/确认回调,避免阻塞等待浏览器响应
1042
+ #(交互由 /terminal PTY 会话中的 jvs 进程处理)
1043
+ pass
1044
+
1045
+ agent_manager = AgentManager(
1046
+ model_group=model_group,
1047
+ tool_group=tool_group,
1048
+ restore_session=restore_session,
1049
+ use_methodology=False if disable_methodology_analysis else None,
1050
+ use_analysis=False if disable_methodology_analysis else None,
1051
+ non_interactive=non_interactive,
1052
+ **extra_kwargs,
144
1053
  )
1054
+ agent = agent_manager.initialize()
1055
+ # CLI 开关:启用/禁用规划(不依赖 AgentManager 支持,直接设置 Agent 属性)
1056
+ if plan is not None:
1057
+ try:
1058
+ agent.plan = bool(plan)
1059
+ except Exception:
1060
+ pass
145
1061
 
146
- # 尝试恢复会话
147
- if args.restore_session:
148
- if agent.restore_session():
149
- PrettyOutput.print("会话已成功恢复。", OutputType.SUCCESS)
150
- else:
151
- PrettyOutput.print("无法恢复会话。", OutputType.WARNING)
152
-
153
- # 优先处理命令行直接传入的任务
154
- if args.task:
155
- agent.run(args.task)
156
- sys.exit(0)
157
-
158
- if agent.first:
159
- tasks = _load_tasks()
160
- if tasks and (selected_task := _select_task(tasks)):
161
- PrettyOutput.print(f"开始执行任务: \n{selected_task}", OutputType.INFO)
162
- agent.run(selected_task)
163
- sys.exit(0)
164
-
165
- user_input = get_multiline_input("请输入你的任务(输入空行退出):")
166
- if user_input:
167
- agent.run(user_input)
168
- sys.exit(0)
1062
+ if web:
1063
+ try:
169
1064
 
1065
+ from jarvis.jarvis_agent.web_server import start_web_server
1066
+ from jarvis.jarvis_agent.stdio_redirect import enable_web_stdio_redirect, enable_web_stdin_redirect
1067
+ # 在 Web 模式下固定TTY宽度为200,改善前端显示效果
1068
+ try:
1069
+ import os as _os
1070
+ _os.environ["COLUMNS"] = "200"
1071
+ # 尝试固定全局 Console 的宽度(PrettyOutput 使用该 Console 实例)
1072
+ try:
1073
+ from jarvis.jarvis_utils.globals import console as _console
1074
+ try:
1075
+ _console._width = 200 # rich Console的固定宽度参数
1076
+ except Exception:
1077
+ pass
1078
+ except Exception:
1079
+ pass
1080
+ except Exception:
1081
+ pass
1082
+ # 使用 STDIO 重定向,取消 Sink 广播以避免重复输出
1083
+ # 启用标准输出/错误的WebSocket重定向(捕获工具直接打印的输出)
1084
+ enable_web_stdio_redirect()
1085
+ # 启用来自前端 xterm 的 STDIN 重定向,使交互式命令可从浏览器获取输入
1086
+ try:
1087
+ enable_web_stdin_redirect()
1088
+ except Exception:
1089
+ pass
1090
+ # 记录用于交互式终端(PTY)重启的 jvs 启动命令(移除 web 相关参数)
1091
+ try:
1092
+ import sys as _sys
1093
+ import os as _os
1094
+ import json as _json
1095
+ _argv = list(_sys.argv)
1096
+ # 去掉程序名(argv[0]),并过滤 --web 相关参数
1097
+ filtered = []
1098
+ i = 1
1099
+ while i < len(_argv):
1100
+ a = _argv[i]
1101
+ if a == "--web" or a.startswith("--web="):
1102
+ i += 1
1103
+ continue
1104
+ if a == "--web-host":
1105
+ i += 2
1106
+ continue
1107
+ if a.startswith("--web-host="):
1108
+ i += 1
1109
+ continue
1110
+ if a == "--web-port":
1111
+ i += 2
1112
+ continue
1113
+ if a.startswith("--web-port="):
1114
+ i += 1
1115
+ continue
1116
+ filtered.append(a)
1117
+ i += 1
1118
+ # 使用 jvs 命令作为可执行文件,保留其余业务参数
1119
+ cmd = ["jvs"] + filtered
1120
+ _os.environ["JARVIS_WEB_LAUNCH_JSON"] = _json.dumps(cmd, ensure_ascii=False)
1121
+ except Exception:
1122
+ pass
1123
+ PrettyOutput.print("以 Web 模式启动,请在浏览器中打开提供的地址进行交互。", OutputType.INFO)
1124
+ # 启动 Web 服务(阻塞调用)
1125
+ start_web_server(agent_manager, host=web_host, port=web_port)
1126
+ return
1127
+ except Exception as e:
1128
+ PrettyOutput.print(f"Web 模式启动失败: {e}", OutputType.ERROR)
1129
+ raise typer.Exit(code=1)
1130
+
1131
+ # 默认 CLI 模式:运行任务(可能来自 --task 或交互输入)
1132
+ agent_manager.run_task(task)
1133
+ except typer.Exit:
1134
+ raise
170
1135
  except Exception as err: # pylint: disable=broad-except
171
1136
  PrettyOutput.print(f"初始化错误: {str(err)}", OutputType.ERROR)
172
- sys.exit(1)
1137
+ raise typer.Exit(code=1)
1138
+
1139
+
1140
+ def main() -> None:
1141
+ """Application entry point."""
1142
+ app()
173
1143
 
174
1144
 
175
1145
  if __name__ == "__main__":