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
fr_cli/main.py ADDED
@@ -0,0 +1,329 @@
1
+ """
2
+ 凡人打字机 - 主脑控制台
3
+ 负责状态初始化、命令路由与 AI 交互循环
4
+ """
5
+ import sys, os, subprocess, platform, shutil
6
+ from pathlib import Path
7
+
8
+ # 添加项目根目录到 Python 路径
9
+ project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
10
+ if project_root not in sys.path:
11
+ sys.path.insert(0, project_root)
12
+
13
+ from fr_cli.conf.config import init_config, ConfigError
14
+ from fr_cli.lang.i18n import T
15
+ from fr_cli.ui.ui import enable_win_ansi, print_banner, print_bye, CYAN, RED, YELLOW, DIM, RESET
16
+ from fr_cli.core.stream import stream_cnt
17
+ from fr_cli.memory.history import load_sess
18
+ from fr_cli.memory.context import load_context
19
+ from fr_cli.core.core import AppState
20
+ from fr_cli.core.chat import handle_ai_chat
21
+
22
+
23
+ def _sync_manual_to_workspace(vfs):
24
+ """将项目根目录的 MANUAL.md 复制到工作空间,使 AI 可通过 read_file 读取"""
25
+ if not vfs.cwd:
26
+ return
27
+ try:
28
+ manual_src = Path(__file__).parent.parent / "MANUAL.md"
29
+ if not manual_src.exists():
30
+ return
31
+ manual_dst = Path(vfs.cwd) / "MANUAL.md"
32
+ if not manual_dst.exists():
33
+ import shutil
34
+ shutil.copy2(manual_src, manual_dst)
35
+ except Exception:
36
+ pass
37
+
38
+
39
+ # ------------------------------------------------------------------
40
+ # 命令路由函数(将 main() 中巨大的 if-elif 链提取为字典映射)
41
+ # 返回 True 表示应退出主循环
42
+ # ------------------------------------------------------------------
43
+
44
+ from fr_cli.repl.commands import (
45
+ _cmd_exit,
46
+ _cmd_help,
47
+ _cmd_model,
48
+ _cmd_key,
49
+ _cmd_limit,
50
+ _cmd_lang,
51
+ _cmd_dir,
52
+ _cmd_dirs,
53
+ _cmd_rmdir,
54
+ _cmd_save,
55
+ _cmd_load,
56
+ _cmd_del,
57
+ _cmd_session_list,
58
+ _cmd_session_load,
59
+ _cmd_session_del,
60
+ _cmd_see,
61
+ _cmd_update,
62
+ _cmd_agent_server,
63
+ _cmd_mode,
64
+ _cmd_gatekeeper,
65
+ _cmd_open,
66
+ _cmd_launch,
67
+ _cmd_apps,
68
+ _cmd_agent_create,
69
+ _cmd_agent_list,
70
+ _cmd_agent_delete,
71
+ _cmd_agent_show,
72
+ _cmd_agent_run,
73
+ _cmd_agent_edit,
74
+ _cmd_agent_forge,
75
+ _cmd_remote_agent_add,
76
+ _cmd_remote_agent_list,
77
+ _cmd_remote_agent_del,
78
+ _cmd_agent_publish,
79
+ _cmd_remote_agent_scan,
80
+ _cmd_remote_agent_import,
81
+ _cmd_remote_setup,
82
+ _cmd_db_setup,
83
+ _cmd_agent_cron_add,
84
+ _cmd_agent_cron_list,
85
+ _cmd_agent_cron_del,
86
+ _cmd_rag_dir,
87
+ _cmd_rag_watch,
88
+ _cmd_rag_sync,
89
+ _cmd_read_excel,
90
+ _cmd_read_csv,
91
+ _cmd_master,
92
+ _cmd_mcp_list,
93
+ _cmd_mcp_add,
94
+ _cmd_mcp_del,
95
+ _cmd_mcp_enable,
96
+ _cmd_mcp_disable,
97
+ _cmd_mcp_refresh,
98
+ )
99
+
100
+
101
+ _COMMAND_ROUTES = {
102
+ "/exit": _cmd_exit,
103
+ "/quit": _cmd_exit,
104
+ "/help": _cmd_help,
105
+ "/model": _cmd_model,
106
+ "/key": _cmd_key,
107
+ "/limit": _cmd_limit,
108
+ "/lang": _cmd_lang,
109
+ "/mode": _cmd_mode,
110
+ "/dir": _cmd_dir,
111
+ "/dirs": _cmd_dirs,
112
+ "/rmdir": _cmd_rmdir,
113
+ "/save": _cmd_save,
114
+ "/load": _cmd_load,
115
+ "/del": _cmd_del,
116
+ "/session_list": _cmd_session_list,
117
+ "/session_load": _cmd_session_load,
118
+ "/session_del": _cmd_session_del,
119
+ "/see": _cmd_see,
120
+ "/update": _cmd_update,
121
+ "/agent_server": _cmd_agent_server,
122
+ "/gatekeeper": _cmd_gatekeeper,
123
+ "/open": _cmd_open,
124
+ "/launch": _cmd_launch,
125
+ "/apps": _cmd_apps,
126
+ "/agent_create": _cmd_agent_create,
127
+ "/agent_list": _cmd_agent_list,
128
+ "/agent_delete": _cmd_agent_delete,
129
+ "/agent_show": _cmd_agent_show,
130
+ "/agent_run": _cmd_agent_run,
131
+ "/agent_edit": _cmd_agent_edit,
132
+ "/agent_forge": _cmd_agent_forge,
133
+ "/remote_agent_add": _cmd_remote_agent_add,
134
+ "/remote_agent_list": _cmd_remote_agent_list,
135
+ "/remote_agent_del": _cmd_remote_agent_del,
136
+ "/agent_publish": _cmd_agent_publish,
137
+ "/remote_agent_scan": _cmd_remote_agent_scan,
138
+ "/remote_agent_import": _cmd_remote_agent_import,
139
+ "/remote_setup": _cmd_remote_setup,
140
+ "/db_setup": _cmd_db_setup,
141
+ "/agent_cron_add": _cmd_agent_cron_add,
142
+ "/agent_cron_list": _cmd_agent_cron_list,
143
+ "/agent_cron_del": _cmd_agent_cron_del,
144
+ "/rag_dir": _cmd_rag_dir,
145
+ "/rag_watch": _cmd_rag_watch,
146
+ "/rag_sync": _cmd_rag_sync,
147
+ "/read_excel": _cmd_read_excel,
148
+ "/read_csv": _cmd_read_csv,
149
+ "/master": _cmd_master,
150
+ "/mcp_list": _cmd_mcp_list,
151
+ "/mcp_add": _cmd_mcp_add,
152
+ "/mcp_del": _cmd_mcp_del,
153
+ "/mcp_enable": _cmd_mcp_enable,
154
+ "/mcp_disable": _cmd_mcp_disable,
155
+ "/mcp_refresh": _cmd_mcp_refresh,
156
+ }
157
+
158
+
159
+ def main():
160
+ enable_win_ansi()
161
+ try:
162
+ cfg = init_config()
163
+ except ConfigError:
164
+ print(f"{RED}配置初始化失败,退出。{RESET}")
165
+ return
166
+ state = AppState(cfg)
167
+
168
+ # 将 MANUAL.md 同步到工作空间
169
+ _sync_manual_to_workspace(state.vfs)
170
+
171
+ # 加载历史会话或初始化系统提示词
172
+ sp = T("sys_prompt", state.lang)
173
+ if state.sn:
174
+ ok, m, _ = load_sess(0, sp)
175
+ if ok:
176
+ state.messages = m
177
+ if not state.messages:
178
+ state.messages = [{"role": "system", "content": sp}]
179
+
180
+ # 加载当前会话的记忆上下文
181
+ state.context_summary = load_context(state.sn)
182
+
183
+ # 启动动画
184
+ print_banner(state.model_name, state.limit, cfg.get("allowed_dirs", [""]), state.sn, state.lang)
185
+
186
+ # ================= 主循环 =================
187
+ while True:
188
+ try:
189
+ u = input(f"{CYAN}>>> {RESET}").strip()
190
+ except (EOFError, KeyboardInterrupt):
191
+ print_bye()
192
+ break
193
+
194
+ if not u:
195
+ continue
196
+
197
+ # 替换别名
198
+ if u in state.aliases:
199
+ u = state.aliases[u]
200
+
201
+ # ----------------- 内置指令路由 -----------------
202
+ if u.startswith("/"):
203
+ parts = u.split()
204
+ cmd = parts[0].lower()
205
+ arg1 = parts[1] if len(parts) > 1 else ""
206
+
207
+ if cmd in _COMMAND_ROUTES:
208
+ if _COMMAND_ROUTES[cmd](state, parts):
209
+ break
210
+ else:
211
+ # 其余命令统一委托给执行引擎
212
+ result, error = state.executor.execute(u, state.messages)
213
+ if error:
214
+ print(f"{RED}{error}{RESET}")
215
+ elif result is not None:
216
+ # 根据命令类型做简单格式化
217
+ if cmd == "/cat" and arg1:
218
+ print(f"\n{DIM}--- {arg1} ---{RESET}\n{result}\n{DIM}--- EOF ---{RESET}")
219
+ elif cmd == "/fetch" and arg1:
220
+ print(f"{DIM}--- Fetch ---{RESET}\n{result}\n{DIM}--- EOF ---{RESET}")
221
+ elif cmd == "/skills":
222
+ print("\n".join([f"{CYAN}{line}{RESET}" for line in result.split("\n")]))
223
+ else:
224
+ print(result)
225
+
226
+ # ----------------- 破壁指令 -----------------
227
+ elif u.startswith("!"):
228
+ is_pipe = "|" in u
229
+ shell_cmd = u[1:].split("|")[0].strip()
230
+
231
+ if state.security.check("sec_shell", shell_cmd):
232
+ try:
233
+ if platform.system() == "Windows":
234
+ ps_exe = shutil.which("pwsh") or shutil.which("powershell")
235
+ if ps_exe:
236
+ res = subprocess.run([ps_exe, "-Command", shell_cmd], capture_output=True, text=True, timeout=15)
237
+ else:
238
+ res = subprocess.run(shell_cmd, shell=True, capture_output=True, text=True, timeout=15)
239
+ else:
240
+ res = subprocess.run(shell_cmd, shell=True, capture_output=True, text=True, timeout=15)
241
+ out = res.stdout + res.stderr
242
+ if is_pipe:
243
+ pipe_prompt = u.split("|", 1)[1].strip()
244
+ final_prompt = f"{T('pipe_prefix', state.lang)}{out}\n\n{pipe_prompt}"
245
+ if state.vfs.cwd:
246
+ final_prompt += T("ctx_dir", state.lang, state.vfs.cwd)
247
+ state.messages.append({"role": "user", "content": final_prompt})
248
+ txt, _, response_time = stream_cnt(
249
+ state.client, state.model_name, state.messages, state.lang,
250
+ max_tokens=state.limit
251
+ )
252
+ state.messages.append({"role": "assistant", "content": txt})
253
+
254
+ # 自动执行 AI 响应中的命令(与 handle_ai_chat 保持一致)
255
+ clean_txt, cmd_results = state.executor.process_ai_commands(txt, state.messages)
256
+ if cmd_results:
257
+ print(f"\n{CYAN}🤖 自动执行命令:{RESET}")
258
+ for result in cmd_results:
259
+ print(f"{DIM}{result}{RESET}")
260
+ state.messages[-1]["content"] = clean_txt if clean_txt.strip() else "[已执行命令]"
261
+ state.messages.append({
262
+ "role": "system",
263
+ "content": f"命令执行结果:\n" + "\n".join(cmd_results)
264
+ })
265
+ sys.stdout.write(f"{CYAN}{T('prompt_ai', state.lang)}{RESET} ")
266
+ sys.stdout.flush()
267
+ final_txt, _, final_response_time = stream_cnt(
268
+ state.client, state.model_name, state.messages, state.lang,
269
+ custom_prefix="", max_tokens=state.limit
270
+ )
271
+ state.messages.append({"role": "assistant", "content": final_txt})
272
+ response_time += final_response_time
273
+
274
+ print(f"{DIM}📊 {T('stats_model', state.lang)}: {state.model_name} | {T('stats_time', state.lang)}: {response_time:.2f}{T('stats_seconds', state.lang)}{RESET}")
275
+ else:
276
+ if out.strip():
277
+ print(out.strip()[:2000])
278
+ except subprocess.TimeoutExpired:
279
+ print(f"{RED}Timeout{RESET}")
280
+ except Exception as e:
281
+ print(f"{RED}{e}{RESET}")
282
+
283
+ # ----------------- 内置 Agent 前缀拦截 -----------------
284
+ elif u.startswith("@local "):
285
+ try:
286
+ from fr_cli.agent.builtins.local import handle_local
287
+ handle_local(u, state)
288
+ except Exception as e:
289
+ print(f"{RED}@local Agent 执行失败: {e}{RESET}")
290
+
291
+ elif u.startswith("@remote "):
292
+ try:
293
+ from fr_cli.agent.builtins.remote import handle_remote
294
+ handle_remote(u, state)
295
+ except Exception as e:
296
+ print(f"{RED}@remote Agent 执行失败: {e}{RESET}")
297
+
298
+ elif u.startswith("@spider "):
299
+ try:
300
+ from fr_cli.agent.builtins.spider import handle_spider
301
+ handle_spider(u, state)
302
+ except Exception as e:
303
+ print(f"{RED}@spider Agent 执行失败: {e}{RESET}")
304
+
305
+ elif u.startswith("@db "):
306
+ try:
307
+ from fr_cli.agent.builtins.db import handle_db
308
+ handle_db(u, state)
309
+ except Exception as e:
310
+ print(f"{RED}@db Agent 执行失败: {e}{RESET}")
311
+
312
+ elif u.startswith("@RAG ") or u.startswith("@rag "):
313
+ try:
314
+ from fr_cli.agent.builtins.rag import handle_rag
315
+ handle_rag(u, state)
316
+ except Exception as e:
317
+ print(f"{RED}@RAG Agent 执行失败: {e}{RESET}")
318
+
319
+ # ----------------- AI 正常对话 -----------------
320
+ else:
321
+ if state.master_agent.is_enabled():
322
+ reply, _ = state.master_agent.handle(u)
323
+ print(f"\n{CYAN}{reply}{RESET}")
324
+ else:
325
+ handle_ai_chat(state, u)
326
+
327
+
328
+ if __name__ == "__main__":
329
+ main()
@@ -0,0 +1,119 @@
1
+ """
2
+ 记忆上下文引擎
3
+ 保存并注入最近 N 轮对话的总结,作为 system prompt 的上下文
4
+ 让每个会话拥有独立的短期记忆
5
+ """
6
+ import json
7
+ import re
8
+ from pathlib import Path
9
+ from datetime import datetime
10
+
11
+ CONTEXT_FILE = Path.home() / ".zhipu_cli_context.json"
12
+
13
+
14
+ def extract_recent_turns(messages, n=5):
15
+ """
16
+ 从消息列表中提取最近 n 轮 user/assistant 对话
17
+ :param messages: 完整消息历史
18
+ :param n: 轮数(每轮包含 user + assistant)
19
+ :return: list of dict, 最多 n*2 条消息
20
+ """
21
+ chat_msgs = [m for m in messages if m.get("role") in ("user", "assistant")]
22
+ return chat_msgs[-n * 2:]
23
+
24
+
25
+ def build_context_summary(turns, lang="zh"):
26
+ """
27
+ 将对话轮次格式化为上下文摘要文本
28
+ :param turns: extract_recent_turns 返回的消息列表
29
+ :param lang: 语言
30
+ :return: str 摘要文本(空字符串表示无内容)
31
+ """
32
+ if not turns:
33
+ return ""
34
+
35
+ header = "\n\n[当前会话上下文摘要]\n" if lang == "zh" else "\n\n[Session Context Summary]\n"
36
+ lines = []
37
+
38
+ for m in turns:
39
+ role = "用户" if m.get("role") == "user" else "AI"
40
+ content = m.get("content", "")
41
+
42
+ # 处理多模态消息(图片等)
43
+ if isinstance(content, list):
44
+ content = "[图片/多模态消息]"
45
+ elif isinstance(content, str):
46
+ # 去除命令标记,避免重复噪音
47
+ content = re.sub(r'【命令:(.*?)】', '', content)
48
+ content = content.strip()
49
+ if not content:
50
+ content = "[已执行命令]"
51
+ elif len(content) > 200:
52
+ content = content[:200] + "..."
53
+
54
+ lines.append(f"{role}:{content}")
55
+
56
+ return header + "\n".join(lines) + "\n"
57
+
58
+
59
+ def save_context(session_name, summary):
60
+ """
61
+ 按会话名持久化上下文摘要
62
+ :param session_name: 会话名(空字符串时使用 __default__)
63
+ :param summary: 摘要文本
64
+ """
65
+ data = {}
66
+ if CONTEXT_FILE.exists():
67
+ try:
68
+ with open(CONTEXT_FILE, 'r', encoding='utf-8') as f:
69
+ data = json.load(f)
70
+ except Exception:
71
+ pass
72
+
73
+ key = session_name if session_name else "__default__"
74
+ data[key] = {
75
+ "summary": summary,
76
+ "ts": datetime.now().isoformat()
77
+ }
78
+
79
+ try:
80
+ with open(CONTEXT_FILE, 'w', encoding='utf-8') as f:
81
+ json.dump(data, f, ensure_ascii=False, indent=2)
82
+ except Exception:
83
+ pass
84
+
85
+
86
+ def load_context(session_name):
87
+ """
88
+ 按会话名加载上下文摘要
89
+ :param session_name: 会话名
90
+ :return: str 摘要文本(不存在时返回空字符串)
91
+ """
92
+ if not CONTEXT_FILE.exists():
93
+ return ""
94
+ try:
95
+ with open(CONTEXT_FILE, 'r', encoding='utf-8') as f:
96
+ data = json.load(f)
97
+ key = session_name if session_name else "__default__"
98
+ return data.get(key, {}).get("summary", "")
99
+ except Exception:
100
+ return ""
101
+
102
+
103
+ def clear_context(session_name):
104
+ """
105
+ 清除指定会话的上下文摘要
106
+ :param session_name: 会话名
107
+ """
108
+ if not CONTEXT_FILE.exists():
109
+ return
110
+ try:
111
+ with open(CONTEXT_FILE, 'r', encoding='utf-8') as f:
112
+ data = json.load(f)
113
+ key = session_name if session_name else "__default__"
114
+ if key in data:
115
+ del data[key]
116
+ with open(CONTEXT_FILE, 'w', encoding='utf-8') as f:
117
+ json.dump(data, f, ensure_ascii=False, indent=2)
118
+ except Exception:
119
+ pass
@@ -0,0 +1,96 @@
1
+ """
2
+ 会话轮回管理引擎
3
+ 负责对话历史的本地保存、加载、删除与 Markdown 导出
4
+ """
5
+ import json, os
6
+ from pathlib import Path
7
+ from datetime import datetime
8
+ from fr_cli.lang.i18n import T
9
+ from fr_cli.ui.ui import RED, RESET
10
+
11
+ HIST_DIR = Path.home() / ".zhipu_cli_history"
12
+
13
+ def init_history():
14
+ """确保历史目录存在"""
15
+ HIST_DIR.mkdir(parents=True, exist_ok=True)
16
+
17
+ def _fpath(name):
18
+ return HIST_DIR / f"{name}.json"
19
+
20
+ def get_sessions():
21
+ """获取所有会话列表"""
22
+ init_history()
23
+ sess = []
24
+ for f in sorted(HIST_DIR.glob("sess_*.json"), key=os.path.getmtime, reverse=True):
25
+ try:
26
+ with open(f, 'r', encoding='utf-8') as fp:
27
+ data = json.load(fp)
28
+ sess.append({"file": f.name, "name": data.get("name", f.stem)})
29
+ except Exception:
30
+ pass
31
+ return sess
32
+
33
+ def save_sess(name, msgs):
34
+ """保存当前对话到轮回石"""
35
+ init_history()
36
+ safe_name = "".join(c for c in name if c.isalnum() or c in ('_', '-'))
37
+ ts = datetime.now().strftime("%Y%m%d_%H%M%S")
38
+ fname = f"sess_{ts}_{safe_name}"
39
+ fp = _fpath(fname)
40
+ try:
41
+ with open(fp, 'w', encoding='utf-8') as f:
42
+ json.dump({"name": name, "ts": ts, "msgs": msgs}, f, ensure_ascii=False, indent=2)
43
+ return True
44
+ except Exception as e:
45
+ print(f"{RED}{e}{RESET}")
46
+ return False
47
+
48
+ def load_sess(index, sp):
49
+ """从轮回石中加载指定索引的会话"""
50
+ ss = get_sessions()
51
+ if not ss or index >= len(ss): return False, None, None
52
+ fp = HIST_DIR / ss[index]["file"]
53
+ try:
54
+ with open(fp, 'r', encoding='utf-8') as f:
55
+ data = json.load(f)
56
+ msgs = data.get("msgs", [])
57
+ # 强制覆盖第一条为最新的系统提示词
58
+ if msgs and msgs[0]["role"] == "system":
59
+ msgs[0]["content"] = sp
60
+ else:
61
+ msgs.insert(0, {"role": "system", "content": sp})
62
+ return True, msgs, data.get("name")
63
+ except Exception as e:
64
+ print(f"{RED}{e}{RESET}")
65
+ return False, None, None
66
+
67
+ def del_sess(index):
68
+ """斩断一段因果(删除会话)"""
69
+ ss = get_sessions()
70
+ if not ss or index >= len(ss): return
71
+ fp = HIST_DIR / ss[index]["file"]
72
+ try:
73
+ os.remove(fp)
74
+ return True
75
+ except Exception:
76
+ return False
77
+
78
+ def export_md(msgs, lang, out_dir=None):
79
+ """将当前会话导出为 Markdown 文件
80
+ :param out_dir: 用户设定的工作目录,若提供且存在则保存到该目录,否则保存到当前运行目录
81
+ """
82
+ if not msgs: return False, T("empty", lang)
83
+ ts = datetime.now().strftime("%Y%m%d_%H%M%S")
84
+ fname = f"glm_export_{ts}.md"
85
+ try:
86
+ target_dir = out_dir if out_dir and os.path.isdir(out_dir) else "."
87
+ fpath = os.path.join(target_dir, fname)
88
+ with open(fpath, 'w', encoding='utf-8') as f:
89
+ for m in msgs:
90
+ role = m.get("role", "unknown")
91
+ content = m.get("content", "")
92
+ if role == "system": continue
93
+ tag = "### 🧑 凡人" if role == "user" else "### 🧙 飞书"
94
+ f.write(f"{tag}\n\n{content}\n\n---\n\n")
95
+ return True, fpath
96
+ except Exception as e: return False, str(e)
@@ -0,0 +1,134 @@
1
+ """
2
+ 自动会话存档引擎 —— 按日期轮回
3
+ 每次启动自动创建日期编号会话文件,实时追加对话记录。
4
+ """
5
+ import json
6
+ import os
7
+ from datetime import datetime
8
+ from pathlib import Path
9
+
10
+ SESSION_DIR = Path.home() / ".fr_cli_sessions"
11
+
12
+
13
+ def _ensure_dir():
14
+ SESSION_DIR.mkdir(parents=True, exist_ok=True)
15
+
16
+
17
+ def _list_session_files():
18
+ """返回所有会话文件路径列表(按修改时间倒序)"""
19
+ _ensure_dir()
20
+ files = list(SESSION_DIR.glob("*.json"))
21
+ files.sort(key=lambda p: p.stat().st_mtime, reverse=True)
22
+ return files
23
+
24
+
25
+ def _get_next_session_filename():
26
+ """根据日期生成下一个可用会话文件名 YYYY-MM-DD_01.json, YYYY-MM-DD_02.json ..."""
27
+ _ensure_dir()
28
+ today = datetime.now().strftime("%Y-%m-%d")
29
+ existing = [f.stem for f in SESSION_DIR.glob(f"{today}_*.json")]
30
+ nums = []
31
+ for stem in existing:
32
+ parts = stem.split("_")
33
+ if len(parts) == 2 and parts[1].isdigit():
34
+ nums.append(int(parts[1]))
35
+ next_num = max(nums, default=0) + 1
36
+ return f"{today}_{next_num:02d}.json"
37
+
38
+
39
+ def create_session(messages):
40
+ """创建新的自动会话文件,返回文件路径"""
41
+ _ensure_dir()
42
+ fname = _get_next_session_filename()
43
+ fpath = SESSION_DIR / fname
44
+ data = {
45
+ "filename": fname,
46
+ "created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
47
+ "updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
48
+ "messages": messages,
49
+ }
50
+ try:
51
+ with open(fpath, "w", encoding="utf-8") as f:
52
+ json.dump(data, f, ensure_ascii=False, indent=2)
53
+ return str(fpath)
54
+ except Exception:
55
+ return None
56
+
57
+
58
+ def update_session(fpath, messages):
59
+ """更新会话文件中的消息记录"""
60
+ if not fpath:
61
+ return False
62
+ try:
63
+ path = Path(fpath)
64
+ data = {}
65
+ if path.exists():
66
+ with open(path, "r", encoding="utf-8") as f:
67
+ data = json.load(f)
68
+ data["updated_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
69
+ data["messages"] = messages
70
+ with open(path, "w", encoding="utf-8") as f:
71
+ json.dump(data, f, ensure_ascii=False, indent=2)
72
+ return True
73
+ except Exception:
74
+ return False
75
+
76
+
77
+ def list_sessions():
78
+ """列出所有会话,返回元数据列表"""
79
+ files = _list_session_files()
80
+ result = []
81
+ for idx, fpath in enumerate(files, start=1):
82
+ try:
83
+ with open(fpath, "r", encoding="utf-8") as f:
84
+ data = json.load(f)
85
+ created = data.get("created_at", "未知")
86
+ updated = data.get("updated_at", "未知")
87
+ msg_count = len(data.get("messages", []))
88
+ result.append({
89
+ "index": idx,
90
+ "filename": fpath.name,
91
+ "created_at": created,
92
+ "updated_at": updated,
93
+ "msg_count": msg_count,
94
+ "path": str(fpath),
95
+ })
96
+ except Exception:
97
+ pass
98
+ return result
99
+
100
+
101
+ def load_session(index, current_system_prompt=None):
102
+ """按索引加载会话消息
103
+ :param index: 1-based 索引
104
+ :param current_system_prompt: 若提供,将覆盖第一条 system prompt
105
+ :return: (success, messages, filename)
106
+ """
107
+ sessions = list_sessions()
108
+ if not sessions or index < 1 or index > len(sessions):
109
+ return False, None, None
110
+ target = sessions[index - 1]
111
+ try:
112
+ with open(target["path"], "r", encoding="utf-8") as f:
113
+ data = json.load(f)
114
+ msgs = data.get("messages", [])
115
+ if current_system_prompt and msgs and msgs[0]["role"] == "system":
116
+ msgs[0]["content"] = current_system_prompt
117
+ elif current_system_prompt:
118
+ msgs.insert(0, {"role": "system", "content": current_system_prompt})
119
+ return True, msgs, target["filename"]
120
+ except Exception:
121
+ return False, None, None
122
+
123
+
124
+ def delete_session(index):
125
+ """按索引删除会话文件"""
126
+ sessions = list_sessions()
127
+ if not sessions or index < 1 or index > len(sessions):
128
+ return False
129
+ target = sessions[index - 1]
130
+ try:
131
+ os.remove(target["path"])
132
+ return True
133
+ except Exception:
134
+ return False
File without changes