fp-core 0.1.1.dev0__tar.gz → 0.1.3__tar.gz
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.
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/PKG-INFO +1 -1
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/README.md +0 -0
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/pyproject.toml +0 -0
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/setup.cfg +0 -0
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core/__init__.py +1 -1
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core/commands/__init__.py +3 -3
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core/commands/back.py +0 -0
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core/commands/clear.py +0 -0
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core/commands/compact.py +0 -0
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core/commands/exit_bang.py +0 -0
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core/commands/exit_cmd.py +0 -0
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core/commands/fork.py +0 -0
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core/commands/help.py +0 -0
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core/commands/history.py +0 -0
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core/commands/memory_cmd.py +0 -0
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core/commands/model.py +0 -0
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core/commands/resume.py +0 -0
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core/commands/session.py +0 -0
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core/config.py +16 -11
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core/core/__init__.py +0 -0
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core/core/agent.py +3 -3
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core/core/conversation.py +0 -0
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core/core/io.py +0 -0
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core/core/lifecycle.py +0 -0
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core/core/llm_client.py +0 -0
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core/core/llm_service.py +0 -0
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core/core/prompt_builder.py +17 -6
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core/core/session.py +0 -0
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core/core/tool_executor.py +0 -0
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core/display.py +0 -0
- fp_core-0.1.3/src/fp_core/platform_utils.py +175 -0
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core/plugins/base/__init__.py +0 -0
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core/plugins/base/plugin.py +0 -0
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core/prompts/__init__.py +0 -0
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core/prompts/agent.md +13 -0
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core/prompts/agent.py +0 -0
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core/skills/__init__.py +0 -0
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core/skills/loader.py +4 -3
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core/skills/modify_skills.md +0 -0
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core/skills/python_syntax_check.md +0 -0
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core/skills/self_modification.md +0 -0
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core/skills/subagent.md +0 -0
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core/tools/__init__.py +4 -3
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core/tools/core.py +47 -9
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core/tools/plugins/__init__.py +0 -0
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core/tools/plugins/memory_read_plugin.py +0 -0
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core/tools/plugins/memory_save_plugin.py +0 -0
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core/tools/plugins/python_plugin.py +2 -1
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core/tools/plugins/subagent_plugin.py +1 -1
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core/tools/plugins/task_clear_plugin.py +0 -0
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core/tools/plugins/task_create_plugin.py +0 -0
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core/tools/plugins/task_list_plugin.py +0 -0
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core/tools/plugins/task_update_plugin.py +0 -0
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core/tools/plugins/web_fetch_plugin.py +0 -0
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core/tools/plugins/web_search_plugin.py +0 -0
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core.egg-info/PKG-INFO +1 -1
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core.egg-info/SOURCES.txt +1 -0
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core.egg-info/dependency_links.txt +0 -0
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core.egg-info/requires.txt +0 -0
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/src/fp_core.egg-info/top_level.txt +0 -0
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/tests/__init__.py +0 -0
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/tests/conftest.py +0 -0
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/tests/test_config.py +0 -0
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/tests/test_conversation.py +0 -0
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/tests/test_session.py +0 -0
- {fp_core-0.1.1.dev0 → fp_core-0.1.3}/tests/test_skills.py +2 -2
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -11,6 +11,7 @@ import importlib.util
|
|
|
11
11
|
import os
|
|
12
12
|
|
|
13
13
|
from fp_core import display
|
|
14
|
+
from fp_core.platform_utils import get_data_dir
|
|
14
15
|
|
|
15
16
|
# 缓存:命令名 → 模块对象
|
|
16
17
|
_commands: dict[str, "CommandModule"] = {}
|
|
@@ -36,9 +37,8 @@ def _discover_commands():
|
|
|
36
37
|
builtin_dir = os.path.dirname(os.path.abspath(__file__))
|
|
37
38
|
_scan_dir(builtin_dir, "fp_core.commands")
|
|
38
39
|
|
|
39
|
-
#
|
|
40
|
-
|
|
41
|
-
user_dir = os.path.join(_xdg_data, "fp", "commands")
|
|
40
|
+
# 用户命令目录(跨平台:Linux ~/.local/share/fp/commands, Windows %LOCALAPPDATA%/fp/commands)
|
|
41
|
+
user_dir = os.path.join(get_data_dir(), "commands")
|
|
42
42
|
_scan_dir(user_dir) # 直接 import 路径,通过 sys.path 解析
|
|
43
43
|
|
|
44
44
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -8,9 +8,10 @@ import os
|
|
|
8
8
|
import sys
|
|
9
9
|
from typing import Any
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
from fp_core.platform_utils import get_config_dir, get_data_dir, is_windows
|
|
12
|
+
|
|
13
|
+
# 用户配置(跨平台:Linux XDG 标准 / Windows %APPDATA%)
|
|
14
|
+
USER_CONFIG_PATH = os.path.join(get_config_dir(), "config.json")
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
def _load_json_config() -> dict:
|
|
@@ -147,15 +148,14 @@ MEMORY_MAX_HISTORY: int = _value("MEMORY_MAX_HISTORY", 100)
|
|
|
147
148
|
|
|
148
149
|
|
|
149
150
|
# ═══════════════════════════════════════════════════════════════
|
|
150
|
-
#
|
|
151
|
+
# 路径配置(跨平台:Linux XDG 标准 / Windows %APPDATA%)
|
|
151
152
|
# ═══════════════════════════════════════════════════════════════
|
|
152
153
|
|
|
153
|
-
|
|
154
|
-
_XDG_DATA_FP = os.path.join(_XDG_DATA_HOME, "fp")
|
|
154
|
+
_FP_DATA_DIR = get_data_dir()
|
|
155
155
|
|
|
156
|
-
SESSIONS_DIR = os.path.join(
|
|
157
|
-
MEMORY_DIR = os.path.join(
|
|
158
|
-
TASKS_FILE = os.path.join(
|
|
156
|
+
SESSIONS_DIR = os.path.join(_FP_DATA_DIR, "sessions")
|
|
157
|
+
MEMORY_DIR = os.path.join(_FP_DATA_DIR, "memory")
|
|
158
|
+
TASKS_FILE = os.path.join(_FP_DATA_DIR, "tasks.json")
|
|
159
159
|
PROMPTS_DIR = os.path.join(os.path.dirname(__file__), "prompts")
|
|
160
160
|
SKILLS_DIR = os.path.join(os.path.dirname(__file__), "skills")
|
|
161
161
|
|
|
@@ -200,7 +200,12 @@ def color_supported() -> bool:
|
|
|
200
200
|
return True
|
|
201
201
|
if os.environ.get("NO_COLOR"):
|
|
202
202
|
return False
|
|
203
|
-
|
|
203
|
+
|
|
204
|
+
if is_windows():
|
|
205
|
+
# Windows 上委托 platform_utils 做更精细的检测
|
|
206
|
+
from fp_core.platform_utils import ansi_supported
|
|
207
|
+
|
|
208
|
+
return ansi_supported()
|
|
204
209
|
|
|
205
210
|
return sys.stdout.isatty()
|
|
206
211
|
|
|
@@ -301,7 +306,7 @@ def get_default_config() -> dict:
|
|
|
301
306
|
|
|
302
307
|
|
|
303
308
|
def init_config(path: str | None = None):
|
|
304
|
-
"""
|
|
309
|
+
"""初始化用户配置文件(跨平台:Linux ~/.config/fp/,Windows %APPDATA%/fp/)"""
|
|
305
310
|
config_path = path or USER_CONFIG_PATH
|
|
306
311
|
if os.path.exists(config_path):
|
|
307
312
|
print(f"[Config] {config_path} already exists")
|
|
File without changes
|
|
@@ -27,6 +27,7 @@ from fp_core.core.lifecycle import HookContext, LifecycleHook, LifecycleManager
|
|
|
27
27
|
from fp_core.core.llm_service import LLMConfig, LLMService
|
|
28
28
|
from fp_core.core.prompt_builder import PromptBuilder
|
|
29
29
|
from fp_core.core.tool_executor import ToolExecutor
|
|
30
|
+
from fp_core.platform_utils import get_data_dir
|
|
30
31
|
from fp_core.plugins.base.plugin import PluginRegistry
|
|
31
32
|
|
|
32
33
|
# ── 上下文 local IO 通道(防并发竞态) ─────────────────
|
|
@@ -143,9 +144,8 @@ class Agent:
|
|
|
143
144
|
plugin_dir=os.path.normpath(_builtin_plugin_dir),
|
|
144
145
|
)
|
|
145
146
|
|
|
146
|
-
#
|
|
147
|
-
|
|
148
|
-
_user_plugin_dir = os.path.join(_xdg_data, "fp", "plugins")
|
|
147
|
+
# 用户插件目录(跨平台,同名覆盖)
|
|
148
|
+
_user_plugin_dir = os.path.join(get_data_dir(), "plugins")
|
|
149
149
|
if os.path.isdir(_user_plugin_dir):
|
|
150
150
|
self.plugins.scan(_user_plugin_dir)
|
|
151
151
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -11,6 +11,8 @@ import os
|
|
|
11
11
|
from datetime import datetime
|
|
12
12
|
from typing import Any
|
|
13
13
|
|
|
14
|
+
from fp_core.platform_utils import check_git_bash, is_windows, platform
|
|
15
|
+
|
|
14
16
|
|
|
15
17
|
class PromptBuilder:
|
|
16
18
|
"""系统提示词构建器"""
|
|
@@ -55,12 +57,21 @@ class PromptBuilder:
|
|
|
55
57
|
except Exception:
|
|
56
58
|
current_user = "unknown"
|
|
57
59
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
当前时间: {current_time}
|
|
61
|
-
当前路径: {current_path}
|
|
62
|
-
当前用户: {current_user}
|
|
63
|
-
""
|
|
60
|
+
# ── 运行时环境信息 ──
|
|
61
|
+
runtime_parts = [
|
|
62
|
+
f"当前时间: {current_time}",
|
|
63
|
+
f"当前路径: {current_path}",
|
|
64
|
+
f"当前用户: {current_user}",
|
|
65
|
+
f"当前平台: {platform()}",
|
|
66
|
+
]
|
|
67
|
+
if is_windows():
|
|
68
|
+
git_ok, git_msg = check_git_bash()
|
|
69
|
+
if git_ok:
|
|
70
|
+
runtime_parts.append("Git Bash: 可用 ✓(写 Unix 命令:ls/grep/awk)")
|
|
71
|
+
else:
|
|
72
|
+
runtime_parts.append("Git Bash: 不可用 ✗(使用 cmd.exe 回退,写 Windows 命令:dir/type/findstr)")
|
|
73
|
+
runtime_info = "\n".join(runtime_parts)
|
|
74
|
+
state_info = f"\n## 当前时间,路径等状态信息\n{runtime_info}\n"
|
|
64
75
|
parts.append(state_info)
|
|
65
76
|
|
|
66
77
|
# 长期记忆索引(name + type 摘要,不含正文)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""
|
|
2
|
+
platform_utils.py - 跨平台工具集
|
|
3
|
+
|
|
4
|
+
单一真相源(Single Source of Truth),所有模块都通过此模块获取:
|
|
5
|
+
- 平台类型(linux / windows / darwin)
|
|
6
|
+
- 数据目录(XDG_DATA_HOME ~/.local/share <-> %APPDATA%)
|
|
7
|
+
- 配置目录(XDG_CONFIG_HOME ~/.config <-> %APPDATA%)
|
|
8
|
+
- Git Bash 定位(Windows 专用)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import shutil
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
# ═══════════════════════════════════════════════════════════════
|
|
16
|
+
# 平台检测
|
|
17
|
+
# ═══════════════════════════════════════════════════════════════
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def platform() -> str:
|
|
21
|
+
"""返回标准平台名:linux / windows / darwin"""
|
|
22
|
+
if sys.platform == "win32":
|
|
23
|
+
return "windows"
|
|
24
|
+
if sys.platform == "darwin":
|
|
25
|
+
return "darwin"
|
|
26
|
+
return "linux"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def is_windows() -> bool:
|
|
30
|
+
"""是否为 Windows 系统"""
|
|
31
|
+
return sys.platform == "win32"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def is_linux() -> bool:
|
|
35
|
+
"""是否为 Linux 系统"""
|
|
36
|
+
return sys.platform == "linux"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ═══════════════════════════════════════════════════════════════
|
|
40
|
+
# 路径工具(跨平台 XDG 等价物)
|
|
41
|
+
# ═══════════════════════════════════════════════════════════════
|
|
42
|
+
|
|
43
|
+
# 参考规范:
|
|
44
|
+
# Linux: XDG_CONFIG_HOME (默认 ~/.config)
|
|
45
|
+
# XDG_DATA_HOME (默认 ~/.local/share)
|
|
46
|
+
# Windows: APPDATA (C:/Users/<user>/AppData/Roaming)
|
|
47
|
+
# LOCALAPPDATA (C:/Users/<user>/AppData/Local)
|
|
48
|
+
# macOS: ~/Library/Application Support
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def get_config_dir() -> str:
|
|
52
|
+
"""获取跨平台用户配置目录
|
|
53
|
+
|
|
54
|
+
Linux: $XDG_CONFIG_HOME/fp -> ~/.config/fp
|
|
55
|
+
Windows: %APPDATA%/fp -> C:/Users/<user>/AppData/Roaming/fp
|
|
56
|
+
macOS: ~/Library/Application Support/fp
|
|
57
|
+
"""
|
|
58
|
+
if is_windows():
|
|
59
|
+
base = os.environ.get("APPDATA", os.path.join(os.path.expanduser("~"), "AppData", "Roaming"))
|
|
60
|
+
elif sys.platform == "darwin":
|
|
61
|
+
base = os.path.join(os.path.expanduser("~"), "Library", "Application Support")
|
|
62
|
+
else:
|
|
63
|
+
base = os.environ.get("XDG_CONFIG_HOME", os.path.join(os.path.expanduser("~"), ".config"))
|
|
64
|
+
return os.path.join(base, "fp")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def get_data_dir() -> str:
|
|
68
|
+
"""获取跨平台用户数据目录
|
|
69
|
+
|
|
70
|
+
Linux: $XDG_DATA_HOME/fp -> ~/.local/share/fp
|
|
71
|
+
Windows: %LOCALAPPDATA%/fp -> C:/Users/<user>/AppData/Local/fp
|
|
72
|
+
macOS: ~/Library/Application Support/fp
|
|
73
|
+
"""
|
|
74
|
+
if is_windows():
|
|
75
|
+
base = os.environ.get("LOCALAPPDATA", os.path.join(os.path.expanduser("~"), "AppData", "Local"))
|
|
76
|
+
elif sys.platform == "darwin":
|
|
77
|
+
base = os.path.join(os.path.expanduser("~"), "Library", "Application Support")
|
|
78
|
+
else:
|
|
79
|
+
base = os.environ.get("XDG_DATA_HOME", os.path.join(os.path.expanduser("~"), ".local", "share"))
|
|
80
|
+
return os.path.join(base, "fp")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# ═══════════════════════════════════════════════════════════════
|
|
84
|
+
# Git Bash 定位(Windows)
|
|
85
|
+
# ═══════════════════════════════════════════════════════════════
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def find_bash() -> str | None:
|
|
89
|
+
"""在 Windows 上找到 Git Bash 的 bash.exe,找不到返回 None
|
|
90
|
+
|
|
91
|
+
搜索路径优先级:
|
|
92
|
+
1. PATH 中的 bash(Git Bash 安装时默认添加)
|
|
93
|
+
2. 常见安装路径
|
|
94
|
+
3. Git for Windows SDK 路径
|
|
95
|
+
|
|
96
|
+
Linux/macOS 上始终返回 "bash"(系统自带)。
|
|
97
|
+
"""
|
|
98
|
+
if not is_windows():
|
|
99
|
+
return "bash" # Unix 系统直接用系统 bash
|
|
100
|
+
|
|
101
|
+
# 1. PATH 中查找
|
|
102
|
+
resolved = shutil.which("bash")
|
|
103
|
+
if resolved:
|
|
104
|
+
return resolved
|
|
105
|
+
|
|
106
|
+
# 2. 常见安装路径
|
|
107
|
+
common_paths = [
|
|
108
|
+
os.path.join("C:", os.sep, "Program Files", "Git", "bin", "bash.exe"),
|
|
109
|
+
os.path.join("C:", os.sep, "Program Files (x86)", "Git", "bin", "bash.exe"),
|
|
110
|
+
os.path.join(os.path.expanduser("~"), "AppData", "Local", "Programs", "Git", "bin", "bash.exe"),
|
|
111
|
+
# MSYS2
|
|
112
|
+
os.path.join("C:", os.sep, "msys64", "usr", "bin", "bash.exe"),
|
|
113
|
+
os.path.join("C:", os.sep, "msys32", "usr", "bin", "bash.exe"),
|
|
114
|
+
]
|
|
115
|
+
for path in common_paths:
|
|
116
|
+
if os.path.isfile(path):
|
|
117
|
+
return path
|
|
118
|
+
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def check_git_bash() -> tuple[bool, str]:
|
|
123
|
+
"""检查 Git Bash 是否可用(Windows 专用),返回 (可用?, 消息)
|
|
124
|
+
|
|
125
|
+
Linux/macOS 上始终返回 (True, "系统自带 bash")。
|
|
126
|
+
"""
|
|
127
|
+
if not is_windows():
|
|
128
|
+
return True, "系统自带 bash"
|
|
129
|
+
|
|
130
|
+
bash_path = find_bash()
|
|
131
|
+
if bash_path:
|
|
132
|
+
return True, f"已找到 Git Bash: {bash_path}"
|
|
133
|
+
return False, "未找到 Git Bash。请安装 Git for Windows (https://git-scm.com) 并确保将 Git Bash 添加到 PATH"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# ═══════════════════════════════════════════════════════════════
|
|
137
|
+
# ANSI 颜色支持检测
|
|
138
|
+
# ═══════════════════════════════════════════════════════════════
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def ansi_supported() -> bool:
|
|
142
|
+
"""检测终端是否支持 ANSI 转义码
|
|
143
|
+
|
|
144
|
+
Linux/macOS: 始终 True(终端原生支持)
|
|
145
|
+
Windows: Windows 10+ 的 Windows Terminal / VS Code 集成终端 支持;
|
|
146
|
+
cmd.exe 仅在 10+ 版本 + ENABLE_VIRTUAL_TERMINAL_PROCESSING 启用时支持
|
|
147
|
+
"""
|
|
148
|
+
# 强制开关优先
|
|
149
|
+
if os.environ.get("FORCE_COLOR"):
|
|
150
|
+
return True
|
|
151
|
+
if os.environ.get("NO_COLOR"):
|
|
152
|
+
return False
|
|
153
|
+
|
|
154
|
+
# 非交互式终端不支持
|
|
155
|
+
if not sys.stdout.isatty():
|
|
156
|
+
return False
|
|
157
|
+
|
|
158
|
+
if is_windows():
|
|
159
|
+
# Windows 10 build 14393+ 通过虚拟终端处理支持 ANSI
|
|
160
|
+
# 但我们保守检测:如果在 Windows Terminal 或 VS Code 终端中,返回 True
|
|
161
|
+
term = os.environ.get("TERM_PROGRAM", "").lower()
|
|
162
|
+
if term in ("vscode", "microsoft-terminal", "windows-terminal"):
|
|
163
|
+
return True
|
|
164
|
+
# 检查 ConEmu / Cmder
|
|
165
|
+
if os.environ.get("CONEMUANSI") == "ON":
|
|
166
|
+
return True
|
|
167
|
+
# 兜底:使用 colorama 的 stream 检测
|
|
168
|
+
try:
|
|
169
|
+
import colorama.ansitowin32 # type: ignore[import-untyped]
|
|
170
|
+
|
|
171
|
+
return colorama.ansitowin32.is_ansi_enabled()
|
|
172
|
+
except ImportError:
|
|
173
|
+
return False
|
|
174
|
+
|
|
175
|
+
return True
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -45,3 +45,16 @@
|
|
|
45
45
|
- 开始执行任务时,请用 task_update 将状态改为 in_progress
|
|
46
46
|
- 任务完成后,请用 task_update 将状态改为 completed
|
|
47
47
|
- 在回复中引用任务时带上 #ID 方便用户追踪
|
|
48
|
+
|
|
49
|
+
【跨平台兼容】
|
|
50
|
+
- 我同时支持 Linux 和 Windows。
|
|
51
|
+
- **bash 工具**:
|
|
52
|
+
- Linux/macOS:直接由系统 shell 执行,写 Unix 命令(`ls`/`grep`/`cat`)
|
|
53
|
+
- Windows + Git Bash 可用:自动路由到 Git Bash 的 `bash.exe`,写 Unix 命令(`ls`/`grep`/`awk`)
|
|
54
|
+
- Windows + 无 Git Bash:降级到 `cmd.exe`,**写 Windows 命令**(`dir`/`type`/`findstr`)
|
|
55
|
+
- 运行时环境信息(平台 + Git Bash 状态)会自动注入到提示词的状态信息区,注意查看。
|
|
56
|
+
- **路径兼容**:配置/会话/记忆等数据目录自动适配——
|
|
57
|
+
- Linux: `~/.config/fp/` + `~/.local/share/fp/`
|
|
58
|
+
- Windows: `%APPDATA%/fp/` + `%LOCALAPPDATA%/fp/`
|
|
59
|
+
- **KDE 专属技能**(KDE Connect 通知/壁纸等)仅在 Linux/KDE 桌面环境可用,Windows 上调用会返回不可用提示。
|
|
60
|
+
- **ANSI 颜色**:Windows Terminal / VS Code 终端原生支持;旧版 cmd.exe 可能禁用颜色输出。
|
|
File without changes
|
|
File without changes
|
|
@@ -7,11 +7,12 @@ import os
|
|
|
7
7
|
import re
|
|
8
8
|
from dataclasses import dataclass, field
|
|
9
9
|
|
|
10
|
+
from fp_core.platform_utils import get_data_dir
|
|
11
|
+
|
|
10
12
|
BUILTIN_SKILLS_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
11
13
|
|
|
12
|
-
#
|
|
13
|
-
|
|
14
|
-
USER_SKILLS_DIR = os.path.join(_XDG_DATA_HOME, "fp", "skills")
|
|
14
|
+
# 用户技能目录(跨平台:Linux ~/.local/share/fp/skills, Windows %LOCALAPPDATA%/fp/skills, 同名技能覆盖内置)
|
|
15
|
+
USER_SKILLS_DIR = os.path.join(get_data_dir(), "skills")
|
|
15
16
|
|
|
16
17
|
# 模板变量:agent 主入口文件路径(用于自修改等技能)
|
|
17
18
|
AGENT_ENTRY = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "cli.py")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -18,6 +18,8 @@ import os
|
|
|
18
18
|
from collections.abc import Callable
|
|
19
19
|
from typing import Any
|
|
20
20
|
|
|
21
|
+
from fp_core.platform_utils import get_data_dir
|
|
22
|
+
|
|
21
23
|
|
|
22
24
|
class ToolRegistry:
|
|
23
25
|
"""工具注册表,管理所有核心工具和插件"""
|
|
@@ -41,9 +43,8 @@ class ToolRegistry:
|
|
|
41
43
|
builtin_dir = os.path.join(os.path.dirname(__file__), "plugins")
|
|
42
44
|
self._load_from_dir(builtin_dir, "fp_core.tools.plugins")
|
|
43
45
|
|
|
44
|
-
#
|
|
45
|
-
|
|
46
|
-
user_dir = os.path.join(_xdg_data, "fp", "tools", "plugins")
|
|
46
|
+
# 用户工具目录(跨平台:Linux ~/.local/share/fp/tools/plugins, Windows %LOCALAPPDATA%/fp/tools/plugins)
|
|
47
|
+
user_dir = os.path.join(get_data_dir(), "tools", "plugins")
|
|
47
48
|
self._load_from_dir(user_dir)
|
|
48
49
|
|
|
49
50
|
def _load_from_dir(self, directory: str, package_prefix: str | None = None):
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
核心工具模块 — 不可插件化的基础工具(全异步版本)
|
|
3
3
|
|
|
4
4
|
这些工具必须保持直接绑定,作为系统的基础设施:
|
|
5
|
-
- bash: Shell
|
|
5
|
+
- bash: Shell 命令执行(跨平台:Linux 用 bash,Windows 用 Git Bash)
|
|
6
6
|
- read_file: 文件读取
|
|
7
7
|
- write_file: 文件写入
|
|
8
8
|
- edit_file: 精确文本替换
|
|
@@ -12,6 +12,8 @@ import asyncio
|
|
|
12
12
|
import os
|
|
13
13
|
from typing import Any
|
|
14
14
|
|
|
15
|
+
from fp_core.platform_utils import find_bash, is_windows
|
|
16
|
+
|
|
15
17
|
# ── 核心工具定义(OpenAI function calling schema) ──────────────────────
|
|
16
18
|
|
|
17
19
|
CORE_TOOL_DEFINITIONS = [
|
|
@@ -19,7 +21,11 @@ CORE_TOOL_DEFINITIONS = [
|
|
|
19
21
|
"type": "function",
|
|
20
22
|
"function": {
|
|
21
23
|
"name": "bash",
|
|
22
|
-
"description":
|
|
24
|
+
"description": (
|
|
25
|
+
"执行 shell 命令,支持管道/重定向。"
|
|
26
|
+
"跨平台:Linux 原生执行,Windows 自动路由到 Git Bash 或降级 cmd.exe。"
|
|
27
|
+
"超时 300 秒。"
|
|
28
|
+
),
|
|
23
29
|
"parameters": {
|
|
24
30
|
"type": "object",
|
|
25
31
|
"properties": {
|
|
@@ -85,16 +91,45 @@ def get_core_definitions() -> list:
|
|
|
85
91
|
|
|
86
92
|
|
|
87
93
|
async def _execute_bash(command: str) -> str:
|
|
88
|
-
"""异步执行 shell 命令
|
|
94
|
+
"""异步执行 shell 命令
|
|
95
|
+
|
|
96
|
+
跨平台策略:
|
|
97
|
+
- Linux/macOS: 使用 asyncio.create_subprocess_shell,由系统 shell 执行
|
|
98
|
+
- Windows + Git Bash: 使用 bash.exe -c,Unix 命令(ls/grep/awk)可用
|
|
99
|
+
- Windows + 无 Git Bash: 降级到 cmd.exe /c,Windows 命令(dir/type/findstr)可用
|
|
100
|
+
"""
|
|
89
101
|
if not command:
|
|
90
102
|
raise ValueError("bash 工具需要 command 参数")
|
|
91
103
|
|
|
104
|
+
# ── 跨平台路由:选择正确的 shell 执行方式 ──
|
|
105
|
+
cmd_prefix = "" # 输出前缀,cmd.exe 回退时标记
|
|
92
106
|
try:
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
107
|
+
if is_windows():
|
|
108
|
+
bash_path = find_bash()
|
|
109
|
+
if bash_path:
|
|
110
|
+
# Git Bash 可用 → 走 bash.exe,Unix 命令
|
|
111
|
+
proc = await asyncio.create_subprocess_exec(
|
|
112
|
+
bash_path,
|
|
113
|
+
"-c",
|
|
114
|
+
command,
|
|
115
|
+
stdout=asyncio.subprocess.PIPE,
|
|
116
|
+
stderr=asyncio.subprocess.PIPE,
|
|
117
|
+
)
|
|
118
|
+
else:
|
|
119
|
+
# Git Bash 不可用 → 降级到 cmd.exe,Windows 命令
|
|
120
|
+
proc = await asyncio.create_subprocess_shell(
|
|
121
|
+
command,
|
|
122
|
+
stdout=asyncio.subprocess.PIPE,
|
|
123
|
+
stderr=asyncio.subprocess.PIPE,
|
|
124
|
+
)
|
|
125
|
+
cmd_prefix = "[cmd.exe 回退] "
|
|
126
|
+
else:
|
|
127
|
+
# Linux/macOS → 系统 shell
|
|
128
|
+
proc = await asyncio.create_subprocess_shell(
|
|
129
|
+
command,
|
|
130
|
+
stdout=asyncio.subprocess.PIPE,
|
|
131
|
+
stderr=asyncio.subprocess.PIPE,
|
|
132
|
+
)
|
|
98
133
|
|
|
99
134
|
try:
|
|
100
135
|
stdout, stderr = await asyncio.wait_for(
|
|
@@ -106,7 +141,6 @@ async def _execute_bash(command: str) -> str:
|
|
|
106
141
|
await proc.wait()
|
|
107
142
|
return "错误:命令执行超时(300秒)"
|
|
108
143
|
except (KeyboardInterrupt, asyncio.CancelledError):
|
|
109
|
-
# 中断时 kill 子进程并重新抛出
|
|
110
144
|
proc.kill()
|
|
111
145
|
await proc.wait()
|
|
112
146
|
raise
|
|
@@ -121,6 +155,10 @@ async def _execute_bash(command: str) -> str:
|
|
|
121
155
|
if len(output) > 10000:
|
|
122
156
|
output = output[:10000] + f"\n...(已截断,原文 {len(output)} 字符)"
|
|
123
157
|
|
|
158
|
+
# cmd.exe 回退模式下加前缀告知 LLM
|
|
159
|
+
if cmd_prefix and output.strip():
|
|
160
|
+
output = cmd_prefix + output.lstrip()
|
|
161
|
+
|
|
124
162
|
return output
|
|
125
163
|
except Exception as e:
|
|
126
164
|
return f"错误:{e}"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -7,6 +7,7 @@ Python 插件 — 执行 Python 代码(异步版本)
|
|
|
7
7
|
import asyncio
|
|
8
8
|
import contextlib
|
|
9
9
|
import os
|
|
10
|
+
import sys
|
|
10
11
|
import tempfile
|
|
11
12
|
from typing import Any
|
|
12
13
|
|
|
@@ -63,7 +64,7 @@ async def execute(params: dict[str, Any]) -> str:
|
|
|
63
64
|
|
|
64
65
|
# 异步执行
|
|
65
66
|
proc = await asyncio.create_subprocess_exec(
|
|
66
|
-
|
|
67
|
+
sys.executable,
|
|
67
68
|
tmp_path,
|
|
68
69
|
stdout=asyncio.subprocess.PIPE,
|
|
69
70
|
stderr=asyncio.subprocess.PIPE,
|
|
@@ -181,7 +181,7 @@ async def execute(params: dict[str, Any]) -> str:
|
|
|
181
181
|
|
|
182
182
|
# ═══════════════════════════════════════════════════════════
|
|
183
183
|
# 定位入口 — 通过 python -m fp_cli.main 启动子进程
|
|
184
|
-
# (不依赖文件系统路径,fp-
|
|
184
|
+
# (不依赖文件系统路径,fp-terminal 必须已安装)
|
|
185
185
|
# ═══════════════════════════════════════════════════════════
|
|
186
186
|
entry = [sys.executable, "-m", "fp_cli.main", "-m"]
|
|
187
187
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -106,9 +106,9 @@ class TestSkillLoader:
|
|
|
106
106
|
)
|
|
107
107
|
|
|
108
108
|
# 创建同名用户技能
|
|
109
|
-
|
|
109
|
+
from fp_core.skills.loader import USER_SKILLS_DIR
|
|
110
110
|
|
|
111
|
-
user_dir =
|
|
111
|
+
user_dir = USER_SKILLS_DIR
|
|
112
112
|
os.makedirs(user_dir, exist_ok=True)
|
|
113
113
|
user_file = os.path.join(user_dir, f"{builtin_name}.md")
|
|
114
114
|
with open(user_file, "w", encoding="utf-8") as f:
|