pycoze 0.1.307__py3-none-any.whl → 0.1.334__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.
- pycoze/bot/__init__.py +1 -1
- pycoze/bot/chat.py +99 -0
- pycoze/bot/chat_base.py +247 -0
- pycoze/bot/lib.py +245 -0
- pycoze/bot/{agent/chat.py → message.py} +0 -1
- pycoze/bot/prompt.md +393 -0
- pycoze/bot/tools.py +281 -0
- pycoze/reference/lib.py +47 -13
- {pycoze-0.1.307.dist-info → pycoze-0.1.334.dist-info}/METADATA +1 -1
- {pycoze-0.1.307.dist-info → pycoze-0.1.334.dist-info}/RECORD +13 -16
- pycoze/bot/agent/__init__.py +0 -5
- pycoze/bot/agent/agent.py +0 -95
- pycoze/bot/agent/agent_types/__init__.py +0 -4
- pycoze/bot/agent/agent_types/const.py +0 -1
- pycoze/bot/agent/agent_types/openai_func_call_agent.py +0 -181
- pycoze/bot/agent/assistant.py +0 -35
- pycoze/bot/agent_chat.py +0 -110
- pycoze/bot/bot.py +0 -23
- {pycoze-0.1.307.dist-info → pycoze-0.1.334.dist-info}/LICENSE +0 -0
- {pycoze-0.1.307.dist-info → pycoze-0.1.334.dist-info}/WHEEL +0 -0
- {pycoze-0.1.307.dist-info → pycoze-0.1.334.dist-info}/top_level.txt +0 -0
pycoze/bot/__init__.py
CHANGED
pycoze/bot/chat.py
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
import json
|
2
|
+
from .chat_base import handle_user_inputs
|
3
|
+
from .lib import get_abilities, get_system_prompt
|
4
|
+
from .message import INPUT_MESSAGE, output, CHAT_DATA, clear_chat_data
|
5
|
+
import os
|
6
|
+
import asyncio
|
7
|
+
from pycoze import utils
|
8
|
+
import tempfile
|
9
|
+
|
10
|
+
|
11
|
+
async def check_interrupt_file(interval, interrupt_file, chat_task):
|
12
|
+
while True:
|
13
|
+
await asyncio.sleep(interval)
|
14
|
+
if os.path.exists(interrupt_file):
|
15
|
+
os.remove(interrupt_file)
|
16
|
+
chat_task.cancel()
|
17
|
+
break
|
18
|
+
|
19
|
+
|
20
|
+
async def run_with_interrupt_check(conversation_history, user_input, cwd: str, abilities, bot_setting, has_any_tool, interrupt_file):
|
21
|
+
clear_chat_data()
|
22
|
+
try:
|
23
|
+
chat_task = asyncio.create_task(
|
24
|
+
handle_user_inputs(conversation_history, user_input, cwd, abilities, bot_setting, has_any_tool)
|
25
|
+
)
|
26
|
+
check_task = asyncio.create_task(
|
27
|
+
check_interrupt_file(0.5, interrupt_file, chat_task)
|
28
|
+
)
|
29
|
+
result = await chat_task
|
30
|
+
return result
|
31
|
+
except asyncio.CancelledError:
|
32
|
+
return CHAT_DATA["info"]
|
33
|
+
except Exception as e:
|
34
|
+
import traceback
|
35
|
+
|
36
|
+
print(traceback.format_exc())
|
37
|
+
return None # 返回 None 或者处理异常后的结果
|
38
|
+
finally:
|
39
|
+
if not chat_task.done():
|
40
|
+
chat_task.cancel()
|
41
|
+
# 确保即使发生异常也会取消检查任务
|
42
|
+
if not check_task.done():
|
43
|
+
check_task.cancel()
|
44
|
+
try:
|
45
|
+
await check_task
|
46
|
+
except asyncio.CancelledError:
|
47
|
+
pass # 忽略取消错误
|
48
|
+
|
49
|
+
|
50
|
+
def chat(bot_setting_file: str):
|
51
|
+
with open(bot_setting_file, encoding="utf-8") as f:
|
52
|
+
bot_setting = json.load(f)
|
53
|
+
abilities = get_abilities(bot_setting)
|
54
|
+
cwd = tempfile.mkdtemp()
|
55
|
+
system_prompt, has_any_tool = get_system_prompt(abilities, bot_setting)
|
56
|
+
conversation_history = [
|
57
|
+
{
|
58
|
+
"role": "system",
|
59
|
+
"content": system_prompt,
|
60
|
+
}
|
61
|
+
]
|
62
|
+
while True:
|
63
|
+
clear_chat_data()
|
64
|
+
input_text = input()
|
65
|
+
if not input_text.startswith(INPUT_MESSAGE):
|
66
|
+
raise ValueError("Invalid message")
|
67
|
+
message = json.loads(input_text[len(INPUT_MESSAGE) :])
|
68
|
+
user_input = message["content"]
|
69
|
+
params = utils.params
|
70
|
+
if "interruptFile" in params:
|
71
|
+
asyncio.run(
|
72
|
+
run_with_interrupt_check(
|
73
|
+
conversation_history, user_input, cwd, abilities, bot_setting, has_any_tool, params["interruptFile"]
|
74
|
+
)
|
75
|
+
)
|
76
|
+
else:
|
77
|
+
asyncio.run(
|
78
|
+
handle_user_inputs(conversation_history, user_input, cwd, abilities, bot_setting, has_any_tool)
|
79
|
+
)
|
80
|
+
|
81
|
+
output("assistant", CHAT_DATA["info"])
|
82
|
+
|
83
|
+
|
84
|
+
def get_chat_response(bot_setting_file: str, user_input: str):
|
85
|
+
with open(bot_setting_file, encoding="utf-8") as f:
|
86
|
+
bot_setting = json.load(f)
|
87
|
+
abilities = get_abilities(bot_setting)
|
88
|
+
cwd = tempfile.mkdtemp()
|
89
|
+
conversation_history = [
|
90
|
+
{
|
91
|
+
"role": "system",
|
92
|
+
"content": get_system_prompt(abilities, bot_setting),
|
93
|
+
}
|
94
|
+
]
|
95
|
+
asyncio.run(
|
96
|
+
handle_user_inputs(conversation_history, user_input, cwd, abilities, bot_setting)
|
97
|
+
)
|
98
|
+
|
99
|
+
return CHAT_DATA["info"]
|
pycoze/bot/chat_base.py
ADDED
@@ -0,0 +1,247 @@
|
|
1
|
+
import json
|
2
|
+
import os
|
3
|
+
from .lib import get_formatted_filelist_str, read_local_file, resolve_relative_path
|
4
|
+
from .message import info
|
5
|
+
from pycoze.ai import chat_stream_async, extract
|
6
|
+
from .tools import ToolExecutor
|
7
|
+
from typing import List
|
8
|
+
|
9
|
+
|
10
|
+
def guess_files_in_message(cwd:str, user_message: str) -> List[str]:
|
11
|
+
|
12
|
+
value = extract({"includedFiles": ['relative path format', 'relative path format', '...']},
|
13
|
+
'Please find the files mentioned in the text. If none, return {"includedFiles": []}:\n' + user_message)
|
14
|
+
return [resolve_relative_path(cwd, p) for p in value["includedFiles"]]
|
15
|
+
|
16
|
+
|
17
|
+
def user_task_prompt(conversation_history, cwd, user_input: str, programmer_mode: bool):
|
18
|
+
if programmer_mode:
|
19
|
+
potential_paths = guess_files_in_message(cwd, user_input)
|
20
|
+
|
21
|
+
exist_files = get_formatted_filelist_str(cwd, True, 200)
|
22
|
+
content = []
|
23
|
+
for file_path in potential_paths:
|
24
|
+
file_path = resolve_relative_path(cwd, file_path)
|
25
|
+
if os.path.isfile(file_path):
|
26
|
+
file_marker = f"[[{file_path}]]'"
|
27
|
+
file_content = read_local_file(file_path)
|
28
|
+
if not any(file_marker in msg["content"] for msg in conversation_history):
|
29
|
+
content.append(f"{file_marker}\n{file_content}")
|
30
|
+
content_str = "Partial contents of files are as follows:" + "\n".join(content) if content else ""
|
31
|
+
return f"""
|
32
|
+
<task>
|
33
|
+
{user_input}
|
34
|
+
</task>
|
35
|
+
|
36
|
+
<environment_details>
|
37
|
+
Current working directory: {cwd}
|
38
|
+
|
39
|
+
List of files under path:
|
40
|
+
{exist_files}
|
41
|
+
|
42
|
+
{content_str}
|
43
|
+
|
44
|
+
</environment_details>
|
45
|
+
"""
|
46
|
+
else:
|
47
|
+
return f"""
|
48
|
+
<task>
|
49
|
+
{user_input}
|
50
|
+
</task>
|
51
|
+
"""
|
52
|
+
|
53
|
+
|
54
|
+
def dumps_markdown_json(data):
|
55
|
+
json_str = json.dumps(data, indent=4, ensure_ascii=False)
|
56
|
+
return f"\n```json\n{json_str}\n```\n"
|
57
|
+
|
58
|
+
|
59
|
+
def process_code_block(language: str, content: str):
|
60
|
+
"""
|
61
|
+
处理并清理代码块内容,返回格式化后的字符串
|
62
|
+
"""
|
63
|
+
if language and content:
|
64
|
+
# 清理代码块内容
|
65
|
+
cleaned_content = "\n".join(
|
66
|
+
line.strip() for line in content.split("\n") if line.strip()
|
67
|
+
)
|
68
|
+
return (language, cleaned_content)
|
69
|
+
return ""
|
70
|
+
|
71
|
+
|
72
|
+
async def stream_openai_response(conversation_history, start_new_stream):
|
73
|
+
"""
|
74
|
+
异步流式传输 OpenAI 聊天完成响应并处理结构化输出
|
75
|
+
"""
|
76
|
+
stream = None
|
77
|
+
buffer = ""
|
78
|
+
in_code_block = False
|
79
|
+
code_block_language = ""
|
80
|
+
code_block_content = ""
|
81
|
+
text_content = ""
|
82
|
+
|
83
|
+
while True:
|
84
|
+
# 检查是否需要重新创建流
|
85
|
+
if stream is None or start_new_stream["value"]:
|
86
|
+
if stream is not None:
|
87
|
+
await stream.aclose() # 关闭之前的流
|
88
|
+
stream = chat_stream_async(conversation_history) # 获取新的异步生成器
|
89
|
+
start_new_stream["value"] = False # 重置标志
|
90
|
+
buffer = ""
|
91
|
+
in_code_block = False
|
92
|
+
code_block_language = ""
|
93
|
+
code_block_content = ""
|
94
|
+
text_content = ""
|
95
|
+
|
96
|
+
# 使用 async for 迭代异步生成器
|
97
|
+
try:
|
98
|
+
async for chunk in stream:
|
99
|
+
info("assistant", chunk)
|
100
|
+
buffer += chunk
|
101
|
+
|
102
|
+
# 检查是否需要重新创建流
|
103
|
+
if start_new_stream["value"]:
|
104
|
+
break # 退出当前的 async for 循环,进入下一次 while 循环
|
105
|
+
|
106
|
+
# 处理 buffer 中的每一行
|
107
|
+
while "\n" in buffer:
|
108
|
+
line, buffer = buffer.split("\n", 1)
|
109
|
+
if not in_code_block:
|
110
|
+
if line.strip().startswith("```"):
|
111
|
+
if text_content:
|
112
|
+
yield ("text", text_content.strip())
|
113
|
+
text_content = ""
|
114
|
+
in_code_block = True
|
115
|
+
code_block_language = line.strip()[3:].strip()
|
116
|
+
else:
|
117
|
+
text_content += line + "\n"
|
118
|
+
else:
|
119
|
+
if line.strip().startswith("```"):
|
120
|
+
in_code_block = False
|
121
|
+
yield process_code_block(
|
122
|
+
code_block_language, code_block_content
|
123
|
+
)
|
124
|
+
code_block_content = ""
|
125
|
+
else:
|
126
|
+
code_block_content += line + "\n"
|
127
|
+
|
128
|
+
# 如果流正常结束,退出 while 循环
|
129
|
+
break
|
130
|
+
|
131
|
+
except Exception as e:
|
132
|
+
# 捕获其他异常(如网络错误)
|
133
|
+
print(f"Error: {e}", style="bold red")
|
134
|
+
break
|
135
|
+
|
136
|
+
# 处理 buffer 中剩余的内容
|
137
|
+
if buffer:
|
138
|
+
if in_code_block:
|
139
|
+
buffer = buffer.split("```")[0]
|
140
|
+
code_block_content += buffer + "\n"
|
141
|
+
yield process_code_block(code_block_language, code_block_content)
|
142
|
+
else:
|
143
|
+
text_content += buffer
|
144
|
+
if text_content:
|
145
|
+
yield ("text", text_content.strip())
|
146
|
+
|
147
|
+
|
148
|
+
async def handle_user_inputs(conversation_history, user_input, cwd, abilities, bot_setting, has_any_tool):
|
149
|
+
no_exit_if_incomplete = bot_setting["systemAbility"]['no_exit_if_incomplete']
|
150
|
+
programmer_mode = bot_setting["systemAbility"]['programmer_mode']
|
151
|
+
|
152
|
+
start_new_stream = {
|
153
|
+
"value": False
|
154
|
+
} # 当遇到AI准备执行JSON,即需要新信息的时候,用于强制停止当前stream,减少后续无效的tokens
|
155
|
+
|
156
|
+
print("Processing user command", user_input)
|
157
|
+
if user_input.lower() in ["exit", "quit"]:
|
158
|
+
exit(0)
|
159
|
+
# 将用户消息添加到对话历史
|
160
|
+
conversation_history.append(
|
161
|
+
{
|
162
|
+
"role": "user",
|
163
|
+
"content": user_task_prompt(conversation_history, cwd, user_input, programmer_mode),
|
164
|
+
}
|
165
|
+
)
|
166
|
+
need_break = False
|
167
|
+
|
168
|
+
if no_exit_if_incomplete:
|
169
|
+
okay_str = 'Okay, please continue. If the tasks within <task>...task content...</task> have been completed, execute the tool "complete_all_tasks". If you have a question, use "ask_follow_up_question".'
|
170
|
+
else:
|
171
|
+
okay_str = "Okay"
|
172
|
+
while True:
|
173
|
+
async for response in stream_openai_response(
|
174
|
+
conversation_history, start_new_stream
|
175
|
+
):
|
176
|
+
if len(response) == 2:
|
177
|
+
if response[0] == "text" and response[1].strip() != "" or (response[0] == "json" and not has_any_tool):
|
178
|
+
conversation_history.append(
|
179
|
+
{"role": "assistant", "content": response[1]}
|
180
|
+
)
|
181
|
+
conversation_history.append(
|
182
|
+
{
|
183
|
+
"role": "user",
|
184
|
+
"content": okay_str,
|
185
|
+
}
|
186
|
+
)
|
187
|
+
continue
|
188
|
+
elif response[0] == "json":
|
189
|
+
info("assistant", "\n")
|
190
|
+
cleaned_content = response[1]
|
191
|
+
try:
|
192
|
+
tool_request = json.loads(cleaned_content)
|
193
|
+
tool_name = list(tool_request.keys())[0]
|
194
|
+
except json.JSONDecodeError as e:
|
195
|
+
conversation_history.append(
|
196
|
+
{
|
197
|
+
"role": "assistant",
|
198
|
+
"content": f"\n```json\n{cleaned_content}\n```\n",
|
199
|
+
}
|
200
|
+
)
|
201
|
+
conversation_history.append(
|
202
|
+
{
|
203
|
+
"role": "user",
|
204
|
+
"content": "Invalid JSON content:" + str(e),
|
205
|
+
}
|
206
|
+
)
|
207
|
+
continue
|
208
|
+
|
209
|
+
ok, result = ToolExecutor.execute_tool(cwd, tool_request, abilities)
|
210
|
+
if ok:
|
211
|
+
info("assistant", "✅\n")
|
212
|
+
else:
|
213
|
+
info("assistant", "❌\n")
|
214
|
+
assistant_content = (
|
215
|
+
"Executing tool: "
|
216
|
+
+ dumps_markdown_json(tool_request)
|
217
|
+
+ "\n\nResult: "
|
218
|
+
+ result
|
219
|
+
)
|
220
|
+
info("assistant", "```text\n" + result + "\n```\n\n")
|
221
|
+
conversation_history.append(
|
222
|
+
{"role": "assistant", "content": assistant_content}
|
223
|
+
)
|
224
|
+
if tool_name in ["complete_all_tasks", 'ask_follow_up_question']:
|
225
|
+
need_break = True
|
226
|
+
break
|
227
|
+
else:
|
228
|
+
conversation_history.append(
|
229
|
+
{
|
230
|
+
"role": "user",
|
231
|
+
"content": okay_str,
|
232
|
+
}
|
233
|
+
)
|
234
|
+
start_new_stream["value"] = True
|
235
|
+
|
236
|
+
if need_break:
|
237
|
+
break
|
238
|
+
if not no_exit_if_incomplete and not start_new_stream["value"]:
|
239
|
+
break
|
240
|
+
|
241
|
+
|
242
|
+
# 示例调用
|
243
|
+
# user_input_list = [
|
244
|
+
# "访问https://api-docs.deepseek.com/zh-cn/guides/chat_prefix_completion,并结合它编写一段代码,并保存"
|
245
|
+
# ]
|
246
|
+
|
247
|
+
# asyncio.run(handle_user_inputs(user_input_list))
|
pycoze/bot/lib.py
ADDED
@@ -0,0 +1,245 @@
|
|
1
|
+
import os
|
2
|
+
from collections import defaultdict
|
3
|
+
import platform
|
4
|
+
from jinja2 import Template
|
5
|
+
from pathlib import Path
|
6
|
+
from pycoze.reference.bot import ref_bot
|
7
|
+
from pycoze.reference.tool import ref_tools
|
8
|
+
from pycoze.reference.workflow import ref_workflow
|
9
|
+
import inspect
|
10
|
+
|
11
|
+
|
12
|
+
def function_to_string(func):
|
13
|
+
# 获取函数签名
|
14
|
+
signature = inspect.signature(func)
|
15
|
+
|
16
|
+
# 获取函数名
|
17
|
+
func_name = func.__name__
|
18
|
+
|
19
|
+
# 获取函数的文档字符串
|
20
|
+
docstring = func.__doc__
|
21
|
+
|
22
|
+
# 格式化函数签名
|
23
|
+
func_signature = f"def {func_name}{signature}:"
|
24
|
+
|
25
|
+
# 构建最终的字符串
|
26
|
+
result = f"#### {func_name.capitalize().replace('_', ' ')}\n"
|
27
|
+
result += "**Function Signature:**\n"
|
28
|
+
result += f"```python\n{func_signature}\n \"\"\"{docstring}\n \"\"\"\n pass\n```\n\n"
|
29
|
+
result += "**Usage:**\n"
|
30
|
+
result += "```json\n"
|
31
|
+
result += f"{{\n \"{func_name}\": {{\n"
|
32
|
+
|
33
|
+
# 添加参数
|
34
|
+
for name, param in signature.parameters.items():
|
35
|
+
result += f" \"{name}\": \"{param.annotation.__name__ if param.annotation != inspect.Parameter.empty else 'value'}\"\n"
|
36
|
+
|
37
|
+
result += " }\n"
|
38
|
+
result += "}\n"
|
39
|
+
result += "```"
|
40
|
+
|
41
|
+
return "\n\n" + result + "\n\n"
|
42
|
+
|
43
|
+
def get_abilities(bot_setting):
|
44
|
+
abilities = []
|
45
|
+
for bot_id in bot_setting["bots"]:
|
46
|
+
bot = ref_bot(bot_id)
|
47
|
+
if bot:
|
48
|
+
abilities.append(bot)
|
49
|
+
for tool_id in bot_setting["tools"]:
|
50
|
+
abilities.extend(ref_tools(tool_id))
|
51
|
+
for workflow_id in bot_setting["workflows"]:
|
52
|
+
workflow = ref_workflow(workflow_id)
|
53
|
+
if workflow:
|
54
|
+
abilities.append(workflow)
|
55
|
+
return abilities
|
56
|
+
|
57
|
+
|
58
|
+
def get_system_prompt(abilities, bot_setting):
|
59
|
+
md_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), "prompt.md")
|
60
|
+
with open(md_file, encoding="utf-8") as f:
|
61
|
+
template = Template(f.read())
|
62
|
+
|
63
|
+
if platform.system() == "Windows":
|
64
|
+
cd_prompt = "When executing outside the working directory, include the CD command, such as cd /path/to/directory ; ls."
|
65
|
+
else:
|
66
|
+
cd_prompt = "When executing outside the working directory, include the CD command, such as cd /path/to/directory && ls."
|
67
|
+
system = platform.system()
|
68
|
+
|
69
|
+
|
70
|
+
abilities_str = "\n".join([function_to_string(a) for a in abilities])
|
71
|
+
|
72
|
+
context = {
|
73
|
+
"prompt": bot_setting["prompt"],
|
74
|
+
"system": system,
|
75
|
+
"cd_prompt": cd_prompt,
|
76
|
+
"abilities_str": abilities_str,
|
77
|
+
"programmer_mode": False,
|
78
|
+
"no_exit_if_incomplete": False,
|
79
|
+
"allow_read_file": False,
|
80
|
+
"allow_read_multiple_files": False,
|
81
|
+
"allow_execute_command": False,
|
82
|
+
"allow_write_or_overwrite_file": False,
|
83
|
+
"allow_replace_part_of_a_file": False,
|
84
|
+
"allow_search_files": False,
|
85
|
+
"allow_list_files": False,
|
86
|
+
"allow_access_webpage": False,
|
87
|
+
}
|
88
|
+
|
89
|
+
context.update(bot_setting["systemAbility"])
|
90
|
+
|
91
|
+
has_any_tool = False
|
92
|
+
if len(abilities) > 0:
|
93
|
+
has_any_tool = True
|
94
|
+
for key in bot_setting["systemAbility"]:
|
95
|
+
if key != "programmer_mode" and bot_setting["systemAbility"][key] == True:
|
96
|
+
has_any_tool = True
|
97
|
+
break
|
98
|
+
|
99
|
+
context["has_any_tool"] = has_any_tool
|
100
|
+
system_prompt = template.render(context)
|
101
|
+
|
102
|
+
return system_prompt, has_any_tool
|
103
|
+
|
104
|
+
|
105
|
+
def resolve_relative_path(cwd:str, path_str: str) -> str:
|
106
|
+
"""返回基于CWD的规范化绝对路径"""
|
107
|
+
path = Path(path_str)
|
108
|
+
if path.is_absolute():
|
109
|
+
return str(path.resolve())
|
110
|
+
else:
|
111
|
+
return str((Path(cwd) / path_str).resolve())
|
112
|
+
|
113
|
+
|
114
|
+
def read_local_file(file_path: str) -> str:
|
115
|
+
"""读取本地文件内容"""
|
116
|
+
try:
|
117
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
118
|
+
return f.read()
|
119
|
+
except:
|
120
|
+
import chardet
|
121
|
+
|
122
|
+
with open(file_path, "rb") as f:
|
123
|
+
rawdata = f.read()
|
124
|
+
encoding = chardet.detect(rawdata)["encoding"]
|
125
|
+
return rawdata.decode(encoding)
|
126
|
+
|
127
|
+
|
128
|
+
# 全局忽略列表
|
129
|
+
IGNORE_LIST = [
|
130
|
+
"node_modules",
|
131
|
+
".git",
|
132
|
+
".vscode",
|
133
|
+
".idea",
|
134
|
+
"gitServer",
|
135
|
+
".DS_Store",
|
136
|
+
"$RECYCLE.BIN",
|
137
|
+
".Trash-1000",
|
138
|
+
".Spotlight-V100",
|
139
|
+
".Trashes",
|
140
|
+
".TemporaryItems",
|
141
|
+
".fseventsd",
|
142
|
+
"System Volume Information",
|
143
|
+
"pycache",
|
144
|
+
"env",
|
145
|
+
"venv",
|
146
|
+
"target/dependency",
|
147
|
+
"build/dependencies",
|
148
|
+
"dist",
|
149
|
+
"out",
|
150
|
+
"bundle",
|
151
|
+
"vendor",
|
152
|
+
"tmp",
|
153
|
+
"temp",
|
154
|
+
"deps",
|
155
|
+
"pkg",
|
156
|
+
"Pods",
|
157
|
+
]
|
158
|
+
|
159
|
+
|
160
|
+
def should_ignore(path):
|
161
|
+
"""检查路径是否在忽略列表中"""
|
162
|
+
parts = path.split(os.sep)
|
163
|
+
return any(part in IGNORE_LIST for part in parts)
|
164
|
+
|
165
|
+
|
166
|
+
def get_files_and_folders(root, recursive: bool):
|
167
|
+
"""递归获取所有文件和文件夹,并记录属性"""
|
168
|
+
items = []
|
169
|
+
|
170
|
+
# 使用 os.walk 遍历目录
|
171
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
172
|
+
# 排除忽略列表中的路径
|
173
|
+
if should_ignore(dirpath):
|
174
|
+
continue
|
175
|
+
|
176
|
+
# 记录文件夹
|
177
|
+
for dirname in dirnames:
|
178
|
+
full_path = os.path.join(dirpath, dirname)
|
179
|
+
if not should_ignore(full_path):
|
180
|
+
relative_path = os.path.relpath(full_path, root)
|
181
|
+
is_empty = not os.listdir(full_path)
|
182
|
+
depth = relative_path.count(os.sep)
|
183
|
+
items.append(
|
184
|
+
(relative_path, "empty_folder" if is_empty else "folder", depth)
|
185
|
+
)
|
186
|
+
|
187
|
+
# 记录文件
|
188
|
+
for filename in filenames:
|
189
|
+
full_path = os.path.join(dirpath, filename)
|
190
|
+
if not should_ignore(full_path):
|
191
|
+
relative_path = os.path.relpath(full_path, root)
|
192
|
+
depth = relative_path.count(os.sep)
|
193
|
+
items.append((relative_path, "file", depth))
|
194
|
+
|
195
|
+
# 如果 recursive 为 False,则只遍历当前目录,不进入子目录
|
196
|
+
if not recursive:
|
197
|
+
break
|
198
|
+
|
199
|
+
return items
|
200
|
+
|
201
|
+
|
202
|
+
def format_filelist_str(items, limit):
|
203
|
+
"""根据limit格式化输出"""
|
204
|
+
depth_groups = defaultdict(list)
|
205
|
+
for item in items:
|
206
|
+
depth_groups[item[2]].append(item)
|
207
|
+
|
208
|
+
max_depth = max(depth_groups.keys(), default=0)
|
209
|
+
show_list = []
|
210
|
+
last_depth = 0
|
211
|
+
|
212
|
+
# 浅层
|
213
|
+
current_items = sorted(depth_groups[0], key=lambda x: x[0])
|
214
|
+
overflow = len(current_items) > limit
|
215
|
+
for item in current_items[:limit]:
|
216
|
+
show_list.append(item)
|
217
|
+
|
218
|
+
for depth in range(1, max_depth + 1):
|
219
|
+
current_items = depth_groups[depth]
|
220
|
+
if len(show_list) + len(current_items) <= limit:
|
221
|
+
last_depth = depth
|
222
|
+
for item in current_items:
|
223
|
+
show_list.append(item)
|
224
|
+
else:
|
225
|
+
break
|
226
|
+
|
227
|
+
result_str_list = []
|
228
|
+
show_list.sort(key=lambda x: x[0])
|
229
|
+
for item in show_list:
|
230
|
+
if item[1] == "file":
|
231
|
+
result_str_list.append(f"{item[0]}")
|
232
|
+
elif item[1] == "folder" and item[2] == last_depth:
|
233
|
+
result_str_list.append(f"{item[0]}/...more...")
|
234
|
+
else:
|
235
|
+
result_str_list.append(f"{item[0]}/")
|
236
|
+
if overflow:
|
237
|
+
result_str_list.append("...more...")
|
238
|
+
|
239
|
+
return "\n".join(result_str_list)
|
240
|
+
|
241
|
+
|
242
|
+
def get_formatted_filelist_str(root: str, recursive: bool, limit=200):
|
243
|
+
items = get_files_and_folders(root, recursive)
|
244
|
+
return format_filelist_str(items, limit=limit)
|
245
|
+
|