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.
Files changed (64) hide show
  1. fr_cli/README.md +148 -0
  2. fr_cli/WEAPON.MD +186 -0
  3. fr_cli/__init__.py +4 -0
  4. fr_cli/addon/plugin.py +69 -0
  5. fr_cli/agent/__init__.py +9 -0
  6. fr_cli/agent/builtins/__init__.py +4 -0
  7. fr_cli/agent/builtins/_utils.py +48 -0
  8. fr_cli/agent/builtins/db.py +269 -0
  9. fr_cli/agent/builtins/local.py +105 -0
  10. fr_cli/agent/builtins/rag.py +652 -0
  11. fr_cli/agent/builtins/rag_watcher_daemon.py +156 -0
  12. fr_cli/agent/builtins/remote.py +214 -0
  13. fr_cli/agent/builtins/spider.py +247 -0
  14. fr_cli/agent/client.py +164 -0
  15. fr_cli/agent/executor.py +86 -0
  16. fr_cli/agent/generator.py +104 -0
  17. fr_cli/agent/manager.py +193 -0
  18. fr_cli/agent/master.py +604 -0
  19. fr_cli/agent/master_prompt.py +118 -0
  20. fr_cli/agent/remote.py +70 -0
  21. fr_cli/agent/server.py +279 -0
  22. fr_cli/agent/workflow.py +164 -0
  23. fr_cli/breakthrough/update.py +154 -0
  24. fr_cli/command/__init__.py +4 -0
  25. fr_cli/command/executor.py +276 -0
  26. fr_cli/command/registry.py +1034 -0
  27. fr_cli/command/security.py +30 -0
  28. fr_cli/conf/config.py +126 -0
  29. fr_cli/conf/wizard.py +172 -0
  30. fr_cli/core/chat.py +280 -0
  31. fr_cli/core/core.py +111 -0
  32. fr_cli/core/intent.py +129 -0
  33. fr_cli/core/recommender.py +71 -0
  34. fr_cli/core/stream.py +83 -0
  35. fr_cli/core/sysmon.py +117 -0
  36. fr_cli/core/thinking.py +215 -0
  37. fr_cli/gatekeeper/__init__.py +7 -0
  38. fr_cli/gatekeeper/daemon.py +216 -0
  39. fr_cli/gatekeeper/manager.py +218 -0
  40. fr_cli/lang/i18n.py +827 -0
  41. fr_cli/main.py +329 -0
  42. fr_cli/memory/context.py +119 -0
  43. fr_cli/memory/history.py +96 -0
  44. fr_cli/memory/session.py +134 -0
  45. fr_cli/repl/__init__.py +0 -0
  46. fr_cli/repl/commands.py +1098 -0
  47. fr_cli/security/security.py +46 -0
  48. fr_cli/ui/ui.py +116 -0
  49. fr_cli/weapon/cron.py +217 -0
  50. fr_cli/weapon/dataframe.py +97 -0
  51. fr_cli/weapon/disk.py +141 -0
  52. fr_cli/weapon/fs.py +206 -0
  53. fr_cli/weapon/launcher.py +249 -0
  54. fr_cli/weapon/loader.py +98 -0
  55. fr_cli/weapon/mail.py +227 -0
  56. fr_cli/weapon/mcp.py +204 -0
  57. fr_cli/weapon/vision.py +74 -0
  58. fr_cli/weapon/web.py +88 -0
  59. fr_cli-2.1.0.dist-info/METADATA +227 -0
  60. fr_cli-2.1.0.dist-info/RECORD +64 -0
  61. fr_cli-2.1.0.dist-info/WHEEL +5 -0
  62. fr_cli-2.1.0.dist-info/entry_points.txt +2 -0
  63. fr_cli-2.1.0.dist-info/licenses/LICENSE +21 -0
  64. fr_cli-2.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,30 @@
1
+ """
2
+ 四阶安全确认管理器
3
+ 将安全状态从 main.py 的闭包中提取为可复用的类
4
+ """
5
+ from fr_cli.security.security import ask
6
+
7
+
8
+ class SecurityManager:
9
+ """
10
+ 封装安全确认状态(Y/A/F/N)
11
+ - fconfirm: 永久放行
12
+ - sconfirm: 本次会话放行
13
+ """
14
+ def __init__(self, lang, cfg):
15
+ self.lang = lang
16
+ self.cfg = cfg
17
+ self.fconfirm = cfg.get("auto_confirm_forever", False)
18
+ self.sconfirm = False
19
+
20
+ def check(self, k, d):
21
+ """
22
+ 执行安全确认检查
23
+ :param k: 操作类型键名 (如 sec_read, sec_exec)
24
+ :param d: 具体操作描述
25
+ :return: bool 是否放行
26
+ """
27
+ allowed, self.sconfirm, self.fconfirm = ask(
28
+ k, d, self.lang, self.fconfirm, self.sconfirm, self.cfg
29
+ )
30
+ return allowed
fr_cli/conf/config.py ADDED
@@ -0,0 +1,126 @@
1
+ """
2
+ 配置文件读写与初始化引擎
3
+ 支持原子写入与自动备份,防止写入中断导致配置丢失
4
+ """
5
+ import json
6
+ import os
7
+ import shutil
8
+ from pathlib import Path
9
+ from fr_cli.ui.ui import YELLOW, RED, GREEN, RESET
10
+
11
+ CONFIG_FILE = Path.home() / ".zhipu_cli_config.json"
12
+ CONFIG_BACKUP = Path.home() / ".zhipu_cli_config.json.bak"
13
+ DEFAULT_WORKSPACE = Path.home() / "fr-cli-workspaces"
14
+ DEFAULT_LIMIT = 20000
15
+
16
+
17
+ def _default_config():
18
+ """返回默认配置字典"""
19
+ return {
20
+ "key": "",
21
+ "model": "glm-4-flash",
22
+ "limit": DEFAULT_LIMIT,
23
+ "allowed_dirs": [],
24
+ "lang": "zh",
25
+ "aliases": {},
26
+ "auto_confirm_forever": False,
27
+ "mail": {},
28
+ "disk": {},
29
+ "thinking_mode": "direct",
30
+ "mcp": {"servers": []},
31
+ }
32
+
33
+
34
+ def load_config():
35
+ """加载配置,如果缺失或损坏则返回带默认值的安全字典"""
36
+ d = _default_config()
37
+
38
+ # 尝试加载主配置文件
39
+ if CONFIG_FILE.exists():
40
+ try:
41
+ with open(CONFIG_FILE, "r", encoding="utf-8") as f:
42
+ c = json.load(f)
43
+ for k, v in d.items():
44
+ if k not in c:
45
+ c[k] = v
46
+ return c
47
+ except Exception as e:
48
+ print(
49
+ f"{YELLOW}⚠️ 配置文件损坏: {e}{RESET}"
50
+ )
51
+
52
+ # 尝试从备份恢复
53
+ if CONFIG_BACKUP.exists():
54
+ try:
55
+ with open(CONFIG_BACKUP, "r", encoding="utf-8") as f:
56
+ c = json.load(f)
57
+ for k, v in d.items():
58
+ if k not in c:
59
+ c[k] = v
60
+ print(f"{GREEN}✅ 已从备份恢复配置{RESET}")
61
+ # 恢复主配置文件
62
+ shutil.copy2(CONFIG_BACKUP, CONFIG_FILE)
63
+ return c
64
+ except Exception as e:
65
+ print(f"{YELLOW}⚠️ 备份文件也损坏: {e}{RESET}")
66
+
67
+ print(f"{YELLOW}⚠️ 使用默认配置,请重新设置{RESET}")
68
+ return d
69
+
70
+
71
+ def save_config(c):
72
+ """将配置字典原子写入本地(先写临时文件再重命名,避免写入中断损坏配置)"""
73
+ try:
74
+ # 1. 备份现有配置
75
+ if CONFIG_FILE.exists():
76
+ shutil.copy2(CONFIG_FILE, CONFIG_BACKUP)
77
+
78
+ # 2. 使用安全临时文件(随机名称 + 600 权限)
79
+ import tempfile
80
+ fd, tmp_path = tempfile.mkstemp(dir=CONFIG_FILE.parent, suffix=".json.tmp")
81
+ try:
82
+ os.chmod(tmp_path, 0o600)
83
+ with os.fdopen(fd, "w", encoding="utf-8") as f:
84
+ json.dump(c, f, indent=4, ensure_ascii=False)
85
+ Path(tmp_path).replace(CONFIG_FILE)
86
+ except Exception:
87
+ try:
88
+ os.remove(tmp_path)
89
+ except Exception:
90
+ pass
91
+ raise
92
+ return True
93
+ except Exception as e:
94
+ print(f"{RED}❌ 保存配置失败: {e}{RESET}")
95
+ return False
96
+
97
+
98
+ class ConfigError(Exception):
99
+ """配置初始化异常(替代 exit,避免作为库导入时终止进程)"""
100
+ pass
101
+
102
+
103
+ def init_config():
104
+ """首次运行引导:检查并要求输入 API Key,自动创建默认工作空间"""
105
+ c = load_config()
106
+
107
+ # 自动创建默认工作空间
108
+ if not c.get("allowed_dirs"):
109
+ DEFAULT_WORKSPACE.mkdir(parents=True, exist_ok=True)
110
+ c["allowed_dirs"] = [str(DEFAULT_WORKSPACE)]
111
+ save_config(c)
112
+ print(f"{GREEN}✅ 默认洞府已开辟: {DEFAULT_WORKSPACE}{RESET}")
113
+
114
+ if not c.get("key"):
115
+ print(f"\n{YELLOW}⚠️ API Key Required{RESET}")
116
+ k = input(f"👉 Enter Zhipu API Key: ").strip()
117
+ if k:
118
+ c["key"] = k
119
+ ok = save_config(c)
120
+ if ok:
121
+ print(f"{GREEN}✅ API Key 已保存至: {CONFIG_FILE}{RESET}")
122
+ else:
123
+ print(f"{RED}❌ 配置保存失败,下次启动可能需要重新输入。{RESET}")
124
+ else:
125
+ raise ConfigError("API Key is required")
126
+ return c
fr_cli/conf/wizard.py ADDED
@@ -0,0 +1,172 @@
1
+ """
2
+ 配置向导引擎
3
+ 当用户首次使用需配置的功能时,通过交互式向导引导配置并保存。
4
+ """
5
+ from fr_cli.conf.config import save_config
6
+ from fr_cli.ui.ui import CYAN, GREEN, YELLOW, RED, DIM, RESET
7
+
8
+
9
+ def _prompt(text, default=""):
10
+ """带默认值的输入提示"""
11
+ if default:
12
+ val = input(f"{CYAN}👉 {text} [{default}]: {RESET}").strip()
13
+ return val if val else default
14
+ return input(f"{CYAN}👉 {text}: {RESET}").strip()
15
+
16
+
17
+ def _confirm(text):
18
+ """Y/N 确认,默认 Y"""
19
+ r = input(f"{YELLOW}{text} (Y/n): {RESET}").strip().lower()
20
+ return r in ("", "y", "yes", "是")
21
+
22
+
23
+ # ── 邮件服务商预设 ──
24
+ MAIL_PRESETS = {
25
+ "1": {"name": "QQ邮箱 / Foxmail", "imap": "imap.qq.com", "smtp": "smtp.qq.com"},
26
+ "2": {"name": "163邮箱", "imap": "imap.163.com", "smtp": "smtp.163.com"},
27
+ "3": {"name": "Gmail", "imap": "imap.gmail.com", "smtp": "smtp.gmail.com"},
28
+ "4": {"name": "Outlook", "imap": "outlook.office365.com", "smtp": "smtp.office365.com"},
29
+ "5": {"name": "阿里云邮箱", "imap": "imap.aliyun.com", "smtp": "smtp.aliyun.com"},
30
+ }
31
+
32
+
33
+ def mail_wizard(cfg, lang="zh"):
34
+ """
35
+ 邮件配置交互向导
36
+ :param cfg: 当前配置字典(会被修改)
37
+ :param lang: 语言
38
+ :return: (success: bool, updated_cfg: dict)
39
+ """
40
+ uf = lang == "zh"
41
+ print(f"\n{YELLOW}{'⚠️ 邮件功能尚未配置' if uf else '⚠️ Mail not configured'}{RESET}")
42
+ if not _confirm("启动配置向导?" if uf else "Launch setup wizard?"):
43
+ return False, cfg
44
+
45
+ print(f"\n{CYAN}{'📧 选择邮箱服务商:' if uf else '📧 Select mail provider:'}{RESET}")
46
+ for k, v in MAIL_PRESETS.items():
47
+ print(f" [{k}] {v['name']}")
48
+ print(f" [6] {'自定义' if uf else 'Custom'}")
49
+
50
+ choice = _prompt("选择" if uf else "Choice", "1")
51
+ if choice in MAIL_PRESETS:
52
+ preset = MAIL_PRESETS[choice]
53
+ imap_server = preset["imap"]
54
+ smtp_server = preset["smtp"]
55
+ print(f"{DIM} IMAP: {imap_server} | SMTP: {smtp_server}{RESET}")
56
+ elif choice == "6":
57
+ imap_server = _prompt("IMAP 服务器" if uf else "IMAP server")
58
+ smtp_server = _prompt("SMTP 服务器" if uf else "SMTP server")
59
+ else:
60
+ print(f"{RED}{'❌ 无效选择' if uf else '❌ Invalid choice'}{RESET}")
61
+ return False, cfg
62
+
63
+ import getpass
64
+ email = _prompt("邮箱地址" if uf else "Email address")
65
+ password = getpass.getpass(f"{CYAN}👉 {'授权码/密码' if uf else 'Auth code / password'}: {RESET}")
66
+
67
+ if not email or not password:
68
+ print(f"{RED}{'❌ 邮箱和密码不能为空' if uf else '❌ Email and password required'}{RESET}")
69
+ return False, cfg
70
+
71
+ # 保存配置
72
+ cfg["mail"] = {
73
+ "imap_server": imap_server,
74
+ "smtp_server": smtp_server,
75
+ "email": email,
76
+ "password": password,
77
+ }
78
+ save_config(cfg)
79
+
80
+ # 尝试验证连接
81
+ print(f"\n{CYAN}{'🔄 正在测试连接...' if uf else '🔄 Testing connection...'}{RESET}")
82
+ try:
83
+ from fr_cli.weapon.mail import MailClient
84
+ client = MailClient(cfg["mail"])
85
+ if client.connected:
86
+ print(f"{GREEN}{'✅ 配置已保存并验证通过!' if uf else '✅ Config saved and verified!'}{RESET}")
87
+ else:
88
+ print(f"{YELLOW}{'⚠️ 配置已保存,但模块加载失败' if uf else '⚠️ Config saved but module load failed'}{RESET}")
89
+ except Exception as e:
90
+ print(f"{YELLOW}{'⚠️ 配置已保存,验证出错:' if uf else '⚠️ Config saved, verify error:'} {e}{RESET}")
91
+
92
+ return True, cfg
93
+
94
+
95
+ # ── 云盘类型预设 ──
96
+ DISK_PRESETS = {
97
+ "1": {"name": "阿里云盘", "type": "aliyundrive"},
98
+ "2": {"name": "百度网盘", "type": "baidu"},
99
+ "3": {"name": "OneDrive", "type": "onedrive"},
100
+ }
101
+
102
+
103
+ def disk_wizard(cfg, lang="zh"):
104
+ """
105
+ 云盘配置交互向导
106
+ :param cfg: 当前配置字典(会被修改)
107
+ :param lang: 语言
108
+ :return: (success: bool, updated_cfg: dict)
109
+ """
110
+ uf = lang == "zh"
111
+ print(f"\n{YELLOW}{'⚠️ 云盘功能尚未配置' if uf else '⚠️ Cloud disk not configured'}{RESET}")
112
+ if not _confirm("启动配置向导?" if uf else "Launch setup wizard?"):
113
+ return False, cfg
114
+
115
+ print(f"\n{CYAN}{'☁️ 选择云盘类型:' if uf else '☁️ Select cloud type:'}{RESET}")
116
+ for k, v in DISK_PRESETS.items():
117
+ print(f" [{k}] {v['name']}")
118
+
119
+ choice = _prompt("选择" if uf else "Choice", "1")
120
+ if choice in DISK_PRESETS:
121
+ disk_type = DISK_PRESETS[choice]["type"]
122
+ else:
123
+ print(f"{RED}{'❌ 无效选择' if uf else '❌ Invalid choice'}{RESET}")
124
+ return False, cfg
125
+
126
+ # 阿里云盘配置(扫码登录)
127
+ if disk_type == "aliyundrive":
128
+ try:
129
+ from aligo import Aligo
130
+ except ImportError:
131
+ print(f"{RED}{'❌ 请先安装 aligo: pip install aligo' if uf else '❌ Please install aligo: pip install aligo'}{RESET}")
132
+ return False, cfg
133
+
134
+ print(f"\n{DIM}{'正在初始化阿里云盘登录...' if uf else 'Initializing Aliyun Drive login...'}{RESET}")
135
+ try:
136
+ # 尝试使用已有 refresh_token 登录
137
+ old_disk = cfg.get("disk", {})
138
+ refresh_token = old_disk.get("refresh_token") if old_disk.get("type") == "aliyundrive" else None
139
+ name = old_disk.get("name", "fr-cli")
140
+
141
+ if refresh_token:
142
+ ali = Aligo(name=name, refresh_token=refresh_token)
143
+ print(f"{GREEN}{'✅ 已使用缓存令牌登录成功' if uf else '✅ Logged in with cached token'}{RESET}")
144
+ else:
145
+ print(f"{YELLOW}{'请使用阿里云盘 App 扫描二维码完成登录' if uf else 'Please scan QR code with Aliyun Drive App'}{RESET}")
146
+ ali = Aligo(name=name)
147
+ print(f"{GREEN}{'✅ 登录成功' if uf else '✅ Login successful'}{RESET}")
148
+
149
+ # 保存配置
150
+ disk_cfg = {
151
+ "type": "aliyundrive",
152
+ "name": name,
153
+ }
154
+ # 尝试提取 refresh_token 以便后续免扫码登录
155
+ try:
156
+ disk_cfg["refresh_token"] = ali.refresh_token
157
+ except AttributeError:
158
+ pass
159
+
160
+ cfg["disk"] = disk_cfg
161
+ except Exception as e:
162
+ print(f"{RED}{'❌ 登录失败:' if uf else '❌ Login failed:'} {e}{RESET}")
163
+ return False, cfg
164
+ else:
165
+ print(f"{YELLOW}{'⚠️ 当前仅支持阿里云盘,其他类型敬请期待' if uf else '⚠️ Only Aliyun Drive is currently supported'}{RESET}")
166
+ return False, cfg
167
+
168
+ save_config(cfg)
169
+ print(f"{GREEN}{'✅ 云盘配置已保存!' if uf else '✅ Cloud disk config saved!'}{RESET}")
170
+ return True, cfg
171
+
172
+
fr_cli/core/chat.py ADDED
@@ -0,0 +1,280 @@
1
+ """
2
+ AI 对话处理核心
3
+
4
+ 负责 system prompt 组装、流式调用、命令执行、
5
+ 多源信息汇总与保存意图检测。
6
+ """
7
+ import copy
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ from fr_cli.lang.i18n import T
12
+ from fr_cli.ui.ui import CYAN, DIM, RESET, YELLOW, RED, GREEN
13
+ from fr_cli.core.stream import stream_cnt
14
+ from fr_cli.weapon.loader import get_available_tools
15
+ from fr_cli.addon.plugin import exec_plugin, extract_code, PLUGIN_DIR
16
+ from fr_cli.core.recommender import recommend_features
17
+ from fr_cli.core.sysmon import get_sys_stats
18
+ from fr_cli.memory.context import extract_recent_turns, build_context_summary, save_context
19
+ from fr_cli.memory.session import create_session, update_session
20
+ from fr_cli.core.intent import should_force_tool, classify_intent, has_info_fetch_intent, has_save_intent
21
+
22
+
23
+ def handle_ai_chat(state, u):
24
+ """处理 AI 正常对话流程"""
25
+ lang = state.lang
26
+ prompt = u
27
+ if state.vfs.cwd:
28
+ prompt += T("ctx_dir", lang, state.vfs.cwd)
29
+
30
+ # 意图判定:先快速关键词预检,未命中再让大模型判定
31
+ tools = get_available_tools(state.weapon_tools, state.plugins)
32
+ # 将 MCP 外部神通纳入意图判定视野
33
+ 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
42
+ if mcp_tools_summary:
43
+ tools.append({
44
+ "name": "mcp_tools",
45
+ "description": "MCP 外部神通: " + ", ".join([t["name"] for t in mcp_tools_summary]),
46
+ "commands": ["mcp_call"],
47
+ })
48
+ if should_force_tool(u):
49
+ intent = "TOOL"
50
+ else:
51
+ intent = classify_intent(state, u, tools, lang)
52
+
53
+ # ---------- 思维推演(CoT / ToT / ReAct)----------
54
+ reasoning_text = None
55
+ if state.thinking_mode != "direct":
56
+ from fr_cli.core.thinking import ThinkingEngine
57
+ engine = ThinkingEngine()
58
+ if engine.is_valid_mode(state.thinking_mode):
59
+ # CoT / ToT 需要额外一次非流式调用
60
+ if state.thinking_mode in ("cot", "tot"):
61
+ mode_label = "思维链" if state.thinking_mode == "cot" else "思维树"
62
+ print(f"{DIM}🧠 启用 {mode_label} 推演...{RESET}")
63
+ reasoning_text = engine.analyze(state, u, state.thinking_mode, intent, lang)
64
+ if reasoning_text:
65
+ # 打印推理摘要(前200字符)
66
+ preview = reasoning_text[:200].replace('\n', ' ')
67
+ print(f"{DIM} 推演完成: {preview}...{RESET}")
68
+ elif state.thinking_mode == "react":
69
+ reasoning_text = engine.analyze(state, u, "react", intent, lang)
70
+
71
+ if intent == "TOOL":
72
+ tools_info = "\n\n当前可用的工具列表:\n"
73
+ for i, tool in enumerate(tools, 1):
74
+ tools_info += f"{i}. {tool['name']}: {tool['description']}\n 可用命令: {', '.join(tool['commands'])}\n"
75
+ # 注入 MCP 外部神通
76
+ mcp_manager = getattr(state, "mcp", None)
77
+ if mcp_manager and hasattr(mcp_manager, "get_server_tools_desc"):
78
+ try:
79
+ mcp_desc = mcp_manager.get_server_tools_desc()
80
+ if isinstance(mcp_desc, str) and mcp_desc:
81
+ tools_info += mcp_desc + "\n"
82
+ tools_info += "\n调用 MCP 工具时,请使用格式:【调用:mcp_call({\"server\": \"服务器名\", \"tool\": \"工具名\", \"arguments\": {...}})】\n"
83
+ except Exception:
84
+ pass
85
+ # 信息获取规范:当用户需要调用外部信息源时,采用双源回答模式
86
+ if has_info_fetch_intent(u):
87
+ tools_info += """\n
88
+ 【信息获取规范 —— 双源回答与汇总】
89
+ 用户的问题涉及信息获取(如搜索、查询、读取远程内容、调用Agent/MCP工具等)。请严格按以下步骤执行:
90
+
91
+ 1. 初步回答(必须):
92
+ 先基于你的内部知识给出一个初步回答或分析框架,直接输出在回复文本中。
93
+ 禁止只写"让我查一下"而不给实质内容。
94
+
95
+ 2. 工具补充:
96
+ 然后调用相应的工具(search_web、mcp_call、agent_call、read_file 等)获取补充信息。
97
+
98
+ 3. 汇总整理(第二轮自动执行):
99
+ 所有工具结果返回后,我会将你的初步回答与所有工具返回结果一起提交给你。
100
+ 请基于多源信息整理成一份完整、准确、结构清晰的最终答案。
101
+ 若不同来源存在冲突,请以最新/最权威来源为准,或明确标注不确定性。
102
+ """
103
+ sp = T("sys_prompt", lang)
104
+ system_content = sp + tools_info + state.context_summary
105
+ else:
106
+ sp = T("sys_prompt", lang)
107
+ system_content = sp + state.context_summary
108
+
109
+ # 注入思维推演结果
110
+ if reasoning_text:
111
+ if state.thinking_mode in ("cot", "tot"):
112
+ system_content += f"\n\n[系统提示:以下是你之前的深度推演结果,请在最终回答中参考这些分析]\n\n{reasoning_text}\n"
113
+ elif state.thinking_mode == "react":
114
+ system_content += reasoning_text
115
+
116
+ # 更新系统提示词
117
+ updated_messages = copy.deepcopy(state.messages)
118
+ if not updated_messages or updated_messages[0]["role"] != "system":
119
+ updated_messages.insert(0, {"role": "system", "content": system_content})
120
+ else:
121
+ updated_messages[0]["content"] = system_content
122
+
123
+ updated_messages.append({"role": "user", "content": prompt})
124
+
125
+ # 检测是否调用了本地技能
126
+ triggered_plugin = None
127
+ for pk in state.plugins:
128
+ if prompt.startswith(f"/{pk} "):
129
+ triggered_plugin = pk
130
+ p_args = prompt[len(f"/{pk} "):].strip()
131
+ break
132
+
133
+ if triggered_plugin:
134
+ if state.security.check("sec_exec", f"/{triggered_plugin}"):
135
+ exec_plugin(triggered_plugin, state.plugins[triggered_plugin], p_args, lang)
136
+ updated_messages.append({"role": "assistant", "content": f"[Executed /{triggered_plugin}]"})
137
+ state.messages = updated_messages
138
+ return
139
+
140
+ # 流式调用 AI
141
+ txt, usage, response_time = stream_cnt(
142
+ state.client, state.model_name, updated_messages, lang,
143
+ max_tokens=state.limit
144
+ )
145
+ updated_messages.append({"role": "assistant", "content": txt})
146
+
147
+ # 自动执行 AI 响应中的命令
148
+ clean_txt, cmd_results = state.executor.process_ai_commands(txt, updated_messages)
149
+
150
+ # 显示 AI 响应(去除命令标记后的内容)
151
+ if clean_txt.strip():
152
+ print(clean_txt)
153
+
154
+ # 显示命令执行结果,并再次调用 AI
155
+ if cmd_results:
156
+ print(f"\n{CYAN}🤖 自动执行命令:{RESET}")
157
+ for result in cmd_results:
158
+ print(f"{DIM}{result}{RESET}")
159
+
160
+ # 重构为【多源信息汇总】模式:将 AI 初步回答与所有工具结果结构化合并
161
+ sources = []
162
+ if clean_txt.strip():
163
+ sources.append(f"【来源一:大模型初步回答】\n{clean_txt.strip()}")
164
+ for idx, result in enumerate(cmd_results, start=2):
165
+ sources.append(f"【来源{idx}:工具执行结果】\n{result}")
166
+
167
+ blend_system_content = "=== 多源信息汇总 ===\n\n"
168
+ blend_system_content += "\n\n---\n\n".join(sources)
169
+ blend_system_content += (
170
+ "\n\n=== 整理要求 ===\n"
171
+ "请基于以上所有信息来源,整理成一份完整、准确、结构清晰的最终答案。\n"
172
+ "- 不同来源的信息若存在冲突,请以最新/最权威来源为准,或明确标注不确定性。\n"
173
+ "- 若大模型初步回答已较完整,但工具结果提供了更新/更详细的数据,请在初步回答基础上补充修正。\n"
174
+ "- 若工具结果与初步回答完全一致,可精简输出,避免冗余。\n"
175
+ "- 最终答案应自成一体,用户无需知道这是多源汇总的结果。"
176
+ )
177
+
178
+ updated_messages[-1]["content"] = clean_txt if clean_txt.strip() else "[已执行命令]"
179
+ updated_messages.append({"role": "system", "content": blend_system_content})
180
+
181
+ # 方案二:检测保存意图,追加提示强制第二轮 AI 调用 write_file
182
+ if has_save_intent(u):
183
+ save_hint = (
184
+ "\n[系统提示:用户原始请求中包含'保存到本地'的意图。"
185
+ "请在给出最终整理后的回答后,使用 write_file 工具将完整内容保存到文件。"
186
+ "如果用户未指定文件名,请使用一个能反映内容主题的简洁文件名(如 a2a_introduction.md)。]"
187
+ )
188
+ updated_messages.append({"role": "system", "content": save_hint})
189
+
190
+ sys.stdout.write(f"{CYAN}{T('prompt_ai', lang)}{RESET} ")
191
+ sys.stdout.flush()
192
+ final_txt, final_usage, final_response_time = stream_cnt(
193
+ state.client, state.model_name, updated_messages, lang,
194
+ custom_prefix="", max_tokens=state.limit
195
+ )
196
+ updated_messages.append({"role": "assistant", "content": final_txt})
197
+
198
+ if final_usage:
199
+ usage = final_usage
200
+ response_time += final_response_time
201
+
202
+ # 显示模型信息和 token 使用情况
203
+ sys_stats = get_sys_stats(lang)
204
+ stats_extra = f" | {sys_stats}" if sys_stats else ""
205
+ if usage:
206
+ input_tokens = usage.get('prompt_tokens', 0)
207
+ output_tokens = usage.get('completion_tokens', 0)
208
+ total_tokens = usage.get('total_tokens', 0)
209
+ print(f"{DIM}📊 模型: {state.model_name} | 输入: {input_tokens} tokens | 输出: {output_tokens} tokens | 总计: {total_tokens} tokens | 耗时: {response_time:.2f}秒{stats_extra}{RESET}")
210
+ else:
211
+ print(f"{DIM}📊 模型: {state.model_name} | 耗时: {response_time:.2f}秒{stats_extra}{RESET}")
212
+
213
+ # 智能功能推荐
214
+ recommendations = recommend_features(u)
215
+ if recommendations:
216
+ print(f"{CYAN}💡 推荐功能:{RESET}")
217
+ for i, rec in enumerate(recommendations[:5], 1):
218
+ print(f" {DIM}[{i}]{RESET} {CYAN}{rec['cmd']}{RESET} - {rec['desc']}")
219
+
220
+ # 智能法宝进化检测(插件)
221
+ if "def run(args='')" in txt and "```python" in txt:
222
+ code = extract_code(txt)
223
+ if code and "def run" in code and len(code) > 50:
224
+ pname = input(f"{YELLOW}{T('artifact_detect', lang)}{RESET}").strip()
225
+ if pname:
226
+ safe_name = "".join(c for c in pname if c.isalnum() or c == '_')
227
+ if not safe_name:
228
+ print(f"{RED}名称无效,仅允许字母/数字/下划线{RESET}")
229
+ elif state.security.check("sec_write", f"/{safe_name}"):
230
+ PLUGIN_DIR.mkdir(parents=True, exist_ok=True)
231
+ p_path = PLUGIN_DIR / f"{safe_name}.py"
232
+ p_path.write_text(code, encoding='utf-8')
233
+ state.plugins[safe_name] = str(p_path)
234
+ print(f"{GREEN}{T('ok_forged', lang, safe_name)}{RESET}")
235
+
236
+ # 智能 Agent 分身检测
237
+ if "def run(context," in txt and "```python" in txt:
238
+ code = extract_code(txt)
239
+ if code and "def run(context," in code and len(code) > 50:
240
+ aname = input(f"{YELLOW}⚡ 检测到 Agent 分身结构,赐名 (回车放弃): {RESET}").strip()
241
+ if aname:
242
+ safe_name = "".join(c for c in aname if c.isalnum() or c == '_')
243
+ if not safe_name:
244
+ print(f"{RED}名称无效,仅允许字母/数字/下划线{RESET}")
245
+ else:
246
+ from fr_cli.agent.manager import create_agent_dir, save_agent_code, save_persona, save_skills, agent_exists
247
+ if agent_exists(safe_name):
248
+ confirm = input(f"{YELLOW}Agent [{safe_name}] 已存在,是否覆盖? [y/N]: {RESET}").strip().lower()
249
+ if confirm not in ("y", "yes"):
250
+ print(f"{DIM}已取消。{RESET}")
251
+ else:
252
+ d = create_agent_dir(safe_name)
253
+ save_agent_code(safe_name, code)
254
+ print(f"{GREEN}✅ Agent [{safe_name}] 已覆盖更新。{RESET}")
255
+ print(f"{DIM} 路径: {d}{RESET}")
256
+ else:
257
+ d = create_agent_dir(safe_name)
258
+ save_agent_code(safe_name, code)
259
+ save_persona(safe_name, f"#{safe_name}\n\n由 AI 对话铸造的 Agent 分身。")
260
+ save_skills(safe_name, "## 技能\n\n- 执行自定义 Python 逻辑\n- 入口: run(context, **kwargs)")
261
+ print(f"{GREEN}✅ Agent [{safe_name}] 铸造完成!{RESET}")
262
+ print(f"{DIM} 路径: {d}{RESET}")
263
+ print(f"{DIM} 运行: /agent_run {safe_name} [参数]{RESET}")
264
+
265
+ # 更新记忆上下文
266
+ recent = extract_recent_turns(updated_messages, 5)
267
+ state.context_summary = build_context_summary(recent, lang)
268
+ save_context(state.sn, state.context_summary)
269
+
270
+ # 更新主消息列表
271
+ state.messages = updated_messages
272
+
273
+ # 自动按日期存档会话
274
+ if not state.auto_session_path:
275
+ path = create_session(state.messages)
276
+ if path:
277
+ state.auto_session_path = path
278
+ print(f"{DIM}📁 自动会话已创建: {Path(path).name}{RESET}")
279
+ else:
280
+ update_session(state.auto_session_path, state.messages)