jarvis-ai-assistant 0.1.187__py3-none-any.whl → 0.1.189__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.
@@ -2,7 +2,6 @@
2
2
  import argparse
3
3
  import os
4
4
  import re
5
- import shlex
6
5
  import subprocess
7
6
  import sys
8
7
  import tempfile
@@ -13,8 +12,7 @@ from yaspin import yaspin
13
12
 
14
13
  from jarvis.jarvis_platform.registry import PlatformRegistry
15
14
  from jarvis.jarvis_utils.config import get_git_commit_prompt
16
- from jarvis.jarvis_utils.git_utils import (find_git_root,
17
- has_uncommitted_changes)
15
+ from jarvis.jarvis_utils.git_utils import find_git_root, has_uncommitted_changes
18
16
  from jarvis.jarvis_utils.output import OutputType, PrettyOutput
19
17
  from jarvis.jarvis_utils.tag import ct, ot
20
18
  from jarvis.jarvis_utils.utils import init_env, is_context_overflow
@@ -23,48 +21,59 @@ from jarvis.jarvis_utils.utils import init_env, is_context_overflow
23
21
  class GitCommitTool:
24
22
  name = "git_commit_agent"
25
23
  description = "根据代码变更自动生成并执行Git提交"
26
- labels = ['git', 'version_control']
24
+ labels = ["git", "version_control"]
27
25
  parameters = {
28
26
  "type": "object",
29
27
  "properties": {
30
28
  "root_dir": {
31
29
  "type": "string",
32
30
  "description": "Git仓库的根目录路径(可选)",
33
- "default": "."
31
+ "default": ".",
34
32
  },
35
- "prefix": {
36
- "type": "string",
37
- "description": "提交信息前缀(可选)",
38
- "default": ""
39
- },
40
- "suffix": {
41
- "type": "string",
42
- "description": "提交信息后缀(可选)",
43
- "default": ""
44
- }
33
+ "prefix": {"type": "string", "description": "提交信息前缀(可选)", "default": ""},
34
+ "suffix": {"type": "string", "description": "提交信息后缀(可选)", "default": ""},
45
35
  },
46
- "required": []
36
+ "required": [],
47
37
  }
48
- def _extract_commit_message(self, message)->Optional[str]:
38
+
39
+ def _extract_commit_message(self, message) -> Optional[str]:
49
40
  """Raw extraction preserving all characters"""
50
41
  r = re.search(
51
42
  r"(?i)" + ot("COMMIT_MESSAGE") + r"\s*([\s\S]*?)\s*" + ct("COMMIT_MESSAGE"),
52
- message
43
+ message,
53
44
  )
54
45
  if r:
55
46
  # 直接返回原始内容,仅去除外围空白
56
- return shlex.quote(r.group(1).strip())
47
+ return r.group(1).strip()
57
48
  return None
58
49
 
59
- def _get_last_commit_hash(self):
50
+ def _get_last_commit_hash(self) -> str:
60
51
  process = subprocess.Popen(
61
52
  ["git", "log", "-1", "--pretty=%H"],
62
53
  stdout=subprocess.PIPE,
63
- stderr=subprocess.PIPE
54
+ stderr=subprocess.PIPE,
64
55
  )
65
56
  stdout, _ = process.communicate()
66
57
  return stdout.decode().strip()
67
58
 
59
+ def _prepare_git_environment(self, root_dir: str) -> Optional[str]:
60
+ """Prepare git environment by changing directory and checking for changes"""
61
+ original_dir = os.getcwd()
62
+ os.chdir(root_dir)
63
+ find_git_root()
64
+ if not has_uncommitted_changes():
65
+ PrettyOutput.print("没有未提交的更改", OutputType.SUCCESS)
66
+ return None
67
+ return original_dir
68
+
69
+ def _stage_changes(self, spinner) -> None:
70
+ """Stage all changes for commit"""
71
+ spinner.text = "正在添加文件到提交..."
72
+ subprocess.Popen(
73
+ ["git", "add", "."], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
74
+ ).wait()
75
+ spinner.write("✅ 添加文件到提交")
76
+
68
77
  def execute(self, args: Dict) -> Dict[str, Any]:
69
78
  """Execute automatic commit process with support for multi-line messages and special characters"""
70
79
  try:
@@ -72,214 +81,240 @@ class GitCommitTool:
72
81
  prefix = args.get("prefix", "")
73
82
  suffix = args.get("suffix", "")
74
83
 
75
- # Store current directory
76
- original_dir = os.getcwd()
84
+ # Prepare git environment
85
+ result = self._prepare_git_environment(root_dir)
86
+ if result is None:
87
+ return {"success": True, "stdout": "No changes to commit", "stderr": ""}
88
+ original_dir = result
77
89
 
78
- try:
79
- # Change to root_dir
80
- os.chdir(root_dir)
90
+ with yaspin(text="正在初始化提交流程...", color="cyan") as spinner:
91
+ # 添加文件到暂存区
92
+ self._stage_changes(spinner)
81
93
 
82
- find_git_root()
83
- if not has_uncommitted_changes():
84
- PrettyOutput.print("没有未提交的更改", OutputType.SUCCESS)
85
- return {"success": True, "stdout": "No changes to commit", "stderr": ""}
94
+ # 获取差异
95
+ spinner.text = "正在获取代码差异..."
96
+ # 获取文件列表
97
+ files_cmd = ["git", "diff", "--cached", "--name-only"]
98
+ process = subprocess.Popen(
99
+ files_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
100
+ )
101
+ files_output = process.communicate()[0].decode()
102
+ files = [f.strip() for f in files_output.split("\n") if f.strip()]
103
+ file_count = len(files)
86
104
 
87
- with yaspin(text="正在初始化提交流程...", color="cyan") as spinner:
88
- # 添加文件
89
- spinner.text = "正在添加文件到提交..."
90
- subprocess.Popen(
91
- ["git", "add", "."],
92
- stdout=subprocess.DEVNULL,
93
- stderr=subprocess.DEVNULL
94
- ).wait()
95
- spinner.write("✅ 添加文件到提交")
105
+ # 获取完整差异
106
+ process = subprocess.Popen(
107
+ ["git", "diff", "--cached", "--exit-code"],
108
+ stdout=subprocess.PIPE,
109
+ stderr=subprocess.PIPE,
110
+ )
111
+ diff = process.communicate()[0].decode()
112
+ spinner.write(f"✅ 获取差异 ({file_count} 个文件)")
113
+ try:
114
+ temp_diff_file_path = None
115
+ # 生成提交信息
116
+ spinner.text = "正在生成提交消息..."
96
117
 
97
- # 获取差异
98
- spinner.text = "正在获取代码差异..."
99
- # 获取文件列表
100
- files_cmd = ["git", "diff", "--cached", "--name-only"]
101
- process = subprocess.Popen(
102
- files_cmd,
103
- stdout=subprocess.PIPE,
104
- stderr=subprocess.PIPE
105
- )
106
- files_output = process.communicate()[0].decode()
107
- files = [f.strip() for f in files_output.split("\n") if f.strip()]
108
- file_count = len(files)
109
-
110
- # 获取完整差异
111
- process = subprocess.Popen(
112
- ["git", "diff", "--cached", "--exit-code"],
113
- stdout=subprocess.PIPE,
114
- stderr=subprocess.PIPE
118
+ # 准备提示信息
119
+ custom_prompt = get_git_commit_prompt()
120
+ base_prompt = (
121
+ custom_prompt
122
+ if custom_prompt
123
+ else f"""根据代码差异生成提交信息:
124
+ 提交信息应使用中文书写
125
+ # 格式模板
126
+ 必须使用以下格式:
127
+
128
+ <类型>(<范围>): <主题>
129
+
130
+ [可选] 详细描述变更内容和原因
131
+
132
+ # 格式规则
133
+ 1. 类型: fix(修复bug), feat(新功能), docs(文档), style(格式), refactor(重构), test(测试), chore(其他)
134
+ 2. 范围表示变更的模块或组件 (例如: auth, database, ui)
135
+ 3. 主题行不超过72个字符,不以句号结尾,使用祈使语气
136
+ 4. 如有详细描述,使用空行分隔主题和详细描述
137
+ 5. 详细描述部分应解释"是什么"和"为什么",而非"如何"
138
+ 6. 仅输出提交信息,不要输出其他内容
139
+ """
115
140
  )
116
- diff = process.communicate()[0].decode()
117
- spinner.write(f"✅ 获取差异 ({file_count} 个文件)")
118
- try:
119
- temp_diff_file_path = None
120
- # 生成提交信息
121
- spinner.text = "正在生成提交消息..."
122
-
123
- # 准备提示信息
124
- custom_prompt = get_git_commit_prompt()
125
- base_prompt = custom_prompt if custom_prompt else f'''根据代码差异生成提交信息:
126
- 提交信息应使用中文书写
127
- # 格式模板
128
- 必须使用以下格式:
141
+ base_prompt += f"""
142
+ # 输出格式
143
+ {ot("COMMIT_MESSAGE")}
144
+ commit信息
145
+ {ct("COMMIT_MESSAGE")}
146
+ """
129
147
 
130
- <类型>(<范围>): <主题>
131
-
132
- [可选] 详细描述变更内容和原因
148
+ # 获取模型并尝试上传文件
149
+ platform = PlatformRegistry().get_normal_platform()
150
+ upload_success = False
133
151
 
134
- # 格式规则
135
- 1. 类型: fix(修复bug), feat(新功能), docs(文档), style(格式), refactor(重构), test(测试), chore(其他)
136
- 2. 范围表示变更的模块或组件 (例如: auth, database, ui)
137
- 3. 主题行不超过72个字符,不以句号结尾,使用祈使语气
138
- 4. 如有详细描述,使用空行分隔主题和详细描述
139
- 5. 详细描述部分应解释"是什么"和"为什么",而非"如何"
140
- 6. 仅输出提交信息,不要输出其他内容
141
- '''
142
- base_prompt += f"""
143
- # 输出格式
144
- {ot("COMMIT_MESSAGE")}
145
- commit信息
146
- {ct("COMMIT_MESSAGE")}
147
- """
152
+ # Check if content is too large
153
+ is_large_content = is_context_overflow(diff)
148
154
 
149
- # 获取模型并尝试上传文件
150
- platform = PlatformRegistry().get_normal_platform()
151
- upload_success = False
152
-
153
- # Check if content is too large
154
- is_large_content = is_context_overflow(diff)
155
-
156
- if is_large_content and hasattr(platform, 'upload_files'):
157
- spinner.text = "正在上传代码差异文件..."
158
- try:
159
- with spinner.hidden():
160
- # 创建临时文件并写入差异内容
161
- with tempfile.NamedTemporaryFile(mode='w', suffix='.diff', delete=False) as temp_diff_file:
162
- temp_diff_file_path = temp_diff_file.name
163
- temp_diff_file.write(diff)
164
- temp_diff_file.flush()
165
- spinner.write(f"✅ 差异内容已写入临时文件")
166
- upload_success = platform.upload_files([temp_diff_file_path])
167
- if upload_success:
168
- spinner.write("✅ 成功上传代码差异文件")
169
- else:
170
- spinner.write("⚠️ 上传代码差异文件失败,将使用分块处理")
171
- except Exception as e:
172
- spinner.write(f"⚠️ 上传文件时出错: {str(e)}")
173
- upload_success = False
174
- # 根据上传状态准备完整的提示
155
+ if is_large_content:
156
+ if not platform.support_upload_files():
157
+ spinner.text = "差异文件太大,无法处理"
158
+ spinner.fail("❌")
159
+ return {
160
+ "success": False,
161
+ "stdout": "",
162
+ "stderr": "错误:差异文件太大,无法处理",
163
+ }
164
+ spinner.text = "正在上传代码差异文件..."
165
+ with spinner.hidden():
166
+ # 创建临时文件并写入差异内容
167
+ with tempfile.NamedTemporaryFile(
168
+ mode="w", suffix=".diff", delete=False
169
+ ) as temp_diff_file:
170
+ temp_diff_file_path = temp_diff_file.name
171
+ temp_diff_file.write(diff)
172
+ temp_diff_file.flush()
173
+ spinner.write(f"✅ 差异内容已写入临时文件")
174
+ upload_success = platform.upload_files(
175
+ [temp_diff_file_path]
176
+ )
175
177
  if upload_success:
176
- # 尝试生成提交信息
177
- spinner.text = "正在生成提交消息..."
178
- # 使用上传的文件
179
- prompt = base_prompt + f'''
180
- # 变更概述
181
- - 变更文件数量: {file_count} 个文件
182
- - 已上传包含完整代码差异的文件
183
-
184
- 请详细分析已上传的代码差异文件,生成符合上述格式的提交信息。
185
- '''
186
- commit_message = platform.chat_until_success(prompt)
178
+ spinner.write("✅ 成功上传代码差异文件")
187
179
  else:
188
- if is_large_content:
189
- return {
190
- "success": False,
191
- "stdout": "",
192
- "stderr": "错误:上传代码差异文件失败"
193
- }
194
- # 直接在提示中包含差异内容
195
- prompt = base_prompt + f'''
196
- # 分析材料
197
- {diff}
198
- '''
199
- commit_message = platform.chat_until_success(prompt)
200
-
201
- while True:
202
- # 只在特定情况下重新获取commit_message
203
- if not upload_success and not is_large_content and not commit_message:
204
- commit_message = platform.chat_until_success(prompt)
205
- extracted_message = self._extract_commit_message(commit_message)
206
- # 如果成功提取,就跳出循环
207
- if extracted_message:
208
- commit_message = extracted_message
209
- # 应用prefix和suffix
210
- if prefix:
211
- commit_message = f"{prefix} {commit_message}"
212
- if suffix:
213
- commit_message = f"{commit_message}\n{suffix}"
214
- break
215
- prompt = f"""格式错误,请按照以下格式重新生成提交信息:
216
- {ot("COMMIT_MESSAGE")}
217
- commit信息
218
- {ct("COMMIT_MESSAGE")}
219
- """
220
- commit_message = platform.chat_until_success(prompt)
221
- spinner.write("✅ 生成提交消息")
180
+ spinner.text = "上传代码差异文件失败"
181
+ spinner.fail("❌")
182
+ return {
183
+ "success": False,
184
+ "stdout": "",
185
+ "stderr": "错误:上传代码差异文件失败",
186
+ }
187
+ # 根据上传状态准备完整的提示
188
+ if is_large_content:
189
+ # 尝试生成提交信息
190
+ spinner.text = "正在生成提交消息..."
191
+ # 使用上传的文件
192
+ prompt = (
193
+ base_prompt
194
+ + f"""
195
+ # 变更概述
196
+ - 变更文件数量: {file_count} 个文件
197
+ - 已上传包含完整代码差异的文件
222
198
 
223
- # 执行提交
224
- spinner.text = "正在准备提交..."
225
- with tempfile.NamedTemporaryFile(mode='w', delete=True) as tmp_file:
226
- tmp_file.write(commit_message)
227
- tmp_file.flush()
228
- spinner.text = "正在执行提交..."
229
- commit_cmd = ["git", "commit", "-F", tmp_file.name]
230
- subprocess.Popen(
231
- commit_cmd,
232
- stdout=subprocess.DEVNULL,
233
- stderr=subprocess.DEVNULL
234
- ).wait()
235
- spinner.write("✅ 提交")
199
+ 请详细分析已上传的代码差异文件,生成符合上述格式的提交信息。
200
+ """
201
+ )
202
+ commit_message = platform.chat_until_success(prompt)
203
+ else:
204
+ prompt = (
205
+ base_prompt
206
+ + f"""
207
+ # 分析材料
208
+ {diff}
209
+ """
210
+ )
211
+ commit_message = platform.chat_until_success(prompt)
236
212
 
237
- commit_hash = self._get_last_commit_hash()
238
- spinner.text = "完成提交"
239
- spinner.ok("✅")
240
- finally:
241
- # 清理临时差异文件
242
- if temp_diff_file_path is not None and os.path.exists(temp_diff_file_path):
243
- try:
244
- os.unlink(temp_diff_file_path)
245
- except Exception as e:
246
- spinner.write(f"⚠️ 无法删除临时文件: {str(e)}")
213
+ while True:
214
+ # 只在特定情况下重新获取commit_message
215
+ if (
216
+ not upload_success
217
+ and not is_large_content
218
+ and not commit_message
219
+ ):
220
+ commit_message = platform.chat_until_success(prompt)
221
+ extracted_message = self._extract_commit_message(commit_message)
222
+ # 如果成功提取,就跳出循环
223
+ if extracted_message:
224
+ commit_message = extracted_message
225
+ # 应用prefix和suffix
226
+ if prefix:
227
+ commit_message = f"{prefix} {commit_message}"
228
+ if suffix:
229
+ commit_message = f"{commit_message}\n{suffix}"
230
+ break
231
+ prompt = f"""格式错误,请按照以下格式重新生成提交信息:
232
+ {ot("COMMIT_MESSAGE")}
233
+ commit信息
234
+ {ct("COMMIT_MESSAGE")}
235
+ """
236
+ commit_message = platform.chat_until_success(prompt)
237
+ spinner.write("✅ 生成提交消息")
247
238
 
248
- PrettyOutput.print(f"提交哈希: {commit_hash}\n提交消息: {commit_message}", OutputType.SUCCESS)
239
+ # 执行提交
240
+ spinner.text = "正在准备提交..."
241
+ with tempfile.NamedTemporaryFile(mode="w", delete=True) as tmp_file:
242
+ tmp_file.write(commit_message)
243
+ tmp_file.flush()
244
+ spinner.text = "正在执行提交..."
245
+ commit_cmd = ["git", "commit", "-F", tmp_file.name]
246
+ subprocess.Popen(
247
+ commit_cmd,
248
+ stdout=subprocess.DEVNULL,
249
+ stderr=subprocess.DEVNULL,
250
+ ).wait()
251
+ spinner.write("✅ 提交")
249
252
 
250
- return {
251
- "success": True,
252
- "stdout": yaml.safe_dump({
253
- "commit_hash": commit_hash,
254
- "commit_message": commit_message
255
- }, allow_unicode=True),
256
- "stderr": ""
257
- }
258
- finally:
259
- # Always restore original directory
260
- os.chdir(original_dir)
253
+ commit_hash = self._get_last_commit_hash()
254
+ spinner.text = "完成提交"
255
+ spinner.ok("")
256
+ finally:
257
+ # 清理临时差异文件
258
+ if temp_diff_file_path is not None and os.path.exists(
259
+ temp_diff_file_path
260
+ ):
261
+ try:
262
+ os.unlink(temp_diff_file_path)
263
+ except Exception as e:
264
+ spinner.write(f"⚠️ 无法删除临时文件: {str(e)}")
261
265
 
266
+ PrettyOutput.print(
267
+ f"提交哈希: {commit_hash}\n提交消息: {commit_message}", OutputType.SUCCESS
268
+ )
269
+
270
+ return {
271
+ "success": True,
272
+ "stdout": yaml.safe_dump(
273
+ {"commit_hash": commit_hash, "commit_message": commit_message},
274
+ allow_unicode=True,
275
+ ),
276
+ "stderr": "",
277
+ }
262
278
  except Exception as e:
263
279
  PrettyOutput.print(f"提交失败: {str(e)}", OutputType.ERROR)
264
280
  return {
265
281
  "success": False,
266
282
  "stdout": "",
267
- "stderr": f"Commit failed: {str(e)}"
283
+ "stderr": f"Commit failed: {str(e)}",
268
284
  }
285
+ finally:
286
+ # Always restore original directory
287
+ os.chdir(original_dir)
288
+
269
289
 
270
290
  def main():
271
291
  init_env("欢迎使用 Jarvis-GitCommitTool,您的Git提交助手已准备就绪!")
272
- parser = argparse.ArgumentParser(description='Git commit tool')
273
- parser.add_argument('--root-dir', type=str, default='.', help='Root directory of the Git repository')
274
- parser.add_argument('--prefix', type=str, default='', help='Prefix to prepend to commit message (separated by space)')
275
- parser.add_argument('--suffix', type=str, default='', help='Suffix to append to commit message (separated by newline)')
292
+ parser = argparse.ArgumentParser(description="Git commit tool")
293
+ parser.add_argument(
294
+ "--root-dir", type=str, default=".", help="Root directory of the Git repository"
295
+ )
296
+ parser.add_argument(
297
+ "--prefix",
298
+ type=str,
299
+ default="",
300
+ help="Prefix to prepend to commit message (separated by space)",
301
+ )
302
+ parser.add_argument(
303
+ "--suffix",
304
+ type=str,
305
+ default="",
306
+ help="Suffix to append to commit message (separated by newline)",
307
+ )
276
308
  args = parser.parse_args()
277
309
  tool = GitCommitTool()
278
- tool.execute({
279
- "root_dir": args.root_dir,
280
- "prefix": args.prefix if hasattr(args, 'prefix') else '',
281
- "suffix": args.suffix if hasattr(args, 'suffix') else ''
282
- })
310
+ tool.execute(
311
+ {
312
+ "root_dir": args.root_dir,
313
+ "prefix": args.prefix if hasattr(args, "prefix") else "",
314
+ "suffix": args.suffix if hasattr(args, "suffix") else "",
315
+ }
316
+ )
317
+
283
318
 
284
319
  if __name__ == "__main__":
285
- sys.exit(main())
320
+ sys.exit(main())
@@ -49,6 +49,10 @@ class BasePlatform(ABC):
49
49
  def upload_files(self, file_list: List[str]) -> bool:
50
50
  raise NotImplementedError("upload_files is not implemented")
51
51
 
52
+ @abstractmethod
53
+ def support_upload_files(self) -> bool:
54
+ """Check if platform supports upload files"""
55
+ return False
52
56
 
53
57
 
54
58
  def _chat(self, message: str):
@@ -56,9 +60,6 @@ class BasePlatform(ABC):
56
60
  start_time = time.time()
57
61
 
58
62
  input_token_count = get_context_token_count(message)
59
- if is_context_overflow(message):
60
- PrettyOutput.print("错误:输入内容超过最大限制", OutputType.WARNING)
61
- return "错误:输入内容超过最大限制"
62
63
 
63
64
  if input_token_count > get_max_input_token_count():
64
65
  max_chunk_size = get_max_input_token_count() - 1024 # 留出一些余量
@@ -90,6 +90,10 @@ class KimiModel(BasePlatform):
90
90
 
91
91
  response = while_success(lambda: requests.post(url, headers=headers, data=payload), sleep_time=5)
92
92
  return response.json()
93
+
94
+ def support_upload_files(self) -> bool:
95
+ """Check if platform supports upload files"""
96
+ return True
93
97
 
94
98
  def _upload_file(self, file_path: str, presigned_url: str) -> bool:
95
99
  """Upload file to presigned URL"""
@@ -67,6 +67,10 @@ class TongyiPlatform(BasePlatform):
67
67
  def _generate_request_id(self):
68
68
  self.request_id = str(uuid.uuid4()).replace("-", "")
69
69
 
70
+ def support_upload_files(self) -> bool:
71
+ """Check if platform supports upload files"""
72
+ return True
73
+
70
74
  def chat(self, message: str) -> Generator[str, None, None]:
71
75
  if not self.request_id:
72
76
  self._generate_request_id()
@@ -107,6 +107,10 @@ class YuanbaoPlatform(BasePlatform):
107
107
  PrettyOutput.print(f"错误:创建会话失败:{e}", OutputType.ERROR)
108
108
  return False
109
109
 
110
+ def support_upload_files(self) -> bool:
111
+ """Check if platform supports upload files"""
112
+ return True
113
+
110
114
  def upload_files(self, file_list: List[str]) -> bool:
111
115
  """上传文件到元宝平台
112
116
 
@@ -105,6 +105,10 @@ def chat_with_model(platform_name: str, model_name: str):
105
105
  # Remove quotes if present
106
106
  if (file_path.startswith('"') and file_path.endswith('"')) or (file_path.startswith("'") and file_path.endswith("'")):
107
107
  file_path = file_path[1:-1]
108
+
109
+ if not platform.support_upload_files():
110
+ PrettyOutput.print("平台不支持上传文件", OutputType.ERROR)
111
+ continue
108
112
 
109
113
  PrettyOutput.print(f"正在上传文件: {file_path}", OutputType.INFO)
110
114
  if platform.upload_files([file_path]):