fr-cli 2.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fr_cli/README.md +148 -0
- fr_cli/WEAPON.MD +186 -0
- fr_cli/__init__.py +4 -0
- fr_cli/addon/plugin.py +69 -0
- fr_cli/agent/__init__.py +9 -0
- fr_cli/agent/builtins/__init__.py +4 -0
- fr_cli/agent/builtins/_utils.py +48 -0
- fr_cli/agent/builtins/db.py +269 -0
- fr_cli/agent/builtins/local.py +105 -0
- fr_cli/agent/builtins/rag.py +652 -0
- fr_cli/agent/builtins/rag_watcher_daemon.py +156 -0
- fr_cli/agent/builtins/remote.py +214 -0
- fr_cli/agent/builtins/spider.py +247 -0
- fr_cli/agent/client.py +164 -0
- fr_cli/agent/executor.py +86 -0
- fr_cli/agent/generator.py +104 -0
- fr_cli/agent/manager.py +193 -0
- fr_cli/agent/master.py +604 -0
- fr_cli/agent/master_prompt.py +118 -0
- fr_cli/agent/remote.py +70 -0
- fr_cli/agent/server.py +279 -0
- fr_cli/agent/workflow.py +164 -0
- fr_cli/breakthrough/update.py +154 -0
- fr_cli/command/__init__.py +4 -0
- fr_cli/command/executor.py +276 -0
- fr_cli/command/registry.py +1034 -0
- fr_cli/command/security.py +30 -0
- fr_cli/conf/config.py +126 -0
- fr_cli/conf/wizard.py +172 -0
- fr_cli/core/chat.py +280 -0
- fr_cli/core/core.py +111 -0
- fr_cli/core/intent.py +129 -0
- fr_cli/core/recommender.py +71 -0
- fr_cli/core/stream.py +83 -0
- fr_cli/core/sysmon.py +117 -0
- fr_cli/core/thinking.py +215 -0
- fr_cli/gatekeeper/__init__.py +7 -0
- fr_cli/gatekeeper/daemon.py +216 -0
- fr_cli/gatekeeper/manager.py +218 -0
- fr_cli/lang/i18n.py +827 -0
- fr_cli/main.py +329 -0
- fr_cli/memory/context.py +119 -0
- fr_cli/memory/history.py +96 -0
- fr_cli/memory/session.py +134 -0
- fr_cli/repl/__init__.py +0 -0
- fr_cli/repl/commands.py +1098 -0
- fr_cli/security/security.py +46 -0
- fr_cli/ui/ui.py +116 -0
- fr_cli/weapon/cron.py +217 -0
- fr_cli/weapon/dataframe.py +97 -0
- fr_cli/weapon/disk.py +141 -0
- fr_cli/weapon/fs.py +206 -0
- fr_cli/weapon/launcher.py +249 -0
- fr_cli/weapon/loader.py +98 -0
- fr_cli/weapon/mail.py +227 -0
- fr_cli/weapon/mcp.py +204 -0
- fr_cli/weapon/vision.py +74 -0
- fr_cli/weapon/web.py +88 -0
- fr_cli-2.1.0.dist-info/METADATA +227 -0
- fr_cli-2.1.0.dist-info/RECORD +64 -0
- fr_cli-2.1.0.dist-info/WHEEL +5 -0
- fr_cli-2.1.0.dist-info/entry_points.txt +2 -0
- fr_cli-2.1.0.dist-info/licenses/LICENSE +21 -0
- fr_cli-2.1.0.dist-info/top_level.txt +1 -0
fr_cli/core/core.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""
|
|
2
|
+
全局状态管理容器 (AppState)
|
|
3
|
+
统一管理配置、子系统实例、运行时状态,实现依赖注入。
|
|
4
|
+
"""
|
|
5
|
+
from zhipuai import ZhipuAI
|
|
6
|
+
from fr_cli.weapon.fs import VFS
|
|
7
|
+
from fr_cli.weapon.mail import MailClient
|
|
8
|
+
from fr_cli.weapon.web import WebRaider
|
|
9
|
+
from fr_cli.weapon.disk import CloudDisk
|
|
10
|
+
from fr_cli.addon.plugin import init_plugins
|
|
11
|
+
from fr_cli.command.security import SecurityManager
|
|
12
|
+
from fr_cli.command.executor import CommandExecutor
|
|
13
|
+
from fr_cli.weapon.loader import load_weapon_md
|
|
14
|
+
from fr_cli.weapon.mcp import MCPManager
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AppState:
|
|
18
|
+
"""应用程序运行时状态容器 —— 本命元神"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, cfg):
|
|
21
|
+
self.cfg = cfg
|
|
22
|
+
self.lang = cfg.get("lang", "zh")
|
|
23
|
+
self.model_name = cfg.get("model", "glm-4-flash")
|
|
24
|
+
self.limit = cfg.get("limit", 4096)
|
|
25
|
+
self.api_key = cfg.get("key", "")
|
|
26
|
+
self.sn = cfg.get("session_name", "")
|
|
27
|
+
self.aliases = cfg.get("aliases", {})
|
|
28
|
+
self.thinking_mode = cfg.get("thinking_mode", "direct")
|
|
29
|
+
|
|
30
|
+
# 核心子系统实例化
|
|
31
|
+
self.client = ZhipuAI(api_key=self.api_key)
|
|
32
|
+
self.vfs = VFS(cfg.get("allowed_dirs", []))
|
|
33
|
+
self.plugins = init_plugins()
|
|
34
|
+
self.mail_c = MailClient(cfg.get("mail", {}))
|
|
35
|
+
self.web_c = WebRaider()
|
|
36
|
+
self.disk_c = CloudDisk(cfg.get("disk", {}))
|
|
37
|
+
self.security = SecurityManager(self.lang, cfg)
|
|
38
|
+
|
|
39
|
+
# MCP 法宝管理器
|
|
40
|
+
self.mcp = MCPManager(cfg)
|
|
41
|
+
|
|
42
|
+
# 运行时消息与上下文
|
|
43
|
+
self.messages = []
|
|
44
|
+
self.context_summary = ""
|
|
45
|
+
self.weapon_tools, self.weapon_triggers = load_weapon_md()
|
|
46
|
+
self.mcp_tools = [] # 延迟加载,避免启动阻塞
|
|
47
|
+
|
|
48
|
+
# 自动会话存档路径(按日期编号)
|
|
49
|
+
self.auto_session_path = None
|
|
50
|
+
|
|
51
|
+
# 命令执行引擎
|
|
52
|
+
self.executor = CommandExecutor(self)
|
|
53
|
+
|
|
54
|
+
# 主控 Agent(自我进化型)
|
|
55
|
+
from fr_cli.agent.master import MasterAgent
|
|
56
|
+
self.master_agent = MasterAgent(self)
|
|
57
|
+
|
|
58
|
+
# Agent HTTP 服务守护
|
|
59
|
+
self.agent_server = None
|
|
60
|
+
|
|
61
|
+
# Gatekeeper 守护进程管理器
|
|
62
|
+
from fr_cli.gatekeeper.manager import GatekeeperManager
|
|
63
|
+
self.gatekeeper = GatekeeperManager()
|
|
64
|
+
|
|
65
|
+
def reinit_client(self):
|
|
66
|
+
"""API Key 或模型变更后重铸客户端"""
|
|
67
|
+
self.api_key = self.cfg.get("key", "")
|
|
68
|
+
self.client = ZhipuAI(api_key=self.api_key)
|
|
69
|
+
|
|
70
|
+
def save_cfg(self):
|
|
71
|
+
"""持久化当前配置"""
|
|
72
|
+
from fr_cli.conf.config import save_config
|
|
73
|
+
save_config(self.cfg)
|
|
74
|
+
|
|
75
|
+
def update_model(self, name):
|
|
76
|
+
"""切换法器模型"""
|
|
77
|
+
self.cfg["model"] = name
|
|
78
|
+
self.model_name = name
|
|
79
|
+
self.save_cfg()
|
|
80
|
+
self.reinit_client()
|
|
81
|
+
|
|
82
|
+
def update_key(self, key):
|
|
83
|
+
"""重铸 API 密钥"""
|
|
84
|
+
self.cfg["key"] = key
|
|
85
|
+
self.save_cfg()
|
|
86
|
+
self.reinit_client()
|
|
87
|
+
|
|
88
|
+
def update_limit(self, limit):
|
|
89
|
+
"""设置 Token 上限"""
|
|
90
|
+
self.cfg["limit"] = limit
|
|
91
|
+
self.limit = limit
|
|
92
|
+
self.save_cfg()
|
|
93
|
+
|
|
94
|
+
def update_lang(self, lang):
|
|
95
|
+
"""切换界面语言"""
|
|
96
|
+
self.cfg["lang"] = lang
|
|
97
|
+
self.lang = lang
|
|
98
|
+
self.save_cfg()
|
|
99
|
+
self.security = SecurityManager(self.lang, self.cfg)
|
|
100
|
+
|
|
101
|
+
def update_session_name(self, name):
|
|
102
|
+
"""更新轮回名"""
|
|
103
|
+
self.sn = name
|
|
104
|
+
self.cfg["session_name"] = name
|
|
105
|
+
self.save_cfg()
|
|
106
|
+
|
|
107
|
+
def update_thinking_mode(self, mode):
|
|
108
|
+
"""切换思维模式"""
|
|
109
|
+
self.cfg["thinking_mode"] = mode
|
|
110
|
+
self.thinking_mode = mode
|
|
111
|
+
self.save_cfg()
|
fr_cli/core/intent.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""
|
|
2
|
+
意图判定引擎
|
|
3
|
+
|
|
4
|
+
封装关键词预检与 LLM 意图分类逻辑。
|
|
5
|
+
负责判定用户输入是直接问答还是需要调用工具。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from fr_cli.core.stream import stream_cnt
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# 明确的工具操作关键词(兜底规则,避免大模型漏判)
|
|
12
|
+
# 同时包含中英文,覆盖用户在任何语言界面下输入任意语言的场景
|
|
13
|
+
_FORCE_TOOL_KEYWORDS = [
|
|
14
|
+
# 中文关键词
|
|
15
|
+
"保存", "保存到", "保存文件", "写入", "写到", "写入文件", "写到文件",
|
|
16
|
+
"创建文件", "生成文件", "输出到文件", "导出到", "导出文件",
|
|
17
|
+
"搜索", "查找", "查一下", "搜一下",
|
|
18
|
+
"发送邮件", "发邮件", "发信", "发邮件给", "发信给",
|
|
19
|
+
"查看目录", "列出文件", "查看文件", "打开文件",
|
|
20
|
+
"画图", "生成图片", "画一张", "画个", "生成图像",
|
|
21
|
+
"运行代码", "执行代码", "执行脚本", "运行脚本",
|
|
22
|
+
"定时任务", "定时执行", "循环任务",
|
|
23
|
+
"上传", "上传到", "下载", "下载到",
|
|
24
|
+
"保存会话", "导出会话", "切换模型", "设置密钥",
|
|
25
|
+
# 英文关键词
|
|
26
|
+
"save", "save to", "save file", "write", "write to", "write file",
|
|
27
|
+
"create file", "generate file", "output to file", "export", "export to",
|
|
28
|
+
"search", "look up", "look for", "find", "google", "bing",
|
|
29
|
+
"send email", "send mail", "send an email", "email to", "mail to",
|
|
30
|
+
"list files", "list directory", "show files", "show directory", "open file",
|
|
31
|
+
"draw", "generate image", "create image", "paint", "image of",
|
|
32
|
+
"run code", "execute code", "run script", "execute script",
|
|
33
|
+
"schedule", "scheduled task", "cron job", "timer",
|
|
34
|
+
"upload", "upload to", "download", "download to",
|
|
35
|
+
"save session", "export session", "switch model", "set key", "set api key",
|
|
36
|
+
# MCP 外部神通关键词
|
|
37
|
+
"mcp", "外部工具", "外部神通", "调用工具", "use tool", "invoke tool",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
# 保存意图关键词(用于第二轮强制保存检测)
|
|
41
|
+
_SAVE_KEYWORDS = [
|
|
42
|
+
"保存", "保存到", "保存文件", "写入", "写到", "写入文件", "写到文件",
|
|
43
|
+
"存储", "存到", "存为", "导出到", "导出文件",
|
|
44
|
+
"save", "save to", "save file", "save as", "write", "write to", "write file",
|
|
45
|
+
"store", "store to", "export to", "export file",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
# 信息获取关键词(触发"先回答再调用工具"双源模式)
|
|
49
|
+
_INFO_FETCH_KEYWORDS = [
|
|
50
|
+
# 搜索/查询
|
|
51
|
+
"搜索", "查询", "查一下", "搜一下", "什么是", "是什么", "介绍", "了解一下",
|
|
52
|
+
"最新", "新闻", "资讯", "百科", "定义", "概念", "解释",
|
|
53
|
+
"search", "look up", "what is", "what are", "introduction to", "latest",
|
|
54
|
+
"news", "wikipedia", "who is", "how to", "define", "explain",
|
|
55
|
+
# 远程/RAG/Agent
|
|
56
|
+
"远程", "rag", "知识库", "agent", "mcp", "外部工具", "外部神通",
|
|
57
|
+
"remote", "knowledge base", "external tool",
|
|
58
|
+
# 文件/数据读取
|
|
59
|
+
"读取", "查看内容", "分析文件", "总结", "提取",
|
|
60
|
+
"read", "analyze file", "summarize", "extract",
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def should_force_tool(user_input):
|
|
65
|
+
"""快速关键词预检:如果包含明确的工具操作关键词,直接判定为需要工具。
|
|
66
|
+
同时检测中英文关键词,不依赖当前界面语言。"""
|
|
67
|
+
u = user_input.lower()
|
|
68
|
+
for kw in _FORCE_TOOL_KEYWORDS:
|
|
69
|
+
if kw.lower() in u:
|
|
70
|
+
return True
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def classify_intent(state, user_input, tools, lang):
|
|
75
|
+
"""
|
|
76
|
+
意图判定:让大模型判定用户提问是直接查询还是需要调用工具。
|
|
77
|
+
将用户提问内容和 fr-cli 功能列表发给大模型,由大模型做判定。
|
|
78
|
+
返回 "DIRECT"(直接回答)或 "TOOL"(需要调用工具)。
|
|
79
|
+
根据 lang 自动切换中英文 prompt。
|
|
80
|
+
"""
|
|
81
|
+
tools_desc = "\n".join([
|
|
82
|
+
f"- {t['name']}: {t['description']} (commands: {', '.join(t['commands'])}"
|
|
83
|
+
for t in tools
|
|
84
|
+
])
|
|
85
|
+
|
|
86
|
+
if lang == "en":
|
|
87
|
+
classify_prompt = f"""You are an intent classifier. Based on the user's question, determine whether they need a direct answer or need to use the following tools to complete their task.
|
|
88
|
+
|
|
89
|
+
Available tools:
|
|
90
|
+
{tools_desc}
|
|
91
|
+
|
|
92
|
+
Rules (strict):
|
|
93
|
+
- DIRECT: The user is only asking for information, concepts, advice, or chatting. No action is required.
|
|
94
|
+
- TOOL: The user requests any specific action, including but not limited to saving files, searching the web, sending emails, listing directories, running code, generating images, etc. If the user mentions any action word like "save", "write", "search", "send", "look up", "list", etc., even if the first half is a question, it MUST be classified as TOOL.
|
|
95
|
+
|
|
96
|
+
User question: {user_input}
|
|
97
|
+
|
|
98
|
+
Output only one word: DIRECT or TOOL. No explanation."""
|
|
99
|
+
else:
|
|
100
|
+
classify_prompt = f"""你是一个意图分类器。请根据用户的提问,判定用户是需要直接获得回答,还是需要调用以下工具来完成任务。
|
|
101
|
+
|
|
102
|
+
可用工具列表:
|
|
103
|
+
{tools_desc}
|
|
104
|
+
|
|
105
|
+
判定规则(请严格遵守):
|
|
106
|
+
- DIRECT:用户只是单纯询问信息、概念、建议、闲聊,没有任何操作要求。
|
|
107
|
+
- TOOL:用户要求执行任何具体操作,包括但不限于保存文件、搜索网页、发送邮件、查看目录、运行代码、画图等。只要用户提到了"保存"、"写入"、"搜索"、"发送"、"查看"等操作性词汇,即使前半句是询问信息,也必须判定为 TOOL。
|
|
108
|
+
|
|
109
|
+
用户提问:{user_input}
|
|
110
|
+
|
|
111
|
+
请只输出一个单词:DIRECT 或 TOOL。不要输出任何解释。"""
|
|
112
|
+
|
|
113
|
+
messages = [{"role": "user", "content": classify_prompt}]
|
|
114
|
+
txt, _, _ = stream_cnt(
|
|
115
|
+
state.client, state.model_name, messages, lang,
|
|
116
|
+
custom_prefix="", max_tokens=10, silent=True
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
return "TOOL" if "TOOL" in txt.upper() else "DIRECT"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def has_info_fetch_intent(user_input):
|
|
123
|
+
"""检测用户输入是否包含信息获取关键词(触发双源回答模式)。"""
|
|
124
|
+
return any(kw in user_input.lower() for kw in _INFO_FETCH_KEYWORDS)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def has_save_intent(user_input):
|
|
128
|
+
"""检测用户输入是否包含保存意图关键词。"""
|
|
129
|
+
return any(kw in user_input.lower() for kw in _SAVE_KEYWORDS)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""
|
|
2
|
+
智能功能推荐系统
|
|
3
|
+
根据用户输入推荐相关功能
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
def recommend_features(user_input):
|
|
7
|
+
"""根据用户输入推荐相关功能"""
|
|
8
|
+
recommendations = []
|
|
9
|
+
input_lower = user_input.lower()
|
|
10
|
+
|
|
11
|
+
# 文件相关
|
|
12
|
+
if any(kw in input_lower for kw in ['文件', 'file', '目录', 'folder', '读取', 'read', '查看', 'view', 'ls', 'cat']):
|
|
13
|
+
recommendations.append({"cmd": "/ls", "desc": "列出当前目录文件"})
|
|
14
|
+
recommendations.append({"cmd": "/cat <file>", "desc": "查看文件内容"})
|
|
15
|
+
recommendations.append({"cmd": "/cd <dir>", "desc": "切换目录"})
|
|
16
|
+
|
|
17
|
+
# 文件写入相关
|
|
18
|
+
if any(kw in input_lower for kw in ['保存', '写入', 'write', '创建', 'create', '生成', 'generate']):
|
|
19
|
+
recommendations.append({"cmd": "/write <file> <content>", "desc": "写入文件内容"})
|
|
20
|
+
recommendations.append({"cmd": "/append <file> <content>", "desc": "追加文件内容"})
|
|
21
|
+
|
|
22
|
+
# 图片相关
|
|
23
|
+
if any(kw in input_lower for kw in ['图片', 'image', 'photo', '看图', 'see', '识别', 'recognize']):
|
|
24
|
+
recommendations.append({"cmd": "/see <image>", "desc": "查看并分析图片"})
|
|
25
|
+
|
|
26
|
+
# 邮件相关
|
|
27
|
+
if any(kw in input_lower for kw in ['邮件', 'mail', 'email', '发送', 'send', '收件', 'inbox']):
|
|
28
|
+
recommendations.append({"cmd": "/mail_inbox", "desc": "查看收件箱"})
|
|
29
|
+
recommendations.append({"cmd": "/mail_send <to> <subject>", "desc": "发送邮件"})
|
|
30
|
+
|
|
31
|
+
# 网络搜索相关
|
|
32
|
+
if any(kw in input_lower for kw in ['搜索', 'search', 'web', '网页', 'website', '查询', 'query']):
|
|
33
|
+
recommendations.append({"cmd": "/web <query>", "desc": "网络搜索"})
|
|
34
|
+
recommendations.append({"cmd": "/fetch <url>", "desc": "获取网页内容"})
|
|
35
|
+
|
|
36
|
+
# 定时任务相关
|
|
37
|
+
if any(kw in input_lower for kw in ['定时', 'schedule', 'cron', '任务', 'task', '周期', 'period']):
|
|
38
|
+
recommendations.append({"cmd": "/cron_add <seconds> <command>", "desc": "添加定时任务"})
|
|
39
|
+
recommendations.append({"cmd": "/cron_list", "desc": "列出定时任务"})
|
|
40
|
+
|
|
41
|
+
# 云盘相关
|
|
42
|
+
if any(kw in input_lower for kw in ['云盘', 'cloud', '上传', 'upload', '下载', 'download', 'disk']):
|
|
43
|
+
recommendations.append({"cmd": "/disk_ls", "desc": "列出云盘文件"})
|
|
44
|
+
recommendations.append({"cmd": "/disk_up <local> <remote>", "desc": "上传到云盘"})
|
|
45
|
+
recommendations.append({"cmd": "/disk_down <remote> <local>", "desc": "从云盘下载"})
|
|
46
|
+
|
|
47
|
+
# 会话相关
|
|
48
|
+
if any(kw in input_lower for kw in ['保存会话', 'save session', '加载会话', 'load session', '会话', 'session', '记录', 'record']):
|
|
49
|
+
recommendations.append({"cmd": "/save <name>", "desc": "保存当前会话"})
|
|
50
|
+
recommendations.append({"cmd": "/load", "desc": "加载历史会话"})
|
|
51
|
+
|
|
52
|
+
# 配置相关
|
|
53
|
+
if any(kw in input_lower for kw in ['模型', 'model', '密钥', 'key', '配置', 'config', '设置', 'setting']):
|
|
54
|
+
recommendations.append({"cmd": "/model <name>", "desc": "切换AI模型"})
|
|
55
|
+
recommendations.append({"cmd": "/key <key>", "desc": "设置API密钥"})
|
|
56
|
+
recommendations.append({"cmd": "/lang <zh/en>", "desc": "切换语言"})
|
|
57
|
+
|
|
58
|
+
# 插件相关
|
|
59
|
+
if any(kw in input_lower for kw in ['插件', 'plugin', '技能', 'skill', '工具', 'tool']):
|
|
60
|
+
recommendations.append({"cmd": "/skills", "desc": "查看可用插件"})
|
|
61
|
+
|
|
62
|
+
# 导出相关
|
|
63
|
+
if any(kw in input_lower for kw in ['导出', 'export', '文档', 'document']):
|
|
64
|
+
recommendations.append({"cmd": "/export", "desc": "导出会话为Markdown"})
|
|
65
|
+
|
|
66
|
+
# 命令执行相关
|
|
67
|
+
if any(kw in input_lower for kw in ['执行', 'exec', '命令', 'command', 'shell', 'bash', 'terminal']):
|
|
68
|
+
recommendations.append({"cmd": "!<command>", "desc": "执行系统命令"})
|
|
69
|
+
recommendations.append({"cmd": "!<cmd> | <prompt>", "desc": "命令输出管道到AI"})
|
|
70
|
+
|
|
71
|
+
return recommendations
|
fr_cli/core/stream.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""
|
|
2
|
+
流式输出与高亮状态机引擎
|
|
3
|
+
"""
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
from fr_cli.ui.ui import RESET, DIM, CYAN, RED, CODE_BG, CODE_FG
|
|
7
|
+
from fr_cli.lang.i18n import T
|
|
8
|
+
|
|
9
|
+
def stream_cnt(client, model, messages, lang, custom_prefix=None, max_tokens=None, silent=False):
|
|
10
|
+
"""
|
|
11
|
+
流式调用智谱API并实时打印,带有简易代码块高亮状态机
|
|
12
|
+
:param silent: 如果为True,则不输出到终端,仅返回文本
|
|
13
|
+
:return: tuple (完整回复文本 str, 使用情况 dict, 响应时间 float)
|
|
14
|
+
"""
|
|
15
|
+
if not silent:
|
|
16
|
+
p = custom_prefix or f"{CYAN}{T('prompt_ai', lang)}{RESET} "
|
|
17
|
+
sys.stdout.write(p); sys.stdout.flush()
|
|
18
|
+
|
|
19
|
+
start_time = time.time()
|
|
20
|
+
full_text = ""
|
|
21
|
+
in_code = False
|
|
22
|
+
usage = {}
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
# 验证API密钥
|
|
26
|
+
if not client.api_key or len(client.api_key) < 10:
|
|
27
|
+
print(f"{RED}[错误] API密钥未配置或格式不正确{RESET}")
|
|
28
|
+
return "[错误] 请先配置有效的API密钥", {}, 0.0
|
|
29
|
+
|
|
30
|
+
response = client.chat.completions.create(
|
|
31
|
+
model=model,
|
|
32
|
+
messages=messages,
|
|
33
|
+
stream=True,
|
|
34
|
+
max_tokens=max_tokens if max_tokens else 4096
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
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}")
|
|
53
|
+
if not silent:
|
|
54
|
+
sys.stdout.write(part)
|
|
55
|
+
sys.stdout.flush()
|
|
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 hasattr(chunk, 'usage') and chunk.usage:
|
|
65
|
+
usage = chunk.usage.model_dump()
|
|
66
|
+
|
|
67
|
+
except Exception as e:
|
|
68
|
+
sys.stdout.write(f"\n{DIM}{str(e)[:50]}{RESET}")
|
|
69
|
+
sys.stdout.flush()
|
|
70
|
+
|
|
71
|
+
if not silent:
|
|
72
|
+
sys.stdout.write(RESET)
|
|
73
|
+
sys.stdout.write("\n")
|
|
74
|
+
sys.stdout.flush()
|
|
75
|
+
|
|
76
|
+
end_time = time.time()
|
|
77
|
+
response_time = end_time - start_time
|
|
78
|
+
|
|
79
|
+
# 如果没有收到任何内容,返回提示信息
|
|
80
|
+
if not full_text:
|
|
81
|
+
return "[错误] 无法获取AI回复,请检查API密钥配置", usage, response_time
|
|
82
|
+
|
|
83
|
+
return full_text, usage, response_time
|
fr_cli/core/sysmon.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""
|
|
2
|
+
系统状态监控器
|
|
3
|
+
提供 CPU、内存、网络带宽的实时采样
|
|
4
|
+
优先使用 psutil,无依赖时优雅降级
|
|
5
|
+
"""
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
# 全局采样状态(单例)
|
|
9
|
+
_last_sample = {
|
|
10
|
+
"time": 0.0,
|
|
11
|
+
"bytes_sent": 0,
|
|
12
|
+
"bytes_recv": 0,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _has_psutil():
|
|
17
|
+
try:
|
|
18
|
+
import psutil
|
|
19
|
+
return True
|
|
20
|
+
except ImportError:
|
|
21
|
+
return False
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_cpu_percent():
|
|
25
|
+
"""获取 CPU 使用百分比,无 psutil 时返回 None"""
|
|
26
|
+
if not _has_psutil():
|
|
27
|
+
return None
|
|
28
|
+
import psutil
|
|
29
|
+
return psutil.cpu_percent(interval=0.1)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_memory_info():
|
|
33
|
+
"""
|
|
34
|
+
获取内存使用情况
|
|
35
|
+
:return: (used_mb, total_mb, percent) 或 None
|
|
36
|
+
"""
|
|
37
|
+
if not _has_psutil():
|
|
38
|
+
return None
|
|
39
|
+
import psutil
|
|
40
|
+
mem = psutil.virtual_memory()
|
|
41
|
+
used_mb = mem.used / (1024 * 1024)
|
|
42
|
+
total_mb = mem.total / (1024 * 1024)
|
|
43
|
+
return used_mb, total_mb, mem.percent
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_network_speed():
|
|
47
|
+
"""
|
|
48
|
+
获取网络带宽(MB/s)
|
|
49
|
+
:return: (upload_mb_s, download_mb_s) 或 None
|
|
50
|
+
"""
|
|
51
|
+
if not _has_psutil():
|
|
52
|
+
return None
|
|
53
|
+
import psutil
|
|
54
|
+
global _last_sample
|
|
55
|
+
|
|
56
|
+
net = psutil.net_io_counters()
|
|
57
|
+
now = time.time()
|
|
58
|
+
|
|
59
|
+
# 首次采样,只记录不返回速率
|
|
60
|
+
if _last_sample["time"] == 0:
|
|
61
|
+
_last_sample = {
|
|
62
|
+
"time": now,
|
|
63
|
+
"bytes_sent": net.bytes_sent,
|
|
64
|
+
"bytes_recv": net.bytes_recv,
|
|
65
|
+
}
|
|
66
|
+
return 0.0, 0.0
|
|
67
|
+
|
|
68
|
+
elapsed = now - _last_sample["time"]
|
|
69
|
+
if elapsed <= 0:
|
|
70
|
+
return 0.0, 0.0
|
|
71
|
+
|
|
72
|
+
sent_diff = net.bytes_sent - _last_sample["bytes_sent"]
|
|
73
|
+
recv_diff = net.bytes_recv - _last_sample["bytes_recv"]
|
|
74
|
+
|
|
75
|
+
upload_mb_s = (sent_diff / (1024 * 1024)) / elapsed
|
|
76
|
+
download_mb_s = (recv_diff / (1024 * 1024)) / elapsed
|
|
77
|
+
|
|
78
|
+
_last_sample = {
|
|
79
|
+
"time": now,
|
|
80
|
+
"bytes_sent": net.bytes_sent,
|
|
81
|
+
"bytes_recv": net.bytes_recv,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return upload_mb_s, download_mb_s
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def get_sys_stats(lang="zh"):
|
|
88
|
+
"""
|
|
89
|
+
获取完整的系统状态字符串(用于直接拼接显示)
|
|
90
|
+
:param lang: 语言代码 "zh" 或 "en"
|
|
91
|
+
:return: 形如 "CPU: 12% | 内存: 4.2/16.0GB(26%) | 网络: ↑0.5 ↓1.2 MB/s" 的字符串
|
|
92
|
+
或空字符串(无 psutil 时)
|
|
93
|
+
"""
|
|
94
|
+
if not _has_psutil():
|
|
95
|
+
return ""
|
|
96
|
+
|
|
97
|
+
cpu = get_cpu_percent()
|
|
98
|
+
mem = get_memory_info()
|
|
99
|
+
net = get_network_speed()
|
|
100
|
+
|
|
101
|
+
L_CPU = "CPU" if lang == "en" else "CPU"
|
|
102
|
+
L_MEM = "Mem" if lang == "en" else "内存"
|
|
103
|
+
L_NET = "Net" if lang == "en" else "网络"
|
|
104
|
+
|
|
105
|
+
parts = []
|
|
106
|
+
if cpu is not None:
|
|
107
|
+
parts.append(f"{L_CPU}:{cpu:.0f}%")
|
|
108
|
+
|
|
109
|
+
if mem is not None:
|
|
110
|
+
used_gb = mem[0] / 1024
|
|
111
|
+
total_gb = mem[1] / 1024
|
|
112
|
+
parts.append(f"{L_MEM}:{used_gb:.1f}/{total_gb:.1f}GB({mem[2]:.0f}%)")
|
|
113
|
+
|
|
114
|
+
if net is not None:
|
|
115
|
+
parts.append(f"{L_NET}:↑{net[0]:.1f}↓{net[1]:.1f}MB/s")
|
|
116
|
+
|
|
117
|
+
return " | ".join(parts) if parts else ""
|