fr-cli 2.1.0__tar.gz → 2.2.0__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.
Files changed (84) hide show
  1. {fr_cli-2.1.0/fr_cli.egg-info → fr_cli-2.2.0}/PKG-INFO +4 -3
  2. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/__init__.py +1 -1
  3. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/conf/config.py +23 -3
  4. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/core/core.py +49 -12
  5. fr_cli-2.2.0/fr_cli/core/llm.py +181 -0
  6. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/core/stream.py +38 -38
  7. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/main.py +1 -1
  8. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/repl/commands.py +20 -3
  9. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/ui/ui.py +4 -3
  10. {fr_cli-2.1.0 → fr_cli-2.2.0/fr_cli.egg-info}/PKG-INFO +4 -3
  11. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli.egg-info/SOURCES.txt +1 -0
  12. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli.egg-info/requires.txt +1 -0
  13. {fr_cli-2.1.0 → fr_cli-2.2.0}/pyproject.toml +4 -3
  14. {fr_cli-2.1.0 → fr_cli-2.2.0}/LICENSE +0 -0
  15. {fr_cli-2.1.0 → fr_cli-2.2.0}/MANIFEST.in +0 -0
  16. {fr_cli-2.1.0 → fr_cli-2.2.0}/README.md +0 -0
  17. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/README.md +0 -0
  18. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/WEAPON.MD +0 -0
  19. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/addon/plugin.py +0 -0
  20. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/agent/__init__.py +0 -0
  21. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/agent/builtins/__init__.py +0 -0
  22. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/agent/builtins/_utils.py +0 -0
  23. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/agent/builtins/db.py +0 -0
  24. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/agent/builtins/local.py +0 -0
  25. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/agent/builtins/rag.py +0 -0
  26. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/agent/builtins/rag_watcher_daemon.py +0 -0
  27. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/agent/builtins/remote.py +0 -0
  28. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/agent/builtins/spider.py +0 -0
  29. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/agent/client.py +0 -0
  30. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/agent/executor.py +0 -0
  31. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/agent/generator.py +0 -0
  32. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/agent/manager.py +0 -0
  33. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/agent/master.py +0 -0
  34. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/agent/master_prompt.py +0 -0
  35. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/agent/remote.py +0 -0
  36. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/agent/server.py +0 -0
  37. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/agent/workflow.py +0 -0
  38. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/breakthrough/update.py +0 -0
  39. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/command/__init__.py +0 -0
  40. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/command/executor.py +0 -0
  41. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/command/registry.py +0 -0
  42. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/command/security.py +0 -0
  43. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/conf/wizard.py +0 -0
  44. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/core/chat.py +0 -0
  45. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/core/intent.py +0 -0
  46. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/core/recommender.py +0 -0
  47. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/core/sysmon.py +0 -0
  48. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/core/thinking.py +0 -0
  49. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/gatekeeper/__init__.py +0 -0
  50. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/gatekeeper/daemon.py +0 -0
  51. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/gatekeeper/manager.py +0 -0
  52. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/lang/i18n.py +0 -0
  53. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/memory/context.py +0 -0
  54. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/memory/history.py +0 -0
  55. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/memory/session.py +0 -0
  56. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/repl/__init__.py +0 -0
  57. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/security/security.py +0 -0
  58. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/weapon/cron.py +0 -0
  59. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/weapon/dataframe.py +0 -0
  60. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/weapon/disk.py +0 -0
  61. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/weapon/fs.py +0 -0
  62. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/weapon/launcher.py +0 -0
  63. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/weapon/loader.py +0 -0
  64. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/weapon/mail.py +0 -0
  65. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/weapon/mcp.py +0 -0
  66. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/weapon/vision.py +0 -0
  67. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli/weapon/web.py +0 -0
  68. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli.egg-info/dependency_links.txt +0 -0
  69. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli.egg-info/entry_points.txt +0 -0
  70. {fr_cli-2.1.0 → fr_cli-2.2.0}/fr_cli.egg-info/top_level.txt +0 -0
  71. {fr_cli-2.1.0 → fr_cli-2.2.0}/setup.cfg +0 -0
  72. {fr_cli-2.1.0 → fr_cli-2.2.0}/tests/test_agent_client.py +0 -0
  73. {fr_cli-2.1.0 → fr_cli-2.2.0}/tests/test_agent_server.py +0 -0
  74. {fr_cli-2.1.0 → fr_cli-2.2.0}/tests/test_ai_save_file_with_verify.py +0 -0
  75. {fr_cli-2.1.0 → fr_cli-2.2.0}/tests/test_all.py +0 -0
  76. {fr_cli-2.1.0 → fr_cli-2.2.0}/tests/test_auto_session.py +0 -0
  77. {fr_cli-2.1.0 → fr_cli-2.2.0}/tests/test_builtins.py +0 -0
  78. {fr_cli-2.1.0 → fr_cli-2.2.0}/tests/test_dataframe.py +0 -0
  79. {fr_cli-2.1.0 → fr_cli-2.2.0}/tests/test_gatekeeper.py +0 -0
  80. {fr_cli-2.1.0 → fr_cli-2.2.0}/tests/test_integration.py +0 -0
  81. {fr_cli-2.1.0 → fr_cli-2.2.0}/tests/test_intent_classification.py +0 -0
  82. {fr_cli-2.1.0 → fr_cli-2.2.0}/tests/test_launcher.py +0 -0
  83. {fr_cli-2.1.0 → fr_cli-2.2.0}/tests/test_master_agent.py +0 -0
  84. {fr_cli-2.1.0 → fr_cli-2.2.0}/tests/test_structured_tools.py +0 -0
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fr-cli
3
- Version: 2.1.0
4
- Summary: 凡人打字机 - 基于智谱AI的终极全能终端工具
3
+ Version: 2.2.0
4
+ Summary: 凡人打字机 - 支持多模型(Zhipu/DeepSeek/Kimi/Qwen/StepFun/MiniMax/Spark)的终极全能终端工具
5
5
  Author: FANREN CLI Author
6
6
  License-Expression: MIT
7
7
  Project-URL: Homepage, https://github.com/yourname/fr-cli
8
8
  Project-URL: Issues, https://github.com/yourname/fr-cli/issues
9
- Keywords: zhipuai,glm,cli,ai,terminal,chatbot
9
+ Keywords: zhipuai,deepseek,kimi,qwen,cli,ai,terminal,chatbot
10
10
  Classifier: Development Status :: 4 - Beta
11
11
  Classifier: Environment :: Console
12
12
  Classifier: Intended Audience :: Developers
@@ -24,6 +24,7 @@ Requires-Python: >=3.8
24
24
  Description-Content-Type: text/markdown
25
25
  License-File: LICENSE
26
26
  Requires-Dist: zhipuai>=2.0.0
27
+ Requires-Dist: openai>=1.0.0
27
28
  Requires-Dist: requests>=2.28.0
28
29
  Requires-Dist: mcp>=1.6.0
29
30
  Provides-Extra: data
@@ -1,4 +1,4 @@
1
1
  """
2
2
  凡人打字机 - 基于智谱AI的终极全能终端工具
3
3
  """
4
- __version__ = "2.1.0"
4
+ __version__ = "2.2.0"
@@ -6,7 +6,7 @@ import json
6
6
  import os
7
7
  import shutil
8
8
  from pathlib import Path
9
- from fr_cli.ui.ui import YELLOW, RED, GREEN, RESET
9
+ from fr_cli.ui.ui import YELLOW, RED, GREEN, RESET, DIM
10
10
 
11
11
  CONFIG_FILE = Path.home() / ".zhipu_cli_config.json"
12
12
  CONFIG_BACKUP = Path.home() / ".zhipu_cli_config.json.bak"
@@ -17,6 +17,7 @@ DEFAULT_LIMIT = 20000
17
17
  def _default_config():
18
18
  """返回默认配置字典"""
19
19
  return {
20
+ "provider": "zhipu",
20
21
  "key": "",
21
22
  "model": "glm-4-flash",
22
23
  "limit": DEFAULT_LIMIT,
@@ -28,6 +29,7 @@ def _default_config():
28
29
  "disk": {},
29
30
  "thinking_mode": "direct",
30
31
  "mcp": {"servers": []},
32
+ "providers": {},
31
33
  }
32
34
 
33
35
 
@@ -111,11 +113,29 @@ def init_config():
111
113
  save_config(c)
112
114
  print(f"{GREEN}✅ 默认洞府已开辟: {DEFAULT_WORKSPACE}{RESET}")
113
115
 
114
- if not c.get("key"):
116
+ # 向后兼容:无 provider 字段的旧配置自动补全
117
+ if "provider" not in c:
118
+ c["provider"] = "zhipu"
119
+ save_config(c)
120
+
121
+ provider = c.get("provider", "zhipu")
122
+ providers_cfg = c.get("providers", {})
123
+ pcfg = providers_cfg.get(provider, {})
124
+
125
+ # 检查当前提供商是否已配置 key
126
+ has_key = bool(pcfg.get("key", c.get("key", "")))
127
+
128
+ if not has_key:
115
129
  print(f"\n{YELLOW}⚠️ API Key Required{RESET}")
116
- k = input(f"👉 Enter Zhipu API Key: ").strip()
130
+ from fr_cli.core.llm import list_providers
131
+ providers = list_providers()
132
+ print(f"{DIM}当前道统: {provider}{RESET}")
133
+ print(f"{DIM}支持道统: {', '.join([p['id'] for p in providers])}{RESET}")
134
+ k = input(f"👉 Enter API Key for [{provider}]: ").strip()
117
135
  if k:
118
136
  c["key"] = k
137
+ providers_cfg.setdefault(provider, {})["key"] = k
138
+ c["providers"] = providers_cfg
119
139
  ok = save_config(c)
120
140
  if ok:
121
141
  print(f"{GREEN}✅ API Key 已保存至: {CONFIG_FILE}{RESET}")
@@ -2,7 +2,6 @@
2
2
  全局状态管理容器 (AppState)
3
3
  统一管理配置、子系统实例、运行时状态,实现依赖注入。
4
4
  """
5
- from zhipuai import ZhipuAI
6
5
  from fr_cli.weapon.fs import VFS
7
6
  from fr_cli.weapon.mail import MailClient
8
7
  from fr_cli.weapon.web import WebRaider
@@ -12,6 +11,7 @@ from fr_cli.command.security import SecurityManager
12
11
  from fr_cli.command.executor import CommandExecutor
13
12
  from fr_cli.weapon.loader import load_weapon_md
14
13
  from fr_cli.weapon.mcp import MCPManager
14
+ from fr_cli.core.llm import create_llm_client, list_providers, get_provider_info, resolve_provider_model
15
15
 
16
16
 
17
17
  class AppState:
@@ -20,15 +20,16 @@ class AppState:
20
20
  def __init__(self, cfg):
21
21
  self.cfg = cfg
22
22
  self.lang = cfg.get("lang", "zh")
23
- self.model_name = cfg.get("model", "glm-4-flash")
24
23
  self.limit = cfg.get("limit", 4096)
25
- self.api_key = cfg.get("key", "")
26
24
  self.sn = cfg.get("session_name", "")
27
25
  self.aliases = cfg.get("aliases", {})
28
26
  self.thinking_mode = cfg.get("thinking_mode", "direct")
29
27
 
28
+ # LLM 客户端统一初始化(万法归一)
29
+ self.client, self.provider, self.model_name = create_llm_client(cfg)
30
+ self.api_key = self.client.api_key
31
+
30
32
  # 核心子系统实例化
31
- self.client = ZhipuAI(api_key=self.api_key)
32
33
  self.vfs = VFS(cfg.get("allowed_dirs", []))
33
34
  self.plugins = init_plugins()
34
35
  self.mail_c = MailClient(cfg.get("mail", {}))
@@ -63,25 +64,61 @@ class AppState:
63
64
  self.gatekeeper = GatekeeperManager()
64
65
 
65
66
  def reinit_client(self):
66
- """API Key 或模型变更后重铸客户端"""
67
- self.api_key = self.cfg.get("key", "")
68
- self.client = ZhipuAI(api_key=self.api_key)
67
+ """API Key、提供商或模型变更后重铸客户端"""
68
+ self.client, self.provider, self.model_name = create_llm_client(self.cfg)
69
+ self.api_key = self.client.api_key
69
70
 
70
71
  def save_cfg(self):
71
72
  """持久化当前配置"""
72
73
  from fr_cli.conf.config import save_config
73
74
  save_config(self.cfg)
74
75
 
75
- def update_model(self, name):
76
- """切换法器模型"""
77
- self.cfg["model"] = name
78
- self.model_name = name
76
+ def update_provider(self, provider_id):
77
+ """切换 LLM 提供商(召唤新的道统)"""
78
+ info = get_provider_info(provider_id)
79
+ if not info:
80
+ return False
81
+ self.cfg["provider"] = provider_id
82
+ # 如果新提供商没有设置过模型,使用其默认模型
83
+ providers_cfg = self.cfg.setdefault("providers", {})
84
+ if provider_id not in providers_cfg or not providers_cfg[provider_id].get("model"):
85
+ self.cfg["model"] = info["default_model"]
86
+ self.model_name = info["default_model"]
87
+ else:
88
+ self.model_name = providers_cfg[provider_id].get("model", info["default_model"])
89
+ self.cfg["model"] = self.model_name
90
+ self.save_cfg()
91
+ self.reinit_client()
92
+ return True
93
+
94
+ def update_model(self, arg):
95
+ """
96
+ 切换法器模型
97
+ 支持格式:
98
+ - "deepseek-chat" 仅切换模型(保持当前提供商)
99
+ - "deepseek:deepseek-chat" 同时切换提供商和模型
100
+ """
101
+ new_provider, new_model = resolve_provider_model(arg)
102
+ if new_provider and new_provider != self.provider:
103
+ # 切换提供商 + 模型
104
+ if not self.update_provider(new_provider):
105
+ return False
106
+ self.cfg["model"] = new_model
107
+ self.model_name = new_model
108
+ # 同步到 providers 配置中当前提供商的 model
109
+ providers_cfg = self.cfg.setdefault("providers", {})
110
+ pcfg = providers_cfg.setdefault(self.provider, {})
111
+ pcfg["model"] = new_model
79
112
  self.save_cfg()
80
113
  self.reinit_client()
114
+ return True
81
115
 
82
116
  def update_key(self, key):
83
- """重铸 API 密钥"""
117
+ """重铸 API 密钥(针对当前提供商)"""
84
118
  self.cfg["key"] = key
119
+ providers_cfg = self.cfg.setdefault("providers", {})
120
+ pcfg = providers_cfg.setdefault(self.provider, {})
121
+ pcfg["key"] = key
85
122
  self.save_cfg()
86
123
  self.reinit_client()
87
124
 
@@ -0,0 +1,181 @@
1
+ """
2
+ LLM 统一召唤接口 —— 万法归一
3
+
4
+ 为各大模型提供商提供统一的流式对话接口,
5
+ 使主程序无需关心底层 SDK 差异。
6
+ """
7
+ from abc import ABC, abstractmethod
8
+ from typing import Iterator, Optional, Dict, Any
9
+
10
+
11
+ class BaseLLMClient(ABC):
12
+ """大模型客户端抽象基类"""
13
+
14
+ def __init__(self, api_key: str, **kwargs):
15
+ self.api_key = api_key
16
+
17
+ @abstractmethod
18
+ def stream_chat(self, model: str, messages: list, max_tokens: int = 4096) -> Iterator[dict]:
19
+ """
20
+ 流式对话,yield 每个 token 块
21
+ 格式: {"content": str, "usage": dict or None}
22
+ """
23
+ pass
24
+
25
+
26
+ class ZhipuLLMClient(BaseLLMClient):
27
+ """智谱 AI 客户端 (zhipuai SDK)"""
28
+
29
+ def __init__(self, api_key: str, **kwargs):
30
+ super().__init__(api_key, **kwargs)
31
+ from zhipuai import ZhipuAI
32
+ self._client = ZhipuAI(api_key=api_key)
33
+
34
+ def stream_chat(self, model: str, messages: list, max_tokens: int = 4096) -> Iterator[dict]:
35
+ response = self._client.chat.completions.create(
36
+ model=model,
37
+ messages=messages,
38
+ stream=True,
39
+ max_tokens=max_tokens,
40
+ )
41
+ for chunk in response:
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}
49
+
50
+
51
+ class OpenAICompatibleClient(BaseLLMClient):
52
+ """
53
+ OpenAI 兼容格式客户端
54
+ 覆盖:DeepSeek / Kimi(Moonshot) / 通义千问(Qwen) / StepFun / MiniMax / 讯飞星火(Spark)
55
+ """
56
+
57
+ def __init__(self, api_key: str, base_url: Optional[str] = None, **kwargs):
58
+ super().__init__(api_key, **kwargs)
59
+ from openai import OpenAI
60
+ self._client = OpenAI(api_key=api_key, base_url=base_url)
61
+
62
+ def stream_chat(self, model: str, messages: list, max_tokens: int = 4096) -> Iterator[dict]:
63
+ response = self._client.chat.completions.create(
64
+ model=model,
65
+ messages=messages,
66
+ stream=True,
67
+ max_tokens=max_tokens,
68
+ )
69
+ for chunk in response:
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}
77
+
78
+
79
+ # 提供商配置表
80
+ _PROVIDERS: Dict[str, Dict[str, Any]] = {
81
+ "zhipu": {
82
+ "name": "智谱AI (Zhipu)",
83
+ "default_model": "glm-4-flash",
84
+ "client_class": ZhipuLLMClient,
85
+ "base_url": None,
86
+ },
87
+ "deepseek": {
88
+ "name": "DeepSeek",
89
+ "default_model": "deepseek-chat",
90
+ "client_class": OpenAICompatibleClient,
91
+ "base_url": "https://api.deepseek.com",
92
+ },
93
+ "kimi": {
94
+ "name": "Kimi (Moonshot)",
95
+ "default_model": "moonshot-v1-8k",
96
+ "client_class": OpenAICompatibleClient,
97
+ "base_url": "https://api.moonshot.cn/v1",
98
+ },
99
+ "qwen": {
100
+ "name": "通义千问 (Qwen)",
101
+ "default_model": "qwen-turbo",
102
+ "client_class": OpenAICompatibleClient,
103
+ "base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
104
+ },
105
+ "stepfun": {
106
+ "name": "阶跃星辰 (StepFun)",
107
+ "default_model": "step-1-8k",
108
+ "client_class": OpenAICompatibleClient,
109
+ "base_url": "https://api.stepfun.com/v1",
110
+ },
111
+ "minimax": {
112
+ "name": "MiniMax",
113
+ "default_model": "abab6.5-chat",
114
+ "client_class": OpenAICompatibleClient,
115
+ "base_url": "https://api.minimax.chat/v1",
116
+ },
117
+ "spark": {
118
+ "name": "讯飞星火 (Spark)",
119
+ "default_model": "generalv3.5",
120
+ "client_class": OpenAICompatibleClient,
121
+ "base_url": "https://spark-api-open.xf-yun.com/v1",
122
+ },
123
+ }
124
+
125
+
126
+ def create_llm_client(cfg: dict):
127
+ """
128
+ 根据配置创建对应的 LLM 客户端
129
+
130
+ cfg 格式支持:
131
+ 新版: {"provider": "deepseek", "providers": {"deepseek": {"key": "xxx", "model": "..."}}}
132
+ 旧版: {"key": "xxx", "model": "glm-4-flash"} (自动识别为 zhipu)
133
+
134
+ 返回: (client_instance, provider_id, model_name)
135
+ """
136
+ provider = cfg.get("provider", "zhipu")
137
+ providers_cfg = cfg.get("providers", {})
138
+
139
+ # 获取当前提供商配置
140
+ pcfg = providers_cfg.get(provider, {})
141
+
142
+ # 向后兼容:如果 providers 中没有当前 provider,从顶层读取 key/model
143
+ api_key = pcfg.get("key", cfg.get("key", ""))
144
+ default_model = _PROVIDERS.get(provider, _PROVIDERS["zhipu"])["default_model"]
145
+ model = pcfg.get("model", cfg.get("model", default_model))
146
+
147
+ info = _PROVIDERS.get(provider, _PROVIDERS["zhipu"])
148
+ client_class = info["client_class"]
149
+ base_url = info.get("base_url")
150
+
151
+ kwargs = {"api_key": api_key}
152
+ if base_url:
153
+ kwargs["base_url"] = base_url
154
+
155
+ return client_class(**kwargs), provider, model
156
+
157
+
158
+ def list_providers():
159
+ """返回所有支持的提供商列表"""
160
+ return [
161
+ {"id": k, "name": v["name"], "default_model": v["default_model"]}
162
+ for k, v in _PROVIDERS.items()
163
+ ]
164
+
165
+
166
+ def get_provider_info(provider_id: str):
167
+ """获取指定提供商信息"""
168
+ return _PROVIDERS.get(provider_id)
169
+
170
+
171
+ def resolve_provider_model(arg: str) -> tuple:
172
+ """
173
+ 解析用户输入的模型参数
174
+ 支持格式:
175
+ - "deepseek:deepseek-chat" → ("deepseek", "deepseek-chat")
176
+ - "deepseek-chat" → (None, "deepseek-chat") (仅模型名,保持当前提供商)
177
+ """
178
+ if ":" in arg:
179
+ parts = arg.split(":", 1)
180
+ return parts[0].strip(), parts[1].strip()
181
+ return None, arg.strip()
@@ -6,78 +6,78 @@ import time
6
6
  from fr_cli.ui.ui import RESET, DIM, CYAN, RED, CODE_BG, CODE_FG
7
7
  from fr_cli.lang.i18n import T
8
8
 
9
+
9
10
  def stream_cnt(client, model, messages, lang, custom_prefix=None, max_tokens=None, silent=False):
10
11
  """
11
- 流式调用智谱API并实时打印,带有简易代码块高亮状态机
12
+ 流式调用 LLM 并实时打印,带有简易代码块高亮状态机
13
+ :param client: LLM 客户端实例 (BaseLLMClient 子类)
12
14
  :param silent: 如果为True,则不输出到终端,仅返回文本
13
15
  :return: tuple (完整回复文本 str, 使用情况 dict, 响应时间 float)
14
16
  """
15
17
  if not silent:
16
18
  p = custom_prefix or f"{CYAN}{T('prompt_ai', lang)}{RESET} "
17
19
  sys.stdout.write(p); sys.stdout.flush()
18
-
20
+
19
21
  start_time = time.time()
20
22
  full_text = ""
21
23
  in_code = False
22
24
  usage = {}
23
-
25
+
24
26
  try:
25
27
  # 验证API密钥
26
28
  if not client.api_key or len(client.api_key) < 10:
27
29
  print(f"{RED}[错误] API密钥未配置或格式不正确{RESET}")
28
30
  return "[错误] 请先配置有效的API密钥", {}, 0.0
29
-
30
- response = client.chat.completions.create(
31
+
32
+ response = client.stream_chat(
31
33
  model=model,
32
34
  messages=messages,
33
- stream=True,
34
35
  max_tokens=max_tokens if max_tokens else 4096
35
36
  )
36
-
37
+
37
38
  for chunk in response:
38
- if chunk.choices and chunk.choices[0].delta:
39
- txt = chunk.choices[0].delta.content
40
- if txt:
41
- full_text += txt
42
- # 简易状态机:检测 ``` 切换代码背景
43
- if "```" in txt:
44
- parts = txt.split("```")
45
- for i, part in enumerate(parts):
46
- if i > 0: # 遇到了一个 ```
47
- in_code = not in_code
48
- if not silent:
49
- if in_code:
50
- sys.stdout.write(f"{CODE_BG}{CODE_FG}")
51
- else:
52
- sys.stdout.write(f"{RESET}")
39
+ txt = chunk.get("content", "")
40
+ if txt:
41
+ full_text += txt
42
+ # 简易状态机:检测 ``` 切换代码背景
43
+ if "```" in txt:
44
+ parts = txt.split("```")
45
+ for i, part in enumerate(parts):
46
+ if i > 0: # 遇到了一个 ```
47
+ in_code = not in_code
53
48
  if not silent:
54
- sys.stdout.write(part)
55
- sys.stdout.flush()
56
- else:
49
+ if in_code:
50
+ sys.stdout.write(f"{CODE_BG}{CODE_FG}")
51
+ else:
52
+ sys.stdout.write(f"{RESET}")
57
53
  if not silent:
58
- if in_code:
59
- sys.stdout.write(f"{CODE_BG}{CODE_FG}{txt}{CODE_FG}")
60
- else:
61
- sys.stdout.write(txt)
54
+ sys.stdout.write(part)
62
55
  sys.stdout.flush()
63
-
64
- if hasattr(chunk, 'usage') and chunk.usage:
65
- usage = chunk.usage.model_dump()
66
-
56
+ else:
57
+ if not silent:
58
+ if in_code:
59
+ sys.stdout.write(f"{CODE_BG}{CODE_FG}{txt}{CODE_FG}")
60
+ else:
61
+ sys.stdout.write(txt)
62
+ sys.stdout.flush()
63
+
64
+ if chunk.get("usage"):
65
+ usage = chunk["usage"]
66
+
67
67
  except Exception as e:
68
68
  sys.stdout.write(f"\n{DIM}{str(e)[:50]}{RESET}")
69
69
  sys.stdout.flush()
70
-
70
+
71
71
  if not silent:
72
72
  sys.stdout.write(RESET)
73
73
  sys.stdout.write("\n")
74
74
  sys.stdout.flush()
75
-
75
+
76
76
  end_time = time.time()
77
77
  response_time = end_time - start_time
78
-
78
+
79
79
  # 如果没有收到任何内容,返回提示信息
80
80
  if not full_text:
81
81
  return "[错误] 无法获取AI回复,请检查API密钥配置", usage, response_time
82
-
83
- return full_text, usage, response_time
82
+
83
+ return full_text, usage, response_time
@@ -181,7 +181,7 @@ def main():
181
181
  state.context_summary = load_context(state.sn)
182
182
 
183
183
  # 启动动画
184
- print_banner(state.model_name, state.limit, cfg.get("allowed_dirs", [""]), state.sn, state.lang)
184
+ print_banner(state.model_name, state.limit, cfg.get("allowed_dirs", [""]), state.sn, state.lang, state.provider)
185
185
 
186
186
  # ================= 主循环 =================
187
187
  while True:
@@ -100,8 +100,23 @@ def _cmd_help(state, parts):
100
100
  def _cmd_model(state, parts):
101
101
  arg1 = parts[1] if len(parts) > 1 else ""
102
102
  if arg1:
103
- state.update_model(arg1)
104
- print(f"{GREEN}{T('ok_model', state.lang, arg1)}{RESET}")
103
+ ok = state.update_model(arg1)
104
+ if ok:
105
+ print(f"{GREEN}✅ 已切换: [{state.provider}] {state.model_name}{RESET}")
106
+ else:
107
+ print(f"{RED}❌ 无效的提供商或模型: {arg1}{RESET}")
108
+ else:
109
+ # 显示当前道统与可用模型
110
+ from fr_cli.core.llm import list_providers, get_provider_info
111
+ print(f"{CYAN}🧠 当前道统: [{state.provider}] {state.model_name}{RESET}")
112
+ print(f"\n{DIM}可用道统与默认模型:{RESET}")
113
+ for p in list_providers():
114
+ marker = " 👈 当前" if p["id"] == state.provider else ""
115
+ print(f" {CYAN}{p['id']}{RESET} — {p['name']}{DIM} (默认: {p['default_model']}){RESET}{marker}")
116
+ print(f"\n{DIM}用法:{RESET}")
117
+ print(f" /model <模型名> — 切换当前道统下的模型")
118
+ print(f" /model <道统>:<模型名> — 同时切换道统和模型")
119
+ print(f" 示例: /model deepseek:deepseek-chat")
105
120
  return False
106
121
 
107
122
 
@@ -109,7 +124,9 @@ def _cmd_key(state, parts):
109
124
  arg1 = parts[1] if len(parts) > 1 else ""
110
125
  if arg1:
111
126
  state.update_key(arg1)
112
- print(f"{GREEN}{T('ok_key', state.lang)}{RESET}")
127
+ print(f"{GREEN}✅ [{state.provider}] API Key 已更新{RESET}")
128
+ else:
129
+ print(f"{YELLOW}⚠️ 用法: /key <API密钥> (为当前道统 [{state.provider}] 设置密钥){RESET}")
113
130
  return False
114
131
 
115
132
 
@@ -48,7 +48,7 @@ def get_display_width(text):
48
48
  width += 1
49
49
  return width
50
50
 
51
- def print_banner(mn, tl, ad, sn, l):
51
+ def print_banner(mn, tl, ad, sn, l, provider="zhipu"):
52
52
  """打印启动时的小乌龟从左向右爬行动画"""
53
53
 
54
54
  # 乌龟身体(6行)
@@ -106,10 +106,11 @@ def print_banner(mn, tl, ad, sn, l):
106
106
  uf = (l == "zh")
107
107
  ds = f"{GREEN}{ad}{RESET}" if ad else f"{RED}{('未开放洞府' if uf else 'No dir')}{RESET}"
108
108
  ss = f"{MAGENTA}{sn}{RESET}" if sn else f"{DIM}{'全新轮回' if uf else 'New'}{RESET}"
109
+ pv = f" {'🏛️ 道统' if uf else 'Provider'}: {CYAN}{provider}{RESET}"
109
110
  i1 = f" {'🔮 模型' if uf else 'Model'}: {GREEN}{BOLD}{mn}{RESET} | {'🛡️ 上限' if uf else 'Limit'}: {YELLOW}{tl}{RESET}"
110
111
  i2 = f" {'📂 洞府' if uf else 'Dir'}: {ds} | {'⏳ 轮回' if uf else 'Sess'}: {ss}"
111
- bl = max(get_display_width(i1), get_display_width(i2)) + 4
112
- print(f"{MAGENTA}┌{'─'*bl}┐{RESET}\n{MAGENTA}│{RESET}{i1}{' '*(bl-get_display_width(i1)-2)}{MAGENTA}│{RESET}\n{MAGENTA}│{RESET}{i2}{' '*(bl-get_display_width(i2)-2)}{MAGENTA}│{RESET}\n{MAGENTA}└{'─'*bl}┘{RESET}\n")
112
+ bl = max(get_display_width(pv), get_display_width(i1), get_display_width(i2)) + 4
113
+ print(f"{MAGENTA}┌{'─'*bl}┐{RESET}\n{MAGENTA}│{RESET}{pv}{' '*(bl-get_display_width(pv)-2)}{MAGENTA}│{RESET}\n{MAGENTA}│{RESET}{i1}{' '*(bl-get_display_width(i1)-2)}{MAGENTA}│{RESET}\n{MAGENTA}│{RESET}{i2}{' '*(bl-get_display_width(i2)-2)}{MAGENTA}│{RESET}\n{MAGENTA}└{'─'*bl}┘{RESET}\n")
113
114
 
114
115
  def print_bye():
115
116
  """打印退出动画"""
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fr-cli
3
- Version: 2.1.0
4
- Summary: 凡人打字机 - 基于智谱AI的终极全能终端工具
3
+ Version: 2.2.0
4
+ Summary: 凡人打字机 - 支持多模型(Zhipu/DeepSeek/Kimi/Qwen/StepFun/MiniMax/Spark)的终极全能终端工具
5
5
  Author: FANREN CLI Author
6
6
  License-Expression: MIT
7
7
  Project-URL: Homepage, https://github.com/yourname/fr-cli
8
8
  Project-URL: Issues, https://github.com/yourname/fr-cli/issues
9
- Keywords: zhipuai,glm,cli,ai,terminal,chatbot
9
+ Keywords: zhipuai,deepseek,kimi,qwen,cli,ai,terminal,chatbot
10
10
  Classifier: Development Status :: 4 - Beta
11
11
  Classifier: Environment :: Console
12
12
  Classifier: Intended Audience :: Developers
@@ -24,6 +24,7 @@ Requires-Python: >=3.8
24
24
  Description-Content-Type: text/markdown
25
25
  License-File: LICENSE
26
26
  Requires-Dist: zhipuai>=2.0.0
27
+ Requires-Dist: openai>=1.0.0
27
28
  Requires-Dist: requests>=2.28.0
28
29
  Requires-Dist: mcp>=1.6.0
29
30
  Provides-Extra: data
@@ -41,6 +41,7 @@ fr_cli/conf/wizard.py
41
41
  fr_cli/core/chat.py
42
42
  fr_cli/core/core.py
43
43
  fr_cli/core/intent.py
44
+ fr_cli/core/llm.py
44
45
  fr_cli/core/recommender.py
45
46
  fr_cli/core/stream.py
46
47
  fr_cli/core/sysmon.py
@@ -1,4 +1,5 @@
1
1
  zhipuai>=2.0.0
2
+ openai>=1.0.0
2
3
  requests>=2.28.0
3
4
  mcp>=1.6.0
4
5
 
@@ -4,15 +4,15 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "fr-cli"
7
- version = "2.1.0"
8
- description = "凡人打字机 - 基于智谱AI的终极全能终端工具"
7
+ version = "2.2.0"
8
+ description = "凡人打字机 - 支持多模型(Zhipu/DeepSeek/Kimi/Qwen/StepFun/MiniMax/Spark)的终极全能终端工具"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
11
11
  license = "MIT"
12
12
  authors = [
13
13
  {name = "FANREN CLI Author"},
14
14
  ]
15
- keywords = ["zhipuai", "glm", "cli", "ai", "terminal", "chatbot"]
15
+ keywords = ["zhipuai", "deepseek", "kimi", "qwen", "cli", "ai", "terminal", "chatbot"]
16
16
  classifiers = [
17
17
  "Development Status :: 4 - Beta",
18
18
  "Environment :: Console",
@@ -30,6 +30,7 @@ classifiers = [
30
30
  ]
31
31
  dependencies = [
32
32
  "zhipuai>=2.0.0",
33
+ "openai>=1.0.0",
33
34
  "requests>=2.28.0",
34
35
  "mcp>=1.6.0",
35
36
  ]
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