From e7719d9d5c765f5ce9b3e8e6c253c335f61d05a3 Mon Sep 17 00:00:00 2001 From: hmo Date: Sun, 14 Jun 2026 02:33:48 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20bot=E8=A8=80=E5=87=BA=E5=BF=85=E8=A1=8C?= =?UTF-8?q?=20=E2=80=94=20=E6=8F=90=E7=A4=BA=E8=AF=8D=E5=BC=BA=E5=8C=96=20?= =?UTF-8?q?+=20git=5Fpull/read/write=20=E5=91=BD=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. chat_bridge.py: 系统提示增加「言出必行」规则 - 说去做某事必须同时用 run_command 真正执行 - 只说不做被视为空话 2. session_router.py: 新增命令处理器 - git_pull 拉代码 / git_push 推代码 - read_file 读文件 / write_file 写文件 - run 执行shell命令 - 命令指南自动注入 LLM prompt --- gateway/scripts/chat_bridge.py | 15 +++-- gateway/scripts/session_router.py | 101 +++++++++++++++++++++++++++++- 2 files changed, 110 insertions(+), 6 deletions(-) diff --git a/gateway/scripts/chat_bridge.py b/gateway/scripts/chat_bridge.py index 505b46a..bf5aaae 100644 --- a/gateway/scripts/chat_bridge.py +++ b/gateway/scripts/chat_bridge.py @@ -545,10 +545,17 @@ class SessionBridge: "你不是 Sisyphus,不是莫荷,不是莫小果。你是 xxm。\n" "老莫让你做事就做,不要推脱,不要反复确认。\n" "回复简洁,不用 emoji。\n" - "用 run_command 工具获取信息。\n" - "写文件的正确方式:用 Python 一次性写完所有内容,不要分多次调用。\n" - "错误示例(会覆盖,每调用一次就清空一次):python -c \"open('file', 'w').write('一行')\"\n" - "正确做法:把全部内容拼在一个 python -c 调用里写完。\n" + "\n" + "=== 言出必行(重要)===\n" + "你可以使用 run_command 工具执行 shell 命令。\n" + "你说「我去做某事」时,必须在同一条回复中使用 run_command 真正执行,\n" + "否则被视为空话。只说不做比不说更糟糕。\n" + "不确定该怎么做时,先 run_command 查一下再决定。\n" + "\n" + "=== 写文件的正确方式 ===\n" + "用 Python 一次性写完所有内容,不要分多次调用。\n" + "错误示例(会覆盖,每调用一次就清空一次):python -c \"open('file', 'w').write('一行')\"\n" + "正确做法:把全部内容拼在一个 python -c 调用里写完。\n" "\n" "=== 上下文说明 ===\n" "下面是最近 200 条对话历史,按时间正序排列(最上面是最旧的消息,最下面是最新的消息)。\n" diff --git a/gateway/scripts/session_router.py b/gateway/scripts/session_router.py index a0d13a8..915f686 100644 --- a/gateway/scripts/session_router.py +++ b/gateway/scripts/session_router.py @@ -172,6 +172,11 @@ class SessionRouter: self._commands = { "list_sessions": self._cmd_list_sessions, "switch_session": self._cmd_switch_session, + "git_pull": self._cmd_git_pull, + "git_push": self._cmd_git_push, + "read_file": self._cmd_read_file, + "write_file": self._cmd_write_file, + "run": self._cmd_run, "help": self._cmd_help, } @@ -180,6 +185,11 @@ class SessionRouter: "你可以使用以下命令让 bot 执行操作,把命令放在回复中即可:\n" "##list_sessions## 列出所有可用的 session\n" "##switch_session:xxx## 切换到标题包含 xxx 的 session\n" + "##git_pull:path## git pull 指定目录,默认 ~/projects/self-growing-knowledge\n" + "##git_push:path;msg## git add/commit/push,message 可选\n" + "##read_file:path## 读取文件内容(展示前 2000 行)\n" + "##write_file:path|content## 写入/覆盖文件(注意:会覆盖!)\n" + "##run:command## 执行任意 shell 命令\n" "##help## 查看所有可用命令\n" ) @@ -322,6 +332,91 @@ class SessionRouter: ) return f"找到 {len(rows)} 个匹配(仅显示前{SESSION_LIST_LIMIT}个),请回复编号选择:\n{items}" + # ── Git 命令 ────────────────────────────────────────── + + def _cmd_git_pull(self, key: str, args: Optional[str]) -> str: + path = args or "~/projects/self-growing-knowledge" + try: + import subprocess + r = subprocess.run( + f"cd {path} && git pull origin master 2>&1", + shell=True, capture_output=True, text=True, timeout=60, + ) + out = r.stdout.strip() + (f"\n{r.stderr.strip()}" if r.stderr.strip() else "") + return out or "(no output)" + except subprocess.TimeoutExpired: + return "(git pull 超时)" + except Exception as e: + return f"(错误: {e})" + + def _cmd_git_push(self, key: str, args: Optional[str]) -> str: + path = "~/projects/self-growing-knowledge" + msg = "auto commit" + if args and ";" in args: + parts = args.split(";", 1) + path = parts[0].strip() + msg = parts[1].strip() + elif args: + path = args + try: + import subprocess + r = subprocess.run( + f"cd {path} && git add . && git commit -m '{msg}' && git push origin master 2>&1", + shell=True, capture_output=True, text=True, timeout=60, + ) + out = r.stdout.strip() + (f"\n{r.stderr.strip()}" if r.stderr.strip() else "") + return out or "(no output)" + except subprocess.TimeoutExpired: + return "(git push 超时)" + except Exception as e: + return f"(错误: {e})" + + def _cmd_read_file(self, key: str, args: Optional[str]) -> str: + if not args: + return "请指定文件路径" + try: + import os + path = os.path.expanduser(args) + with open(path, "r", encoding="utf-8", errors="replace") as f: + content = f.read(50000) # 前 50000 字符 + lines = content.split("\n") + shown = "\n".join(lines[:2000]) + if len(lines) > 2000: + shown += f"\n... ({len(lines) - 2000} 行已截断)" + return shown or "(空文件)" + except FileNotFoundError: + return f"(文件不存在: {args})" + except Exception as e: + return f"(读取失败: {e})" + + def _cmd_write_file(self, key: str, args: Optional[str]) -> str: + if not args or "|" not in args: + return "用法: ##write_file:路径|内容##" + try: + import os + path_part, content = args.split("|", 1) + path = os.path.expanduser(path_part.strip()) + with open(path, "w", encoding="utf-8") as f: + f.write(content) + return f"(已写入 {os.path.getsize(path)} 字节到 {path})" + except Exception as e: + return f"(写入失败: {e})" + + def _cmd_run(self, key: str, args: Optional[str]) -> str: + if not args: + return "请指定命令" + try: + import subprocess + r = subprocess.run( + args, shell=True, capture_output=True, text=True, timeout=60, + ) + out = r.stdout.strip() + (f"\n{r.stderr.strip()}" if r.stderr.strip() else "") + return out or f"(exit {r.returncode})" + except subprocess.TimeoutExpired: + return "(命令超时)" + except Exception as e: + return f"(错误: {e})" + def _cmd_help(self, key: str, args: Optional[str]) -> str: return self._cmd_guide @@ -424,8 +519,10 @@ class SessionRouter: # lines.append("") lines.append( - "[可用命令] 切换session用 ##switch_session:xxx## ," - "列表用 ##list_sessions## ,帮助用 ##help## 。普通聊天无视。" + "[可用命令] ##switch_session:xxx## 切换session,##list_sessions## 列表," + "##git_pull:路径## 拉代码,##git_push:路径;消息## 推代码," + "##read_file:路径## 读文件,##write_file:路径|内容## 写文件," + "##run:命令## 执行shell。说「去做」时必须同时用命令执行。" ) lines.append("---")