fr-cli 2.2.3__tar.gz → 2.2.5__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.
- {fr_cli-2.2.3/fr_cli.egg-info → fr_cli-2.2.5}/PKG-INFO +3 -3
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/__init__.py +1 -1
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/agent/master.py +1 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/core/chat.py +27 -16
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/core/intent.py +1 -1
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/core/llm.py +19 -19
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/main.py +2 -1
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/repl/commands.py +112 -12
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/weapon/mcp.py +0 -2
- {fr_cli-2.2.3 → fr_cli-2.2.5/fr_cli.egg-info}/PKG-INFO +3 -3
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli.egg-info/requires.txt +2 -2
- {fr_cli-2.2.3 → fr_cli-2.2.5}/pyproject.toml +3 -3
- {fr_cli-2.2.3 → fr_cli-2.2.5}/LICENSE +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/MANIFEST.in +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/README.md +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/README.md +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/WEAPON.MD +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/addon/plugin.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/agent/__init__.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/agent/builtins/__init__.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/agent/builtins/_utils.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/agent/builtins/db.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/agent/builtins/local.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/agent/builtins/rag.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/agent/builtins/rag_watcher_daemon.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/agent/builtins/remote.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/agent/builtins/spider.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/agent/client.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/agent/executor.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/agent/generator.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/agent/manager.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/agent/master_prompt.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/agent/remote.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/agent/server.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/agent/workflow.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/breakthrough/update.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/command/__init__.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/command/executor.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/command/registry.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/command/security.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/conf/config.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/conf/wizard.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/core/core.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/core/recommender.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/core/stream.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/core/sysmon.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/core/thinking.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/gatekeeper/__init__.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/gatekeeper/daemon.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/gatekeeper/manager.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/lang/i18n.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/memory/context.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/memory/history.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/memory/session.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/repl/__init__.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/security/security.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/ui/ui.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/weapon/cron.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/weapon/dataframe.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/weapon/disk.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/weapon/fs.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/weapon/launcher.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/weapon/loader.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/weapon/mail.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/weapon/vision.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli/weapon/web.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli.egg-info/SOURCES.txt +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli.egg-info/dependency_links.txt +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli.egg-info/entry_points.txt +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/fr_cli.egg-info/top_level.txt +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/setup.cfg +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/tests/test_agent_client.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/tests/test_agent_server.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/tests/test_ai_save_file_with_verify.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/tests/test_all.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/tests/test_auto_session.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/tests/test_builtins.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/tests/test_dataframe.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/tests/test_gatekeeper.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/tests/test_integration.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/tests/test_intent_classification.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/tests/test_launcher.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/tests/test_master_agent.py +0 -0
- {fr_cli-2.2.3 → fr_cli-2.2.5}/tests/test_structured_tools.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fr-cli
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.5
|
|
4
4
|
Summary: 凡人打字机 - 支持多模型(Zhipu/DeepSeek/Kimi/Qwen/StepFun/MiniMax/Spark)的终极全能终端工具
|
|
5
5
|
Author: FANREN CLI Author
|
|
6
6
|
License-Expression: MIT
|
|
@@ -37,7 +37,7 @@ Requires-Dist: pyodbc>=4.0.0; extra == "db"
|
|
|
37
37
|
Requires-Dist: oracledb>=1.3.0; extra == "db"
|
|
38
38
|
Provides-Extra: rag
|
|
39
39
|
Requires-Dist: chromadb>=0.4.0; extra == "rag"
|
|
40
|
-
Requires-Dist: sentence-transformers>=2.2.
|
|
40
|
+
Requires-Dist: sentence-transformers>=2.2.5; extra == "rag"
|
|
41
41
|
Provides-Extra: remote
|
|
42
42
|
Requires-Dist: paramiko>=3.0.0; extra == "remote"
|
|
43
43
|
Provides-Extra: spider
|
|
@@ -56,7 +56,7 @@ Requires-Dist: psycopg2-binary>=2.9.0; extra == "all"
|
|
|
56
56
|
Requires-Dist: pyodbc>=4.0.0; extra == "all"
|
|
57
57
|
Requires-Dist: oracledb>=1.3.0; extra == "all"
|
|
58
58
|
Requires-Dist: chromadb>=0.4.0; extra == "all"
|
|
59
|
-
Requires-Dist: sentence-transformers>=2.2.
|
|
59
|
+
Requires-Dist: sentence-transformers>=2.2.5; extra == "all"
|
|
60
60
|
Requires-Dist: paramiko>=3.0.0; extra == "all"
|
|
61
61
|
Requires-Dist: selenium>=4.10.0; extra == "all"
|
|
62
62
|
Requires-Dist: bypy; extra == "all"
|
|
@@ -20,6 +20,7 @@ from fr_cli.memory.context import extract_recent_turns, build_context_summary, s
|
|
|
20
20
|
from fr_cli.memory.session import create_session, update_session
|
|
21
21
|
from fr_cli.addon.plugin import extract_code, PLUGIN_DIR
|
|
22
22
|
from fr_cli.ui.ui import RED, YELLOW, GREEN, DIM, RESET
|
|
23
|
+
from fr_cli.lang.i18n import T
|
|
23
24
|
|
|
24
25
|
MASTER_DIR = Path.home() / ".fr_cli_master"
|
|
25
26
|
PERSONA_FILE = MASTER_DIR / "persona.md"
|
|
@@ -20,6 +20,28 @@ from fr_cli.memory.session import create_session, update_session
|
|
|
20
20
|
from fr_cli.core.intent import should_force_tool, classify_intent, has_info_fetch_intent, has_save_intent
|
|
21
21
|
|
|
22
22
|
|
|
23
|
+
def _fetch_mcp_tools(mcp_manager):
|
|
24
|
+
"""安全获取 MCP 工具列表"""
|
|
25
|
+
if mcp_manager and hasattr(mcp_manager, "list_all_tools"):
|
|
26
|
+
try:
|
|
27
|
+
tools = mcp_manager.list_all_tools()
|
|
28
|
+
return tools if isinstance(tools, list) else []
|
|
29
|
+
except Exception:
|
|
30
|
+
pass
|
|
31
|
+
return []
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _fetch_mcp_desc(mcp_manager):
|
|
35
|
+
"""安全获取 MCP 工具描述文本"""
|
|
36
|
+
if mcp_manager and hasattr(mcp_manager, "get_server_tools_desc"):
|
|
37
|
+
try:
|
|
38
|
+
desc = mcp_manager.get_server_tools_desc()
|
|
39
|
+
return desc if isinstance(desc, str) and desc else ""
|
|
40
|
+
except Exception:
|
|
41
|
+
pass
|
|
42
|
+
return ""
|
|
43
|
+
|
|
44
|
+
|
|
23
45
|
def handle_ai_chat(state, u):
|
|
24
46
|
"""处理 AI 正常对话流程"""
|
|
25
47
|
lang = state.lang
|
|
@@ -31,14 +53,7 @@ def handle_ai_chat(state, u):
|
|
|
31
53
|
tools = get_available_tools(state.weapon_tools, state.plugins)
|
|
32
54
|
# 将 MCP 外部神通纳入意图判定视野
|
|
33
55
|
mcp_manager = getattr(state, "mcp", None)
|
|
34
|
-
mcp_tools_summary =
|
|
35
|
-
if mcp_manager and hasattr(mcp_manager, "list_all_tools"):
|
|
36
|
-
try:
|
|
37
|
-
_mcp_tools = mcp_manager.list_all_tools()
|
|
38
|
-
if isinstance(_mcp_tools, list):
|
|
39
|
-
mcp_tools_summary = _mcp_tools
|
|
40
|
-
except Exception:
|
|
41
|
-
pass
|
|
56
|
+
mcp_tools_summary = _fetch_mcp_tools(mcp_manager)
|
|
42
57
|
if mcp_tools_summary:
|
|
43
58
|
tools.append({
|
|
44
59
|
"name": "mcp_tools",
|
|
@@ -74,14 +89,10 @@ def handle_ai_chat(state, u):
|
|
|
74
89
|
tools_info += f"{i}. {tool['name']}: {tool['description']}\n 可用命令: {', '.join(tool['commands'])}\n"
|
|
75
90
|
# 注入 MCP 外部神通
|
|
76
91
|
mcp_manager = getattr(state, "mcp", None)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
tools_info += mcp_desc + "\n"
|
|
82
|
-
tools_info += "\n调用 MCP 工具时,请使用格式:【调用:mcp_call({\"server\": \"服务器名\", \"tool\": \"工具名\", \"arguments\": {...}})】\n"
|
|
83
|
-
except Exception:
|
|
84
|
-
pass
|
|
92
|
+
mcp_desc = _fetch_mcp_desc(mcp_manager)
|
|
93
|
+
if mcp_desc:
|
|
94
|
+
tools_info += mcp_desc + "\n"
|
|
95
|
+
tools_info += "\n调用 MCP 工具时,请使用格式:【调用:mcp_call({\"server\": \"服务器名\", \"tool\": \"工具名\", \"arguments\": {...}})】\n"
|
|
85
96
|
# 信息获取规范:当用户需要调用外部信息源时,采用双源回答模式
|
|
86
97
|
if has_info_fetch_intent(u):
|
|
87
98
|
tools_info += """\n
|
|
@@ -22,6 +22,18 @@ class BaseLLMClient(ABC):
|
|
|
22
22
|
"""
|
|
23
23
|
pass
|
|
24
24
|
|
|
25
|
+
@staticmethod
|
|
26
|
+
def _yield_chunks(response) -> Iterator[dict]:
|
|
27
|
+
"""通用 chunk 解析生成器,供各子类复用"""
|
|
28
|
+
for chunk in response:
|
|
29
|
+
content = ""
|
|
30
|
+
usage = None
|
|
31
|
+
if chunk.choices and chunk.choices[0].delta:
|
|
32
|
+
content = chunk.choices[0].delta.content or ""
|
|
33
|
+
if hasattr(chunk, 'usage') and chunk.usage:
|
|
34
|
+
usage = chunk.usage.model_dump() if hasattr(chunk.usage, 'model_dump') else vars(chunk.usage)
|
|
35
|
+
yield {"content": content, "usage": usage}
|
|
36
|
+
|
|
25
37
|
|
|
26
38
|
class ZhipuLLMClient(BaseLLMClient):
|
|
27
39
|
"""智谱 AI 客户端 (zhipuai SDK)"""
|
|
@@ -38,14 +50,7 @@ class ZhipuLLMClient(BaseLLMClient):
|
|
|
38
50
|
stream=True,
|
|
39
51
|
max_tokens=max_tokens,
|
|
40
52
|
)
|
|
41
|
-
|
|
42
|
-
content = ""
|
|
43
|
-
usage = None
|
|
44
|
-
if chunk.choices and chunk.choices[0].delta:
|
|
45
|
-
content = chunk.choices[0].delta.content or ""
|
|
46
|
-
if hasattr(chunk, 'usage') and chunk.usage:
|
|
47
|
-
usage = chunk.usage.model_dump() if hasattr(chunk.usage, 'model_dump') else vars(chunk.usage)
|
|
48
|
-
yield {"content": content, "usage": usage}
|
|
53
|
+
yield from self._yield_chunks(response)
|
|
49
54
|
|
|
50
55
|
|
|
51
56
|
class OpenAICompatibleClient(BaseLLMClient):
|
|
@@ -66,14 +71,7 @@ class OpenAICompatibleClient(BaseLLMClient):
|
|
|
66
71
|
stream=True,
|
|
67
72
|
max_tokens=max_tokens,
|
|
68
73
|
)
|
|
69
|
-
|
|
70
|
-
content = ""
|
|
71
|
-
usage = None
|
|
72
|
-
if chunk.choices and chunk.choices[0].delta:
|
|
73
|
-
content = chunk.choices[0].delta.content or ""
|
|
74
|
-
if hasattr(chunk, 'usage') and chunk.usage:
|
|
75
|
-
usage = chunk.usage.model_dump() if hasattr(chunk.usage, 'model_dump') else vars(chunk.usage)
|
|
76
|
-
yield {"content": content, "usage": usage}
|
|
74
|
+
yield from self._yield_chunks(response)
|
|
77
75
|
|
|
78
76
|
|
|
79
77
|
# 提供商配置表
|
|
@@ -140,13 +138,15 @@ def create_llm_client(cfg: dict):
|
|
|
140
138
|
pcfg = providers_cfg.get(provider, {})
|
|
141
139
|
|
|
142
140
|
# 向后兼容:如果 providers 中没有当前 provider,从顶层读取 key/model
|
|
143
|
-
|
|
141
|
+
# 使用 'or' 确保空字符串也能正确回退到顶层 key
|
|
142
|
+
api_key = pcfg.get("key") or cfg.get("key", "")
|
|
144
143
|
default_model = _PROVIDERS.get(provider, _PROVIDERS["zhipu"])["default_model"]
|
|
145
|
-
model = pcfg.get("model"
|
|
144
|
+
model = pcfg.get("model") or cfg.get("model", default_model)
|
|
146
145
|
|
|
147
146
|
info = _PROVIDERS.get(provider, _PROVIDERS["zhipu"])
|
|
148
147
|
client_class = info["client_class"]
|
|
149
|
-
|
|
148
|
+
# 优先使用用户自定义的 base_url,其次使用内置默认
|
|
149
|
+
base_url = pcfg.get("base_url") or info.get("base_url")
|
|
150
150
|
|
|
151
151
|
kwargs = {"api_key": api_key}
|
|
152
152
|
if base_url:
|
|
@@ -30,7 +30,6 @@ def _sync_manual_to_workspace(vfs):
|
|
|
30
30
|
return
|
|
31
31
|
manual_dst = Path(vfs.cwd) / "MANUAL.md"
|
|
32
32
|
if not manual_dst.exists():
|
|
33
|
-
import shutil
|
|
34
33
|
shutil.copy2(manual_src, manual_dst)
|
|
35
34
|
except Exception:
|
|
36
35
|
pass
|
|
@@ -89,6 +88,7 @@ from fr_cli.repl.commands import (
|
|
|
89
88
|
_cmd_read_excel,
|
|
90
89
|
_cmd_read_csv,
|
|
91
90
|
_cmd_master,
|
|
91
|
+
_cmd_providers,
|
|
92
92
|
_cmd_mcp_list,
|
|
93
93
|
_cmd_mcp_add,
|
|
94
94
|
_cmd_mcp_del,
|
|
@@ -147,6 +147,7 @@ _COMMAND_ROUTES = {
|
|
|
147
147
|
"/read_excel": _cmd_read_excel,
|
|
148
148
|
"/read_csv": _cmd_read_csv,
|
|
149
149
|
"/master": _cmd_master,
|
|
150
|
+
"/providers": _cmd_providers,
|
|
150
151
|
"/mcp_list": _cmd_mcp_list,
|
|
151
152
|
"/mcp_add": _cmd_mcp_add,
|
|
152
153
|
"/mcp_del": _cmd_mcp_del,
|
|
@@ -10,7 +10,7 @@ from fr_cli.ui.ui import (
|
|
|
10
10
|
print_bye
|
|
11
11
|
)
|
|
12
12
|
from fr_cli.memory.history import save_sess, load_sess, del_sess, get_sessions
|
|
13
|
-
from fr_cli.memory.context import load_context
|
|
13
|
+
from fr_cli.memory.context import load_context, extract_recent_turns, build_context_summary, save_context
|
|
14
14
|
from fr_cli.memory.session import (
|
|
15
15
|
list_sessions as list_auto_sessions,
|
|
16
16
|
load_session as load_auto_session,
|
|
@@ -18,6 +18,7 @@ from fr_cli.memory.session import (
|
|
|
18
18
|
)
|
|
19
19
|
from fr_cli.addon.plugin import extract_code
|
|
20
20
|
from fr_cli.core.stream import stream_cnt
|
|
21
|
+
from fr_cli.core.sysmon import get_sys_stats
|
|
21
22
|
from fr_cli.agent.manager import (
|
|
22
23
|
create_agent_dir, save_agent_code, save_persona, save_skills,
|
|
23
24
|
save_memory, agent_exists, list_agents, delete_agent,
|
|
@@ -26,6 +27,16 @@ from fr_cli.agent.manager import (
|
|
|
26
27
|
from fr_cli.agent.executor import run_agent
|
|
27
28
|
|
|
28
29
|
|
|
30
|
+
def _provider_has_key(state, provider_id):
|
|
31
|
+
"""检查指定道统是否已配置 API Key(zhipu 向后兼容顶层 key)"""
|
|
32
|
+
providers_cfg = state.cfg.get("providers", {})
|
|
33
|
+
pcfg = providers_cfg.get(provider_id, {})
|
|
34
|
+
has_key = bool(pcfg.get("key"))
|
|
35
|
+
if not has_key and provider_id == "zhipu":
|
|
36
|
+
has_key = bool(state.cfg.get("key", ""))
|
|
37
|
+
return has_key
|
|
38
|
+
|
|
39
|
+
|
|
29
40
|
def _print_help(state, topic):
|
|
30
41
|
"""打印修仙指南"""
|
|
31
42
|
topic_map = {
|
|
@@ -104,13 +115,7 @@ def _cmd_model(state, parts):
|
|
|
104
115
|
if ok:
|
|
105
116
|
print(f"{GREEN}✅ 已切换: [{state.provider}] {state.model_name}{RESET}")
|
|
106
117
|
# 检查新道统是否已配置 API Key,若未配置则引导输入
|
|
107
|
-
|
|
108
|
-
pcfg = providers_cfg.get(state.provider, {})
|
|
109
|
-
# 只检查该道统自己专属的 key;zhipu 向后兼容:未单独配置时回退顶层 key
|
|
110
|
-
has_key = bool(pcfg.get("key"))
|
|
111
|
-
if not has_key and state.provider == "zhipu":
|
|
112
|
-
has_key = bool(state.cfg.get("key", ""))
|
|
113
|
-
if not has_key:
|
|
118
|
+
if not _provider_has_key(state, state.provider):
|
|
114
119
|
print(f"{YELLOW}⚠️ [{state.provider}] 尚未配置 API Key{RESET}")
|
|
115
120
|
k = input(f"👉 请输入 [{state.provider}] 的 API Key: ").strip()
|
|
116
121
|
if k:
|
|
@@ -128,10 +133,7 @@ def _cmd_model(state, parts):
|
|
|
128
133
|
providers_cfg = state.cfg.get("providers", {})
|
|
129
134
|
for p in list_providers():
|
|
130
135
|
marker = " 👈 当前" if p["id"] == state.provider else ""
|
|
131
|
-
|
|
132
|
-
has_key = bool(pcfg.get("key"))
|
|
133
|
-
if not has_key and p["id"] == "zhipu":
|
|
134
|
-
has_key = bool(state.cfg.get("key", ""))
|
|
136
|
+
has_key = _provider_has_key(state, p["id"])
|
|
135
137
|
key_status = f"{GREEN}✅ 已配置{RESET}" if has_key else f"{RED}❌ 未配置{RESET}"
|
|
136
138
|
print(f" {CYAN}{p['id']}{RESET} — {p['name']}{DIM} (默认: {p['default_model']}){RESET} {key_status}{marker}")
|
|
137
139
|
print(f"\n{DIM}用法:{RESET}")
|
|
@@ -176,6 +178,104 @@ def _cmd_key(state, parts):
|
|
|
176
178
|
return False
|
|
177
179
|
|
|
178
180
|
|
|
181
|
+
def _cmd_providers(state, parts):
|
|
182
|
+
"""
|
|
183
|
+
多模型道统配置管理
|
|
184
|
+
用法:
|
|
185
|
+
/providers — 查看所有道统配置
|
|
186
|
+
/providers add <道统> <key> [模型] — 添加/更新道统配置
|
|
187
|
+
/providers del <道统> — 删除道统配置
|
|
188
|
+
/providers use <道统> — 切换到指定道统
|
|
189
|
+
"""
|
|
190
|
+
sub = parts[1] if len(parts) > 1 else ""
|
|
191
|
+
arg1 = parts[2] if len(parts) > 2 else ""
|
|
192
|
+
arg2 = parts[3] if len(parts) > 3 else ""
|
|
193
|
+
|
|
194
|
+
providers_cfg = state.cfg.setdefault("providers", {})
|
|
195
|
+
|
|
196
|
+
if not sub or sub == "list":
|
|
197
|
+
from fr_cli.core.llm import list_providers, get_provider_info
|
|
198
|
+
print(f"{CYAN}📜 道统配置总览{RESET}")
|
|
199
|
+
for p in list_providers():
|
|
200
|
+
has_key = _provider_has_key(state, p["id"])
|
|
201
|
+
key_status = f"{GREEN}✅{RESET}" if has_key else f"{RED}❌{RESET}"
|
|
202
|
+
model = pcfg.get("model", p["default_model"])
|
|
203
|
+
info = get_provider_info(p["id"])
|
|
204
|
+
base_url = pcfg.get("base_url") or info.get("base_url", "默认")
|
|
205
|
+
active = f" {YELLOW}👈 当前使用{RESET}" if p["id"] == state.provider else ""
|
|
206
|
+
print(f"\n {key_status} {CYAN}{p['id']}{RESET} — {p['name']}{active}")
|
|
207
|
+
print(f" 模型: {DIM}{model}{RESET}")
|
|
208
|
+
print(f" 接口: {DIM}{base_url}{RESET}")
|
|
209
|
+
if has_key:
|
|
210
|
+
raw_key = pcfg.get("key", state.cfg.get("key", ""))
|
|
211
|
+
key_display = raw_key[:8] + "****" if len(raw_key) > 8 else raw_key
|
|
212
|
+
print(f" Key: {DIM}{key_display}{RESET}")
|
|
213
|
+
print(f"\n{DIM}用法:{RESET}")
|
|
214
|
+
print(f" /providers add <道统> <key> [模型] — 添加/更新道统配置")
|
|
215
|
+
print(f" /providers del <道统> — 删除道统配置")
|
|
216
|
+
print(f" /providers use <道统> — 切换到指定道统")
|
|
217
|
+
return False
|
|
218
|
+
|
|
219
|
+
if sub == "add":
|
|
220
|
+
if not arg1 or not arg2:
|
|
221
|
+
print(f"{RED}❌ 用法: /providers add <道统> <key> [模型]{RESET}")
|
|
222
|
+
return False
|
|
223
|
+
provider_id = arg1
|
|
224
|
+
from fr_cli.core.llm import get_provider_info
|
|
225
|
+
info = get_provider_info(provider_id)
|
|
226
|
+
if not info:
|
|
227
|
+
print(f"{RED}❌ 无效道统: {provider_id}{RESET}")
|
|
228
|
+
return False
|
|
229
|
+
pcfg = providers_cfg.setdefault(provider_id, {})
|
|
230
|
+
pcfg["key"] = arg2
|
|
231
|
+
model = parts[4] if len(parts) > 4 else info["default_model"]
|
|
232
|
+
pcfg["model"] = model
|
|
233
|
+
# 支持自定义 base_url: /providers add <provider> <key> [model] --base-url <url>
|
|
234
|
+
for i, token in enumerate(parts):
|
|
235
|
+
if token in ("--base-url", "--base_url") and i + 1 < len(parts):
|
|
236
|
+
pcfg["base_url"] = parts[i + 1]
|
|
237
|
+
break
|
|
238
|
+
state.cfg["providers"] = providers_cfg
|
|
239
|
+
state.save_cfg()
|
|
240
|
+
extra = f" 自定义接口={pcfg.get('base_url')}" if pcfg.get("base_url") else ""
|
|
241
|
+
print(f"{GREEN}✅ [{provider_id}] 配置已更新: 模型={model}{extra}{RESET}")
|
|
242
|
+
return False
|
|
243
|
+
|
|
244
|
+
if sub == "del":
|
|
245
|
+
if not arg1:
|
|
246
|
+
print(f"{RED}❌ 用法: /providers del <道统>{RESET}")
|
|
247
|
+
return False
|
|
248
|
+
if arg1 in providers_cfg:
|
|
249
|
+
del providers_cfg[arg1]
|
|
250
|
+
state.cfg["providers"] = providers_cfg
|
|
251
|
+
state.save_cfg()
|
|
252
|
+
print(f"{GREEN}✅ [{arg1}] 配置已删除{RESET}")
|
|
253
|
+
else:
|
|
254
|
+
print(f"{YELLOW}⚠️ [{arg1}] 无配置可删除{RESET}")
|
|
255
|
+
return False
|
|
256
|
+
|
|
257
|
+
if sub == "use":
|
|
258
|
+
if not arg1:
|
|
259
|
+
print(f"{RED}❌ 用法: /providers use <道统>{RESET}")
|
|
260
|
+
return False
|
|
261
|
+
ok = state.update_provider(arg1)
|
|
262
|
+
if ok:
|
|
263
|
+
print(f"{GREEN}✅ 已切换到: [{state.provider}] {state.model_name}{RESET}")
|
|
264
|
+
# 检查新道统是否已配置 API Key
|
|
265
|
+
if not _provider_has_key(state, state.provider):
|
|
266
|
+
print(f"{YELLOW}⚠️ [{state.provider}] 尚未配置 API Key{RESET}")
|
|
267
|
+
k = input(f"👉 请输入 [{state.provider}] 的 API Key: ").strip()
|
|
268
|
+
if k:
|
|
269
|
+
state.update_key(k)
|
|
270
|
+
print(f"{GREEN}✅ [{state.provider}] API Key 已保存{RESET}")
|
|
271
|
+
else:
|
|
272
|
+
print(f"{RED}❌ 无效道统: {arg1}{RESET}")
|
|
273
|
+
return False
|
|
274
|
+
|
|
275
|
+
print(f"{RED}❌ 未知子命令: {sub}{RESET}")
|
|
276
|
+
return False
|
|
277
|
+
|
|
278
|
+
|
|
179
279
|
def _cmd_limit(state, parts):
|
|
180
280
|
arg1 = parts[1] if len(parts) > 1 else ""
|
|
181
281
|
if arg1:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fr-cli
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.5
|
|
4
4
|
Summary: 凡人打字机 - 支持多模型(Zhipu/DeepSeek/Kimi/Qwen/StepFun/MiniMax/Spark)的终极全能终端工具
|
|
5
5
|
Author: FANREN CLI Author
|
|
6
6
|
License-Expression: MIT
|
|
@@ -37,7 +37,7 @@ Requires-Dist: pyodbc>=4.0.0; extra == "db"
|
|
|
37
37
|
Requires-Dist: oracledb>=1.3.0; extra == "db"
|
|
38
38
|
Provides-Extra: rag
|
|
39
39
|
Requires-Dist: chromadb>=0.4.0; extra == "rag"
|
|
40
|
-
Requires-Dist: sentence-transformers>=2.2.
|
|
40
|
+
Requires-Dist: sentence-transformers>=2.2.5; extra == "rag"
|
|
41
41
|
Provides-Extra: remote
|
|
42
42
|
Requires-Dist: paramiko>=3.0.0; extra == "remote"
|
|
43
43
|
Provides-Extra: spider
|
|
@@ -56,7 +56,7 @@ Requires-Dist: psycopg2-binary>=2.9.0; extra == "all"
|
|
|
56
56
|
Requires-Dist: pyodbc>=4.0.0; extra == "all"
|
|
57
57
|
Requires-Dist: oracledb>=1.3.0; extra == "all"
|
|
58
58
|
Requires-Dist: chromadb>=0.4.0; extra == "all"
|
|
59
|
-
Requires-Dist: sentence-transformers>=2.2.
|
|
59
|
+
Requires-Dist: sentence-transformers>=2.2.5; extra == "all"
|
|
60
60
|
Requires-Dist: paramiko>=3.0.0; extra == "all"
|
|
61
61
|
Requires-Dist: selenium>=4.10.0; extra == "all"
|
|
62
62
|
Requires-Dist: bypy; extra == "all"
|
|
@@ -11,7 +11,7 @@ psycopg2-binary>=2.9.0
|
|
|
11
11
|
pyodbc>=4.0.0
|
|
12
12
|
oracledb>=1.3.0
|
|
13
13
|
chromadb>=0.4.0
|
|
14
|
-
sentence-transformers>=2.2.
|
|
14
|
+
sentence-transformers>=2.2.5
|
|
15
15
|
paramiko>=3.0.0
|
|
16
16
|
selenium>=4.10.0
|
|
17
17
|
bypy
|
|
@@ -44,7 +44,7 @@ watchdog>=3.0.0
|
|
|
44
44
|
|
|
45
45
|
[rag]
|
|
46
46
|
chromadb>=0.4.0
|
|
47
|
-
sentence-transformers>=2.2.
|
|
47
|
+
sentence-transformers>=2.2.5
|
|
48
48
|
|
|
49
49
|
[remote]
|
|
50
50
|
paramiko>=3.0.0
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "fr-cli"
|
|
7
|
-
version = "2.2.
|
|
7
|
+
version = "2.2.5"
|
|
8
8
|
description = "凡人打字机 - 支持多模型(Zhipu/DeepSeek/Kimi/Qwen/StepFun/MiniMax/Spark)的终极全能终端工具"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.8"
|
|
@@ -38,7 +38,7 @@ dependencies = [
|
|
|
38
38
|
[project.optional-dependencies]
|
|
39
39
|
data = ["pandas>=1.5.0", "openpyxl>=3.0.0"]
|
|
40
40
|
db = ["pymysql>=1.0.0", "psycopg2-binary>=2.9.0", "pyodbc>=4.0.0", "oracledb>=1.3.0"]
|
|
41
|
-
rag = ["chromadb>=0.4.0", "sentence-transformers>=2.2.
|
|
41
|
+
rag = ["chromadb>=0.4.0", "sentence-transformers>=2.2.5"]
|
|
42
42
|
remote = ["paramiko>=3.0.0"]
|
|
43
43
|
spider = ["selenium>=4.10.0"]
|
|
44
44
|
cloud = ["bypy", "aligo", "msal"]
|
|
@@ -46,7 +46,7 @@ monitor = ["watchdog>=3.0.0"]
|
|
|
46
46
|
all = [
|
|
47
47
|
"pandas>=1.5.0", "openpyxl>=3.0.0",
|
|
48
48
|
"pymysql>=1.0.0", "psycopg2-binary>=2.9.0", "pyodbc>=4.0.0", "oracledb>=1.3.0",
|
|
49
|
-
"chromadb>=0.4.0", "sentence-transformers>=2.2.
|
|
49
|
+
"chromadb>=0.4.0", "sentence-transformers>=2.2.5",
|
|
50
50
|
"paramiko>=3.0.0",
|
|
51
51
|
"selenium>=4.10.0",
|
|
52
52
|
"bypy", "aligo", "msal",
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|