jarvis-ai-assistant 0.2.7__py3-none-any.whl → 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. jarvis/__init__.py +1 -1
  2. jarvis/jarvis_agent/__init__.py +267 -240
  3. jarvis/jarvis_agent/agent_manager.py +85 -0
  4. jarvis/jarvis_agent/config_editor.py +53 -0
  5. jarvis/jarvis_agent/file_methodology_manager.py +105 -0
  6. jarvis/jarvis_agent/jarvis.py +37 -398
  7. jarvis/jarvis_agent/memory_manager.py +133 -0
  8. jarvis/jarvis_agent/methodology_share_manager.py +174 -0
  9. jarvis/jarvis_agent/prompts.py +18 -3
  10. jarvis/jarvis_agent/share_manager.py +176 -0
  11. jarvis/jarvis_agent/task_analyzer.py +126 -0
  12. jarvis/jarvis_agent/task_manager.py +111 -0
  13. jarvis/jarvis_agent/tool_share_manager.py +139 -0
  14. jarvis/jarvis_code_agent/code_agent.py +26 -20
  15. jarvis/jarvis_data/config_schema.json +37 -0
  16. jarvis/jarvis_platform/ai8.py +13 -1
  17. jarvis/jarvis_platform/base.py +20 -5
  18. jarvis/jarvis_platform/human.py +11 -1
  19. jarvis/jarvis_platform/kimi.py +10 -0
  20. jarvis/jarvis_platform/openai.py +20 -0
  21. jarvis/jarvis_platform/tongyi.py +14 -9
  22. jarvis/jarvis_platform/yuanbao.py +10 -0
  23. jarvis/jarvis_platform_manager/main.py +12 -12
  24. jarvis/jarvis_tools/registry.py +79 -20
  25. jarvis/jarvis_tools/retrieve_memory.py +36 -8
  26. jarvis/jarvis_utils/clipboard.py +90 -0
  27. jarvis/jarvis_utils/config.py +64 -0
  28. jarvis/jarvis_utils/git_utils.py +17 -7
  29. jarvis/jarvis_utils/globals.py +18 -12
  30. jarvis/jarvis_utils/input.py +118 -16
  31. jarvis/jarvis_utils/methodology.py +48 -5
  32. jarvis/jarvis_utils/utils.py +196 -106
  33. {jarvis_ai_assistant-0.2.7.dist-info → jarvis_ai_assistant-0.3.0.dist-info}/METADATA +1 -1
  34. {jarvis_ai_assistant-0.2.7.dist-info → jarvis_ai_assistant-0.3.0.dist-info}/RECORD +38 -28
  35. {jarvis_ai_assistant-0.2.7.dist-info → jarvis_ai_assistant-0.3.0.dist-info}/WHEEL +0 -0
  36. {jarvis_ai_assistant-0.2.7.dist-info → jarvis_ai_assistant-0.3.0.dist-info}/entry_points.txt +0 -0
  37. {jarvis_ai_assistant-0.2.7.dist-info → jarvis_ai_assistant-0.3.0.dist-info}/licenses/LICENSE +0 -0
  38. {jarvis_ai_assistant-0.2.7.dist-info → jarvis_ai_assistant-0.3.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,174 @@
1
+ # -*- coding: utf-8 -*-
2
+ """方法论分享管理模块"""
3
+ import os
4
+ import glob
5
+ import json
6
+ import shutil
7
+ from typing import List, Dict, Any
8
+
9
+ import typer
10
+
11
+ from jarvis.jarvis_agent import OutputType, PrettyOutput, user_confirm
12
+ from jarvis.jarvis_agent.share_manager import ShareManager
13
+ from jarvis.jarvis_utils.config import (
14
+ get_central_methodology_repo,
15
+ get_methodology_dirs,
16
+ )
17
+
18
+
19
+ class MethodologyShareManager(ShareManager):
20
+ """方法论分享管理器"""
21
+
22
+ def __init__(self):
23
+ central_repo = get_central_methodology_repo()
24
+ if not central_repo:
25
+ PrettyOutput.print(
26
+ "错误:未配置中心方法论仓库(JARVIS_CENTRAL_METHODOLOGY_REPO)",
27
+ OutputType.ERROR,
28
+ )
29
+ PrettyOutput.print(
30
+ "请在配置文件中设置中心方法论仓库的Git地址", OutputType.INFO
31
+ )
32
+ raise typer.Exit(code=1)
33
+
34
+ super().__init__(central_repo, "central_methodology_repo")
35
+
36
+ def get_resource_type(self) -> str:
37
+ """获取资源类型名称"""
38
+ return "方法论"
39
+
40
+ def format_resource_display(self, resource: Dict[str, Any]) -> str:
41
+ """格式化资源显示"""
42
+ dir_name = os.path.basename(resource["directory"])
43
+ return f"{resource['problem_type']} (来自: {dir_name})"
44
+
45
+ def get_existing_resources(self) -> Dict[str, str]:
46
+ """获取中心仓库中已有的方法论"""
47
+ existing_methodologies = {} # 存储 problem_type -> content 的映射
48
+ for filepath in glob.glob(os.path.join(self.repo_path, "*.json")):
49
+ try:
50
+ with open(filepath, "r", encoding="utf-8", errors="ignore") as f:
51
+ methodology = json.load(f)
52
+ problem_type = methodology.get("problem_type", "")
53
+ content = methodology.get("content", "")
54
+ if problem_type and content:
55
+ existing_methodologies[problem_type] = content
56
+ except Exception:
57
+ pass
58
+ return existing_methodologies
59
+
60
+ def get_local_resources(self) -> List[Dict[str, Any]]:
61
+ """获取本地方法论"""
62
+ # 获取中心仓库中已有的方法论
63
+ existing_methodologies = self.get_existing_resources()
64
+
65
+ # 获取所有方法论目录
66
+ from jarvis.jarvis_utils.methodology import _get_methodology_directory
67
+
68
+ methodology_dirs = [_get_methodology_directory()] + get_methodology_dirs()
69
+
70
+ # 收集所有方法论文件(排除中心仓库目录和已存在的方法论)
71
+ methodology_files = []
72
+ seen_problem_types = set() # 用于去重
73
+
74
+ for directory in set(methodology_dirs):
75
+ # 跳过中心仓库目录
76
+ if os.path.abspath(directory) == os.path.abspath(self.repo_path):
77
+ continue
78
+
79
+ if not os.path.isdir(directory):
80
+ continue
81
+
82
+ for filepath in glob.glob(os.path.join(directory, "*.json")):
83
+ try:
84
+ with open(filepath, "r", encoding="utf-8", errors="ignore") as f:
85
+ methodology = json.load(f)
86
+ problem_type = methodology.get("problem_type", "")
87
+ content = methodology.get("content", "")
88
+
89
+ # 基于内容判断是否已存在于中心仓库
90
+ is_duplicate = False
91
+ if problem_type in existing_methodologies:
92
+ # 如果problem_type相同,比较内容
93
+ if (
94
+ content.strip()
95
+ == existing_methodologies[problem_type].strip()
96
+ ):
97
+ is_duplicate = True
98
+
99
+ # 排除已存在于中心仓库的方法论(基于内容),以及本地重复的方法论
100
+ if (
101
+ problem_type
102
+ and content
103
+ and not is_duplicate
104
+ and problem_type not in seen_problem_types
105
+ ):
106
+ methodology_files.append(
107
+ {
108
+ "path": filepath,
109
+ "problem_type": problem_type,
110
+ "directory": directory,
111
+ "methodology": methodology,
112
+ }
113
+ )
114
+ seen_problem_types.add(problem_type)
115
+ except Exception:
116
+ pass
117
+
118
+ return methodology_files
119
+
120
+ def share_resources(self, resources: List[Dict[str, Any]]) -> List[str]:
121
+ """分享方法论到中心仓库"""
122
+ # 确认操作
123
+ share_list = ["\n将要分享以下方法论到中心仓库:"]
124
+ for meth in resources:
125
+ share_list.append(f"- {meth['problem_type']}")
126
+ PrettyOutput.print("\n".join(share_list), OutputType.INFO)
127
+
128
+ if not user_confirm("确认分享这些方法论吗?"):
129
+ return []
130
+
131
+ # 复制选中的方法论到中心仓库
132
+ copied_list = []
133
+ for meth in resources:
134
+ src_file = meth["path"]
135
+ dst_file = os.path.join(self.repo_path, os.path.basename(src_file))
136
+ shutil.copy2(src_file, dst_file)
137
+ copied_list.append(f"已复制: {meth['problem_type']}")
138
+
139
+ return copied_list
140
+
141
+ def run(self) -> None:
142
+ """执行方法论分享流程"""
143
+ try:
144
+ # 更新中心仓库
145
+ self.update_central_repo()
146
+
147
+ # 获取本地资源
148
+ local_resources = self.get_local_resources()
149
+ if not local_resources:
150
+ PrettyOutput.print(
151
+ "没有找到新的方法论文件(所有方法论可能已存在于中心仓库)",
152
+ OutputType.WARNING,
153
+ )
154
+ raise typer.Exit(code=0)
155
+
156
+ # 选择要分享的资源
157
+ selected_resources = self.select_resources(local_resources)
158
+ if not selected_resources:
159
+ raise typer.Exit(code=0)
160
+
161
+ # 分享资源
162
+ copied_list = self.share_resources(selected_resources)
163
+ if copied_list:
164
+ # 一次性显示所有复制结果
165
+ PrettyOutput.print("\n".join(copied_list), OutputType.SUCCESS)
166
+
167
+ # 提交并推送
168
+ self.commit_and_push(len(selected_resources))
169
+
170
+ PrettyOutput.print("\n方法论已成功分享到中心仓库!", OutputType.SUCCESS)
171
+
172
+ except Exception as e:
173
+ PrettyOutput.print(f"分享方法论时出错: {str(e)}", OutputType.ERROR)
174
+ raise typer.Exit(code=1)
@@ -43,13 +43,28 @@ SUMMARY_REQUEST_PROMPT = """<summary_request>
43
43
 
44
44
  TASK_ANALYSIS_PROMPT = f"""<task_analysis>
45
45
  <request>
46
- 当前任务已结束,请分析该任务的解决方案:
47
- 1. 首先检查现有工具或方法论是否已经可以完成该任务,如果可以,直接说明即可,无需生成新内容
46
+ 当前任务已结束,请按以下步骤分析该任务:
47
+
48
+ 第一步:记忆值得保存的信息
49
+ 1. 识别任务中的关键信息和知识点
50
+ 2. 评估是否有值得保存的项目长期记忆或全局长期记忆
51
+ 3. 使用 save_memory 工具保存有价值的信息:
52
+ - project_long_term: 保存与当前项目相关的长期信息(如项目配置、架构决策、开发规范等)
53
+ - global_long_term: 保存通用的信息、用户偏好、知识或方法(如技术知识、最佳实践、用户习惯等)
54
+
55
+ 第二步:分析任务解决方案
56
+ 1. 检查现有工具或方法论是否已经可以完成该任务,如果可以,直接说明即可,无需生成新内容
48
57
  2. 如果现有工具/方法论不足,评估当前任务是否可以通过编写新工具来自动化解决
49
58
  3. 如果可以通过工具解决,请设计并提供工具代码
50
59
  4. 如果无法通过编写通用工具完成,评估当前的执行流程是否可以总结为通用方法论
51
60
  5. 如果以上都不可行,给出详细理由
52
- 请根据分析结果采取相应行动:说明现有工具/方法论、创建新工具、生成新方法论或说明原因。
61
+
62
+ 请根据分析结果采取相应行动。
63
+
64
+ 重要提示:每次只能执行一个操作!
65
+ - 如果有记忆需要保存,可以调用一次 save_memory 批量保存多条记忆
66
+ - 保存完所有记忆后,再进行工具/方法论的创建或说明
67
+ - 不要在一次响应中同时调用多个工具(如同时保存记忆和创建工具/方法论)
53
68
  </request>
54
69
  <evaluation_criteria>
55
70
  现有资源评估:
@@ -0,0 +1,176 @@
1
+ # -*- coding: utf-8 -*-
2
+ """分享管理模块,负责工具和方法论的分享功能"""
3
+ import os
4
+ import subprocess
5
+ from typing import List, Dict, Any
6
+ from abc import ABC, abstractmethod
7
+
8
+ from prompt_toolkit import prompt
9
+
10
+ from jarvis.jarvis_agent import OutputType, PrettyOutput, user_confirm
11
+ from jarvis.jarvis_utils.config import get_data_dir
12
+
13
+
14
+ def parse_selection(selection_str: str, max_value: int) -> List[int]:
15
+ """解析用户输入的选择字符串,支持逗号分隔和范围选择
16
+
17
+ 例如: "1,2,3,4-9,20" -> [1, 2, 3, 4, 5, 6, 7, 8, 9, 20]
18
+ """
19
+ selected: set[int] = set()
20
+ parts = selection_str.split(",")
21
+
22
+ for part in parts:
23
+ part = part.strip()
24
+ if "-" in part:
25
+ # 处理范围选择
26
+ try:
27
+ start_str, end_str = part.split("-")
28
+ start_num = int(start_str.strip())
29
+ end_num = int(end_str.strip())
30
+ if 1 <= start_num <= max_value and 1 <= end_num <= max_value:
31
+ selected.update(range(start_num, end_num + 1))
32
+ except ValueError:
33
+ continue
34
+ else:
35
+ # 处理单个数字
36
+ try:
37
+ num = int(part)
38
+ if 1 <= num <= max_value:
39
+ selected.add(num)
40
+ except ValueError:
41
+ continue
42
+
43
+ return sorted(list(selected))
44
+
45
+
46
+ class ShareManager(ABC):
47
+ """分享管理器基类"""
48
+
49
+ def __init__(self, central_repo_url: str, repo_name: str):
50
+ self.central_repo_url = central_repo_url
51
+ self.repo_name = repo_name
52
+ self.repo_path = os.path.join(get_data_dir(), repo_name)
53
+
54
+ def update_central_repo(self) -> None:
55
+ """克隆或更新中心仓库"""
56
+ if not os.path.exists(self.repo_path):
57
+ PrettyOutput.print(
58
+ f"正在克隆中心{self.get_resource_type()}仓库...", OutputType.INFO
59
+ )
60
+ subprocess.run(
61
+ ["git", "clone", self.central_repo_url, self.repo_path], check=True
62
+ )
63
+ else:
64
+ PrettyOutput.print(
65
+ f"正在更新中心{self.get_resource_type()}仓库...", OutputType.INFO
66
+ )
67
+ # 检查是否是空仓库
68
+ try:
69
+ # 先尝试获取远程分支信息
70
+ result = subprocess.run(
71
+ ["git", "ls-remote", "--heads", "origin"],
72
+ cwd=self.repo_path,
73
+ capture_output=True,
74
+ text=True,
75
+ check=True,
76
+ )
77
+ # 如果有远程分支,执行pull
78
+ if result.stdout.strip():
79
+ subprocess.run(["git", "pull"], cwd=self.repo_path, check=True)
80
+ else:
81
+ PrettyOutput.print(
82
+ f"中心{self.get_resource_type()}仓库是空的,将初始化为新仓库",
83
+ OutputType.INFO,
84
+ )
85
+ except subprocess.CalledProcessError:
86
+ # 如果命令失败,可能是网络问题或其他错误
87
+ PrettyOutput.print("无法连接到远程仓库,将跳过更新", OutputType.WARNING)
88
+
89
+ def commit_and_push(self, count: int) -> None:
90
+ """提交并推送更改"""
91
+ PrettyOutput.print("\n正在提交更改...", OutputType.INFO)
92
+ subprocess.run(["git", "add", "."], cwd=self.repo_path, check=True)
93
+
94
+ commit_msg = f"Add {count} {self.get_resource_type()}(s) from local collection"
95
+ subprocess.run(
96
+ ["git", "commit", "-m", commit_msg], cwd=self.repo_path, check=True
97
+ )
98
+
99
+ PrettyOutput.print("正在推送到远程仓库...", OutputType.INFO)
100
+ # 检查是否需要设置上游分支(空仓库的情况)
101
+ try:
102
+ # 先尝试普通推送
103
+ subprocess.run(["git", "push"], cwd=self.repo_path, check=True)
104
+ except subprocess.CalledProcessError:
105
+ # 如果失败,可能是空仓库,尝试设置上游分支
106
+ try:
107
+ subprocess.run(
108
+ ["git", "push", "-u", "origin", "main"],
109
+ cwd=self.repo_path,
110
+ check=True,
111
+ )
112
+ except subprocess.CalledProcessError:
113
+ # 如果main分支不存在,尝试master分支
114
+ subprocess.run(
115
+ ["git", "push", "-u", "origin", "master"],
116
+ cwd=self.repo_path,
117
+ check=True,
118
+ )
119
+
120
+ def select_resources(self, resources: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
121
+ """让用户选择要分享的资源"""
122
+ # 显示可选的资源
123
+ resource_list = [
124
+ f"\n可分享的{self.get_resource_type()}(已排除中心仓库中已有的):"
125
+ ]
126
+ for i, resource in enumerate(resources, 1):
127
+ resource_list.append(f"[{i}] {self.format_resource_display(resource)}")
128
+
129
+ # 一次性打印所有资源
130
+ PrettyOutput.print("\n".join(resource_list), OutputType.INFO)
131
+
132
+ # 让用户选择
133
+ while True:
134
+ try:
135
+ choice_str = prompt(
136
+ f"\n请选择要分享的{self.get_resource_type()}编号(支持格式: 1,2,3,4-9,20 或 all):"
137
+ ).strip()
138
+ if choice_str == "0":
139
+ return []
140
+
141
+ if choice_str.lower() == "all":
142
+ return resources
143
+ else:
144
+ selected_indices = parse_selection(choice_str, len(resources))
145
+ if not selected_indices:
146
+ PrettyOutput.print("无效的选择", OutputType.WARNING)
147
+ continue
148
+ return [resources[i - 1] for i in selected_indices]
149
+
150
+ except ValueError:
151
+ PrettyOutput.print("请输入有效的数字", OutputType.WARNING)
152
+
153
+ @abstractmethod
154
+ def get_resource_type(self) -> str:
155
+ """获取资源类型名称"""
156
+ pass
157
+
158
+ @abstractmethod
159
+ def format_resource_display(self, resource: Dict[str, Any]) -> str:
160
+ """格式化资源显示"""
161
+ pass
162
+
163
+ @abstractmethod
164
+ def get_existing_resources(self) -> Any:
165
+ """获取中心仓库中已有的资源"""
166
+ pass
167
+
168
+ @abstractmethod
169
+ def get_local_resources(self) -> List[Dict[str, Any]]:
170
+ """获取本地资源"""
171
+ pass
172
+
173
+ @abstractmethod
174
+ def share_resources(self, resources: List[Dict[str, Any]]) -> List[str]:
175
+ """分享资源到中心仓库"""
176
+ pass
@@ -0,0 +1,126 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ 任务分析器模块
4
+ 负责处理任务分析和方法论生成功能
5
+ """
6
+ from typing import Optional
7
+
8
+ from jarvis.jarvis_utils.globals import get_interrupt, set_interrupt
9
+ from jarvis.jarvis_utils.input import user_confirm
10
+ from jarvis.jarvis_agent.prompts import TASK_ANALYSIS_PROMPT
11
+
12
+
13
+ class TaskAnalyzer:
14
+ """任务分析器,负责任务分析和满意度反馈处理"""
15
+
16
+ def __init__(self, agent):
17
+ """
18
+ 初始化任务分析器
19
+
20
+ 参数:
21
+ agent: Agent实例
22
+ """
23
+ self.agent = agent
24
+
25
+ def analysis_task(self, satisfaction_feedback: str = ""):
26
+ """分析任务并生成方法论"""
27
+ print("🔍 正在分析任务...")
28
+ try:
29
+ # 准备分析提示
30
+ self.agent.session.prompt = self._prepare_analysis_prompt(
31
+ satisfaction_feedback
32
+ )
33
+
34
+ if not self.agent.model:
35
+ raise RuntimeError("Model not initialized")
36
+
37
+ # 循环处理工具调用,直到没有工具调用为止
38
+ self._process_analysis_loop()
39
+
40
+ print("✅ 分析完成")
41
+ except Exception as e:
42
+ print("❌ 分析失败")
43
+
44
+ def _prepare_analysis_prompt(self, satisfaction_feedback: str) -> str:
45
+ """准备分析提示"""
46
+ analysis_prompt = TASK_ANALYSIS_PROMPT
47
+ if satisfaction_feedback:
48
+ analysis_prompt += satisfaction_feedback
49
+ return analysis_prompt
50
+
51
+ def _process_analysis_loop(self):
52
+ """处理分析循环"""
53
+ while True:
54
+ response = self.agent.model.chat_until_success(self.agent.session.prompt) # type: ignore
55
+ self.agent.session.prompt = ""
56
+
57
+ # 处理用户中断
58
+ if get_interrupt():
59
+ if not self._handle_analysis_interrupt(response):
60
+ break
61
+
62
+ # 执行工具调用
63
+ need_return, self.agent.session.prompt = self.agent._call_tools(response)
64
+
65
+ # 如果没有工具调用或者没有新的提示,退出循环
66
+ if not self.agent.session.prompt:
67
+ break
68
+
69
+ def _handle_analysis_interrupt(self, response: str) -> bool:
70
+ """处理分析过程中的用户中断
71
+
72
+ 返回:
73
+ bool: True 继续分析,False 退出分析
74
+ """
75
+ set_interrupt(False)
76
+ user_input = self.agent.multiline_inputer(
77
+ f"分析任务期间被中断,请输入用户干预信息:"
78
+ )
79
+
80
+ if not user_input:
81
+ # 用户输入为空,退出分析
82
+ return False
83
+
84
+ if self._has_tool_calls(response):
85
+ self.agent.session.prompt = self._handle_interrupt_with_tool_calls(
86
+ user_input
87
+ )
88
+ else:
89
+ self.agent.session.prompt = f"被用户中断,用户补充信息为:{user_input}"
90
+
91
+ return True
92
+
93
+ def _has_tool_calls(self, response: str) -> bool:
94
+ """检查响应中是否有工具调用"""
95
+ return any(
96
+ handler.can_handle(response) for handler in self.agent.output_handler
97
+ )
98
+
99
+ def _handle_interrupt_with_tool_calls(self, user_input: str) -> str:
100
+ """处理有工具调用时的中断"""
101
+ if user_confirm("检测到有工具调用,是否继续处理工具调用?", True):
102
+ return f"被用户中断,用户补充信息为:{user_input}\n\n用户同意继续工具调用。"
103
+ else:
104
+ return f"被用户中断,用户补充信息为:{user_input}\n\n检测到有工具调用,但被用户拒绝执行。请根据用户的补充信息重新考虑下一步操作。"
105
+
106
+ def collect_satisfaction_feedback(self, auto_completed: bool) -> str:
107
+ """收集满意度反馈"""
108
+ satisfaction_feedback = ""
109
+
110
+ if not auto_completed and self.agent.use_analysis:
111
+ if user_confirm("您对本次任务的完成是否满意?", True):
112
+ satisfaction_feedback = "\n\n用户对本次任务的完成表示满意。"
113
+ else:
114
+ feedback = self.agent.multiline_inputer(
115
+ "请提供您的反馈意见(可留空直接回车):"
116
+ )
117
+ if feedback:
118
+ satisfaction_feedback = (
119
+ f"\n\n用户对本次任务的完成不满意,反馈意见如下:\n{feedback}"
120
+ )
121
+ else:
122
+ satisfaction_feedback = (
123
+ "\n\n用户对本次任务的完成不满意,未提供具体反馈意见。"
124
+ )
125
+
126
+ return satisfaction_feedback
@@ -0,0 +1,111 @@
1
+ # -*- coding: utf-8 -*-
2
+ """任务管理模块,负责加载和选择预定义任务"""
3
+ import os
4
+ from typing import Dict
5
+
6
+ import yaml
7
+ from prompt_toolkit import prompt
8
+
9
+ from jarvis.jarvis_agent import (
10
+ OutputType,
11
+ PrettyOutput,
12
+ get_multiline_input,
13
+ user_confirm,
14
+ )
15
+ from jarvis.jarvis_utils.config import get_data_dir
16
+
17
+
18
+ class TaskManager:
19
+ """任务管理器,负责预定义任务的加载和选择"""
20
+
21
+ @staticmethod
22
+ def load_tasks() -> Dict[str, str]:
23
+ """Load tasks from .jarvis files in user home and current directory."""
24
+ tasks: Dict[str, str] = {}
25
+
26
+ # Check pre-command in data directory
27
+ data_dir = get_data_dir()
28
+ pre_command_path = os.path.join(data_dir, "pre-command")
29
+ if os.path.exists(pre_command_path):
30
+ print(f"🔍 从{pre_command_path}加载预定义任务...")
31
+ try:
32
+ with open(
33
+ pre_command_path, "r", encoding="utf-8", errors="ignore"
34
+ ) as f:
35
+ user_tasks = yaml.safe_load(f)
36
+ if isinstance(user_tasks, dict):
37
+ for name, desc in user_tasks.items():
38
+ if desc:
39
+ tasks[str(name)] = str(desc)
40
+ print(f"✅ 预定义任务加载完成 {pre_command_path}")
41
+ except (yaml.YAMLError, OSError):
42
+ print(f"❌ 预定义任务加载失败 {pre_command_path}")
43
+
44
+ # Check .jarvis/pre-command in current directory
45
+ pre_command_path = ".jarvis/pre-command"
46
+ if os.path.exists(pre_command_path):
47
+ abs_path = os.path.abspath(pre_command_path)
48
+ print(f"🔍 从{abs_path}加载预定义任务...")
49
+ try:
50
+ with open(
51
+ pre_command_path, "r", encoding="utf-8", errors="ignore"
52
+ ) as f:
53
+ local_tasks = yaml.safe_load(f)
54
+ if isinstance(local_tasks, dict):
55
+ for name, desc in local_tasks.items():
56
+ if desc:
57
+ tasks[str(name)] = str(desc)
58
+ print(f"✅ 预定义任务加载完成 {pre_command_path}")
59
+ except (yaml.YAMLError, OSError):
60
+ print(f"❌ 预定义任务加载失败 {pre_command_path}")
61
+
62
+ return tasks
63
+
64
+ @staticmethod
65
+ def select_task(tasks: Dict[str, str]) -> str:
66
+ """Let user select a task from the list or skip. Returns task description if selected."""
67
+ if not tasks:
68
+ return ""
69
+
70
+ task_names = list(tasks.keys())
71
+ task_list = ["可用任务:"]
72
+ for i, name in enumerate(task_names, 1):
73
+ task_list.append(f"[{i}] {name}")
74
+ task_list.append("[0] 跳过预定义任务")
75
+ PrettyOutput.print("\n".join(task_list), OutputType.INFO)
76
+
77
+ while True:
78
+ try:
79
+ choice_str = prompt(
80
+ "\n请选择一个任务编号(0 跳过预定义任务):"
81
+ ).strip()
82
+ if not choice_str:
83
+ return ""
84
+
85
+ choice = int(choice_str)
86
+ if choice == 0:
87
+ return ""
88
+ if 1 <= choice <= len(task_names):
89
+ selected_task = tasks[task_names[choice - 1]]
90
+ PrettyOutput.print(
91
+ f"将要执行任务:\n {selected_task}", OutputType.INFO
92
+ )
93
+ # 询问是否需要补充信息
94
+ need_additional = user_confirm(
95
+ "需要为此任务添加补充信息吗?", default=False
96
+ )
97
+ if need_additional:
98
+ additional_input = get_multiline_input("请输入补充信息:")
99
+ if additional_input:
100
+ selected_task = (
101
+ f"{selected_task}\n\n补充信息:\n{additional_input}"
102
+ )
103
+ return selected_task
104
+ PrettyOutput.print(
105
+ "无效的选择。请选择列表中的一个号码。", OutputType.WARNING
106
+ )
107
+
108
+ except (KeyboardInterrupt, EOFError):
109
+ return ""
110
+ except ValueError as val_err:
111
+ PrettyOutput.print(f"选择任务失败: {str(val_err)}", OutputType.ERROR)