jarvis-ai-assistant 0.1.92__py3-none-any.whl → 0.1.96__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.

Potentially problematic release.


This version of jarvis-ai-assistant might be problematic. Click here for more details.

@@ -0,0 +1,630 @@
1
+ import os
2
+ import threading
3
+ from typing import Dict, Any, List, Optional
4
+ import re
5
+
6
+ from jarvis.utils import OutputType, PrettyOutput, find_git_root, get_max_context_length, is_long_context, load_env_from_file, while_success
7
+ from jarvis.models.registry import PlatformRegistry
8
+ from jarvis.jarvis_codebase.main import CodeBase
9
+ from prompt_toolkit import PromptSession
10
+ from prompt_toolkit.completion import WordCompleter, Completer, Completion
11
+ from prompt_toolkit.formatted_text import FormattedText
12
+ from prompt_toolkit.styles import Style
13
+ import fnmatch
14
+ from .patch_handler import PatchHandler
15
+ from .git_utils import generate_commit_message, save_edit_record
16
+ from .plan_generator import PlanGenerator
17
+
18
+ # 全局锁对象
19
+ index_lock = threading.Lock()
20
+
21
+ class JarvisCoder:
22
+ def __init__(self, root_dir: str, language: Optional[str] = "python"):
23
+ """初始化代码修改工具"""
24
+ self.root_dir = root_dir
25
+ self.language = language
26
+ self._init_directories()
27
+ self._init_codebase()
28
+
29
+ def _init_directories(self):
30
+ """初始化目录"""
31
+ self.max_context_length = get_max_context_length()
32
+
33
+ root_dir = find_git_root(self.root_dir)
34
+ if not root_dir:
35
+ root_dir = self.root_dir
36
+
37
+ self.root_dir = root_dir
38
+
39
+ PrettyOutput.print(f"Git根目录: {self.root_dir}", OutputType.INFO)
40
+
41
+ # 1. 判断代码库路径是否存在,如果不存在,创建
42
+ if not os.path.exists(self.root_dir):
43
+ PrettyOutput.print(
44
+ "Root directory does not exist, creating...", OutputType.INFO)
45
+ os.makedirs(self.root_dir)
46
+
47
+ os.chdir(self.root_dir)
48
+
49
+ # 2. 创建 .jarvis-coder 目录
50
+ self.jarvis_dir = os.path.join(self.root_dir, ".jarvis-coder")
51
+ if not os.path.exists(self.jarvis_dir):
52
+ os.makedirs(self.jarvis_dir)
53
+
54
+ self.record_dir = os.path.join(self.jarvis_dir, "record")
55
+ if not os.path.exists(self.record_dir):
56
+ os.makedirs(self.record_dir)
57
+
58
+ # 3. 处理 .gitignore 文件
59
+ gitignore_path = os.path.join(self.root_dir, ".gitignore")
60
+ gitignore_modified = False
61
+ jarvis_ignore_pattern = ".jarvis-*"
62
+
63
+ # 3.1 如果 .gitignore 不存在,创建它
64
+ if not os.path.exists(gitignore_path):
65
+ PrettyOutput.print("创建 .gitignore 文件", OutputType.INFO)
66
+ with open(gitignore_path, "w", encoding="utf-8") as f:
67
+ f.write(f"{jarvis_ignore_pattern}\n")
68
+ gitignore_modified = True
69
+ else:
70
+ # 3.2 检查是否已经包含 .jarvis-* 模式
71
+ with open(gitignore_path, "r", encoding="utf-8") as f:
72
+ content = f.read()
73
+
74
+ # 检查是否需要添加 .jarvis-* 模式
75
+ if jarvis_ignore_pattern not in content.split("\n"):
76
+ PrettyOutput.print("将 .jarvis-* 添加到 .gitignore", OutputType.INFO)
77
+ with open(gitignore_path, "a", encoding="utf-8") as f:
78
+ # 确保文件以换行符结尾
79
+ if not content.endswith("\n"):
80
+ f.write("\n")
81
+ f.write(f"{jarvis_ignore_pattern}\n")
82
+ gitignore_modified = True
83
+
84
+ # 4. 判断代码库是否是git仓库,如果不是,初始化git仓库
85
+ if not os.path.exists(os.path.join(self.root_dir, ".git")):
86
+ PrettyOutput.print("初始化 Git 仓库", OutputType.INFO)
87
+ os.system("git init")
88
+ os.system("git add .")
89
+ os.system("git commit -m 'Initial commit'")
90
+ # 5. 如果修改了 .gitignore,提交更改
91
+ elif gitignore_modified:
92
+ PrettyOutput.print("提交 .gitignore 更改", OutputType.INFO)
93
+ os.system("git add .gitignore")
94
+ os.system("git commit -m 'chore: update .gitignore to exclude .jarvis-* files'")
95
+ # 6. 查看代码库是否有未提交的文件,如果有,提交一次
96
+ elif self._has_uncommitted_files():
97
+ PrettyOutput.print("提交未保存的更改", OutputType.INFO)
98
+ os.system("git add .")
99
+ git_diff = os.popen("git diff --cached").read()
100
+ commit_message = generate_commit_message(git_diff, "Pre-edit commit")
101
+ os.system(f"git commit -m '{commit_message}'")
102
+
103
+ def _init_codebase(self):
104
+ """初始化代码库"""
105
+ self._codebase = CodeBase(self.root_dir)
106
+
107
+ def _has_uncommitted_files(self) -> bool:
108
+ """判断代码库是否有未提交的文件"""
109
+ # 获取未暂存的修改
110
+ unstaged = os.popen("git diff --name-only").read()
111
+ # 获取已暂存但未提交的修改
112
+ staged = os.popen("git diff --cached --name-only").read()
113
+ # 获取未跟踪的文件
114
+ untracked = os.popen("git ls-files --others --exclude-standard").read()
115
+
116
+ return bool(unstaged or staged or untracked)
117
+
118
+ def _prepare_execution(self) -> None:
119
+ """准备执行环境"""
120
+ self._codebase.generate_codebase()
121
+
122
+
123
+ def _load_related_files(self, feature: str) -> List[Dict]:
124
+ """加载相关文件内容"""
125
+ ret = []
126
+ # 确保索引数据库已生成
127
+ if not self._codebase.is_index_generated():
128
+ PrettyOutput.print("检测到索引数据库未生成,正在生成...", OutputType.WARNING)
129
+ self._codebase.generate_codebase()
130
+
131
+ related_files = self._codebase.search_similar(feature)
132
+ for file, score, _ in related_files:
133
+ PrettyOutput.print(f"相关文件: {file} 相关度: {score:.3f}", OutputType.SUCCESS)
134
+ with open(file, "r", encoding="utf-8") as f:
135
+ content = f.read()
136
+ ret.append({"file_path": file, "file_content": content})
137
+ return ret
138
+
139
+ def _parse_file_selection(self, input_str: str, max_index: int) -> List[int]:
140
+ """解析文件选择表达式
141
+
142
+ 支持的格式:
143
+ - 单个数字: "1"
144
+ - 逗号分隔: "1,3,5"
145
+ - 范围: "1-5"
146
+ - 组合: "1,3-5,7"
147
+
148
+ Args:
149
+ input_str: 用户输入的选择表达式
150
+ max_index: 最大可选择的索引
151
+
152
+ Returns:
153
+ List[int]: 选中的索引列表(从0开始)
154
+ """
155
+ selected = set()
156
+
157
+ # 移除所有空白字符
158
+ input_str = "".join(input_str.split())
159
+
160
+ # 处理逗号分隔的部分
161
+ for part in input_str.split(","):
162
+ if not part:
163
+ continue
164
+
165
+ # 处理范围(例如:3-6)
166
+ if "-" in part:
167
+ try:
168
+ start, end = map(int, part.split("-"))
169
+ # 转换为从0开始的索引
170
+ start = max(0, start - 1)
171
+ end = min(max_index, end - 1)
172
+ if start <= end:
173
+ selected.update(range(start, end + 1))
174
+ except ValueError:
175
+ PrettyOutput.print(f"忽略无效的范围表达式: {part}", OutputType.WARNING)
176
+ # 处理单个数字
177
+ else:
178
+ try:
179
+ index = int(part) - 1 # 转换为从0开始的索引
180
+ if 0 <= index < max_index:
181
+ selected.add(index)
182
+ else:
183
+ PrettyOutput.print(f"忽略超出范围的索引: {part}", OutputType.WARNING)
184
+ except ValueError:
185
+ PrettyOutput.print(f"忽略无效的数字: {part}", OutputType.WARNING)
186
+
187
+ return sorted(list(selected))
188
+
189
+ def _get_file_completer(self) -> Completer:
190
+ """创建文件路径补全器"""
191
+ class FileCompleter(Completer):
192
+ def __init__(self, root_dir: str):
193
+ self.root_dir = root_dir
194
+
195
+ def get_completions(self, document, complete_event):
196
+ # 获取当前输入的文本
197
+ text = document.text_before_cursor
198
+
199
+ # 如果输入为空,返回根目录下的所有文件
200
+ if not text:
201
+ for path in self._list_files(""):
202
+ yield Completion(path, start_position=0)
203
+ return
204
+
205
+ # 获取当前目录和部分文件名
206
+ current_dir = os.path.dirname(text)
207
+ file_prefix = os.path.basename(text)
208
+
209
+ # 列出匹配的文件
210
+ search_dir = os.path.join(self.root_dir, current_dir) if current_dir else self.root_dir
211
+ if os.path.isdir(search_dir):
212
+ for path in self._list_files(current_dir):
213
+ if path.startswith(text):
214
+ yield Completion(path, start_position=-len(text))
215
+
216
+ def _list_files(self, current_dir: str) -> List[str]:
217
+ """列出指定目录下的所有文件(递归)"""
218
+ files = []
219
+ search_dir = os.path.join(self.root_dir, current_dir)
220
+
221
+ for root, _, filenames in os.walk(search_dir):
222
+ for filename in filenames:
223
+ full_path = os.path.join(root, filename)
224
+ rel_path = os.path.relpath(full_path, self.root_dir)
225
+ # 忽略 .git 目录和其他隐藏文件
226
+ if not any(part.startswith('.') for part in rel_path.split(os.sep)):
227
+ files.append(rel_path)
228
+
229
+ return sorted(files)
230
+
231
+ return FileCompleter(self.root_dir)
232
+
233
+ def _fuzzy_match_files(self, pattern: str) -> List[str]:
234
+ """模糊匹配文件路径
235
+
236
+ Args:
237
+ pattern: 匹配模式
238
+
239
+ Returns:
240
+ List[str]: 匹配的文件路径列表
241
+ """
242
+ matches = []
243
+
244
+ # 将模式转换为正则表达式
245
+ pattern = pattern.replace('.', r'\.').replace('*', '.*').replace('?', '.')
246
+ pattern = f".*{pattern}.*" # 允许部分匹配
247
+ regex = re.compile(pattern, re.IGNORECASE)
248
+
249
+ # 遍历所有文件
250
+ for root, _, files in os.walk(self.root_dir):
251
+ for file in files:
252
+ full_path = os.path.join(root, file)
253
+ rel_path = os.path.relpath(full_path, self.root_dir)
254
+ # 忽略 .git 目录和其他隐藏文件
255
+ if not any(part.startswith('.') for part in rel_path.split(os.sep)):
256
+ if regex.match(rel_path):
257
+ matches.append(rel_path)
258
+
259
+ return sorted(matches)
260
+
261
+ def _select_files(self, related_files: List[Dict]) -> List[Dict]:
262
+ """让用户选择和补充相关文件"""
263
+ PrettyOutput.section("相关文件", OutputType.INFO)
264
+
265
+ # 显示找到的文件
266
+ selected_files = list(related_files) # 默认全选
267
+ for i, file in enumerate(related_files, 1):
268
+ PrettyOutput.print(f"[{i}] {file['file_path']}", OutputType.INFO)
269
+
270
+ # 询问用户是否需要调整
271
+ user_input = input("\n是否需要调整文件列表?(y/n) [n]: ").strip().lower() or 'n'
272
+ if user_input == 'y':
273
+ # 让用户选择文件
274
+ PrettyOutput.print("\n请输入要包含的文件编号(支持: 1,3-6 格式,直接回车保持当前选择):", OutputType.INFO)
275
+ numbers = input(">>> ").strip()
276
+ if numbers:
277
+ selected_indices = self._parse_file_selection(numbers, len(related_files))
278
+ if selected_indices:
279
+ selected_files = [related_files[i] for i in selected_indices]
280
+ else:
281
+ PrettyOutput.print("未选择任何有效文件,保持原有选择", OutputType.WARNING)
282
+
283
+ # 询问是否需要补充文件
284
+ user_input = input("\n是否需要补充其他文件?(y/n) [n]: ").strip().lower() or 'n'
285
+ if user_input == 'y':
286
+ # 创建文件补全会话
287
+ session = PromptSession(
288
+ completer=self._get_file_completer(),
289
+ complete_while_typing=True
290
+ )
291
+
292
+ while True:
293
+ PrettyOutput.print("\n请输入要补充的文件路径(支持Tab补全和*?通配符,输入空行结束):", OutputType.INFO)
294
+ try:
295
+ file_path = session.prompt(">>> ").strip()
296
+ except KeyboardInterrupt:
297
+ break
298
+
299
+ if not file_path:
300
+ break
301
+
302
+ # 处理通配符匹配
303
+ if '*' in file_path or '?' in file_path:
304
+ matches = self._fuzzy_match_files(file_path)
305
+ if not matches:
306
+ PrettyOutput.print("未找到匹配的文件", OutputType.WARNING)
307
+ continue
308
+
309
+ # 显示匹配的文件
310
+ PrettyOutput.print("\n找到以下匹配的文件:", OutputType.INFO)
311
+ for i, path in enumerate(matches, 1):
312
+ PrettyOutput.print(f"[{i}] {path}", OutputType.INFO)
313
+
314
+ # 让用户选择
315
+ numbers = input("\n请选择要添加的文件编号(支持: 1,3-6 格式,直接回车全选): ").strip()
316
+ if numbers:
317
+ indices = self._parse_file_selection(numbers, len(matches))
318
+ if not indices:
319
+ continue
320
+ paths_to_add = [matches[i] for i in indices]
321
+ else:
322
+ paths_to_add = matches
323
+ else:
324
+ paths_to_add = [file_path]
325
+
326
+ # 添加选中的文件
327
+ for path in paths_to_add:
328
+ full_path = os.path.join(self.root_dir, path)
329
+ if not os.path.isfile(full_path):
330
+ PrettyOutput.print(f"文件不存在: {path}", OutputType.ERROR)
331
+ continue
332
+
333
+ try:
334
+ with open(full_path, "r", encoding="utf-8") as f:
335
+ content = f.read()
336
+ selected_files.append({
337
+ "file_path": path,
338
+ "file_content": content
339
+ })
340
+ PrettyOutput.print(f"已添加文件: {path}", OutputType.SUCCESS)
341
+ except Exception as e:
342
+ PrettyOutput.print(f"读取文件失败: {str(e)}", OutputType.ERROR)
343
+
344
+ return selected_files
345
+
346
+ def _finalize_changes(self, feature: str) -> None:
347
+ """完成修改并提交"""
348
+ PrettyOutput.print("修改确认成功,提交修改", OutputType.INFO)
349
+
350
+ # 只添加已经在 git 控制下的修改文件
351
+ os.system("git add -u")
352
+
353
+ # 然后获取 git diff
354
+ git_diff = os.popen("git diff --cached").read()
355
+
356
+ # 自动生成commit信息,传入feature
357
+ commit_message = generate_commit_message(git_diff, feature)
358
+
359
+ # 显示并确认commit信息
360
+ PrettyOutput.print(f"自动生成的commit信息: {commit_message}", OutputType.INFO)
361
+ user_confirm = input("是否使用该commit信息?(y/n) [y]: ") or "y"
362
+
363
+ if user_confirm.lower() != "y":
364
+ commit_message = input("请输入新的commit信息: ")
365
+
366
+ # 不需要再次 git add,因为已经添加过了
367
+ os.system(f"git commit -m '{commit_message}'")
368
+ save_edit_record(self.record_dir, commit_message, git_diff)
369
+
370
+ def _revert_changes(self) -> None:
371
+ """回退所有修改"""
372
+ PrettyOutput.print("修改已取消,回退更改", OutputType.INFO)
373
+ os.system(f"git reset --hard")
374
+ os.system(f"git clean -df")
375
+
376
+ def get_key_code(self, files: List[Dict], feature: str):
377
+ """提取文件中与需求相关的关键代码片段"""
378
+ for file_info in files:
379
+ PrettyOutput.print(f"分析文件: {file_info['file_path']}", OutputType.INFO)
380
+ model = PlatformRegistry.get_global_platform_registry().get_codegen_platform()
381
+ model.set_suppress_output(True)
382
+ file_path = file_info["file_path"]
383
+ content = file_info["file_content"]
384
+
385
+ # 生成分析提示
386
+ system_message = f"""你是一个代码分析专家,可以从代码中提取出与需求相关的片段。
387
+ 请按以下格式返回:
388
+ <PART>
389
+ content
390
+ </PART>
391
+
392
+ 可返回多个片段。如果文件内容与需求无关,则返回空。
393
+ """
394
+ model.set_system_message(system_message)
395
+
396
+ try:
397
+
398
+ prompt = f"""需求:{feature}
399
+ 文件路径:{file_path}
400
+ 代码内容:
401
+ {content}
402
+ """
403
+
404
+ # 调用大模型进行分析
405
+ response = while_success(lambda: model.chat(prompt))
406
+
407
+ parts = re.findall(r'<PART>\n(.*?)\n</PART>', response, re.DOTALL)
408
+ file_info["parts"] = parts
409
+ except Exception as e:
410
+ PrettyOutput.print(f"分析文件失败: {str(e)}", OutputType.ERROR)
411
+
412
+ def execute(self, feature: str) -> Dict[str, Any]:
413
+ """执行代码修改"""
414
+ try:
415
+ self._prepare_execution()
416
+
417
+ # 获取并选择相关文件
418
+ initial_files = self._load_related_files(feature)
419
+ selected_files = self._select_files(initial_files)
420
+
421
+ # 是否是长上下文
422
+ if is_long_context([file['file_path'] for file in selected_files]):
423
+ self.get_key_code(selected_files, feature)
424
+ else:
425
+ for file in selected_files:
426
+ file["parts"] = [file["file_content"]]
427
+
428
+ # 获取修改方案
429
+ modification_plan = PlanGenerator().generate_plan(feature, selected_files)
430
+ if not modification_plan:
431
+ return {
432
+ "success": False,
433
+ "stdout": "",
434
+ "stderr": "用户取消修改",
435
+ }
436
+
437
+ # 执行修改
438
+ if PatchHandler().handle_patch_application(selected_files, feature, modification_plan):
439
+ self._finalize_changes(feature)
440
+ return {
441
+ "success": True,
442
+ "stdout": "代码修改成功",
443
+ "stderr": "",
444
+ }
445
+ else:
446
+ self._revert_changes()
447
+ return {
448
+ "success": False,
449
+ "stdout": "",
450
+ "stderr": "代码修改失败,请修改需求后重试",
451
+ }
452
+
453
+ except Exception as e:
454
+ self._revert_changes()
455
+ return {
456
+ "success": False,
457
+ "stdout": "",
458
+ "stderr": f"执行失败: {str(e)},请修改需求后重试",
459
+ "error": e
460
+ }
461
+
462
+ def main():
463
+ """命令行入口"""
464
+ import argparse
465
+
466
+ load_env_from_file()
467
+
468
+ parser = argparse.ArgumentParser(description='代码修改工具')
469
+ parser.add_argument('-d', '--dir', help='项目根目录', default=os.getcwd())
470
+ parser.add_argument('-l', '--language', help='编程语言', default="python")
471
+ args = parser.parse_args()
472
+
473
+ tool = JarvisCoder(args.dir, args.language)
474
+
475
+ # 循环处理需求
476
+ while True:
477
+ try:
478
+ # 获取需求,传入项目根目录
479
+ feature = get_multiline_input("请输入开发需求 (输入空行退出):", tool.root_dir)
480
+
481
+ if not feature or feature == "__interrupt__":
482
+ break
483
+
484
+ # 执行修改
485
+ result = tool.execute(feature)
486
+
487
+ # 显示结果
488
+ if result["success"]:
489
+ PrettyOutput.print(result["stdout"], OutputType.SUCCESS)
490
+ else:
491
+ if result.get("stderr"):
492
+ PrettyOutput.print(result["stderr"], OutputType.WARNING)
493
+ if result.get("error"): # 使用 get() 方法避免 KeyError
494
+ error = result["error"]
495
+ PrettyOutput.print(f"错误类型: {type(error).__name__}", OutputType.WARNING)
496
+ PrettyOutput.print(f"错误信息: {str(error)}", OutputType.WARNING)
497
+ # 提示用户可以继续输入
498
+ PrettyOutput.print("\n您可以修改需求后重试", OutputType.INFO)
499
+
500
+ except KeyboardInterrupt:
501
+ print("\n用户中断执行")
502
+ break
503
+ except Exception as e:
504
+ PrettyOutput.print(f"执行出错: {str(e)}", OutputType.ERROR)
505
+ PrettyOutput.print("\n您可以修改需求后重试", OutputType.INFO)
506
+ continue
507
+
508
+ return 0
509
+
510
+ if __name__ == "__main__":
511
+ exit(main())
512
+
513
+ class FilePathCompleter(Completer):
514
+ """文件路径自动完成器"""
515
+
516
+ def __init__(self, root_dir: str):
517
+ self.root_dir = root_dir
518
+ self._file_list = None
519
+
520
+ def _get_files(self) -> List[str]:
521
+ """获取git管理的文件列表"""
522
+ if self._file_list is None:
523
+ try:
524
+ # 切换到项目根目录
525
+ old_cwd = os.getcwd()
526
+ os.chdir(self.root_dir)
527
+
528
+ # 获取git管理的文件列表
529
+ self._file_list = os.popen("git ls-files").read().splitlines()
530
+
531
+ # 恢复工作目录
532
+ os.chdir(old_cwd)
533
+ except Exception as e:
534
+ PrettyOutput.print(f"获取文件列表失败: {str(e)}", OutputType.WARNING)
535
+ self._file_list = []
536
+ return self._file_list
537
+
538
+ def get_completions(self, document, complete_event):
539
+ """获取补全建议"""
540
+ text_before_cursor = document.text_before_cursor
541
+
542
+ # 检查是否刚输入了@
543
+ if text_before_cursor.endswith('@'):
544
+ # 显示所有文件
545
+ for path in self._get_files():
546
+ yield Completion(path, start_position=0)
547
+ return
548
+
549
+ # 检查之前是否有@,并获取@后的搜索词
550
+ at_pos = text_before_cursor.rfind('@')
551
+ if at_pos == -1:
552
+ return
553
+
554
+ search = text_before_cursor[at_pos + 1:].lower().strip()
555
+
556
+ # 提供匹配的文件建议
557
+ for path in self._get_files():
558
+ path_lower = path.lower()
559
+ if (search in path_lower or # 直接包含
560
+ search in os.path.basename(path_lower) or # 文件名包含
561
+ any(fnmatch.fnmatch(path_lower, f'*{s}*') for s in search.split())): # 通配符匹配
562
+ # 计算正确的start_position
563
+ yield Completion(path, start_position=-(len(search)))
564
+
565
+ class SmartCompleter(Completer):
566
+ """智能自动完成器,组合词语和文件路径补全"""
567
+
568
+ def __init__(self, word_completer: WordCompleter, file_completer: FilePathCompleter):
569
+ self.word_completer = word_completer
570
+ self.file_completer = file_completer
571
+
572
+ def get_completions(self, document, complete_event):
573
+ """获取补全建议"""
574
+ # 如果当前行以@结尾,使用文件补全
575
+ if document.text_before_cursor.strip().endswith('@'):
576
+ yield from self.file_completer.get_completions(document, complete_event)
577
+ else:
578
+ # 否则使用词语补全
579
+ yield from self.word_completer.get_completions(document, complete_event)
580
+
581
+ def get_multiline_input(prompt_text: str, root_dir: Optional[str] = ".") -> str:
582
+ """获取多行输入,支持文件路径自动完成功能
583
+
584
+ Args:
585
+ prompt_text: 提示文本
586
+ root_dir: 项目根目录,用于文件补全
587
+
588
+ Returns:
589
+ str: 用户输入的文本
590
+ """
591
+ # 创建文件补全器
592
+ file_completer = FilePathCompleter(root_dir or os.getcwd())
593
+
594
+ # 创建提示样式
595
+ style = Style.from_dict({
596
+ 'prompt': 'ansicyan bold',
597
+ 'input': 'ansiwhite',
598
+ })
599
+
600
+ # 创建会话
601
+ session = PromptSession(
602
+ completer=file_completer,
603
+ style=style,
604
+ multiline=False,
605
+ enable_history_search=True,
606
+ complete_while_typing=True
607
+ )
608
+
609
+ # 显示初始提示文本
610
+ print(f"\n{prompt_text}")
611
+
612
+ # 创建提示符
613
+ prompt = FormattedText([
614
+ ('class:prompt', ">>> ")
615
+ ])
616
+
617
+ # 获取输入
618
+ lines = []
619
+ try:
620
+ while True:
621
+ line = session.prompt(prompt).strip()
622
+ if not line: # 空行表示输入结束
623
+ break
624
+ lines.append(line)
625
+ except KeyboardInterrupt:
626
+ return "__interrupt__"
627
+ except EOFError:
628
+ pass
629
+
630
+ return "\n".join(lines)