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.
- jarvis/__init__.py +1 -1
- jarvis/jarvis_agent/__init__.py +1143 -245
- jarvis/jarvis_agent/agent_manager.py +97 -0
- jarvis/jarvis_agent/builtin_input_handler.py +12 -10
- jarvis/jarvis_agent/config_editor.py +57 -0
- jarvis/jarvis_agent/edit_file_handler.py +392 -99
- jarvis/jarvis_agent/event_bus.py +48 -0
- jarvis/jarvis_agent/events.py +157 -0
- jarvis/jarvis_agent/file_context_handler.py +79 -0
- jarvis/jarvis_agent/file_methodology_manager.py +117 -0
- jarvis/jarvis_agent/jarvis.py +1117 -147
- jarvis/jarvis_agent/main.py +78 -34
- jarvis/jarvis_agent/memory_manager.py +195 -0
- jarvis/jarvis_agent/methodology_share_manager.py +174 -0
- jarvis/jarvis_agent/prompt_manager.py +82 -0
- jarvis/jarvis_agent/prompts.py +46 -9
- jarvis/jarvis_agent/protocols.py +4 -1
- jarvis/jarvis_agent/rewrite_file_handler.py +141 -0
- jarvis/jarvis_agent/run_loop.py +146 -0
- jarvis/jarvis_agent/session_manager.py +9 -9
- jarvis/jarvis_agent/share_manager.py +228 -0
- jarvis/jarvis_agent/shell_input_handler.py +23 -3
- jarvis/jarvis_agent/stdio_redirect.py +295 -0
- jarvis/jarvis_agent/task_analyzer.py +212 -0
- jarvis/jarvis_agent/task_manager.py +154 -0
- jarvis/jarvis_agent/task_planner.py +496 -0
- jarvis/jarvis_agent/tool_executor.py +8 -4
- jarvis/jarvis_agent/tool_share_manager.py +139 -0
- jarvis/jarvis_agent/user_interaction.py +42 -0
- jarvis/jarvis_agent/utils.py +54 -0
- jarvis/jarvis_agent/web_bridge.py +189 -0
- jarvis/jarvis_agent/web_output_sink.py +53 -0
- jarvis/jarvis_agent/web_server.py +751 -0
- jarvis/jarvis_c2rust/__init__.py +26 -0
- jarvis/jarvis_c2rust/cli.py +613 -0
- jarvis/jarvis_c2rust/collector.py +258 -0
- jarvis/jarvis_c2rust/library_replacer.py +1122 -0
- jarvis/jarvis_c2rust/llm_module_agent.py +1300 -0
- jarvis/jarvis_c2rust/optimizer.py +960 -0
- jarvis/jarvis_c2rust/scanner.py +1681 -0
- jarvis/jarvis_c2rust/transpiler.py +2325 -0
- jarvis/jarvis_code_agent/build_validation_config.py +133 -0
- jarvis/jarvis_code_agent/code_agent.py +1605 -178
- jarvis/jarvis_code_agent/code_analyzer/__init__.py +62 -0
- jarvis/jarvis_code_agent/code_analyzer/base_language.py +74 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/__init__.py +44 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/base.py +102 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/cmake.py +59 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/detector.py +125 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/fallback.py +69 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/go.py +38 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/java_gradle.py +44 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/java_maven.py +38 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/makefile.py +50 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/nodejs.py +93 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/python.py +129 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/rust.py +54 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/validator.py +154 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator.py +43 -0
- jarvis/jarvis_code_agent/code_analyzer/context_manager.py +363 -0
- jarvis/jarvis_code_agent/code_analyzer/context_recommender.py +18 -0
- jarvis/jarvis_code_agent/code_analyzer/dependency_analyzer.py +132 -0
- jarvis/jarvis_code_agent/code_analyzer/file_ignore.py +330 -0
- jarvis/jarvis_code_agent/code_analyzer/impact_analyzer.py +781 -0
- jarvis/jarvis_code_agent/code_analyzer/language_registry.py +185 -0
- jarvis/jarvis_code_agent/code_analyzer/language_support.py +89 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/__init__.py +31 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/c_cpp_language.py +231 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/go_language.py +183 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/python_language.py +219 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/rust_language.py +209 -0
- jarvis/jarvis_code_agent/code_analyzer/llm_context_recommender.py +451 -0
- jarvis/jarvis_code_agent/code_analyzer/symbol_extractor.py +77 -0
- jarvis/jarvis_code_agent/code_analyzer/tree_sitter_extractor.py +48 -0
- jarvis/jarvis_code_agent/lint.py +275 -13
- jarvis/jarvis_code_agent/utils.py +142 -0
- jarvis/jarvis_code_analysis/checklists/loader.py +20 -6
- jarvis/jarvis_code_analysis/code_review.py +583 -548
- jarvis/jarvis_data/config_schema.json +339 -28
- jarvis/jarvis_git_squash/main.py +22 -13
- jarvis/jarvis_git_utils/git_commiter.py +171 -55
- jarvis/jarvis_mcp/sse_mcp_client.py +22 -15
- jarvis/jarvis_mcp/stdio_mcp_client.py +4 -4
- jarvis/jarvis_mcp/streamable_mcp_client.py +36 -16
- jarvis/jarvis_memory_organizer/memory_organizer.py +753 -0
- jarvis/jarvis_methodology/main.py +48 -63
- jarvis/jarvis_multi_agent/__init__.py +302 -43
- jarvis/jarvis_multi_agent/main.py +70 -24
- jarvis/jarvis_platform/ai8.py +40 -23
- jarvis/jarvis_platform/base.py +210 -49
- jarvis/jarvis_platform/human.py +11 -1
- jarvis/jarvis_platform/kimi.py +82 -76
- jarvis/jarvis_platform/openai.py +73 -1
- jarvis/jarvis_platform/registry.py +8 -15
- jarvis/jarvis_platform/tongyi.py +115 -101
- jarvis/jarvis_platform/yuanbao.py +89 -63
- jarvis/jarvis_platform_manager/main.py +194 -132
- jarvis/jarvis_platform_manager/service.py +122 -86
- jarvis/jarvis_rag/cli.py +156 -53
- jarvis/jarvis_rag/embedding_manager.py +155 -12
- jarvis/jarvis_rag/llm_interface.py +10 -13
- jarvis/jarvis_rag/query_rewriter.py +63 -12
- jarvis/jarvis_rag/rag_pipeline.py +222 -40
- jarvis/jarvis_rag/reranker.py +26 -3
- jarvis/jarvis_rag/retriever.py +270 -14
- jarvis/jarvis_sec/__init__.py +3605 -0
- jarvis/jarvis_sec/checkers/__init__.py +32 -0
- jarvis/jarvis_sec/checkers/c_checker.py +2680 -0
- jarvis/jarvis_sec/checkers/rust_checker.py +1108 -0
- jarvis/jarvis_sec/cli.py +116 -0
- jarvis/jarvis_sec/report.py +257 -0
- jarvis/jarvis_sec/status.py +264 -0
- jarvis/jarvis_sec/types.py +20 -0
- jarvis/jarvis_sec/workflow.py +219 -0
- jarvis/jarvis_smart_shell/main.py +405 -137
- jarvis/jarvis_stats/__init__.py +13 -0
- jarvis/jarvis_stats/cli.py +387 -0
- jarvis/jarvis_stats/stats.py +711 -0
- jarvis/jarvis_stats/storage.py +612 -0
- jarvis/jarvis_stats/visualizer.py +282 -0
- jarvis/jarvis_tools/ask_user.py +1 -0
- jarvis/jarvis_tools/base.py +18 -2
- jarvis/jarvis_tools/clear_memory.py +239 -0
- jarvis/jarvis_tools/cli/main.py +220 -144
- jarvis/jarvis_tools/execute_script.py +52 -12
- jarvis/jarvis_tools/file_analyzer.py +17 -12
- jarvis/jarvis_tools/generate_new_tool.py +46 -24
- jarvis/jarvis_tools/read_code.py +277 -18
- jarvis/jarvis_tools/read_symbols.py +141 -0
- jarvis/jarvis_tools/read_webpage.py +86 -13
- jarvis/jarvis_tools/registry.py +294 -90
- jarvis/jarvis_tools/retrieve_memory.py +227 -0
- jarvis/jarvis_tools/save_memory.py +194 -0
- jarvis/jarvis_tools/search_web.py +62 -28
- jarvis/jarvis_tools/sub_agent.py +205 -0
- jarvis/jarvis_tools/sub_code_agent.py +217 -0
- jarvis/jarvis_tools/virtual_tty.py +330 -62
- jarvis/jarvis_utils/builtin_replace_map.py +4 -5
- jarvis/jarvis_utils/clipboard.py +90 -0
- jarvis/jarvis_utils/config.py +607 -50
- jarvis/jarvis_utils/embedding.py +3 -0
- jarvis/jarvis_utils/fzf.py +57 -0
- jarvis/jarvis_utils/git_utils.py +251 -29
- jarvis/jarvis_utils/globals.py +174 -17
- jarvis/jarvis_utils/http.py +58 -79
- jarvis/jarvis_utils/input.py +899 -153
- jarvis/jarvis_utils/methodology.py +210 -83
- jarvis/jarvis_utils/output.py +220 -137
- jarvis/jarvis_utils/utils.py +1906 -135
- jarvis_ai_assistant-0.7.0.dist-info/METADATA +465 -0
- jarvis_ai_assistant-0.7.0.dist-info/RECORD +192 -0
- {jarvis_ai_assistant-0.1.222.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/entry_points.txt +8 -2
- jarvis/jarvis_git_details/main.py +0 -265
- jarvis/jarvis_platform/oyi.py +0 -357
- jarvis/jarvis_tools/edit_file.py +0 -255
- jarvis/jarvis_tools/rewrite_file.py +0 -195
- jarvis_ai_assistant-0.1.222.dist-info/METADATA +0 -767
- jarvis_ai_assistant-0.1.222.dist-info/RECORD +0 -110
- /jarvis/{jarvis_git_details → jarvis_memory_organizer}/__init__.py +0 -0
- {jarvis_ai_assistant-0.1.222.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/WHEEL +0 -0
- {jarvis_ai_assistant-0.1.222.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/licenses/LICENSE +0 -0
- {jarvis_ai_assistant-0.1.222.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/top_level.txt +0 -0
jarvis/jarvis_utils/utils.py
CHANGED
|
@@ -6,10 +6,15 @@ import signal
|
|
|
6
6
|
import subprocess
|
|
7
7
|
import sys
|
|
8
8
|
import time
|
|
9
|
+
import atexit
|
|
10
|
+
import errno
|
|
9
11
|
from pathlib import Path
|
|
10
|
-
from typing import Any, Callable, Dict, Optional
|
|
12
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
13
|
+
from datetime import datetime, date
|
|
11
14
|
|
|
12
15
|
import yaml # type: ignore
|
|
16
|
+
from rich.align import Align
|
|
17
|
+
from rich.console import RenderableType
|
|
13
18
|
|
|
14
19
|
from jarvis import __version__
|
|
15
20
|
from jarvis.jarvis_utils.config import (
|
|
@@ -19,10 +24,176 @@ from jarvis.jarvis_utils.config import (
|
|
|
19
24
|
)
|
|
20
25
|
from jarvis.jarvis_utils.embedding import get_context_token_count
|
|
21
26
|
from jarvis.jarvis_utils.globals import get_in_chat, get_interrupt, set_interrupt
|
|
27
|
+
from jarvis.jarvis_utils.input import user_confirm
|
|
22
28
|
from jarvis.jarvis_utils.output import OutputType, PrettyOutput
|
|
23
29
|
|
|
30
|
+
# 向后兼容:导出 get_yes_no 供外部模块引用
|
|
31
|
+
get_yes_no = user_confirm
|
|
32
|
+
|
|
24
33
|
g_config_file = None
|
|
25
34
|
|
|
35
|
+
COMMAND_MAPPING = {
|
|
36
|
+
# jarvis主命令
|
|
37
|
+
"jvs": "jarvis",
|
|
38
|
+
# 代码代理
|
|
39
|
+
"jca": "jarvis-code-agent",
|
|
40
|
+
# 智能shell
|
|
41
|
+
"jss": "jarvis-smart-shell",
|
|
42
|
+
# 平台管理
|
|
43
|
+
"jpm": "jarvis-platform-manager",
|
|
44
|
+
# Git提交
|
|
45
|
+
"jgc": "jarvis-git-commit",
|
|
46
|
+
# 代码审查
|
|
47
|
+
"jcr": "jarvis-code-review",
|
|
48
|
+
# Git压缩
|
|
49
|
+
"jgs": "jarvis-git-squash",
|
|
50
|
+
# 多代理
|
|
51
|
+
"jma": "jarvis-multi-agent",
|
|
52
|
+
# 代理
|
|
53
|
+
"ja": "jarvis-agent",
|
|
54
|
+
# 工具
|
|
55
|
+
"jt": "jarvis-tool",
|
|
56
|
+
# 方法论
|
|
57
|
+
"jm": "jarvis-methodology",
|
|
58
|
+
# RAG
|
|
59
|
+
"jrg": "jarvis-rag",
|
|
60
|
+
# 统计
|
|
61
|
+
"jst": "jarvis-stats",
|
|
62
|
+
# 记忆整理
|
|
63
|
+
"jmo": "jarvis-memory-organizer",
|
|
64
|
+
# 安全分析
|
|
65
|
+
"jsec": "jarvis-sec",
|
|
66
|
+
# C2Rust迁移
|
|
67
|
+
"jc2r": "jarvis-c2rust",
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# RAG 依赖检测工具函数(更精确)
|
|
71
|
+
_RAG_REQUIRED_MODULES = [
|
|
72
|
+
"langchain",
|
|
73
|
+
"langchain_community",
|
|
74
|
+
"chromadb",
|
|
75
|
+
"sentence_transformers",
|
|
76
|
+
"rank_bm25",
|
|
77
|
+
"unstructured",
|
|
78
|
+
]
|
|
79
|
+
_RAG_OPTIONAL_MODULES = [
|
|
80
|
+
"langchain_huggingface",
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def get_missing_rag_modules() -> List[str]:
|
|
85
|
+
"""
|
|
86
|
+
返回缺失的 RAG 关键依赖模块列表。
|
|
87
|
+
仅检查必要模块,不导入模块,避免副作用。
|
|
88
|
+
"""
|
|
89
|
+
try:
|
|
90
|
+
from importlib.util import find_spec
|
|
91
|
+
|
|
92
|
+
missing = [m for m in _RAG_REQUIRED_MODULES if find_spec(m) is None]
|
|
93
|
+
return missing
|
|
94
|
+
except Exception:
|
|
95
|
+
# 任何异常都视为无法确认,保持保守策略
|
|
96
|
+
return _RAG_REQUIRED_MODULES[:] # 视为全部缺失
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def is_rag_installed() -> bool:
|
|
100
|
+
"""
|
|
101
|
+
更准确的 RAG 安装检测:确认关键依赖模块均可用。
|
|
102
|
+
"""
|
|
103
|
+
return len(get_missing_rag_modules()) == 0
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def is_editable_install() -> bool:
|
|
107
|
+
"""
|
|
108
|
+
检测当前 Jarvis 是否以可编辑模式安装(pip/uv install -e .)。
|
|
109
|
+
|
|
110
|
+
判断顺序:
|
|
111
|
+
1. 读取 PEP 610 的 direct_url.json(dir_info.editable)
|
|
112
|
+
2. 兼容旧式 .egg-link 安装
|
|
113
|
+
3. 启发式回退:源码路径上游存在 .git 且不在 site-packages/dist-packages
|
|
114
|
+
"""
|
|
115
|
+
# 优先使用 importlib.metadata 读取 distribution 的 direct_url.json
|
|
116
|
+
try:
|
|
117
|
+
import importlib.metadata as metadata # Python 3.8+
|
|
118
|
+
except Exception:
|
|
119
|
+
metadata = None # type: ignore
|
|
120
|
+
|
|
121
|
+
def _check_direct_url() -> Optional[bool]:
|
|
122
|
+
if metadata is None:
|
|
123
|
+
return None
|
|
124
|
+
candidates = ["jarvis-ai-assistant", "jarvis_ai_assistant"]
|
|
125
|
+
for name in candidates:
|
|
126
|
+
try:
|
|
127
|
+
dist = metadata.distribution(name)
|
|
128
|
+
except Exception:
|
|
129
|
+
continue
|
|
130
|
+
try:
|
|
131
|
+
files = dist.files or []
|
|
132
|
+
for f in files:
|
|
133
|
+
try:
|
|
134
|
+
if f.name == "direct_url.json":
|
|
135
|
+
p = Path(str(dist.locate_file(f)))
|
|
136
|
+
if p.exists():
|
|
137
|
+
with open(p, "r", encoding="utf-8", errors="ignore") as fp:
|
|
138
|
+
info = json.load(fp)
|
|
139
|
+
dir_info = info.get("dir_info") or {}
|
|
140
|
+
if isinstance(dir_info, dict) and bool(dir_info.get("editable")):
|
|
141
|
+
return True
|
|
142
|
+
# 兼容部分工具可能写入顶层 editable 字段
|
|
143
|
+
if bool(info.get("editable")):
|
|
144
|
+
return True
|
|
145
|
+
return False # 找到了 direct_url.json 但未标记 editable
|
|
146
|
+
except Exception:
|
|
147
|
+
continue
|
|
148
|
+
except Exception:
|
|
149
|
+
continue
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
res = _check_direct_url()
|
|
153
|
+
if res is True:
|
|
154
|
+
return True
|
|
155
|
+
if res is False:
|
|
156
|
+
# 明确不是可编辑安装
|
|
157
|
+
return False
|
|
158
|
+
|
|
159
|
+
# 兼容旧式 .egg-link 可编辑安装
|
|
160
|
+
try:
|
|
161
|
+
module_path = Path(__file__).resolve()
|
|
162
|
+
pkg_root = module_path.parent.parent # jarvis 包根目录
|
|
163
|
+
for entry in sys.path:
|
|
164
|
+
try:
|
|
165
|
+
p = Path(entry)
|
|
166
|
+
if not p.exists() or not p.is_dir():
|
|
167
|
+
continue
|
|
168
|
+
for egg in p.glob("*.egg-link"):
|
|
169
|
+
try:
|
|
170
|
+
text = egg.read_text(encoding="utf-8", errors="ignore")
|
|
171
|
+
first_line = (text.strip().splitlines() or [""])[0]
|
|
172
|
+
if not first_line:
|
|
173
|
+
continue
|
|
174
|
+
src_path = Path(first_line).resolve()
|
|
175
|
+
# 当前包根目录在 egg-link 指向的源码路径下,视为可编辑安装
|
|
176
|
+
if str(pkg_root).startswith(str(src_path)):
|
|
177
|
+
return True
|
|
178
|
+
except Exception:
|
|
179
|
+
continue
|
|
180
|
+
except Exception:
|
|
181
|
+
continue
|
|
182
|
+
except Exception:
|
|
183
|
+
pass
|
|
184
|
+
|
|
185
|
+
# 启发式回退:源码仓库路径
|
|
186
|
+
try:
|
|
187
|
+
parents = list(Path(__file__).resolve().parents)
|
|
188
|
+
has_git = any((d / ".git").exists() for d in parents)
|
|
189
|
+
in_site = any(("site-packages" in str(d)) or ("dist-packages" in str(d)) for d in parents)
|
|
190
|
+
if has_git and not in_site:
|
|
191
|
+
return True
|
|
192
|
+
except Exception:
|
|
193
|
+
pass
|
|
194
|
+
|
|
195
|
+
return False
|
|
196
|
+
|
|
26
197
|
|
|
27
198
|
def _setup_signal_handler() -> None:
|
|
28
199
|
"""设置SIGINT信号处理函数"""
|
|
@@ -40,40 +211,690 @@ def _setup_signal_handler() -> None:
|
|
|
40
211
|
signal.signal(signal.SIGINT, sigint_handler)
|
|
41
212
|
|
|
42
213
|
|
|
43
|
-
|
|
44
|
-
|
|
214
|
+
# ----------------------------
|
|
215
|
+
# 单实例文件锁(放置于初始化早期使用)
|
|
216
|
+
# ----------------------------
|
|
217
|
+
_INSTANCE_LOCK_PATH: Optional[Path] = None
|
|
45
218
|
|
|
46
|
-
|
|
47
|
-
|
|
219
|
+
|
|
220
|
+
def _get_instance_lock_path(lock_name: str = "instance.lock") -> Path:
|
|
221
|
+
try:
|
|
222
|
+
data_dir = Path(str(get_data_dir()))
|
|
223
|
+
except Exception:
|
|
224
|
+
data_dir = Path(os.path.expanduser("~/.jarvis"))
|
|
225
|
+
data_dir.mkdir(parents=True, exist_ok=True)
|
|
226
|
+
return data_dir / lock_name
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _read_lock_owner_pid(lock_path: Path) -> Optional[int]:
|
|
230
|
+
try:
|
|
231
|
+
txt = lock_path.read_text(encoding="utf-8", errors="ignore").strip()
|
|
232
|
+
if not txt:
|
|
233
|
+
return None
|
|
234
|
+
try:
|
|
235
|
+
info = json.loads(txt)
|
|
236
|
+
pid = info.get("pid")
|
|
237
|
+
return int(pid) if pid is not None else None
|
|
238
|
+
except Exception:
|
|
239
|
+
# 兼容纯数字PID
|
|
240
|
+
return int(txt)
|
|
241
|
+
except Exception:
|
|
242
|
+
return None
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _is_process_alive(pid: int) -> bool:
|
|
246
|
+
if pid is None or pid <= 0:
|
|
247
|
+
return False
|
|
248
|
+
try:
|
|
249
|
+
os.kill(pid, 0)
|
|
250
|
+
except ProcessLookupError:
|
|
251
|
+
return False
|
|
252
|
+
except PermissionError:
|
|
253
|
+
# 无权限但进程存在
|
|
254
|
+
return True
|
|
255
|
+
except OSError as e:
|
|
256
|
+
# 某些平台上,EPERM 表示进程存在但无权限
|
|
257
|
+
if getattr(e, "errno", None) == errno.EPERM:
|
|
258
|
+
return True
|
|
259
|
+
return False
|
|
260
|
+
else:
|
|
261
|
+
return True
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _release_instance_lock() -> None:
|
|
265
|
+
global _INSTANCE_LOCK_PATH
|
|
266
|
+
try:
|
|
267
|
+
if _INSTANCE_LOCK_PATH and _INSTANCE_LOCK_PATH.exists():
|
|
268
|
+
_INSTANCE_LOCK_PATH.unlink()
|
|
269
|
+
except Exception:
|
|
270
|
+
# 清理失败不影响退出
|
|
271
|
+
pass
|
|
272
|
+
_INSTANCE_LOCK_PATH = None
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _acquire_single_instance_lock(lock_name: str = "instance.lock") -> None:
|
|
48
276
|
"""
|
|
49
|
-
|
|
50
|
-
|
|
277
|
+
在数据目录(~/.jarvis 或配置的数据目录)下创建实例锁,防止重复启动。
|
|
278
|
+
如果检测到已有存活实例,提示后退出。
|
|
279
|
+
"""
|
|
280
|
+
global _INSTANCE_LOCK_PATH
|
|
281
|
+
lock_path = _get_instance_lock_path(lock_name)
|
|
51
282
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
283
|
+
# 已存在锁:检查是否为有效存活实例
|
|
284
|
+
if lock_path.exists():
|
|
285
|
+
pid = _read_lock_owner_pid(lock_path)
|
|
286
|
+
if pid and _is_process_alive(pid):
|
|
287
|
+
PrettyOutput.print(
|
|
288
|
+
f"检测到已有一个 Jarvis 实例正在运行 (PID: {pid})。\n"
|
|
289
|
+
f"如果确认不存在正在运行的实例,请删除锁文件后重试:{lock_path}",
|
|
290
|
+
OutputType.WARNING,
|
|
291
|
+
)
|
|
292
|
+
sys.exit(0)
|
|
293
|
+
# 尝试移除陈旧锁
|
|
294
|
+
try:
|
|
295
|
+
lock_path.unlink()
|
|
296
|
+
except Exception:
|
|
297
|
+
PrettyOutput.print(
|
|
298
|
+
f"无法删除旧锁文件:{lock_path},请手动清理后重试。",
|
|
299
|
+
OutputType.ERROR,
|
|
300
|
+
)
|
|
301
|
+
sys.exit(1)
|
|
60
302
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
303
|
+
# 原子创建锁文件,避免并发竞争
|
|
304
|
+
flags = os.O_CREAT | os.O_EXCL | os.O_WRONLY
|
|
305
|
+
try:
|
|
306
|
+
fd = os.open(str(lock_path), flags)
|
|
307
|
+
with os.fdopen(fd, "w", encoding="utf-8") as fp:
|
|
308
|
+
payload = {
|
|
309
|
+
"pid": os.getpid(),
|
|
310
|
+
"time": int(time.time()),
|
|
311
|
+
"argv": sys.argv[:10],
|
|
312
|
+
}
|
|
313
|
+
try:
|
|
314
|
+
fp.write(json.dumps(payload, ensure_ascii=False))
|
|
315
|
+
except Exception:
|
|
316
|
+
fp.write(str(os.getpid()))
|
|
317
|
+
_INSTANCE_LOCK_PATH = lock_path
|
|
318
|
+
atexit.register(_release_instance_lock)
|
|
319
|
+
except FileExistsError:
|
|
320
|
+
# 极端并发下再次校验
|
|
321
|
+
pid = _read_lock_owner_pid(lock_path)
|
|
322
|
+
if pid and _is_process_alive(pid):
|
|
323
|
+
PrettyOutput.print(
|
|
324
|
+
f"检测到已有一个 Jarvis 实例正在运行 (PID: {pid})。",
|
|
325
|
+
OutputType.WARNING,
|
|
326
|
+
)
|
|
327
|
+
sys.exit(0)
|
|
328
|
+
PrettyOutput.print(
|
|
329
|
+
f"锁文件已存在但可能为陈旧状态:{lock_path},请手动删除后重试。",
|
|
330
|
+
OutputType.ERROR,
|
|
331
|
+
)
|
|
332
|
+
sys.exit(1)
|
|
333
|
+
except Exception as e:
|
|
334
|
+
PrettyOutput.print(f"创建实例锁失败: {e}", OutputType.ERROR)
|
|
335
|
+
sys.exit(1)
|
|
65
336
|
|
|
66
337
|
|
|
67
|
-
def
|
|
68
|
-
"""
|
|
338
|
+
def _check_pip_updates() -> bool:
|
|
339
|
+
"""检查pip安装的Jarvis是否有更新
|
|
340
|
+
|
|
341
|
+
返回:
|
|
342
|
+
bool: 是否执行了更新(成功更新返回True以触发重启)
|
|
343
|
+
"""
|
|
344
|
+
import urllib.request
|
|
345
|
+
import urllib.error
|
|
346
|
+
from packaging import version
|
|
347
|
+
|
|
348
|
+
# 检查上次检查日期
|
|
349
|
+
last_check_file = Path(str(get_data_dir())) / "last_pip_check"
|
|
350
|
+
today_str = date.today().strftime("%Y-%m-%d")
|
|
351
|
+
|
|
352
|
+
if last_check_file.exists():
|
|
353
|
+
try:
|
|
354
|
+
last_check_date = last_check_file.read_text().strip()
|
|
355
|
+
if last_check_date == today_str:
|
|
356
|
+
return False
|
|
357
|
+
except Exception:
|
|
358
|
+
pass
|
|
359
|
+
|
|
360
|
+
try:
|
|
361
|
+
# 获取PyPI上的最新版本
|
|
362
|
+
url = "https://pypi.org/pypi/jarvis-ai-assistant/json"
|
|
363
|
+
try:
|
|
364
|
+
with urllib.request.urlopen(url, timeout=5) as response:
|
|
365
|
+
data = json.loads(response.read().decode())
|
|
366
|
+
latest_version = data["info"]["version"]
|
|
367
|
+
except (urllib.error.URLError, KeyError, json.JSONDecodeError):
|
|
368
|
+
return False
|
|
369
|
+
|
|
370
|
+
# 比较版本
|
|
371
|
+
current_ver = version.parse(__version__)
|
|
372
|
+
latest_ver = version.parse(latest_version)
|
|
373
|
+
|
|
374
|
+
if latest_ver > current_ver:
|
|
375
|
+
PrettyOutput.print(
|
|
376
|
+
f"检测到新版本 v{latest_version} (当前版本: v{__version__})",
|
|
377
|
+
OutputType.INFO,
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
# 检测是否在虚拟环境中
|
|
381
|
+
hasattr(sys, "real_prefix") or (
|
|
382
|
+
hasattr(sys, "base_prefix") and sys.base_prefix != sys.prefix
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
# 检测是否可用 uv(优先使用虚拟环境内的uv,其次PATH中的uv)
|
|
386
|
+
from shutil import which as _which
|
|
387
|
+
uv_executable: Optional[str] = None
|
|
388
|
+
if sys.platform == "win32":
|
|
389
|
+
venv_uv = Path(sys.prefix) / "Scripts" / "uv.exe"
|
|
390
|
+
else:
|
|
391
|
+
venv_uv = Path(sys.prefix) / "bin" / "uv"
|
|
392
|
+
if venv_uv.exists():
|
|
393
|
+
uv_executable = str(venv_uv)
|
|
394
|
+
else:
|
|
395
|
+
path_uv = _which("uv")
|
|
396
|
+
if path_uv:
|
|
397
|
+
uv_executable = path_uv
|
|
398
|
+
|
|
399
|
+
# 检测是否安装了 RAG 特性(更精确)
|
|
400
|
+
from jarvis.jarvis_utils.utils import (
|
|
401
|
+
is_rag_installed as _is_rag_installed,
|
|
402
|
+
) # 延迟导入避免潜在循环依赖
|
|
403
|
+
rag_installed = _is_rag_installed()
|
|
404
|
+
|
|
405
|
+
# 更新命令
|
|
406
|
+
package_spec = (
|
|
407
|
+
"jarvis-ai-assistant[rag]" if rag_installed else "jarvis-ai-assistant"
|
|
408
|
+
)
|
|
409
|
+
if uv_executable:
|
|
410
|
+
cmd_list = [uv_executable, "pip", "install", "--upgrade", package_spec]
|
|
411
|
+
update_cmd = f"uv pip install --upgrade {package_spec}"
|
|
412
|
+
else:
|
|
413
|
+
cmd_list = [
|
|
414
|
+
sys.executable,
|
|
415
|
+
"-m",
|
|
416
|
+
"pip",
|
|
417
|
+
"install",
|
|
418
|
+
"--upgrade",
|
|
419
|
+
package_spec,
|
|
420
|
+
]
|
|
421
|
+
update_cmd = f"{sys.executable} -m pip install --upgrade {package_spec}"
|
|
422
|
+
|
|
423
|
+
# 自动尝试升级(失败时提供手动命令)
|
|
424
|
+
try:
|
|
425
|
+
PrettyOutput.print("正在自动更新 Jarvis,请稍候...", OutputType.INFO)
|
|
426
|
+
result = subprocess.run(
|
|
427
|
+
cmd_list,
|
|
428
|
+
capture_output=True,
|
|
429
|
+
text=True,
|
|
430
|
+
encoding="utf-8",
|
|
431
|
+
errors="replace",
|
|
432
|
+
timeout=600,
|
|
433
|
+
)
|
|
434
|
+
if result.returncode == 0:
|
|
435
|
+
PrettyOutput.print("更新成功,正在重启以应用新版本...", OutputType.SUCCESS)
|
|
436
|
+
# 更新检查日期,避免重复触发
|
|
437
|
+
last_check_file.write_text(today_str)
|
|
438
|
+
return True
|
|
439
|
+
else:
|
|
440
|
+
err = (result.stderr or result.stdout or "").strip()
|
|
441
|
+
if err:
|
|
442
|
+
PrettyOutput.print(
|
|
443
|
+
f"自动更新失败,错误信息(已截断): {err[:500]}",
|
|
444
|
+
OutputType.WARNING,
|
|
445
|
+
)
|
|
446
|
+
PrettyOutput.print(
|
|
447
|
+
f"请手动执行以下命令更新: {update_cmd}", OutputType.INFO
|
|
448
|
+
)
|
|
449
|
+
except Exception:
|
|
450
|
+
PrettyOutput.print("自动更新出现异常,已切换为手动更新方式。", OutputType.WARNING)
|
|
451
|
+
PrettyOutput.print(
|
|
452
|
+
f"请手动执行以下命令更新: {update_cmd}", OutputType.INFO
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
# 更新检查日期
|
|
456
|
+
last_check_file.write_text(today_str)
|
|
457
|
+
|
|
458
|
+
except Exception:
|
|
459
|
+
# 静默处理错误,不影响正常使用
|
|
460
|
+
pass
|
|
461
|
+
|
|
462
|
+
return False
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def _check_jarvis_updates() -> bool:
|
|
466
|
+
"""检查并更新Jarvis本身(git仓库或pip包)
|
|
69
467
|
|
|
70
468
|
返回:
|
|
71
469
|
bool: 是否需要重启进程
|
|
72
470
|
"""
|
|
73
|
-
|
|
74
|
-
|
|
471
|
+
# 从当前文件目录向上查找包含 .git 的仓库根目录,修复原先只检查 src/jarvis 的问题
|
|
472
|
+
try:
|
|
473
|
+
script_path = Path(__file__).resolve()
|
|
474
|
+
repo_root: Optional[Path] = None
|
|
475
|
+
for d in [script_path.parent] + list(script_path.parents):
|
|
476
|
+
if (d / ".git").exists():
|
|
477
|
+
repo_root = d
|
|
478
|
+
break
|
|
479
|
+
except Exception:
|
|
480
|
+
repo_root = None
|
|
75
481
|
|
|
76
|
-
|
|
482
|
+
# 先检查是否是git源码安装(找到仓库根目录即认为是源码安装)
|
|
483
|
+
if repo_root and (repo_root / ".git").exists():
|
|
484
|
+
from jarvis.jarvis_utils.git_utils import check_and_update_git_repo
|
|
485
|
+
|
|
486
|
+
return check_and_update_git_repo(str(repo_root))
|
|
487
|
+
|
|
488
|
+
# 检查是否是pip/uv pip安装的版本
|
|
489
|
+
return _check_pip_updates()
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def _show_usage_stats(welcome_str: str) -> None:
|
|
493
|
+
"""显示Jarvis使用统计信息"""
|
|
494
|
+
from jarvis.jarvis_utils.output import OutputType, PrettyOutput
|
|
495
|
+
|
|
496
|
+
try:
|
|
497
|
+
|
|
498
|
+
from rich.console import Console, Group
|
|
499
|
+
from rich.panel import Panel
|
|
500
|
+
from rich.table import Table
|
|
501
|
+
from rich.text import Text
|
|
502
|
+
|
|
503
|
+
console = Console()
|
|
504
|
+
|
|
505
|
+
from jarvis.jarvis_stats.stats import StatsManager
|
|
506
|
+
|
|
507
|
+
# 获取所有可用的指标
|
|
508
|
+
all_metrics = StatsManager.list_metrics()
|
|
509
|
+
|
|
510
|
+
# 根据指标名称和标签自动分类
|
|
511
|
+
categorized_stats: Dict[str, Dict[str, Any]] = {
|
|
512
|
+
"tool": {"title": "🔧 工具调用", "metrics": {}, "suffix": "次"},
|
|
513
|
+
"code": {"title": "📝 代码修改", "metrics": {}, "suffix": "次"},
|
|
514
|
+
"lines": {"title": "📊 代码行数", "metrics": {}, "suffix": "行"},
|
|
515
|
+
"commit": {"title": "💾 提交统计", "metrics": {}, "suffix": "个"},
|
|
516
|
+
"command": {"title": "📱 命令使用", "metrics": {}, "suffix": "次"},
|
|
517
|
+
"adoption": {"title": "🎯 采纳情况", "metrics": {}, "suffix": ""},
|
|
518
|
+
"other": {"title": "📦 其他指标", "metrics": {}, "suffix": ""},
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
# 遍历所有指标,使用快速总量读取以避免全量扫描
|
|
522
|
+
for metric in all_metrics:
|
|
523
|
+
try:
|
|
524
|
+
total = StatsManager.get_metric_total(metric)
|
|
525
|
+
except Exception:
|
|
526
|
+
total = 0.0
|
|
527
|
+
|
|
528
|
+
if not total or total <= 0:
|
|
529
|
+
continue
|
|
530
|
+
|
|
531
|
+
# 优先使用元信息中的分组(在写入指标时已记录)
|
|
532
|
+
info = StatsManager.get_metric_info(metric) or {}
|
|
533
|
+
group = info.get("group", "other")
|
|
534
|
+
|
|
535
|
+
if group == "tool":
|
|
536
|
+
categorized_stats["tool"]["metrics"][metric] = int(total)
|
|
537
|
+
elif group == "code_agent":
|
|
538
|
+
# 根据指标名称细分
|
|
539
|
+
if metric.startswith("code_lines_"):
|
|
540
|
+
categorized_stats["lines"]["metrics"][metric] = int(total)
|
|
541
|
+
elif "commit" in metric:
|
|
542
|
+
categorized_stats["commit"]["metrics"][metric] = int(total)
|
|
543
|
+
else:
|
|
544
|
+
categorized_stats["code"]["metrics"][metric] = int(total)
|
|
545
|
+
elif group == "command":
|
|
546
|
+
categorized_stats["command"]["metrics"][metric] = int(total)
|
|
547
|
+
else:
|
|
548
|
+
categorized_stats["other"]["metrics"][metric] = int(total)
|
|
549
|
+
|
|
550
|
+
# 合并长短命令的历史统计数据
|
|
551
|
+
command_stats = categorized_stats["command"]["metrics"]
|
|
552
|
+
if command_stats:
|
|
553
|
+
merged_stats: Dict[str, int] = {}
|
|
554
|
+
for metric, count in command_stats.items():
|
|
555
|
+
long_command = COMMAND_MAPPING.get(metric, metric)
|
|
556
|
+
merged_stats[long_command] = merged_stats.get(long_command, 0) + count
|
|
557
|
+
categorized_stats["command"]["metrics"] = merged_stats
|
|
558
|
+
|
|
559
|
+
# 计算采纳率并添加到统计中
|
|
560
|
+
commit_stats = categorized_stats["commit"]["metrics"]
|
|
561
|
+
# 使用精确的指标名称
|
|
562
|
+
generated_commits = commit_stats.get("commits_generated", 0)
|
|
563
|
+
accepted_commits = commit_stats.get("commits_accepted", 0)
|
|
564
|
+
|
|
565
|
+
# 如果有 generated,则计算采纳率
|
|
566
|
+
if generated_commits > 0:
|
|
567
|
+
adoption_rate = (accepted_commits / generated_commits) * 100
|
|
568
|
+
categorized_stats["adoption"]["metrics"][
|
|
569
|
+
"adoption_rate"
|
|
570
|
+
] = f"{adoption_rate:.1f}%"
|
|
571
|
+
categorized_stats["adoption"]["metrics"][
|
|
572
|
+
"commits_status"
|
|
573
|
+
] = f"{accepted_commits}/{generated_commits}"
|
|
574
|
+
|
|
575
|
+
# 构建输出
|
|
576
|
+
has_data = False
|
|
577
|
+
stats_output = []
|
|
578
|
+
|
|
579
|
+
for category, data in categorized_stats.items():
|
|
580
|
+
if data["metrics"]:
|
|
581
|
+
has_data = True
|
|
582
|
+
stats_output.append((data["title"], data["metrics"], data["suffix"]))
|
|
583
|
+
|
|
584
|
+
# 显示统计信息
|
|
585
|
+
if has_data:
|
|
586
|
+
# 1. 创建统计表格
|
|
587
|
+
from rich import box
|
|
588
|
+
|
|
589
|
+
table = Table(
|
|
590
|
+
show_header=True,
|
|
591
|
+
header_style="bold magenta",
|
|
592
|
+
title_justify="center",
|
|
593
|
+
box=box.ROUNDED,
|
|
594
|
+
padding=(0, 1),
|
|
595
|
+
)
|
|
596
|
+
table.add_column("分类", style="cyan", no_wrap=True, width=12)
|
|
597
|
+
table.add_column("指标", style="white", width=20)
|
|
598
|
+
table.add_column("数量", style="green", justify="right", width=10)
|
|
599
|
+
table.add_column("分类", style="cyan", no_wrap=True, width=12)
|
|
600
|
+
table.add_column("指标", style="white", width=20)
|
|
601
|
+
table.add_column("数量", style="green", justify="right", width=10)
|
|
602
|
+
|
|
603
|
+
# 收集所有要显示的数据
|
|
604
|
+
all_rows = []
|
|
605
|
+
for title, stats, suffix in stats_output:
|
|
606
|
+
if stats:
|
|
607
|
+
sorted_stats = sorted(
|
|
608
|
+
stats.items(), key=lambda item: item[1], reverse=True
|
|
609
|
+
)
|
|
610
|
+
for i, (metric, count) in enumerate(sorted_stats):
|
|
611
|
+
display_name = metric.replace("_", " ").title()
|
|
612
|
+
category_title = title if i == 0 else ""
|
|
613
|
+
# 处理不同类型的count值
|
|
614
|
+
if isinstance(count, (int, float)):
|
|
615
|
+
count_str = f"{count:,} {suffix}"
|
|
616
|
+
else:
|
|
617
|
+
# 对于字符串类型的count(如百分比或比率),直接使用
|
|
618
|
+
count_str = str(count)
|
|
619
|
+
all_rows.append((category_title, display_name, count_str))
|
|
620
|
+
|
|
621
|
+
# 以3行2列的方式添加数据
|
|
622
|
+
has_content = len(all_rows) > 0
|
|
623
|
+
# 计算需要多少行来显示所有数据
|
|
624
|
+
total_rows = len(all_rows)
|
|
625
|
+
rows_needed = (total_rows + 1) // 2 # 向上取整,因为是2列布局
|
|
626
|
+
|
|
627
|
+
for i in range(rows_needed):
|
|
628
|
+
left_idx = i
|
|
629
|
+
right_idx = i + rows_needed
|
|
630
|
+
|
|
631
|
+
if left_idx < len(all_rows):
|
|
632
|
+
left_row = all_rows[left_idx]
|
|
633
|
+
else:
|
|
634
|
+
left_row = ("", "", "")
|
|
635
|
+
|
|
636
|
+
if right_idx < len(all_rows):
|
|
637
|
+
right_row = all_rows[right_idx]
|
|
638
|
+
else:
|
|
639
|
+
right_row = ("", "", "")
|
|
640
|
+
|
|
641
|
+
table.add_row(
|
|
642
|
+
left_row[0],
|
|
643
|
+
left_row[1],
|
|
644
|
+
left_row[2],
|
|
645
|
+
right_row[0],
|
|
646
|
+
right_row[1],
|
|
647
|
+
right_row[2],
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
# 2. 创建总结面板
|
|
651
|
+
summary_content = []
|
|
652
|
+
|
|
653
|
+
# 总结统计
|
|
654
|
+
total_tools = sum(
|
|
655
|
+
count
|
|
656
|
+
for title, stats, _ in stats_output
|
|
657
|
+
if "工具" in title
|
|
658
|
+
for metric, count in stats.items()
|
|
659
|
+
)
|
|
660
|
+
total_changes = sum(
|
|
661
|
+
count
|
|
662
|
+
for title, stats, _ in stats_output
|
|
663
|
+
if "代码修改" in title
|
|
664
|
+
for metric, count in stats.items()
|
|
665
|
+
)
|
|
666
|
+
|
|
667
|
+
# 统计代码行数
|
|
668
|
+
lines_stats = categorized_stats["lines"]["metrics"]
|
|
669
|
+
total_lines_added = lines_stats.get(
|
|
670
|
+
"code_lines_inserted", lines_stats.get("code_lines_added", 0)
|
|
671
|
+
)
|
|
672
|
+
total_lines_deleted = lines_stats.get("code_lines_deleted", 0)
|
|
673
|
+
total_lines_modified = total_lines_added + total_lines_deleted
|
|
674
|
+
|
|
675
|
+
if total_tools > 0 or total_changes > 0 or total_lines_modified > 0:
|
|
676
|
+
parts = []
|
|
677
|
+
if total_tools > 0:
|
|
678
|
+
parts.append(f"工具调用 {total_tools:,} 次")
|
|
679
|
+
if total_changes > 0:
|
|
680
|
+
parts.append(f"代码修改 {total_changes:,} 次")
|
|
681
|
+
if total_lines_modified > 0:
|
|
682
|
+
parts.append(f"修改代码行数 {total_lines_modified:,} 行")
|
|
683
|
+
|
|
684
|
+
if parts:
|
|
685
|
+
summary_content.append(f"📈 总计: {', '.join(parts)}")
|
|
686
|
+
|
|
687
|
+
# 添加代码采纳率显示
|
|
688
|
+
adoption_metrics = categorized_stats["adoption"]["metrics"]
|
|
689
|
+
if "adoption_rate" in adoption_metrics:
|
|
690
|
+
summary_content.append(
|
|
691
|
+
f"✅ 代码采纳率: {adoption_metrics['adoption_rate']}"
|
|
692
|
+
)
|
|
693
|
+
|
|
694
|
+
# 计算节省的时间
|
|
695
|
+
time_saved_seconds = 0
|
|
696
|
+
tool_stats = categorized_stats["tool"]["metrics"]
|
|
697
|
+
code_agent_changes = categorized_stats["code"]["metrics"]
|
|
698
|
+
lines_stats = categorized_stats["lines"]["metrics"]
|
|
699
|
+
# commit_stats is already defined above
|
|
700
|
+
command_stats = categorized_stats["command"]["metrics"]
|
|
701
|
+
|
|
702
|
+
# 统一的工具使用时间估算(每次调用节省2分钟)
|
|
703
|
+
DEFAULT_TOOL_TIME_SAVINGS = 2 * 60 # 秒
|
|
704
|
+
|
|
705
|
+
# 计算所有工具的时间节省
|
|
706
|
+
for tool_name, count in tool_stats.items():
|
|
707
|
+
time_saved_seconds += count * DEFAULT_TOOL_TIME_SAVINGS
|
|
708
|
+
|
|
709
|
+
# 其他类型的时间计算
|
|
710
|
+
total_code_agent_calls = sum(code_agent_changes.values())
|
|
711
|
+
time_saved_seconds += total_code_agent_calls * 10 * 60
|
|
712
|
+
time_saved_seconds += lines_stats.get("code_lines_added", 0) * 0.8 * 60
|
|
713
|
+
time_saved_seconds += lines_stats.get("code_lines_deleted", 0) * 0.2 * 60
|
|
714
|
+
time_saved_seconds += sum(commit_stats.values()) * 10 * 60
|
|
715
|
+
time_saved_seconds += sum(command_stats.values()) * 1 * 60
|
|
716
|
+
|
|
717
|
+
time_str = ""
|
|
718
|
+
hours = 0
|
|
719
|
+
if time_saved_seconds > 0:
|
|
720
|
+
total_minutes = int(time_saved_seconds / 60)
|
|
721
|
+
seconds = int(time_saved_seconds % 60)
|
|
722
|
+
hours = total_minutes // 60
|
|
723
|
+
minutes = total_minutes % 60
|
|
724
|
+
# 只显示小时和分钟
|
|
725
|
+
if hours > 0:
|
|
726
|
+
time_str = f"{hours} 小时 {minutes} 分钟"
|
|
727
|
+
elif total_minutes > 0:
|
|
728
|
+
time_str = f"{minutes} 分钟 {seconds} 秒"
|
|
729
|
+
else:
|
|
730
|
+
time_str = f"{seconds} 秒"
|
|
731
|
+
|
|
732
|
+
if summary_content:
|
|
733
|
+
summary_content.append("") # Add a separator line
|
|
734
|
+
summary_content.append(f"⏱️ 节省时间: 约 {time_str}")
|
|
735
|
+
|
|
736
|
+
encouragement = ""
|
|
737
|
+
# 计算各级时间单位
|
|
738
|
+
total_work_days = hours // 8 # 总工作日数
|
|
739
|
+
work_years = total_work_days // 240 # 每年约240个工作日
|
|
740
|
+
remaining_days_after_years = total_work_days % 240
|
|
741
|
+
work_months = remaining_days_after_years // 20 # 每月约20个工作日
|
|
742
|
+
remaining_days_after_months = remaining_days_after_years % 20
|
|
743
|
+
work_days = remaining_days_after_months
|
|
744
|
+
remaining_hours = int(hours % 8) # 剩余不足一个工作日的小时数
|
|
745
|
+
|
|
746
|
+
# 构建时间描述
|
|
747
|
+
time_parts = []
|
|
748
|
+
if work_years > 0:
|
|
749
|
+
time_parts.append(f"{work_years} 年")
|
|
750
|
+
if work_months > 0:
|
|
751
|
+
time_parts.append(f"{work_months} 个月")
|
|
752
|
+
if work_days > 0:
|
|
753
|
+
time_parts.append(f"{work_days} 个工作日")
|
|
754
|
+
if remaining_hours > 0:
|
|
755
|
+
time_parts.append(f"{remaining_hours} 小时")
|
|
756
|
+
|
|
757
|
+
if time_parts:
|
|
758
|
+
time_description = "、".join(time_parts)
|
|
759
|
+
if work_years >= 1:
|
|
760
|
+
encouragement = (
|
|
761
|
+
f"🎉 相当于节省了 {time_description} 的工作时间!"
|
|
762
|
+
)
|
|
763
|
+
elif work_months >= 1:
|
|
764
|
+
encouragement = (
|
|
765
|
+
f"🚀 相当于节省了 {time_description} 的工作时间!"
|
|
766
|
+
)
|
|
767
|
+
elif work_days >= 1:
|
|
768
|
+
encouragement = (
|
|
769
|
+
f"💪 相当于节省了 {time_description} 的工作时间!"
|
|
770
|
+
)
|
|
771
|
+
else:
|
|
772
|
+
encouragement = (
|
|
773
|
+
f"✨ 相当于节省了 {time_description} 的工作时间!"
|
|
774
|
+
)
|
|
775
|
+
elif hours >= 1:
|
|
776
|
+
encouragement = f"⭐ 相当于节省了 {int(hours)} 小时的工作时间,积少成多,继续保持!"
|
|
777
|
+
if encouragement:
|
|
778
|
+
summary_content.append(encouragement)
|
|
779
|
+
|
|
780
|
+
# 3. 组合并打印
|
|
781
|
+
from rich import box
|
|
782
|
+
|
|
783
|
+
# 右侧内容:总体表现 + 使命与愿景
|
|
784
|
+
right_column_items = []
|
|
785
|
+
|
|
786
|
+
# 欢迎信息 Panel
|
|
787
|
+
if welcome_str:
|
|
788
|
+
jarvis_ascii_art_str = """
|
|
789
|
+
██╗ █████╗ ██████╗ ██╗ ██╗██╗███████╗
|
|
790
|
+
██║██╔══██╗██╔══██╗██║ ██║██║██╔════╝
|
|
791
|
+
██║███████║██████╔╝██║ ██║██║███████╗
|
|
792
|
+
██╗██║██╔══██║██╔══██╗╚██╗ ██╔╝██║╚════██║
|
|
793
|
+
╚████║██║ ██║██║ ██║ ╚████╔╝ ██║███████║
|
|
794
|
+
╚═══╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═══╝ ╚═╝╚══════╝"""
|
|
795
|
+
|
|
796
|
+
welcome_panel_content = Group(
|
|
797
|
+
Align.center(Text(jarvis_ascii_art_str, style="bold blue")),
|
|
798
|
+
Align.center(Text(welcome_str, style="bold")),
|
|
799
|
+
"", # for a blank line
|
|
800
|
+
Align.center(Text(f"v{__version__}")),
|
|
801
|
+
Align.center(Text("https://github.com/skyfireitdiy/Jarvis")),
|
|
802
|
+
)
|
|
803
|
+
|
|
804
|
+
welcome_panel = Panel(
|
|
805
|
+
welcome_panel_content, border_style="yellow", expand=True
|
|
806
|
+
)
|
|
807
|
+
right_column_items.append(welcome_panel)
|
|
808
|
+
if summary_content:
|
|
809
|
+
summary_panel = Panel(
|
|
810
|
+
Text("\n".join(summary_content), justify="left"),
|
|
811
|
+
title="✨ 总体表现 ✨",
|
|
812
|
+
title_align="center",
|
|
813
|
+
border_style="green",
|
|
814
|
+
expand=True,
|
|
815
|
+
)
|
|
816
|
+
right_column_items.append(summary_panel)
|
|
817
|
+
|
|
818
|
+
# 愿景 Panel
|
|
819
|
+
vision_text = Text(
|
|
820
|
+
"让开发者与AI成为共生伙伴",
|
|
821
|
+
justify="center",
|
|
822
|
+
style="italic",
|
|
823
|
+
)
|
|
824
|
+
vision_panel = Panel(
|
|
825
|
+
vision_text,
|
|
826
|
+
title="🔭 愿景 (Vision) 🔭",
|
|
827
|
+
title_align="center",
|
|
828
|
+
border_style="cyan",
|
|
829
|
+
expand=True,
|
|
830
|
+
)
|
|
831
|
+
right_column_items.append(vision_panel)
|
|
832
|
+
|
|
833
|
+
# 使命 Panel
|
|
834
|
+
mission_text = Text(
|
|
835
|
+
"让灵感高效落地为代码与行动",
|
|
836
|
+
justify="center",
|
|
837
|
+
style="italic",
|
|
838
|
+
)
|
|
839
|
+
mission_panel = Panel(
|
|
840
|
+
mission_text,
|
|
841
|
+
title="🎯 使命 (Mission) 🎯",
|
|
842
|
+
title_align="center",
|
|
843
|
+
border_style="magenta",
|
|
844
|
+
expand=True,
|
|
845
|
+
)
|
|
846
|
+
right_column_items.append(mission_panel)
|
|
847
|
+
|
|
848
|
+
right_column_group = Group(*right_column_items)
|
|
849
|
+
|
|
850
|
+
layout_renderable: RenderableType
|
|
851
|
+
|
|
852
|
+
if console.width < 200:
|
|
853
|
+
# 上下布局
|
|
854
|
+
layout_items: List[RenderableType] = []
|
|
855
|
+
layout_items.append(right_column_group)
|
|
856
|
+
if has_content:
|
|
857
|
+
layout_items.append(Align.center(table))
|
|
858
|
+
layout_renderable = Group(*layout_items)
|
|
859
|
+
else:
|
|
860
|
+
# 左右布局(当前)
|
|
861
|
+
layout_table = Table(
|
|
862
|
+
show_header=False,
|
|
863
|
+
box=None,
|
|
864
|
+
padding=0,
|
|
865
|
+
expand=True,
|
|
866
|
+
pad_edge=False,
|
|
867
|
+
)
|
|
868
|
+
# 左右布局,左侧为总结信息,右侧为统计表格
|
|
869
|
+
layout_table.add_column(ratio=5) # 左侧
|
|
870
|
+
layout_table.add_column(ratio=5) # 右侧
|
|
871
|
+
|
|
872
|
+
if has_content:
|
|
873
|
+
# 将总结信息放在左侧,统计表格放在右侧(表格居中显示)
|
|
874
|
+
layout_table.add_row(right_column_group, Align.center(table))
|
|
875
|
+
else:
|
|
876
|
+
# 如果没有统计数据,则总结信息占满
|
|
877
|
+
layout_table.add_row(right_column_group)
|
|
878
|
+
layout_renderable = layout_table
|
|
879
|
+
|
|
880
|
+
# 打印最终的布局
|
|
881
|
+
if has_content or summary_content:
|
|
882
|
+
# 将整体布局封装在一个最终的Panel中,以提供整体边框
|
|
883
|
+
final_panel = Panel(
|
|
884
|
+
layout_renderable,
|
|
885
|
+
title="Jarvis AI Assistant",
|
|
886
|
+
title_align="center",
|
|
887
|
+
border_style="blue",
|
|
888
|
+
box=box.HEAVY,
|
|
889
|
+
padding=(0, 1),
|
|
890
|
+
)
|
|
891
|
+
console.print(final_panel)
|
|
892
|
+
except Exception as e:
|
|
893
|
+
# 输出错误信息以便调试
|
|
894
|
+
import traceback
|
|
895
|
+
|
|
896
|
+
PrettyOutput.print(f"统计显示出错: {str(e)}", OutputType.ERROR)
|
|
897
|
+
PrettyOutput.print(traceback.format_exc(), OutputType.ERROR)
|
|
77
898
|
|
|
78
899
|
|
|
79
900
|
def init_env(welcome_str: str, config_file: Optional[str] = None) -> None:
|
|
@@ -83,27 +904,182 @@ def init_env(welcome_str: str, config_file: Optional[str] = None) -> None:
|
|
|
83
904
|
welcome_str: 欢迎信息字符串
|
|
84
905
|
config_file: 配置文件路径,默认为None(使用~/.jarvis/config.yaml)
|
|
85
906
|
"""
|
|
907
|
+
# 0. 检查是否处于Jarvis打开的终端环境,避免嵌套
|
|
908
|
+
try:
|
|
909
|
+
if os.environ.get("JARVIS_TERMINAL") == "1":
|
|
910
|
+
PrettyOutput.print(
|
|
911
|
+
"检测到当前终端由 Jarvis 打开。再次启动可能导致嵌套。",
|
|
912
|
+
OutputType.WARNING,
|
|
913
|
+
)
|
|
914
|
+
if not user_confirm("是否仍要继续启动 Jarvis?", default=False):
|
|
915
|
+
PrettyOutput.print("已取消启动以避免终端嵌套。", OutputType.INFO)
|
|
916
|
+
sys.exit(0)
|
|
917
|
+
except Exception:
|
|
918
|
+
pass
|
|
919
|
+
|
|
86
920
|
# 1. 设置信号处理
|
|
87
921
|
_setup_signal_handler()
|
|
88
922
|
|
|
89
923
|
# 2. 统计命令使用
|
|
90
924
|
count_cmd_usage()
|
|
91
925
|
|
|
92
|
-
# 3.
|
|
93
|
-
if welcome_str:
|
|
94
|
-
_show_welcome_message(welcome_str)
|
|
95
|
-
|
|
96
|
-
# 4. 设置配置文件
|
|
926
|
+
# 3. 设置配置文件
|
|
97
927
|
global g_config_file
|
|
98
928
|
g_config_file = config_file
|
|
99
929
|
load_config()
|
|
100
930
|
|
|
101
|
-
#
|
|
102
|
-
if
|
|
931
|
+
# 4. 显示历史统计数据(仅在显示欢迎信息时显示)
|
|
932
|
+
if welcome_str:
|
|
933
|
+
_show_usage_stats(welcome_str)
|
|
934
|
+
|
|
935
|
+
# 5. 检查Jarvis更新
|
|
936
|
+
if _check_jarvis_updates():
|
|
103
937
|
os.execv(sys.executable, [sys.executable] + sys.argv)
|
|
104
938
|
sys.exit(0)
|
|
105
939
|
|
|
106
940
|
|
|
941
|
+
def _interactive_config_setup(config_file_path: Path):
|
|
942
|
+
"""交互式配置引导"""
|
|
943
|
+
from jarvis.jarvis_platform.registry import PlatformRegistry
|
|
944
|
+
from jarvis.jarvis_utils.input import (
|
|
945
|
+
get_choice,
|
|
946
|
+
get_single_line_input as get_input,
|
|
947
|
+
user_confirm as get_yes_no,
|
|
948
|
+
)
|
|
949
|
+
|
|
950
|
+
PrettyOutput.print(
|
|
951
|
+
"欢迎使用 Jarvis!未找到配置文件,现在开始引导配置。", OutputType.INFO
|
|
952
|
+
)
|
|
953
|
+
|
|
954
|
+
# 1. 选择平台
|
|
955
|
+
registry = PlatformRegistry.get_global_platform_registry()
|
|
956
|
+
platforms = registry.get_available_platforms()
|
|
957
|
+
platform_name = get_choice("请选择您要使用的AI平台", platforms)
|
|
958
|
+
|
|
959
|
+
# 2. 配置环境变量
|
|
960
|
+
platform_class = registry.platforms.get(platform_name)
|
|
961
|
+
if not platform_class:
|
|
962
|
+
PrettyOutput.print(f"平台 '{platform_name}' 加载失败。", OutputType.ERROR)
|
|
963
|
+
sys.exit(1)
|
|
964
|
+
|
|
965
|
+
env_vars = {}
|
|
966
|
+
required_keys = platform_class.get_required_env_keys()
|
|
967
|
+
defaults = platform_class.get_env_defaults()
|
|
968
|
+
config_guide = platform_class.get_env_config_guide()
|
|
969
|
+
if required_keys:
|
|
970
|
+
PrettyOutput.print(
|
|
971
|
+
f"请输入 {platform_name} 平台所需的配置信息:", OutputType.INFO
|
|
972
|
+
)
|
|
973
|
+
|
|
974
|
+
# 如果有配置指导,先显示总体说明
|
|
975
|
+
if config_guide:
|
|
976
|
+
# 为避免 PrettyOutput 在循环中为每行加框,先拼接后统一打印
|
|
977
|
+
guide_lines = ["", "配置获取方法:"]
|
|
978
|
+
for key in required_keys:
|
|
979
|
+
if key in config_guide and config_guide[key]:
|
|
980
|
+
guide_lines.append("")
|
|
981
|
+
guide_lines.append(f"{key} 获取方法:")
|
|
982
|
+
guide_lines.append(str(config_guide[key]))
|
|
983
|
+
PrettyOutput.print("\n".join(guide_lines), OutputType.INFO)
|
|
984
|
+
else:
|
|
985
|
+
# 若无指导,仍需遍历以保持后续逻辑一致
|
|
986
|
+
pass
|
|
987
|
+
|
|
988
|
+
for key in required_keys:
|
|
989
|
+
# 显示该环境变量的配置指导(上文已统一打印,此处不再逐条打印)
|
|
990
|
+
|
|
991
|
+
default_value = defaults.get(key, "")
|
|
992
|
+
prompt_text = f" - {key}"
|
|
993
|
+
if default_value:
|
|
994
|
+
prompt_text += f" (默认: {default_value})"
|
|
995
|
+
prompt_text += ": "
|
|
996
|
+
|
|
997
|
+
value = get_input(prompt_text, default=default_value)
|
|
998
|
+
env_vars[key] = value
|
|
999
|
+
os.environ[key] = value # 立即设置环境变量以便后续测试
|
|
1000
|
+
|
|
1001
|
+
# 3. 选择模型
|
|
1002
|
+
try:
|
|
1003
|
+
platform_instance = registry.create_platform(platform_name)
|
|
1004
|
+
if not platform_instance:
|
|
1005
|
+
PrettyOutput.print(f"无法创建平台 '{platform_name}'。", OutputType.ERROR)
|
|
1006
|
+
sys.exit(1)
|
|
1007
|
+
|
|
1008
|
+
model_list_tuples = platform_instance.get_model_list()
|
|
1009
|
+
model_choices = [f"{name} ({desc})" for name, desc in model_list_tuples]
|
|
1010
|
+
model_display_name = get_choice("请选择要使用的模型", model_choices)
|
|
1011
|
+
|
|
1012
|
+
# 从显示名称反向查找模型ID
|
|
1013
|
+
selected_index = model_choices.index(model_display_name)
|
|
1014
|
+
model_name, _ = model_list_tuples[selected_index]
|
|
1015
|
+
|
|
1016
|
+
except Exception:
|
|
1017
|
+
PrettyOutput.print("获取模型列表失败", OutputType.ERROR)
|
|
1018
|
+
if not get_yes_no("无法获取模型列表,是否继续配置?"):
|
|
1019
|
+
sys.exit(1)
|
|
1020
|
+
model_name = get_input("请输入模型名称:")
|
|
1021
|
+
|
|
1022
|
+
# 4. 测试配置
|
|
1023
|
+
PrettyOutput.print("正在测试配置...", OutputType.INFO)
|
|
1024
|
+
test_passed = False
|
|
1025
|
+
try:
|
|
1026
|
+
platform_instance = registry.create_platform(platform_name)
|
|
1027
|
+
if platform_instance:
|
|
1028
|
+
platform_instance.set_model_name(model_name)
|
|
1029
|
+
response_generator = platform_instance.chat("hello")
|
|
1030
|
+
response = "".join(response_generator)
|
|
1031
|
+
if response:
|
|
1032
|
+
PrettyOutput.print(
|
|
1033
|
+
f"测试成功,模型响应: {response}", OutputType.SUCCESS
|
|
1034
|
+
)
|
|
1035
|
+
test_passed = True
|
|
1036
|
+
else:
|
|
1037
|
+
PrettyOutput.print("测试失败,模型没有响应。", OutputType.ERROR)
|
|
1038
|
+
else:
|
|
1039
|
+
PrettyOutput.print("测试失败,无法创建平台实例。", OutputType.ERROR)
|
|
1040
|
+
except Exception:
|
|
1041
|
+
PrettyOutput.print("测试失败", OutputType.ERROR)
|
|
1042
|
+
|
|
1043
|
+
# 5. 交互式确认并应用配置(不直接生成配置文件)
|
|
1044
|
+
config_data = {
|
|
1045
|
+
"ENV": env_vars,
|
|
1046
|
+
"JARVIS_PLATFORM": platform_name,
|
|
1047
|
+
"JARVIS_MODEL": model_name,
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
if not test_passed:
|
|
1051
|
+
if not get_yes_no("配置测试失败,是否仍要应用该配置并继续?", default=False):
|
|
1052
|
+
PrettyOutput.print("已取消配置。", OutputType.INFO)
|
|
1053
|
+
sys.exit(0)
|
|
1054
|
+
|
|
1055
|
+
# 6. 选择其他功能开关与可选项(复用统一逻辑)
|
|
1056
|
+
_collect_optional_config_interactively(config_data)
|
|
1057
|
+
|
|
1058
|
+
# 7. 应用到当前会话并写入配置文件(基于交互结果,不从默认值生成)
|
|
1059
|
+
set_global_env_data(config_data)
|
|
1060
|
+
_process_env_variables(config_data)
|
|
1061
|
+
try:
|
|
1062
|
+
schema_path = (
|
|
1063
|
+
Path(__file__).parent.parent / "jarvis_data" / "config_schema.json"
|
|
1064
|
+
)
|
|
1065
|
+
config_file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1066
|
+
header = ""
|
|
1067
|
+
if schema_path.exists():
|
|
1068
|
+
header = f"# yaml-language-server: $schema={str(schema_path.absolute())}\n"
|
|
1069
|
+
_prune_defaults_with_schema(config_data)
|
|
1070
|
+
yaml_str = yaml.dump(config_data, allow_unicode=True, sort_keys=False)
|
|
1071
|
+
with open(config_file_path, "w", encoding="utf-8") as f:
|
|
1072
|
+
if header:
|
|
1073
|
+
f.write(header)
|
|
1074
|
+
f.write(yaml_str)
|
|
1075
|
+
PrettyOutput.print(f"配置文件已生成: {config_file_path}", OutputType.SUCCESS)
|
|
1076
|
+
PrettyOutput.print("配置完成,请重新启动Jarvis。", OutputType.INFO)
|
|
1077
|
+
sys.exit(0)
|
|
1078
|
+
except Exception:
|
|
1079
|
+
PrettyOutput.print("写入配置文件失败", OutputType.ERROR)
|
|
1080
|
+
sys.exit(1)
|
|
1081
|
+
|
|
1082
|
+
|
|
107
1083
|
def load_config():
|
|
108
1084
|
config_file = g_config_file
|
|
109
1085
|
config_file_path = (
|
|
@@ -118,24 +1094,11 @@ def load_config():
|
|
|
118
1094
|
if old_config_file.exists(): # 旧的配置文件存在
|
|
119
1095
|
_read_old_config_file(old_config_file)
|
|
120
1096
|
else:
|
|
121
|
-
|
|
122
|
-
schema_path = (
|
|
123
|
-
Path(__file__).parent.parent / "jarvis_data" / "config_schema.json"
|
|
124
|
-
)
|
|
125
|
-
if schema_path.exists():
|
|
126
|
-
try:
|
|
127
|
-
config_file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
128
|
-
generate_default_config(str(schema_path), str(config_file_path))
|
|
129
|
-
PrettyOutput.print(
|
|
130
|
-
f"已生成默认配置文件: {config_file_path}", OutputType.INFO
|
|
131
|
-
)
|
|
132
|
-
except Exception as e:
|
|
133
|
-
PrettyOutput.print(f"生成默认配置文件失败: {e}", OutputType.ERROR)
|
|
1097
|
+
_interactive_config_setup(config_file_path)
|
|
134
1098
|
else:
|
|
135
1099
|
_load_and_process_config(str(config_file_path.parent), str(config_file_path))
|
|
136
1100
|
|
|
137
1101
|
|
|
138
|
-
from typing import Tuple
|
|
139
1102
|
|
|
140
1103
|
|
|
141
1104
|
def _load_config_file(config_file: str) -> Tuple[str, dict]:
|
|
@@ -191,6 +1154,563 @@ def _process_env_variables(config_data: dict) -> None:
|
|
|
191
1154
|
)
|
|
192
1155
|
|
|
193
1156
|
|
|
1157
|
+
def _ask_config_bool(config_data: dict, ask_all: bool, _key: str, _tip: str, _default: bool) -> bool:
|
|
1158
|
+
"""询问并设置布尔类型配置项"""
|
|
1159
|
+
try:
|
|
1160
|
+
if not ask_all and _key in config_data:
|
|
1161
|
+
return False
|
|
1162
|
+
from jarvis.jarvis_utils.input import user_confirm as get_yes_no
|
|
1163
|
+
cur = bool(config_data.get(_key, _default))
|
|
1164
|
+
val = get_yes_no(_tip, default=cur)
|
|
1165
|
+
if bool(val) == cur:
|
|
1166
|
+
return False
|
|
1167
|
+
config_data[_key] = bool(val)
|
|
1168
|
+
return True
|
|
1169
|
+
except Exception:
|
|
1170
|
+
return False
|
|
1171
|
+
|
|
1172
|
+
|
|
1173
|
+
def _ask_config_str(config_data: dict, ask_all: bool, _key: str, _tip: str, _default: str = "") -> bool:
|
|
1174
|
+
"""询问并设置字符串类型配置项"""
|
|
1175
|
+
try:
|
|
1176
|
+
if not ask_all and _key in config_data:
|
|
1177
|
+
return False
|
|
1178
|
+
from jarvis.jarvis_utils.input import get_single_line_input
|
|
1179
|
+
cur = str(config_data.get(_key, _default or ""))
|
|
1180
|
+
val = get_single_line_input(f"{_tip}", default=cur)
|
|
1181
|
+
v = ("" if val is None else str(val)).strip()
|
|
1182
|
+
if v == cur:
|
|
1183
|
+
return False
|
|
1184
|
+
config_data[_key] = v
|
|
1185
|
+
return True
|
|
1186
|
+
except Exception:
|
|
1187
|
+
return False
|
|
1188
|
+
|
|
1189
|
+
|
|
1190
|
+
def _ask_config_optional_str(config_data: dict, ask_all: bool, _key: str, _tip: str, _default: str = "") -> bool:
|
|
1191
|
+
"""询问并设置可选字符串类型配置项(空输入表示不改变)"""
|
|
1192
|
+
try:
|
|
1193
|
+
if not ask_all and _key in config_data:
|
|
1194
|
+
return False
|
|
1195
|
+
from jarvis.jarvis_utils.input import get_single_line_input
|
|
1196
|
+
cur = str(config_data.get(_key, _default or ""))
|
|
1197
|
+
val = get_single_line_input(f"{_tip}", default=cur)
|
|
1198
|
+
if val is None:
|
|
1199
|
+
return False
|
|
1200
|
+
s = str(val).strip()
|
|
1201
|
+
if s == "" or s == cur:
|
|
1202
|
+
return False
|
|
1203
|
+
config_data[_key] = s
|
|
1204
|
+
return True
|
|
1205
|
+
except Exception:
|
|
1206
|
+
return False
|
|
1207
|
+
|
|
1208
|
+
|
|
1209
|
+
def _ask_config_int(config_data: dict, ask_all: bool, _key: str, _tip: str, _default: int) -> bool:
|
|
1210
|
+
"""询问并设置整数类型配置项"""
|
|
1211
|
+
try:
|
|
1212
|
+
if not ask_all and _key in config_data:
|
|
1213
|
+
return False
|
|
1214
|
+
from jarvis.jarvis_utils.input import get_single_line_input
|
|
1215
|
+
cur = str(config_data.get(_key, _default))
|
|
1216
|
+
val_str = get_single_line_input(f"{_tip}", default=cur)
|
|
1217
|
+
s = "" if val_str is None else str(val_str).strip()
|
|
1218
|
+
if s == "" or s == cur:
|
|
1219
|
+
return False
|
|
1220
|
+
try:
|
|
1221
|
+
v = int(s)
|
|
1222
|
+
except Exception:
|
|
1223
|
+
return False
|
|
1224
|
+
if str(v) == cur:
|
|
1225
|
+
return False
|
|
1226
|
+
config_data[_key] = v
|
|
1227
|
+
return True
|
|
1228
|
+
except Exception:
|
|
1229
|
+
return False
|
|
1230
|
+
|
|
1231
|
+
|
|
1232
|
+
def _ask_config_list(config_data: dict, ask_all: bool, _key: str, _tip: str) -> bool:
|
|
1233
|
+
"""询问并设置列表类型配置项(逗号分隔)"""
|
|
1234
|
+
try:
|
|
1235
|
+
if not ask_all and _key in config_data:
|
|
1236
|
+
return False
|
|
1237
|
+
from jarvis.jarvis_utils.input import get_single_line_input
|
|
1238
|
+
cur_val = config_data.get(_key, [])
|
|
1239
|
+
if isinstance(cur_val, list):
|
|
1240
|
+
cur_display = ", ".join([str(x) for x in cur_val])
|
|
1241
|
+
else:
|
|
1242
|
+
cur_display = str(cur_val or "")
|
|
1243
|
+
val = get_single_line_input(f"{_tip}", default=cur_display)
|
|
1244
|
+
if val is None:
|
|
1245
|
+
return False
|
|
1246
|
+
s = str(val).strip()
|
|
1247
|
+
if s == cur_display.strip():
|
|
1248
|
+
return False
|
|
1249
|
+
if not s:
|
|
1250
|
+
return False
|
|
1251
|
+
items = [x.strip() for x in s.split(",") if x.strip()]
|
|
1252
|
+
if isinstance(cur_val, list) and items == cur_val:
|
|
1253
|
+
return False
|
|
1254
|
+
config_data[_key] = items
|
|
1255
|
+
return True
|
|
1256
|
+
except Exception:
|
|
1257
|
+
return False
|
|
1258
|
+
|
|
1259
|
+
|
|
1260
|
+
def _collect_basic_switches(config_data: dict, ask_all: bool) -> bool:
|
|
1261
|
+
"""收集基础开关配置"""
|
|
1262
|
+
changed = False
|
|
1263
|
+
changed = _ask_config_bool(
|
|
1264
|
+
config_data, ask_all,
|
|
1265
|
+
"JARVIS_ENABLE_GIT_JCA_SWITCH",
|
|
1266
|
+
"是否在检测到Git仓库时,提示并可自动切换到代码开发模式(jca)?",
|
|
1267
|
+
False,
|
|
1268
|
+
) or changed
|
|
1269
|
+
changed = _ask_config_bool(
|
|
1270
|
+
config_data, ask_all,
|
|
1271
|
+
"JARVIS_ENABLE_STARTUP_CONFIG_SELECTOR",
|
|
1272
|
+
"在进入默认通用代理前,是否先列出可用配置(agent/multi_agent/roles)供选择?",
|
|
1273
|
+
False,
|
|
1274
|
+
) or changed
|
|
1275
|
+
return changed
|
|
1276
|
+
|
|
1277
|
+
|
|
1278
|
+
def _collect_ui_experience_config(config_data: dict, ask_all: bool) -> bool:
|
|
1279
|
+
"""收集UI体验相关配置"""
|
|
1280
|
+
changed = False
|
|
1281
|
+
try:
|
|
1282
|
+
import platform as _platform_mod
|
|
1283
|
+
_default_pretty = False if _platform_mod.system() == "Windows" else True
|
|
1284
|
+
except Exception:
|
|
1285
|
+
_default_pretty = True
|
|
1286
|
+
|
|
1287
|
+
changed = _ask_config_bool(
|
|
1288
|
+
config_data, ask_all,
|
|
1289
|
+
"JARVIS_PRETTY_OUTPUT",
|
|
1290
|
+
"是否启用更美观的终端输出(Pretty Output)?",
|
|
1291
|
+
_default_pretty,
|
|
1292
|
+
) or changed
|
|
1293
|
+
changed = _ask_config_bool(
|
|
1294
|
+
config_data, ask_all,
|
|
1295
|
+
"JARVIS_PRINT_PROMPT",
|
|
1296
|
+
"是否打印发送给模型的提示词(Prompt)?",
|
|
1297
|
+
False,
|
|
1298
|
+
) or changed
|
|
1299
|
+
changed = _ask_config_bool(
|
|
1300
|
+
config_data, ask_all,
|
|
1301
|
+
"JARVIS_IMMEDIATE_ABORT",
|
|
1302
|
+
"是否启用立即中断?\n- 选择 是/true:在对话输出流的每次迭代中检测到用户中断(例如 Ctrl+C)时,立即返回当前已生成的内容并停止继续输出。\n- 选择 否/false:不会在输出过程中立刻返回,而是按既有流程处理(不中途打断输出)。",
|
|
1303
|
+
False,
|
|
1304
|
+
) or changed
|
|
1305
|
+
return changed
|
|
1306
|
+
|
|
1307
|
+
|
|
1308
|
+
def _collect_analysis_config(config_data: dict, ask_all: bool) -> bool:
|
|
1309
|
+
"""收集代码分析相关配置"""
|
|
1310
|
+
changed = False
|
|
1311
|
+
changed = _ask_config_bool(
|
|
1312
|
+
config_data, ask_all,
|
|
1313
|
+
"JARVIS_ENABLE_STATIC_ANALYSIS",
|
|
1314
|
+
"是否启用静态代码分析(Static Analysis)?",
|
|
1315
|
+
True,
|
|
1316
|
+
) or changed
|
|
1317
|
+
changed = _ask_config_bool(
|
|
1318
|
+
config_data, ask_all,
|
|
1319
|
+
"JARVIS_ENABLE_BUILD_VALIDATION",
|
|
1320
|
+
"是否启用构建验证(Build Validation)?在代码编辑后自动验证代码能否成功编译/构建。",
|
|
1321
|
+
True,
|
|
1322
|
+
) or changed
|
|
1323
|
+
changed = _ask_config_int(
|
|
1324
|
+
config_data, ask_all,
|
|
1325
|
+
"JARVIS_BUILD_VALIDATION_TIMEOUT",
|
|
1326
|
+
"构建验证的超时时间(秒,默认30秒)",
|
|
1327
|
+
30,
|
|
1328
|
+
) or changed
|
|
1329
|
+
changed = _ask_config_bool(
|
|
1330
|
+
config_data, ask_all,
|
|
1331
|
+
"JARVIS_ENABLE_IMPACT_ANALYSIS",
|
|
1332
|
+
"是否启用编辑影响范围分析(Impact Analysis)?分析代码编辑的影响范围,识别可能受影响的文件、函数、测试等。",
|
|
1333
|
+
True,
|
|
1334
|
+
) or changed
|
|
1335
|
+
return changed
|
|
1336
|
+
|
|
1337
|
+
|
|
1338
|
+
def _collect_agent_features_config(config_data: dict, ask_all: bool) -> bool:
|
|
1339
|
+
"""收集Agent功能相关配置"""
|
|
1340
|
+
changed = False
|
|
1341
|
+
changed = _ask_config_bool(
|
|
1342
|
+
config_data, ask_all,
|
|
1343
|
+
"JARVIS_USE_METHODOLOGY",
|
|
1344
|
+
"是否启用方法论系统(Methodology)?",
|
|
1345
|
+
True,
|
|
1346
|
+
) or changed
|
|
1347
|
+
changed = _ask_config_bool(
|
|
1348
|
+
config_data, ask_all,
|
|
1349
|
+
"JARVIS_USE_ANALYSIS",
|
|
1350
|
+
"是否启用分析流程(Analysis)?",
|
|
1351
|
+
True,
|
|
1352
|
+
) or changed
|
|
1353
|
+
changed = _ask_config_bool(
|
|
1354
|
+
config_data, ask_all,
|
|
1355
|
+
"JARVIS_FORCE_SAVE_MEMORY",
|
|
1356
|
+
"是否强制保存会话记忆?",
|
|
1357
|
+
False,
|
|
1358
|
+
) or changed
|
|
1359
|
+
return changed
|
|
1360
|
+
|
|
1361
|
+
|
|
1362
|
+
def _collect_session_config(config_data: dict, ask_all: bool) -> bool:
|
|
1363
|
+
"""收集会话与调试相关配置"""
|
|
1364
|
+
changed = False
|
|
1365
|
+
changed = _ask_config_bool(
|
|
1366
|
+
config_data, ask_all,
|
|
1367
|
+
"JARVIS_SAVE_SESSION_HISTORY",
|
|
1368
|
+
"是否保存会话记录?",
|
|
1369
|
+
False,
|
|
1370
|
+
) or changed
|
|
1371
|
+
changed = _ask_config_bool(
|
|
1372
|
+
config_data, ask_all,
|
|
1373
|
+
"JARVIS_PRINT_ERROR_TRACEBACK",
|
|
1374
|
+
"是否在错误输出时打印回溯调用链?",
|
|
1375
|
+
False,
|
|
1376
|
+
) or changed
|
|
1377
|
+
changed = _ask_config_bool(
|
|
1378
|
+
config_data, ask_all,
|
|
1379
|
+
"JARVIS_SKIP_PREDEFINED_TASKS",
|
|
1380
|
+
"是否跳过预定义任务加载(不读取 pre-command 列表)?",
|
|
1381
|
+
False,
|
|
1382
|
+
) or changed
|
|
1383
|
+
return changed
|
|
1384
|
+
|
|
1385
|
+
|
|
1386
|
+
def _collect_safety_config(config_data: dict, ask_all: bool) -> bool:
|
|
1387
|
+
"""收集代码与工具操作安全提示配置"""
|
|
1388
|
+
changed = False
|
|
1389
|
+
changed = _ask_config_bool(
|
|
1390
|
+
config_data, ask_all,
|
|
1391
|
+
"JARVIS_EXECUTE_TOOL_CONFIRM",
|
|
1392
|
+
"执行工具前是否需要确认?",
|
|
1393
|
+
False,
|
|
1394
|
+
) or changed
|
|
1395
|
+
changed = _ask_config_bool(
|
|
1396
|
+
config_data, ask_all,
|
|
1397
|
+
"JARVIS_CONFIRM_BEFORE_APPLY_PATCH",
|
|
1398
|
+
"应用补丁前是否需要确认?",
|
|
1399
|
+
False,
|
|
1400
|
+
) or changed
|
|
1401
|
+
return changed
|
|
1402
|
+
|
|
1403
|
+
|
|
1404
|
+
def _collect_data_and_token_config(config_data: dict, ask_all: bool) -> bool:
|
|
1405
|
+
"""收集数据目录与最大输入Token配置"""
|
|
1406
|
+
changed = False
|
|
1407
|
+
from jarvis.jarvis_utils.config import get_data_dir as _get_data_dir
|
|
1408
|
+
changed = _ask_config_optional_str(
|
|
1409
|
+
config_data, ask_all,
|
|
1410
|
+
"JARVIS_DATA_PATH",
|
|
1411
|
+
f"是否自定义数据目录路径(JARVIS_DATA_PATH)?留空使用默认: {_get_data_dir()}",
|
|
1412
|
+
) or changed
|
|
1413
|
+
changed = _ask_config_int(
|
|
1414
|
+
config_data, ask_all,
|
|
1415
|
+
"JARVIS_MAX_INPUT_TOKEN_COUNT",
|
|
1416
|
+
"自定义最大输入Token数量(留空使用默认: 32000)",
|
|
1417
|
+
32000,
|
|
1418
|
+
) or changed
|
|
1419
|
+
changed = _ask_config_int(
|
|
1420
|
+
config_data, ask_all,
|
|
1421
|
+
"JARVIS_TOOL_FILTER_THRESHOLD",
|
|
1422
|
+
"设置AI工具筛选阈值 (当可用工具数超过此值时触发AI筛选, 默认30)",
|
|
1423
|
+
30,
|
|
1424
|
+
) or changed
|
|
1425
|
+
return changed
|
|
1426
|
+
|
|
1427
|
+
|
|
1428
|
+
def _collect_planning_config(config_data: dict, ask_all: bool) -> bool:
|
|
1429
|
+
"""收集规划相关配置"""
|
|
1430
|
+
changed = False
|
|
1431
|
+
changed = _ask_config_bool(
|
|
1432
|
+
config_data, ask_all,
|
|
1433
|
+
"JARVIS_PLAN_ENABLED",
|
|
1434
|
+
"是否默认启用任务规划?当 Agent 初始化时 plan 参数未指定,将从此配置加载",
|
|
1435
|
+
True,
|
|
1436
|
+
) or changed
|
|
1437
|
+
changed = _ask_config_int(
|
|
1438
|
+
config_data, ask_all,
|
|
1439
|
+
"JARVIS_PLAN_MAX_DEPTH",
|
|
1440
|
+
"任务规划的最大层数(限制递归拆分深度,默认2;仅在启用规划时生效)",
|
|
1441
|
+
2,
|
|
1442
|
+
) or changed
|
|
1443
|
+
return changed
|
|
1444
|
+
|
|
1445
|
+
|
|
1446
|
+
def _collect_advanced_config(config_data: dict, ask_all: bool) -> bool:
|
|
1447
|
+
"""收集高级配置(自动总结、脚本超时等)"""
|
|
1448
|
+
changed = False
|
|
1449
|
+
changed = _ask_config_int(
|
|
1450
|
+
config_data, ask_all,
|
|
1451
|
+
"JARVIS_AUTO_SUMMARY_ROUNDS",
|
|
1452
|
+
"基于对话轮次的自动总结阈值(达到该轮次后自动总结并清理历史,默认50)",
|
|
1453
|
+
50,
|
|
1454
|
+
) or changed
|
|
1455
|
+
changed = _ask_config_int(
|
|
1456
|
+
config_data, ask_all,
|
|
1457
|
+
"JARVIS_SCRIPT_EXECUTION_TIMEOUT",
|
|
1458
|
+
"脚本执行超时时间(秒,默认300,仅非交互模式生效)",
|
|
1459
|
+
300,
|
|
1460
|
+
) or changed
|
|
1461
|
+
changed = _ask_config_int(
|
|
1462
|
+
config_data, ask_all,
|
|
1463
|
+
"JARVIS_ADDON_PROMPT_THRESHOLD",
|
|
1464
|
+
"附加提示的触发阈值(字符数,默认1024)。当消息长度超过此值时,会自动添加默认的附加提示",
|
|
1465
|
+
1024,
|
|
1466
|
+
) or changed
|
|
1467
|
+
changed = _ask_config_bool(
|
|
1468
|
+
config_data, ask_all,
|
|
1469
|
+
"JARVIS_ENABLE_INTENT_RECOGNITION",
|
|
1470
|
+
"是否启用意图识别功能?用于智能上下文推荐中的LLM意图提取和语义分析",
|
|
1471
|
+
True,
|
|
1472
|
+
) or changed
|
|
1473
|
+
return changed
|
|
1474
|
+
|
|
1475
|
+
|
|
1476
|
+
def _collect_directory_config(config_data: dict, ask_all: bool) -> bool:
|
|
1477
|
+
"""收集目录类配置(逗号分隔)"""
|
|
1478
|
+
changed = False
|
|
1479
|
+
changed = _ask_config_list(
|
|
1480
|
+
config_data, ask_all,
|
|
1481
|
+
"JARVIS_TOOL_LOAD_DIRS",
|
|
1482
|
+
"指定工具加载目录(逗号分隔,留空跳过):",
|
|
1483
|
+
) or changed
|
|
1484
|
+
changed = _ask_config_list(
|
|
1485
|
+
config_data, ask_all,
|
|
1486
|
+
"JARVIS_METHODOLOGY_DIRS",
|
|
1487
|
+
"指定方法论加载目录(逗号分隔,留空跳过):",
|
|
1488
|
+
) or changed
|
|
1489
|
+
changed = _ask_config_list(
|
|
1490
|
+
config_data, ask_all,
|
|
1491
|
+
"JARVIS_AGENT_DEFINITION_DIRS",
|
|
1492
|
+
"指定 agent 定义加载目录(逗号分隔,留空跳过):",
|
|
1493
|
+
) or changed
|
|
1494
|
+
changed = _ask_config_list(
|
|
1495
|
+
config_data, ask_all,
|
|
1496
|
+
"JARVIS_MULTI_AGENT_DIRS",
|
|
1497
|
+
"指定 multi_agent 加载目录(逗号分隔,留空跳过):",
|
|
1498
|
+
) or changed
|
|
1499
|
+
changed = _ask_config_list(
|
|
1500
|
+
config_data, ask_all,
|
|
1501
|
+
"JARVIS_ROLES_DIRS",
|
|
1502
|
+
"指定 roles 加载目录(逗号分隔,留空跳过):",
|
|
1503
|
+
) or changed
|
|
1504
|
+
changed = _ask_config_list(
|
|
1505
|
+
config_data, ask_all,
|
|
1506
|
+
"JARVIS_AFTER_TOOL_CALL_CB_DIRS",
|
|
1507
|
+
"指定工具调用后回调实现目录(逗号分隔,留空跳过):",
|
|
1508
|
+
) or changed
|
|
1509
|
+
return changed
|
|
1510
|
+
|
|
1511
|
+
|
|
1512
|
+
def _collect_web_search_config(config_data: dict, ask_all: bool) -> bool:
|
|
1513
|
+
"""收集Web搜索配置"""
|
|
1514
|
+
changed = False
|
|
1515
|
+
changed = _ask_config_optional_str(
|
|
1516
|
+
config_data, ask_all,
|
|
1517
|
+
"JARVIS_WEB_SEARCH_PLATFORM",
|
|
1518
|
+
"配置 Web 搜索平台名称(留空跳过):",
|
|
1519
|
+
) or changed
|
|
1520
|
+
changed = _ask_config_optional_str(
|
|
1521
|
+
config_data, ask_all,
|
|
1522
|
+
"JARVIS_WEB_SEARCH_MODEL",
|
|
1523
|
+
"配置 Web 搜索模型名称(留空跳过):",
|
|
1524
|
+
) or changed
|
|
1525
|
+
return changed
|
|
1526
|
+
|
|
1527
|
+
|
|
1528
|
+
def _ask_git_check_mode(config_data: dict, ask_all: bool) -> bool:
|
|
1529
|
+
"""询问Git校验模式"""
|
|
1530
|
+
try:
|
|
1531
|
+
_key = "JARVIS_GIT_CHECK_MODE"
|
|
1532
|
+
if not ask_all and _key in config_data:
|
|
1533
|
+
return False
|
|
1534
|
+
from jarvis.jarvis_utils.input import get_choice
|
|
1535
|
+
from jarvis.jarvis_utils.config import get_git_check_mode
|
|
1536
|
+
current_mode = config_data.get(_key, get_git_check_mode())
|
|
1537
|
+
choices = ["strict", "warn"]
|
|
1538
|
+
tip = (
|
|
1539
|
+
"请选择 Git 仓库检查模式 (JARVIS_GIT_CHECK_MODE):\n"
|
|
1540
|
+
"此设置决定了当在 Git 仓库中检测到未提交的更改时,Jarvis应如何处理。\n"
|
|
1541
|
+
"这对于确保代码修改和提交操作在干净的工作区上进行至关重要。\n"
|
|
1542
|
+
" - strict: (推荐) 如果存在未提交的更改,则中断相关操作(如代码修改、自动提交)。\n"
|
|
1543
|
+
" 这可以防止意外覆盖或丢失本地工作。\n"
|
|
1544
|
+
" - warn: 如果存在未提交的更改,仅显示警告信息,然后继续执行操作。\n"
|
|
1545
|
+
" 适用于您希望绕过检查并自行管理仓库状态的场景。"
|
|
1546
|
+
)
|
|
1547
|
+
new_mode = get_choice(tip, choices)
|
|
1548
|
+
if new_mode == current_mode:
|
|
1549
|
+
return False
|
|
1550
|
+
config_data[_key] = new_mode
|
|
1551
|
+
return True
|
|
1552
|
+
except Exception:
|
|
1553
|
+
return False
|
|
1554
|
+
|
|
1555
|
+
|
|
1556
|
+
def _ask_patch_format_mode(config_data: dict, ask_all: bool) -> bool:
|
|
1557
|
+
"""询问补丁格式模式"""
|
|
1558
|
+
try:
|
|
1559
|
+
_key = "JARVIS_PATCH_FORMAT"
|
|
1560
|
+
if not ask_all and _key in config_data:
|
|
1561
|
+
return False
|
|
1562
|
+
from jarvis.jarvis_utils.input import get_choice
|
|
1563
|
+
from jarvis.jarvis_utils.config import get_patch_format
|
|
1564
|
+
current_mode = config_data.get(_key, get_patch_format())
|
|
1565
|
+
choices = ["all", "search", "search_range"]
|
|
1566
|
+
tip = (
|
|
1567
|
+
"请选择补丁格式处理模式 (JARVIS_PATCH_FORMAT):\n"
|
|
1568
|
+
"该设置影响 edit_file_handler 在处理补丁时允许的匹配方式。\n"
|
|
1569
|
+
" - all: 同时支持 SEARCH 与 SEARCH_START/SEARCH_END 两种模式(默认)。\n"
|
|
1570
|
+
" - search: 仅允许精确片段匹配(SEARCH)。更稳定,适合较弱模型或严格控制改动。\n"
|
|
1571
|
+
" - search_range: 仅允许范围匹配(SEARCH_START/SEARCH_END)。更灵活,适合较强模型和块内细粒度修改。"
|
|
1572
|
+
)
|
|
1573
|
+
new_mode = get_choice(tip, choices)
|
|
1574
|
+
if new_mode == current_mode:
|
|
1575
|
+
return False
|
|
1576
|
+
config_data[_key] = new_mode
|
|
1577
|
+
return True
|
|
1578
|
+
except Exception:
|
|
1579
|
+
return False
|
|
1580
|
+
|
|
1581
|
+
|
|
1582
|
+
def _collect_git_config(config_data: dict, ask_all: bool) -> bool:
|
|
1583
|
+
"""收集Git相关配置"""
|
|
1584
|
+
changed = False
|
|
1585
|
+
changed = _ask_git_check_mode(config_data, ask_all) or changed
|
|
1586
|
+
changed = _ask_patch_format_mode(config_data, ask_all) or changed
|
|
1587
|
+
changed = _ask_config_optional_str(
|
|
1588
|
+
config_data, ask_all,
|
|
1589
|
+
"JARVIS_GIT_COMMIT_PROMPT",
|
|
1590
|
+
"自定义 Git 提交提示模板(留空跳过):",
|
|
1591
|
+
) or changed
|
|
1592
|
+
return changed
|
|
1593
|
+
|
|
1594
|
+
|
|
1595
|
+
def _collect_rag_config(config_data: dict, ask_all: bool) -> bool:
|
|
1596
|
+
"""收集RAG配置"""
|
|
1597
|
+
changed = False
|
|
1598
|
+
try:
|
|
1599
|
+
from jarvis.jarvis_utils.config import (
|
|
1600
|
+
get_rag_embedding_model as _get_rag_embedding_model,
|
|
1601
|
+
get_rag_rerank_model as _get_rag_rerank_model,
|
|
1602
|
+
)
|
|
1603
|
+
from jarvis.jarvis_utils.input import user_confirm as get_yes_no
|
|
1604
|
+
from jarvis.jarvis_utils.input import get_single_line_input
|
|
1605
|
+
|
|
1606
|
+
rag_default_embed = _get_rag_embedding_model()
|
|
1607
|
+
rag_default_rerank = _get_rag_rerank_model()
|
|
1608
|
+
except Exception:
|
|
1609
|
+
rag_default_embed = "BAAI/bge-m3"
|
|
1610
|
+
rag_default_rerank = "BAAI/bge-reranker-v2-m3"
|
|
1611
|
+
get_yes_no = None
|
|
1612
|
+
get_single_line_input = None
|
|
1613
|
+
|
|
1614
|
+
try:
|
|
1615
|
+
if "JARVIS_RAG" not in config_data and get_yes_no:
|
|
1616
|
+
if get_yes_no("是否配置 RAG 检索增强参数?", default=False):
|
|
1617
|
+
rag_conf: Dict[str, Any] = {}
|
|
1618
|
+
emb = get_single_line_input(
|
|
1619
|
+
f"RAG 嵌入模型(留空使用默认: {rag_default_embed}):",
|
|
1620
|
+
default="",
|
|
1621
|
+
).strip()
|
|
1622
|
+
rerank = get_single_line_input(
|
|
1623
|
+
f"RAG rerank 模型(留空使用默认: {rag_default_rerank}):",
|
|
1624
|
+
default="",
|
|
1625
|
+
).strip()
|
|
1626
|
+
use_bm25 = get_yes_no("RAG 是否使用 BM25?", default=True)
|
|
1627
|
+
use_rerank = get_yes_no("RAG 是否使用 rerank?", default=True)
|
|
1628
|
+
if emb:
|
|
1629
|
+
rag_conf["embedding_model"] = emb
|
|
1630
|
+
else:
|
|
1631
|
+
rag_conf["embedding_model"] = rag_default_embed
|
|
1632
|
+
if rerank:
|
|
1633
|
+
rag_conf["rerank_model"] = rerank
|
|
1634
|
+
else:
|
|
1635
|
+
rag_conf["rerank_model"] = rag_default_rerank
|
|
1636
|
+
rag_conf["use_bm25"] = bool(use_bm25)
|
|
1637
|
+
rag_conf["use_rerank"] = bool(use_rerank)
|
|
1638
|
+
config_data["JARVIS_RAG"] = rag_conf
|
|
1639
|
+
changed = True
|
|
1640
|
+
except Exception:
|
|
1641
|
+
pass
|
|
1642
|
+
return changed
|
|
1643
|
+
|
|
1644
|
+
|
|
1645
|
+
def _collect_central_repo_config(config_data: dict, ask_all: bool) -> bool:
|
|
1646
|
+
"""收集中心仓库配置"""
|
|
1647
|
+
changed = False
|
|
1648
|
+
changed = _ask_config_str(
|
|
1649
|
+
config_data, ask_all,
|
|
1650
|
+
"JARVIS_CENTRAL_METHODOLOGY_REPO",
|
|
1651
|
+
"请输入中心方法论仓库路径或Git地址(可留空跳过):",
|
|
1652
|
+
"",
|
|
1653
|
+
) or changed
|
|
1654
|
+
changed = _ask_config_str(
|
|
1655
|
+
config_data, ask_all,
|
|
1656
|
+
"JARVIS_CENTRAL_TOOL_REPO",
|
|
1657
|
+
"请输入中心工具仓库路径或Git地址(可留空跳过):",
|
|
1658
|
+
"",
|
|
1659
|
+
) or changed
|
|
1660
|
+
return changed
|
|
1661
|
+
|
|
1662
|
+
|
|
1663
|
+
def _collect_shell_config(config_data: dict, ask_all: bool) -> bool:
|
|
1664
|
+
"""收集SHELL覆盖配置"""
|
|
1665
|
+
changed = False
|
|
1666
|
+
try:
|
|
1667
|
+
import os
|
|
1668
|
+
default_shell = os.getenv("SHELL", "/bin/bash")
|
|
1669
|
+
changed = _ask_config_optional_str(
|
|
1670
|
+
config_data, ask_all,
|
|
1671
|
+
"SHELL",
|
|
1672
|
+
f"覆盖 SHELL 路径(留空使用系统默认: {default_shell}):",
|
|
1673
|
+
default_shell,
|
|
1674
|
+
) or changed
|
|
1675
|
+
except Exception:
|
|
1676
|
+
pass
|
|
1677
|
+
return changed
|
|
1678
|
+
|
|
1679
|
+
|
|
1680
|
+
def _collect_optional_config_interactively(
|
|
1681
|
+
config_data: dict, ask_all: bool = False
|
|
1682
|
+
) -> bool:
|
|
1683
|
+
"""
|
|
1684
|
+
复用的交互式配置收集逻辑:
|
|
1685
|
+
- ask_all=False(默认):仅对缺省的新功能开关/可选项逐项询问,已存在项跳过
|
|
1686
|
+
- ask_all=True:对所有项进行询问,默认值取自当前配置文件,可覆盖现有设置
|
|
1687
|
+
- 修改传入的 config_data
|
|
1688
|
+
- 包含更多来自 config.py 的可选项
|
|
1689
|
+
返回:
|
|
1690
|
+
bool: 是否有变更
|
|
1691
|
+
"""
|
|
1692
|
+
changed = False
|
|
1693
|
+
|
|
1694
|
+
# 收集各类配置
|
|
1695
|
+
changed = _collect_basic_switches(config_data, ask_all) or changed
|
|
1696
|
+
changed = _collect_ui_experience_config(config_data, ask_all) or changed
|
|
1697
|
+
changed = _collect_analysis_config(config_data, ask_all) or changed
|
|
1698
|
+
changed = _collect_agent_features_config(config_data, ask_all) or changed
|
|
1699
|
+
changed = _collect_session_config(config_data, ask_all) or changed
|
|
1700
|
+
changed = _collect_safety_config(config_data, ask_all) or changed
|
|
1701
|
+
changed = _collect_data_and_token_config(config_data, ask_all) or changed
|
|
1702
|
+
changed = _collect_planning_config(config_data, ask_all) or changed
|
|
1703
|
+
changed = _collect_advanced_config(config_data, ask_all) or changed
|
|
1704
|
+
changed = _collect_directory_config(config_data, ask_all) or changed
|
|
1705
|
+
changed = _collect_web_search_config(config_data, ask_all) or changed
|
|
1706
|
+
changed = _collect_git_config(config_data, ask_all) or changed
|
|
1707
|
+
changed = _collect_rag_config(config_data, ask_all) or changed
|
|
1708
|
+
changed = _collect_central_repo_config(config_data, ask_all) or changed
|
|
1709
|
+
changed = _collect_shell_config(config_data, ask_all) or changed
|
|
1710
|
+
|
|
1711
|
+
return changed
|
|
1712
|
+
|
|
1713
|
+
|
|
194
1714
|
def _load_and_process_config(jarvis_dir: str, config_file: str) -> None:
|
|
195
1715
|
"""加载并处理配置文件
|
|
196
1716
|
|
|
@@ -204,10 +1724,54 @@ def _load_and_process_config(jarvis_dir: str, config_file: str) -> None:
|
|
|
204
1724
|
jarvis_dir: Jarvis数据目录路径
|
|
205
1725
|
config_file: 配置文件路径
|
|
206
1726
|
"""
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
1727
|
+
from jarvis.jarvis_utils.input import user_confirm as get_yes_no
|
|
1728
|
+
|
|
1729
|
+
try:
|
|
1730
|
+
content, config_data = _load_config_file(config_file)
|
|
1731
|
+
_ensure_schema_declaration(jarvis_dir, config_file, content, config_data)
|
|
1732
|
+
set_global_env_data(config_data)
|
|
1733
|
+
_process_env_variables(config_data)
|
|
1734
|
+
|
|
1735
|
+
# 加载 schema 默认并剔除等于默认值的项
|
|
1736
|
+
pruned = _prune_defaults_with_schema(config_data)
|
|
1737
|
+
|
|
1738
|
+
if pruned:
|
|
1739
|
+
# 保留schema声明,如无则自动补充
|
|
1740
|
+
header = ""
|
|
1741
|
+
try:
|
|
1742
|
+
with open(config_file, "r", encoding="utf-8") as rf:
|
|
1743
|
+
first_line = rf.readline()
|
|
1744
|
+
if first_line.startswith("# yaml-language-server: $schema="):
|
|
1745
|
+
header = first_line
|
|
1746
|
+
except Exception:
|
|
1747
|
+
header = ""
|
|
1748
|
+
yaml_str = yaml.dump(config_data, allow_unicode=True, sort_keys=False)
|
|
1749
|
+
if not header:
|
|
1750
|
+
schema_path = Path(
|
|
1751
|
+
os.path.relpath(
|
|
1752
|
+
Path(__file__).parent.parent
|
|
1753
|
+
/ "jarvis_data"
|
|
1754
|
+
/ "config_schema.json",
|
|
1755
|
+
start=jarvis_dir,
|
|
1756
|
+
)
|
|
1757
|
+
)
|
|
1758
|
+
header = f"# yaml-language-server: $schema={schema_path}\n"
|
|
1759
|
+
with open(config_file, "w", encoding="utf-8") as wf:
|
|
1760
|
+
wf.write(header)
|
|
1761
|
+
wf.write(yaml_str)
|
|
1762
|
+
# 更新全局配置
|
|
1763
|
+
set_global_env_data(config_data)
|
|
1764
|
+
except Exception:
|
|
1765
|
+
PrettyOutput.print("加载配置文件失败", OutputType.ERROR)
|
|
1766
|
+
if get_yes_no("配置文件格式错误,是否删除并重新配置?"):
|
|
1767
|
+
try:
|
|
1768
|
+
os.remove(config_file)
|
|
1769
|
+
PrettyOutput.print(
|
|
1770
|
+
"已删除损坏的配置文件,请重启Jarvis以重新配置。", OutputType.SUCCESS
|
|
1771
|
+
)
|
|
1772
|
+
except Exception:
|
|
1773
|
+
PrettyOutput.print("删除配置文件失败", OutputType.ERROR)
|
|
1774
|
+
sys.exit(1)
|
|
211
1775
|
|
|
212
1776
|
|
|
213
1777
|
def generate_default_config(schema_path: str, output_path: str) -> None:
|
|
@@ -241,20 +1805,76 @@ def generate_default_config(schema_path: str, output_path: str) -> None:
|
|
|
241
1805
|
|
|
242
1806
|
default_config = _generate_from_schema(schema)
|
|
243
1807
|
|
|
244
|
-
#
|
|
245
|
-
rel_schema_path = Path(
|
|
246
|
-
os.path.relpath(
|
|
247
|
-
Path(schema_path),
|
|
248
|
-
start=Path(output_path).parent,
|
|
249
|
-
)
|
|
250
|
-
)
|
|
251
|
-
content = f"# yaml-language-server: $schema={rel_schema_path}\n"
|
|
1808
|
+
content = f"# yaml-language-server: $schema={schema_path}\n"
|
|
252
1809
|
content += yaml.dump(default_config, allow_unicode=True, sort_keys=False)
|
|
253
1810
|
|
|
254
1811
|
with open(output_path, "w", encoding="utf-8") as f:
|
|
255
1812
|
f.write(content)
|
|
256
1813
|
|
|
257
1814
|
|
|
1815
|
+
def _load_default_config_from_schema() -> dict:
|
|
1816
|
+
"""从 schema 生成默认配置字典,用于对比并剔除等于默认值的键"""
|
|
1817
|
+
try:
|
|
1818
|
+
schema_path = (
|
|
1819
|
+
Path(__file__).parent.parent / "jarvis_data" / "config_schema.json"
|
|
1820
|
+
)
|
|
1821
|
+
if not schema_path.exists():
|
|
1822
|
+
return {}
|
|
1823
|
+
with open(schema_path, "r", encoding="utf-8") as f:
|
|
1824
|
+
schema = json.load(f)
|
|
1825
|
+
|
|
1826
|
+
def _generate_from_schema(schema_dict: Dict[str, Any]) -> Dict[str, Any]:
|
|
1827
|
+
cfg: Dict[str, Any] = {}
|
|
1828
|
+
if isinstance(schema_dict, dict) and "properties" in schema_dict:
|
|
1829
|
+
for key, value in schema_dict["properties"].items():
|
|
1830
|
+
if "default" in value:
|
|
1831
|
+
cfg[key] = value["default"]
|
|
1832
|
+
elif value.get("type") == "array":
|
|
1833
|
+
cfg[key] = []
|
|
1834
|
+
elif "properties" in value:
|
|
1835
|
+
cfg[key] = _generate_from_schema(value)
|
|
1836
|
+
return cfg
|
|
1837
|
+
|
|
1838
|
+
return _generate_from_schema(schema)
|
|
1839
|
+
except Exception:
|
|
1840
|
+
return {}
|
|
1841
|
+
|
|
1842
|
+
|
|
1843
|
+
def _prune_defaults_with_schema(config_data: dict) -> bool:
|
|
1844
|
+
"""
|
|
1845
|
+
删除与 schema 默认值一致的配置项,返回是否发生了变更
|
|
1846
|
+
仅处理 schema 中定义的键,未在 schema 中的键不会被修改
|
|
1847
|
+
"""
|
|
1848
|
+
defaults = _load_default_config_from_schema()
|
|
1849
|
+
if not defaults or not isinstance(config_data, dict):
|
|
1850
|
+
return False
|
|
1851
|
+
|
|
1852
|
+
changed = False
|
|
1853
|
+
|
|
1854
|
+
def _prune_node(node: dict, default_node: dict):
|
|
1855
|
+
nonlocal changed
|
|
1856
|
+
for key in list(node.keys()):
|
|
1857
|
+
if key in default_node:
|
|
1858
|
+
dv = default_node[key]
|
|
1859
|
+
v = node[key]
|
|
1860
|
+
if isinstance(dv, dict) and isinstance(v, dict):
|
|
1861
|
+
_prune_node(v, dv)
|
|
1862
|
+
if not v:
|
|
1863
|
+
del node[key]
|
|
1864
|
+
changed = True
|
|
1865
|
+
elif isinstance(dv, list) and isinstance(v, list):
|
|
1866
|
+
if v == dv:
|
|
1867
|
+
del node[key]
|
|
1868
|
+
changed = True
|
|
1869
|
+
else:
|
|
1870
|
+
if v == dv:
|
|
1871
|
+
del node[key]
|
|
1872
|
+
changed = True
|
|
1873
|
+
|
|
1874
|
+
_prune_node(config_data, defaults)
|
|
1875
|
+
return changed
|
|
1876
|
+
|
|
1877
|
+
|
|
258
1878
|
def _read_old_config_file(config_file):
|
|
259
1879
|
"""读取并解析旧格式的env配置文件
|
|
260
1880
|
|
|
@@ -308,38 +1928,47 @@ def _read_old_config_file(config_file):
|
|
|
308
1928
|
)
|
|
309
1929
|
set_global_env_data(config_data)
|
|
310
1930
|
PrettyOutput.print(
|
|
311
|
-
|
|
1931
|
+
"检测到旧格式配置文件,旧格式以后将不再支持,请尽快迁移到新格式",
|
|
312
1932
|
OutputType.WARNING,
|
|
313
1933
|
)
|
|
314
1934
|
|
|
315
1935
|
|
|
316
|
-
def while_success(func: Callable[[], Any], sleep_time: float = 0.1) -> Any:
|
|
317
|
-
"""
|
|
1936
|
+
def while_success(func: Callable[[], Any], sleep_time: float = 0.1, max_retries: int = 5) -> Any:
|
|
1937
|
+
"""循环执行函数直到成功(累计日志后统一打印,避免逐次加框)
|
|
318
1938
|
|
|
319
1939
|
参数:
|
|
320
1940
|
func -- 要执行的函数
|
|
321
1941
|
sleep_time -- 每次失败后的等待时间(秒)
|
|
1942
|
+
max_retries -- 最大重试次数,默认5次
|
|
322
1943
|
|
|
323
1944
|
返回:
|
|
324
1945
|
函数执行结果
|
|
325
1946
|
"""
|
|
326
|
-
|
|
1947
|
+
result: Any = None
|
|
1948
|
+
retry_count = 0
|
|
1949
|
+
while retry_count < max_retries:
|
|
327
1950
|
try:
|
|
328
|
-
|
|
1951
|
+
result = func()
|
|
1952
|
+
break
|
|
329
1953
|
except Exception as e:
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
1954
|
+
retry_count += 1
|
|
1955
|
+
if retry_count < max_retries:
|
|
1956
|
+
PrettyOutput.print(
|
|
1957
|
+
f"发生异常:\n{e}\n重试中 ({retry_count}/{max_retries}),等待 {sleep_time}s...",
|
|
1958
|
+
OutputType.WARNING,
|
|
1959
|
+
)
|
|
1960
|
+
time.sleep(sleep_time)
|
|
334
1961
|
continue
|
|
1962
|
+
return result
|
|
335
1963
|
|
|
336
1964
|
|
|
337
|
-
def while_true(func: Callable[[], bool], sleep_time: float = 0.1) -> Any:
|
|
338
|
-
"""循环执行函数直到返回True
|
|
1965
|
+
def while_true(func: Callable[[], bool], sleep_time: float = 0.1, max_retries: int = 5) -> Any:
|
|
1966
|
+
"""循环执行函数直到返回True(累计日志后统一打印,避免逐次加框)
|
|
339
1967
|
|
|
340
1968
|
参数:
|
|
341
1969
|
func: 要执行的函数,必须返回布尔值
|
|
342
1970
|
sleep_time: 每次失败后的等待时间(秒)
|
|
1971
|
+
max_retries: 最大重试次数,默认5次
|
|
343
1972
|
|
|
344
1973
|
返回:
|
|
345
1974
|
函数最终返回的True值
|
|
@@ -348,12 +1977,19 @@ def while_true(func: Callable[[], bool], sleep_time: float = 0.1) -> Any:
|
|
|
348
1977
|
与while_success不同,此函数只检查返回是否为True,
|
|
349
1978
|
不捕获异常,异常会直接抛出
|
|
350
1979
|
"""
|
|
351
|
-
|
|
1980
|
+
ret: bool = False
|
|
1981
|
+
retry_count = 0
|
|
1982
|
+
while retry_count < max_retries:
|
|
352
1983
|
ret = func()
|
|
353
1984
|
if ret:
|
|
354
1985
|
break
|
|
355
|
-
|
|
356
|
-
|
|
1986
|
+
retry_count += 1
|
|
1987
|
+
if retry_count < max_retries:
|
|
1988
|
+
PrettyOutput.print(
|
|
1989
|
+
f"返回空值,重试中 ({retry_count}/{max_retries}),等待 {sleep_time}s...",
|
|
1990
|
+
OutputType.WARNING,
|
|
1991
|
+
)
|
|
1992
|
+
time.sleep(sleep_time)
|
|
357
1993
|
return ret
|
|
358
1994
|
|
|
359
1995
|
|
|
@@ -364,9 +2000,22 @@ def get_file_md5(filepath: str) -> str:
|
|
|
364
2000
|
filepath: 要计算哈希的文件路径
|
|
365
2001
|
|
|
366
2002
|
返回:
|
|
367
|
-
str: 文件内容的MD5
|
|
2003
|
+
str: 文件内容的MD5哈希值(为降低内存占用,仅读取前100MB进行计算)
|
|
368
2004
|
"""
|
|
369
|
-
|
|
2005
|
+
# 采用流式读取,避免一次性加载100MB到内存
|
|
2006
|
+
h = hashlib.md5()
|
|
2007
|
+
max_bytes = 100 * 1024 * 1024 # 与原实现保持一致:仅读取前100MB
|
|
2008
|
+
buf_size = 8 * 1024 * 1024 # 8MB缓冲
|
|
2009
|
+
read_bytes = 0
|
|
2010
|
+
with open(filepath, "rb") as f:
|
|
2011
|
+
while read_bytes < max_bytes:
|
|
2012
|
+
to_read = min(buf_size, max_bytes - read_bytes)
|
|
2013
|
+
chunk = f.read(to_read)
|
|
2014
|
+
if not chunk:
|
|
2015
|
+
break
|
|
2016
|
+
h.update(chunk)
|
|
2017
|
+
read_bytes += len(chunk)
|
|
2018
|
+
return h.hexdigest()
|
|
370
2019
|
|
|
371
2020
|
|
|
372
2021
|
def get_file_line_count(filename: str) -> int:
|
|
@@ -379,45 +2028,40 @@ def get_file_line_count(filename: str) -> int:
|
|
|
379
2028
|
int: 文件中的行数,如果文件无法读取则返回0
|
|
380
2029
|
"""
|
|
381
2030
|
try:
|
|
382
|
-
|
|
383
|
-
|
|
2031
|
+
# 使用流式逐行计数,避免将整个文件读入内存
|
|
2032
|
+
with open(filename, "r", encoding="utf-8", errors="ignore") as f:
|
|
2033
|
+
return sum(1 for _ in f)
|
|
2034
|
+
except Exception:
|
|
384
2035
|
return 0
|
|
385
2036
|
|
|
386
2037
|
|
|
387
|
-
def _get_cmd_stats() -> Dict[str, int]:
|
|
388
|
-
"""从数据目录获取命令调用统计"""
|
|
389
|
-
stats_file = Path(get_data_dir()) / "cmd_stat.yaml"
|
|
390
|
-
if stats_file.exists():
|
|
391
|
-
try:
|
|
392
|
-
with open(stats_file, "r", encoding="utf-8") as f:
|
|
393
|
-
return yaml.safe_load(f) or {}
|
|
394
|
-
except Exception as e:
|
|
395
|
-
PrettyOutput.print(f"加载命令调用统计失败: {str(e)}", OutputType.WARNING)
|
|
396
|
-
return {}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
def _update_cmd_stats(cmd_name: str) -> None:
|
|
400
|
-
"""更新命令调用统计"""
|
|
401
|
-
stats = _get_cmd_stats()
|
|
402
|
-
stats[cmd_name] = stats.get(cmd_name, 0) + 1
|
|
403
|
-
stats_file = Path(get_data_dir()) / "cmd_stat.yaml"
|
|
404
|
-
try:
|
|
405
|
-
with open(stats_file, "w", encoding="utf-8") as f:
|
|
406
|
-
yaml.safe_dump(stats, f, allow_unicode=True)
|
|
407
|
-
except Exception as e:
|
|
408
|
-
PrettyOutput.print(f"保存命令调用统计失败: {str(e)}", OutputType.WARNING)
|
|
409
|
-
|
|
410
|
-
|
|
411
2038
|
def count_cmd_usage() -> None:
|
|
412
2039
|
"""统计当前命令的使用次数"""
|
|
413
2040
|
import sys
|
|
2041
|
+
import os
|
|
2042
|
+
from jarvis.jarvis_stats.stats import StatsManager
|
|
414
2043
|
|
|
415
|
-
|
|
2044
|
+
# 从完整路径中提取命令名称
|
|
2045
|
+
cmd_path = sys.argv[0]
|
|
2046
|
+
cmd_name = os.path.basename(cmd_path)
|
|
2047
|
+
|
|
2048
|
+
# 如果是短命令,映射到长命令
|
|
2049
|
+
if cmd_name in COMMAND_MAPPING:
|
|
2050
|
+
metric_name = COMMAND_MAPPING[cmd_name]
|
|
2051
|
+
else:
|
|
2052
|
+
metric_name = cmd_name
|
|
416
2053
|
|
|
2054
|
+
# 使用 StatsManager 记录命令使用统计
|
|
2055
|
+
StatsManager.increment(metric_name, group="command")
|
|
417
2056
|
|
|
418
|
-
|
|
2057
|
+
|
|
2058
|
+
def is_context_overflow(
|
|
2059
|
+
content: str, model_group_override: Optional[str] = None
|
|
2060
|
+
) -> bool:
|
|
419
2061
|
"""判断文件内容是否超出上下文限制"""
|
|
420
|
-
return get_context_token_count(content) > get_max_big_content_size(
|
|
2062
|
+
return get_context_token_count(content) > get_max_big_content_size(
|
|
2063
|
+
model_group_override
|
|
2064
|
+
)
|
|
421
2065
|
|
|
422
2066
|
|
|
423
2067
|
def get_loc_stats() -> str:
|
|
@@ -427,50 +2071,177 @@ def get_loc_stats() -> str:
|
|
|
427
2071
|
str: loc命令输出的原始字符串,失败时返回空字符串
|
|
428
2072
|
"""
|
|
429
2073
|
try:
|
|
430
|
-
result = subprocess.run(
|
|
2074
|
+
result = subprocess.run(
|
|
2075
|
+
["loc"], capture_output=True, text=True, encoding="utf-8", errors="replace"
|
|
2076
|
+
)
|
|
431
2077
|
return result.stdout if result.returncode == 0 else ""
|
|
432
2078
|
except FileNotFoundError:
|
|
433
2079
|
return ""
|
|
434
2080
|
|
|
435
2081
|
|
|
436
|
-
def
|
|
437
|
-
"""
|
|
2082
|
+
def _pull_git_repo(repo_path: Path, repo_type: str):
|
|
2083
|
+
"""对指定的git仓库执行git pull操作,并根据commit hash判断是否有更新。"""
|
|
2084
|
+
git_dir = repo_path / ".git"
|
|
2085
|
+
if not git_dir.is_dir():
|
|
2086
|
+
return
|
|
438
2087
|
|
|
439
|
-
参数:
|
|
440
|
-
text: 要复制的文本
|
|
441
|
-
"""
|
|
442
|
-
# 尝试使用 xsel
|
|
443
2088
|
try:
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
2089
|
+
# 检查是否有远程仓库
|
|
2090
|
+
remote_result = subprocess.run(
|
|
2091
|
+
["git", "remote"],
|
|
2092
|
+
cwd=repo_path,
|
|
2093
|
+
capture_output=True,
|
|
2094
|
+
text=True,
|
|
2095
|
+
encoding="utf-8",
|
|
2096
|
+
errors="replace",
|
|
2097
|
+
check=True,
|
|
2098
|
+
timeout=10,
|
|
2099
|
+
)
|
|
2100
|
+
if not remote_result.stdout.strip():
|
|
2101
|
+
return
|
|
2102
|
+
|
|
2103
|
+
# 检查git仓库状态
|
|
2104
|
+
status_result = subprocess.run(
|
|
2105
|
+
["git", "status", "--porcelain"],
|
|
2106
|
+
cwd=repo_path,
|
|
2107
|
+
capture_output=True,
|
|
2108
|
+
text=True,
|
|
2109
|
+
encoding="utf-8",
|
|
2110
|
+
errors="replace",
|
|
2111
|
+
check=True,
|
|
2112
|
+
timeout=10,
|
|
2113
|
+
)
|
|
2114
|
+
if status_result.stdout:
|
|
2115
|
+
if user_confirm(
|
|
2116
|
+
f"检测到 '{repo_path.name}' 存在未提交的更改,是否放弃这些更改并更新?"
|
|
2117
|
+
):
|
|
2118
|
+
try:
|
|
2119
|
+
subprocess.run(
|
|
2120
|
+
["git", "checkout", "."],
|
|
2121
|
+
cwd=repo_path,
|
|
2122
|
+
capture_output=True,
|
|
2123
|
+
text=True,
|
|
2124
|
+
encoding="utf-8",
|
|
2125
|
+
errors="replace",
|
|
2126
|
+
check=True,
|
|
2127
|
+
timeout=10,
|
|
2128
|
+
)
|
|
2129
|
+
except (
|
|
2130
|
+
subprocess.CalledProcessError,
|
|
2131
|
+
subprocess.TimeoutExpired,
|
|
2132
|
+
FileNotFoundError,
|
|
2133
|
+
) as e:
|
|
2134
|
+
PrettyOutput.print(
|
|
2135
|
+
f"放弃 '{repo_path.name}' 的更改失败: {str(e)}",
|
|
2136
|
+
OutputType.ERROR,
|
|
2137
|
+
)
|
|
2138
|
+
return
|
|
2139
|
+
else:
|
|
2140
|
+
PrettyOutput.print(
|
|
2141
|
+
f"跳过更新 '{repo_path.name}' 以保留未提交的更改。",
|
|
2142
|
+
OutputType.INFO,
|
|
2143
|
+
)
|
|
2144
|
+
return
|
|
2145
|
+
|
|
2146
|
+
# 获取更新前的commit hash
|
|
2147
|
+
before_hash_result = subprocess.run(
|
|
2148
|
+
["git", "rev-parse", "HEAD"],
|
|
2149
|
+
cwd=repo_path,
|
|
2150
|
+
capture_output=True,
|
|
2151
|
+
text=True,
|
|
2152
|
+
encoding="utf-8",
|
|
2153
|
+
errors="replace",
|
|
2154
|
+
check=True,
|
|
2155
|
+
timeout=10,
|
|
2156
|
+
)
|
|
2157
|
+
before_hash = before_hash_result.stdout.strip()
|
|
2158
|
+
|
|
2159
|
+
# 检查是否是空仓库
|
|
2160
|
+
ls_remote_result = subprocess.run(
|
|
2161
|
+
["git", "ls-remote", "--heads", "origin"],
|
|
2162
|
+
cwd=repo_path,
|
|
2163
|
+
capture_output=True,
|
|
2164
|
+
text=True,
|
|
2165
|
+
encoding="utf-8",
|
|
2166
|
+
errors="replace",
|
|
2167
|
+
check=True,
|
|
2168
|
+
timeout=10,
|
|
449
2169
|
)
|
|
450
|
-
if process.stdin:
|
|
451
|
-
process.stdin.write(text.encode("utf-8"))
|
|
452
|
-
process.stdin.close()
|
|
453
|
-
return
|
|
454
|
-
except FileNotFoundError:
|
|
455
|
-
pass # xsel 未安装,继续尝试下一个
|
|
456
|
-
except Exception as e:
|
|
457
|
-
PrettyOutput.print(f"使用xsel时出错: {e}", OutputType.WARNING)
|
|
458
2170
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
2171
|
+
if not ls_remote_result.stdout.strip():
|
|
2172
|
+
return
|
|
2173
|
+
|
|
2174
|
+
# 执行 git pull
|
|
2175
|
+
subprocess.run(
|
|
2176
|
+
["git", "pull"],
|
|
2177
|
+
cwd=repo_path,
|
|
2178
|
+
capture_output=True,
|
|
2179
|
+
text=True,
|
|
2180
|
+
check=True,
|
|
2181
|
+
timeout=60,
|
|
466
2182
|
)
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
2183
|
+
|
|
2184
|
+
# 获取更新后的commit hash
|
|
2185
|
+
after_hash_result = subprocess.run(
|
|
2186
|
+
["git", "rev-parse", "HEAD"],
|
|
2187
|
+
cwd=repo_path,
|
|
2188
|
+
capture_output=True,
|
|
2189
|
+
text=True,
|
|
2190
|
+
check=True,
|
|
2191
|
+
timeout=10,
|
|
2192
|
+
)
|
|
2193
|
+
after_hash = after_hash_result.stdout.strip()
|
|
2194
|
+
|
|
2195
|
+
if before_hash != after_hash:
|
|
2196
|
+
PrettyOutput.print(
|
|
2197
|
+
f"{repo_type}库 '{repo_path.name}' 已更新。", OutputType.SUCCESS
|
|
2198
|
+
)
|
|
2199
|
+
|
|
471
2200
|
except FileNotFoundError:
|
|
472
2201
|
PrettyOutput.print(
|
|
473
|
-
"
|
|
2202
|
+
f"git 命令未找到,跳过更新 '{repo_path.name}'。", OutputType.WARNING
|
|
2203
|
+
)
|
|
2204
|
+
except subprocess.TimeoutExpired:
|
|
2205
|
+
PrettyOutput.print(f"更新 '{repo_path.name}' 超时。", OutputType.ERROR)
|
|
2206
|
+
except subprocess.CalledProcessError as e:
|
|
2207
|
+
error_message = e.stderr.strip() if e.stderr else str(e)
|
|
2208
|
+
PrettyOutput.print(
|
|
2209
|
+
f"更新 '{repo_path.name}' 失败: {error_message}", OutputType.ERROR
|
|
474
2210
|
)
|
|
475
2211
|
except Exception as e:
|
|
476
|
-
PrettyOutput.print(
|
|
2212
|
+
PrettyOutput.print(
|
|
2213
|
+
f"更新 '{repo_path.name}' 时发生未知错误: {str(e)}", OutputType.ERROR
|
|
2214
|
+
)
|
|
2215
|
+
|
|
2216
|
+
|
|
2217
|
+
def daily_check_git_updates(repo_dirs: List[str], repo_type: str):
|
|
2218
|
+
"""
|
|
2219
|
+
对指定的目录列表执行每日一次的git更新检查。
|
|
2220
|
+
|
|
2221
|
+
Args:
|
|
2222
|
+
repo_dirs (List[str]): 需要检查的git仓库目录列表。
|
|
2223
|
+
repo_type (str): 仓库的类型名称,例如 "工具" 或 "方法论",用于日志输出。
|
|
2224
|
+
"""
|
|
2225
|
+
data_dir = Path(str(get_data_dir()))
|
|
2226
|
+
last_check_file = data_dir / f"{repo_type}_updates_last_check.txt"
|
|
2227
|
+
should_check_for_updates = True
|
|
2228
|
+
|
|
2229
|
+
if last_check_file.exists():
|
|
2230
|
+
try:
|
|
2231
|
+
last_check_timestamp = float(last_check_file.read_text())
|
|
2232
|
+
last_check_date = datetime.fromtimestamp(last_check_timestamp).date()
|
|
2233
|
+
if last_check_date == datetime.now().date():
|
|
2234
|
+
should_check_for_updates = False
|
|
2235
|
+
except (ValueError, IOError):
|
|
2236
|
+
pass
|
|
2237
|
+
|
|
2238
|
+
if should_check_for_updates:
|
|
2239
|
+
|
|
2240
|
+
for repo_dir in repo_dirs:
|
|
2241
|
+
p_repo_dir = Path(repo_dir)
|
|
2242
|
+
if p_repo_dir.exists() and p_repo_dir.is_dir():
|
|
2243
|
+
_pull_git_repo(p_repo_dir, repo_type)
|
|
2244
|
+
try:
|
|
2245
|
+
last_check_file.write_text(str(time.time()))
|
|
2246
|
+
except IOError as e:
|
|
2247
|
+
PrettyOutput.print(f"无法写入git更新检查时间戳: {e}", OutputType.WARNING)
|