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,46 @@
1
+ """
2
+ 四阶安全确认引擎
3
+ """
4
+ import os
5
+ from fr_cli.ui.ui import RED, BOLD, YELLOW, CYAN, RESET
6
+ from fr_cli.lang.i18n import T
7
+ from fr_cli.conf.config import save_config
8
+
9
+ def ask(k, d, l, fconfirm, sconfirm, config):
10
+ """
11
+ 安全询问逻辑
12
+ :param k: 操作类型键名 (如 sec_read, sec_exec)
13
+ :param d: 具体操作描述 (如文件名或命令)
14
+ :param l: 当前语言
15
+ :param fconfirm: 永久放行状态
16
+ :param sconfirm: 本次轮回放行状态
17
+ :param config: 配置字典对象 (用于持久化永久放行状态)
18
+ :return: tuple (是否放行:bool, 更新后的sconfirm:bool, 更新后的fconfirm:bool)
19
+ """
20
+ # 如果已经处于放行状态,直接放行
21
+ if fconfirm or sconfirm:
22
+ return True, sconfirm, fconfirm
23
+
24
+ # 非交互环境(如 Agent HTTP 服务、CI)默认拒绝,避免阻塞或崩溃
25
+ if os.environ.get("FR_CLI_NON_INTERACTIVE"):
26
+ return False, sconfirm, fconfirm
27
+
28
+ print(f"\n{RED}{BOLD}{T('sec_title', l)}{RESET}")
29
+ print(f"{YELLOW} >> {T(k, l)}: {d}{RESET}")
30
+ print(f" {CYAN}{T('sec_opt_y', l)} {T('sec_opt_a', l)} {T('sec_opt_f', l)} {T('sec_opt_n', l)}{RESET}")
31
+
32
+ while True:
33
+ c = input(f"{BOLD} 👉 {RESET}").strip().lower()
34
+ if c == 'y':
35
+ return True, sconfirm, fconfirm
36
+ elif c == 'a':
37
+ return True, True, fconfirm
38
+ elif c == 'f':
39
+ # 永久放行,写入配置文件
40
+ sconfirm = True
41
+ fconfirm = True
42
+ config["auto_confirm_forever"] = True
43
+ save_config(config)
44
+ return True, sconfirm, fconfirm
45
+ elif c == 'n' or c == '':
46
+ return False, sconfirm, fconfirm
fr_cli/ui/ui.py ADDED
@@ -0,0 +1,116 @@
1
+ """
2
+ 终端UI、颜色定义与动画引擎
3
+ """
4
+ import sys, time, platform, os
5
+
6
+ # ANSI 颜色与样式常量
7
+ RESET = '\033[0m'; BOLD = '\033[1m'; DIM = '\033[2m'
8
+ RED = '\033[91m'; GREEN = '\033[92m'; YELLOW = '\033[93m'
9
+ BLUE = '\033[94m'; MAGENTA = '\033[95m'; CYAN = '\033[96m'; WHITE = '\033[97m'
10
+ CODE_BG = '\033[48;5;236m'; CODE_FG = '\033[38;5;255m'
11
+
12
+ # 动画用的字符集
13
+ C_HALF = r"!@#$%^&*()_+-=[]{}|;:<>?/~0123456789ABCDEFabcdef"
14
+
15
+ def enable_win_ansi():
16
+ """在 Windows 上启用 ANSI 转义序列支持 (VT100)"""
17
+ if platform.system() == "Windows":
18
+ try:
19
+ import ctypes
20
+ kernel32 = ctypes.windll.kernel32
21
+ handle = kernel32.GetStdHandle(-11)
22
+ mode = ctypes.c_ulong()
23
+ kernel32.GetConsoleMode(handle, ctypes.byref(mode))
24
+ kernel32.SetConsoleMode(handle, mode.value | 0x0004)
25
+ except Exception:
26
+ os.system("")
27
+
28
+ def safe_clear():
29
+ """安全地清除当前行"""
30
+ sys.stdout.write("\r\033[K")
31
+ sys.stdout.flush()
32
+
33
+ def is_wide(c):
34
+ """判断字符是否为全角字符(用于动画对齐)"""
35
+ return len(c.encode('utf-8')) > 1
36
+
37
+ def get_display_width(text):
38
+ """计算字符串的实际显示宽度,考虑ANSI颜色代码和全角字符"""
39
+ import re
40
+ # 移除ANSI颜色代码
41
+ clean_text = re.sub(r'\033\[[0-9;]*m', '', text)
42
+ # 计算显示宽度
43
+ width = 0
44
+ for char in clean_text:
45
+ if is_wide(char):
46
+ width += 2
47
+ else:
48
+ width += 1
49
+ return width
50
+
51
+ def print_banner(mn, tl, ad, sn, l):
52
+ """打印启动时的小乌龟从左向右爬行动画"""
53
+
54
+ # 乌龟身体(6行)
55
+ turtle_body = [
56
+ ' _____',
57
+ " .-' o o '-.",
58
+ ' / \\',
59
+ ' | ___ |',
60
+ ' \\ / \\ /',
61
+ " `-._____.-'",
62
+ ]
63
+
64
+ # 三帧腿部姿态(每帧2行腿部)
65
+ turtle_frames = [
66
+ turtle_body + [' / \\ / \\ ', ' / \\ / \\'],
67
+ turtle_body + [' | | | | ', ' / \\ / \\'],
68
+ turtle_body + [' \\ / \\ / ', ' / \\ / \\'],
69
+ ]
70
+
71
+ total_lines = len(turtle_frames[0]) # 8 行
72
+
73
+ # 爬行路径:从左到右,共 8 个位置,每步 3 格
74
+ positions = [0, 3, 6, 9, 12, 15, 18, 21]
75
+
76
+ # 打印初始空白占位
77
+ print("\n" * total_lines)
78
+
79
+ # 爬行动画:每个位置循环三帧腿部
80
+ for pos in positions:
81
+ for frame in turtle_frames:
82
+ sys.stdout.write(f"\033[{total_lines}A")
83
+ for line in frame:
84
+ # 整体右移 pos 格,加绿色
85
+ padded = " " * pos + line
86
+ sys.stdout.write(f"\033[K{GREEN}{padded}{RESET}\n")
87
+ sys.stdout.flush()
88
+ time.sleep(0.15)
89
+
90
+ # 最终定格:再显示一帧(停顿一下)
91
+ sys.stdout.write(f"\033[{total_lines}A")
92
+ for line in turtle_frames[0]:
93
+ padded = " " * positions[-1] + line
94
+ sys.stdout.write(f"\033[K{GREEN}{padded}{RESET}\n")
95
+ sys.stdout.flush()
96
+ time.sleep(0.3)
97
+
98
+ # 显示标题
99
+ if l == "zh":
100
+ print(f"\n{CYAN}{BOLD} 凡 人 打 字 机 {RESET}")
101
+ print(f" 【 修 仙 者 的 编 码 引 擎 】\n")
102
+ else:
103
+ print(f"\n{CYAN}{BOLD} F A N R E N C L I T O O L{RESET}")
104
+ print(f" [ Advanced Code Engine v1.0 ]\n")
105
+
106
+ uf = (l == "zh")
107
+ ds = f"{GREEN}{ad}{RESET}" if ad else f"{RED}{('未开放洞府' if uf else 'No dir')}{RESET}"
108
+ ss = f"{MAGENTA}{sn}{RESET}" if sn else f"{DIM}{'全新轮回' if uf else 'New'}{RESET}"
109
+ i1 = f" {'🔮 模型' if uf else 'Model'}: {GREEN}{BOLD}{mn}{RESET} | {'🛡️ 上限' if uf else 'Limit'}: {YELLOW}{tl}{RESET}"
110
+ 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")
113
+
114
+ def print_bye():
115
+ """打印退出动画"""
116
+ print(f"\n{DIM}欢 迎 下 次 继 续 修 仙{RESET}\n")
fr_cli/weapon/cron.py ADDED
@@ -0,0 +1,217 @@
1
+ """
2
+ 结界定时引擎
3
+ 使用线程实现轻量级的后台定时任务
4
+ 支持 shell 命令和 Agent 分身两种任务类型
5
+ """
6
+ import threading
7
+ import subprocess
8
+ import shlex
9
+ from fr_cli.ui.ui import RED, GREEN, DIM, YELLOW, RESET
10
+ from fr_cli.lang.i18n import T
11
+
12
+
13
+ class CronManager:
14
+ """定时任务管理器 —— 结界掌控者"""
15
+
16
+ def __init__(self):
17
+ self.jobs = []
18
+ self._job_id_counter = 0
19
+ self._lock = threading.Lock()
20
+
21
+ def _job_runner(self, job_id, cmd, interval, lang, job_type="shell", agent_name=None, agent_input="", state=None):
22
+ """内部递归执行器,实现每隔 interval 秒执行一次"""
23
+ with self._lock:
24
+ job = next((j for j in self.jobs if j["id"] == job_id), None)
25
+ if not job:
26
+ return
27
+
28
+ try:
29
+ if job_type == "agent" and agent_name:
30
+ # 执行 Agent 分身
31
+ if state is None:
32
+ print(f"{RED}[Cron {job_id}] Error: Agent 任务需要 AppState{RESET}")
33
+ else:
34
+ from fr_cli.agent.executor import run_agent
35
+ result, err = run_agent(agent_name, state, user_input=agent_input)
36
+ out = (result or "")[:200]
37
+ if err:
38
+ out = f"Error: {err}"
39
+ print(f"{DIM}[Cron {job_id}] Agent[{agent_name}]{RESET} {out}")
40
+ else:
41
+ # 执行 shell 命令(安全:不使用 shell=True)
42
+ try:
43
+ cmd_list = shlex.split(cmd)
44
+ except ValueError:
45
+ cmd_list = [cmd]
46
+ res = subprocess.run(cmd_list, shell=False, capture_output=True, text=True, timeout=30)
47
+ out = res.stdout.strip()[:100] # 截断输出
48
+ print(f"{DIM}[Cron {job_id}]{RESET} {out}")
49
+ except Exception as e:
50
+ print(f"{RED}[Cron {job_id}] Error: {e}{RESET}")
51
+
52
+ # 重新注册定时器
53
+ job["timer"] = threading.Timer(
54
+ interval, self._job_runner,
55
+ args=(job_id, cmd, interval, lang),
56
+ kwargs={"job_type": job_type, "agent_name": agent_name, "agent_input": agent_input, "state": state}
57
+ )
58
+ job["timer"].daemon = True
59
+ job["timer"].start()
60
+
61
+ def add_job(self, cmd, interval, lang, job_type="shell", agent_name=None, agent_input="", state=None):
62
+ """添加一个定时循环任务
63
+
64
+ Args:
65
+ cmd: 命令字符串(shell 类型)或 Agent 名称(agent 类型)
66
+ interval: 执行间隔(秒)
67
+ lang: 界面语言
68
+ job_type: "shell" 或 "agent"
69
+ agent_name: Agent 分身名称(agent 类型时有效)
70
+ agent_input: 传递给 Agent 的输入内容(agent 类型时有效)
71
+ state: AppState 实例(agent 类型时必需)
72
+ """
73
+ try:
74
+ interval = float(interval)
75
+ except ValueError:
76
+ return None, f"{RED}Invalid seconds{RESET}"
77
+
78
+ if interval < 5:
79
+ return None, f"{RED}间隔不能小于 5 秒{RESET}"
80
+
81
+ with self._lock:
82
+ self._job_id_counter += 1
83
+ job_id = self._job_id_counter
84
+
85
+ job = {
86
+ "id": job_id,
87
+ "cmd": cmd,
88
+ "interval": interval,
89
+ "timer": None,
90
+ "job_type": job_type,
91
+ "agent_name": agent_name,
92
+ "agent_input": agent_input,
93
+ }
94
+ self.jobs.append(job)
95
+
96
+ # 启动首次任务
97
+ job["timer"] = threading.Timer(
98
+ interval, self._job_runner,
99
+ args=(job_id, cmd, interval, lang),
100
+ kwargs={"job_type": job_type, "agent_name": agent_name, "agent_input": agent_input, "state": state}
101
+ )
102
+ job["timer"].daemon = True
103
+ job["timer"].start()
104
+
105
+ return job_id, T("cron_ok", lang, job_id, interval)
106
+
107
+ def list_jobs(self, lang):
108
+ """列出当前运行中的任务"""
109
+ with self._lock:
110
+ jobs_copy = list(self.jobs)
111
+ if not jobs_copy:
112
+ return None, T("empty", lang)
113
+ res = []
114
+ for j in jobs_copy:
115
+ jtype = j.get("job_type", "shell")
116
+ type_tag = f"[{jtype}]" if jtype == "agent" else "[shell]"
117
+ target = j.get("agent_name", j["cmd"]) if jtype == "agent" else j["cmd"]
118
+ res.append(f"{GREEN}ID:{j['id']}{RESET} | {type_tag} | {YELLOW}{j['interval']}s{RESET} | {target[:30]}")
119
+ return res, None
120
+
121
+ def del_job(self, job_id, lang):
122
+ """根据 ID 终止定时任务"""
123
+ with self._lock:
124
+ job = next((j for j in self.jobs if j["id"] == job_id), None)
125
+ if not job:
126
+ return False, f"{RED}Not found{RESET}"
127
+ if job["timer"]:
128
+ job["timer"].cancel()
129
+ self.jobs.remove(job)
130
+ return True, T("cron_killed", lang, job_id)
131
+
132
+ def sync_jobs(self, job_configs, lang="zh", state=None):
133
+ """同步任务列表:根据配置增删任务,保持当前任务与配置一致
134
+
135
+ Args:
136
+ job_configs: 任务配置列表,每项为 dict,包含 cmd/interval/job_type/agent_name/agent_input
137
+ lang: 界面语言
138
+ state: AppState 实例(agent 类型任务必需)
139
+ """
140
+ with self._lock:
141
+ current_ids = {j["id"] for j in self.jobs}
142
+ target_ids = {j.get("id") for j in job_configs if j.get("id")}
143
+
144
+ # 删除不在配置中的任务
145
+ for j in list(self.jobs):
146
+ if j["id"] not in target_ids:
147
+ if j["timer"]:
148
+ j["timer"].cancel()
149
+ self.jobs.remove(j)
150
+
151
+ # 添加配置中有但当前没有的任务(在锁外调用 add_job,避免死锁)
152
+ for cfg in job_configs:
153
+ jid = cfg.get("id")
154
+ if jid and jid not in current_ids:
155
+ self.add_job(
156
+ cmd=cfg.get("cmd", ""),
157
+ interval=cfg.get("interval", 60),
158
+ lang=lang,
159
+ job_type=cfg.get("job_type", "shell"),
160
+ agent_name=cfg.get("agent_name"),
161
+ agent_input=cfg.get("agent_input", ""),
162
+ state=state,
163
+ )
164
+
165
+ def export_jobs(self):
166
+ """导出所有定时任务为可持久化的字典列表(不含线程对象)"""
167
+ with self._lock:
168
+ jobs_copy = list(self.jobs)
169
+ return [
170
+ {
171
+ "id": j["id"],
172
+ "cmd": j["cmd"],
173
+ "interval": j["interval"],
174
+ "job_type": j.get("job_type", "shell"),
175
+ "agent_name": j.get("agent_name"),
176
+ "agent_input": j.get("agent_input", ""),
177
+ }
178
+ for j in jobs_copy
179
+ ]
180
+
181
+ def import_jobs(self, jobs, lang="zh", state=None):
182
+ """从字典列表恢复定时任务"""
183
+ for job in jobs:
184
+ try:
185
+ self.add_job(
186
+ cmd=job.get("cmd", ""),
187
+ interval=job.get("interval", 60),
188
+ lang=lang,
189
+ job_type=job.get("job_type", "shell"),
190
+ agent_name=job.get("agent_name"),
191
+ agent_input=job.get("agent_input", ""),
192
+ state=state,
193
+ )
194
+ except Exception:
195
+ pass
196
+
197
+
198
+ # ------------------------------------------------------------------
199
+ # 默认全局实例(保持向后兼容)
200
+ # ------------------------------------------------------------------
201
+ _default_manager = CronManager()
202
+ JOBS = _default_manager.jobs
203
+
204
+
205
+ def add_job(cmd, interval, lang):
206
+ """添加定时任务(委托给默认管理器)"""
207
+ return _default_manager.add_job(cmd, interval, lang)
208
+
209
+
210
+ def list_jobs(lang):
211
+ """列出定时任务(委托给默认管理器)"""
212
+ return _default_manager.list_jobs(lang)
213
+
214
+
215
+ def del_job(job_id, lang):
216
+ """删除定时任务(委托给默认管理器)"""
217
+ return _default_manager.del_job(job_id, lang)
@@ -0,0 +1,97 @@
1
+ """
2
+ 数据卷轴读取器 —— Excel / CSV 分析神通
3
+ 支持读取表格文件并提交给大模型进行智能分析。
4
+ """
5
+
6
+ def _try_import_pandas():
7
+ try:
8
+ import pandas as pd
9
+ return pd
10
+ except ImportError:
11
+ return None
12
+
13
+
14
+ def read_excel(path, max_rows=1000, lang="zh"):
15
+ """读取 Excel 文件,返回文本摘要"""
16
+ pd = _try_import_pandas()
17
+ if not pd:
18
+ return None, "缺少 pandas/openpyxl (pip install pandas openpyxl)"
19
+ try:
20
+ df = pd.read_excel(path, nrows=max_rows)
21
+ return _df_to_summary(df, path, lang), None
22
+ except Exception as e:
23
+ return None, str(e)
24
+
25
+
26
+ def read_csv(path, max_rows=1000, lang="zh"):
27
+ """读取 CSV 文件,返回文本摘要"""
28
+ pd = _try_import_pandas()
29
+ if not pd:
30
+ return None, "缺少 pandas (pip install pandas)"
31
+ try:
32
+ df = pd.read_csv(path, nrows=max_rows)
33
+ return _df_to_summary(df, path, lang), None
34
+ except Exception as e:
35
+ return None, str(e)
36
+
37
+
38
+ def _df_to_summary(df, path, lang):
39
+ """将 DataFrame 转换为文本摘要,供 LLM 分析"""
40
+ lines = []
41
+ lines.append(f"文件: {path}")
42
+ lines.append(f"总行数: {len(df)}")
43
+ lines.append(f"总列数: {len(df.columns)}")
44
+ lines.append(f"列名: {list(df.columns)}")
45
+ lines.append("")
46
+ lines.append("数据类型:")
47
+ for col in df.columns:
48
+ dtype = str(df[col].dtype)
49
+ non_null = df[col].notna().sum()
50
+ unique = df[col].nunique()
51
+ lines.append(f" {col}: {dtype} | 非空: {non_null} | 唯一值: {unique}")
52
+ lines.append("")
53
+ lines.append("前10行预览:")
54
+ lines.append(df.head(10).to_string(index=False))
55
+ lines.append("")
56
+ lines.append("数值列统计:")
57
+ try:
58
+ desc = df.describe().to_string()
59
+ lines.append(desc)
60
+ except Exception:
61
+ pass
62
+ return "\n".join(lines)
63
+
64
+
65
+ def analyze_dataframe(path, query, client, model, lang="zh"):
66
+ """读取表格并提交给大模型分析"""
67
+ pd = _try_import_pandas()
68
+ if not pd:
69
+ return None, "缺少 pandas"
70
+
71
+ # 根据扩展名判断类型
72
+ ext = str(path).lower().split(".")[-1] if "." in str(path) else ""
73
+ if ext in ("xlsx", "xls"):
74
+ summary, err = read_excel(path, lang=lang)
75
+ elif ext in ("csv",):
76
+ summary, err = read_csv(path, lang=lang)
77
+ else:
78
+ # 尝试两种
79
+ summary, err = read_csv(path, lang=lang)
80
+ if err:
81
+ summary, err = read_excel(path, lang=lang)
82
+
83
+ if err:
84
+ return None, err
85
+
86
+ prompt = f"""你是一个数据分析专家。请根据以下表格数据和用户问题进行分析。
87
+
88
+ {summary}
89
+
90
+ 用户问题: {query}
91
+
92
+ 请用中文给出清晰、简洁的分析结果。如果涉及计算,请展示计算过程。
93
+ """
94
+ from fr_cli.core.stream import stream_cnt
95
+ messages = [{"role": "user", "content": prompt}]
96
+ result, _, _ = stream_cnt(client, model, messages, lang, custom_prefix="", max_tokens=4096)
97
+ return result, None
fr_cli/weapon/disk.py ADDED
@@ -0,0 +1,141 @@
1
+ """
2
+ 云端腾云引擎
3
+ 支持阿里云盘(aligo)的文件及目录结构获取、上传、下载
4
+ """
5
+ import os
6
+ import logging
7
+ from fr_cli.lang.i18n import T
8
+ from fr_cli.ui.ui import RED, GREEN, RESET
9
+
10
+ class CloudDisk:
11
+ def __init__(self, cfg):
12
+ self.type = cfg.get("type", "")
13
+ self.client = None
14
+ self._name = cfg.get("name", "fr-cli")
15
+ self._cwd = "root" # 当前云盘目录 ID
16
+ self._path_map = {} # 名称 -> {file_id, type, parent_id}
17
+ self._cwd_stack = [("root", "/")] # 目录历史栈,支持 cd ..
18
+
19
+ if self.type == "aliyundrive":
20
+ try:
21
+ from aligo import Aligo
22
+ # 抑制 aligo 及其子模块的 INFO 日志输出(避免显示 HTTP POST 调用详情)
23
+ for logger_name in list(logging.root.manager.loggerDict.keys()):
24
+ if logger_name.startswith("aligo") or logger_name == "fr-cli":
25
+ logging.getLogger(logger_name).setLevel(logging.WARNING)
26
+ refresh_token = cfg.get("refresh_token")
27
+ if refresh_token:
28
+ self.client = Aligo(name=self._name, refresh_token=refresh_token)
29
+ else:
30
+ self.client = Aligo(name=self._name)
31
+ except ImportError:
32
+ self.client = "MISSING:aligo"
33
+ except Exception as e:
34
+ self.client = f"ERR:{e}"
35
+
36
+ def _check_client(self, lang):
37
+ """检查客户端状态并返回错误信息"""
38
+ if not self.client:
39
+ return None, T("disk_no_cfg", lang)
40
+ if isinstance(self.client, str):
41
+ if self.client.startswith("MISSING:"):
42
+ lib = self.client.split(":")[1]
43
+ return None, T("disk_miss_dep", lang, lib, lib)
44
+ if self.client.startswith("ERR:"):
45
+ return None, f"{T('disk_err', lang)} {self.client[4:]}"
46
+ return self.client, None
47
+
48
+ def ls(self, lang):
49
+ """列出当前云盘目录的文件和文件夹(带类型标识)"""
50
+ client, err = self._check_client(lang)
51
+ if err:
52
+ return None, err
53
+ try:
54
+ files = client.get_file_list(parent_file_id=self._cwd)
55
+ self._path_map = {}
56
+ items = []
57
+ for f in files:
58
+ is_folder = getattr(f, "type", "") == "folder"
59
+ self._path_map[f.name] = {
60
+ "file_id": f.file_id,
61
+ "type": f.type,
62
+ "parent_id": self._cwd,
63
+ }
64
+ prefix = "📁" if is_folder else "📄"
65
+ items.append(f"{prefix} {f.name}")
66
+ return items, None
67
+ except Exception as e:
68
+ return None, f"{T('disk_err', lang)} {e}"
69
+
70
+ def cd(self, path, lang):
71
+ """切换云盘目录,支持 .. 返回上级"""
72
+ client, err = self._check_client(lang)
73
+ if err:
74
+ return False, err
75
+
76
+ if path == "..":
77
+ if len(self._cwd_stack) <= 1:
78
+ return False, f"{RED}已在根目录{RESET}"
79
+ self._cwd_stack.pop()
80
+ self._cwd = self._cwd_stack[-1][0]
81
+ return True, f"{GREEN}✅ 已切换至: {self._cwd_stack[-1][1]}{RESET}"
82
+
83
+ # 进入子目录:先刷新当前目录列表
84
+ self.ls(lang)
85
+ file_info = self._path_map.get(path)
86
+ if not file_info:
87
+ return False, f"{RED}⚠️ 目录不存在: {path}{RESET}"
88
+ if file_info["type"] != "folder":
89
+ return False, f"{RED}⚠️ {path} 不是目录{RESET}"
90
+
91
+ self._cwd = file_info["file_id"]
92
+ self._cwd_stack.append((file_info["file_id"], path))
93
+ return True, f"{GREEN}✅ 已穿梭至: {path}{RESET}"
94
+
95
+ def up(self, local_path, cloud_name, lang):
96
+ """本地文件上传至当前云盘目录"""
97
+ client, err = self._check_client(lang)
98
+ if err:
99
+ return False, err
100
+ if not os.path.exists(local_path):
101
+ return False, T("err_no_file", lang)
102
+ try:
103
+ result = client.upload_file(
104
+ file_path=local_path,
105
+ parent_file_id=self._cwd,
106
+ name=cloud_name
107
+ )
108
+ self._path_map[result.name] = {
109
+ "file_id": result.file_id,
110
+ "type": "file",
111
+ "parent_id": self._cwd,
112
+ }
113
+ return True, T("disk_ok_up", lang, result.name)
114
+ except Exception as e:
115
+ return False, f"{T('disk_err', lang)} {e}"
116
+
117
+ def down(self, cloud_name, local_path, lang):
118
+ """从当前云盘目录下载文件"""
119
+ client, err = self._check_client(lang)
120
+ if err:
121
+ return False, err
122
+
123
+ file_info = self._path_map.get(cloud_name)
124
+ if not file_info:
125
+ self.ls(lang)
126
+ file_info = self._path_map.get(cloud_name)
127
+
128
+ if not file_info:
129
+ return False, T("err_no_file", lang)
130
+ if file_info["type"] == "folder":
131
+ return False, f"{RED}⚠️ {cloud_name} 是文件夹,暂不支持单文件下载方式下载文件夹{RED}"
132
+
133
+ try:
134
+ local_folder = os.path.dirname(local_path) or "."
135
+ client.download_file(
136
+ file_id=file_info["file_id"],
137
+ local_folder=local_folder
138
+ )
139
+ return True, T("disk_ok_down", lang, local_path)
140
+ except Exception as e:
141
+ return False, f"{T('disk_err', lang)} {e}"