From d537c976f2c7d50b7d93b060ecdb2a5931deea46 Mon Sep 17 00:00:00 2001 From: xn <3395884215@qq.com> Date: Fri, 17 Apr 2026 11:22:12 +0800 Subject: [PATCH 1/5] =?UTF-8?q?feat.=20=E6=B3=A8=E5=85=A5=E4=B8=8A?= =?UTF-8?q?=E4=B8=8B=E6=96=87=E5=AE=8C=E6=95=B4=E8=AE=B0=E5=BD=95=E5=88=B0?= =?UTF-8?q?=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .code-flow/scripts/cf_inject_hook.py | 14 ++-- .code-flow/scripts/cf_log.py | 69 ++++++++++++++++++++ src/core/code-flow/scripts/cf_inject_hook.py | 14 ++-- src/core/code-flow/scripts/cf_log.py | 69 ++++++++++++++++++++ 4 files changed, 158 insertions(+), 8 deletions(-) create mode 100644 .code-flow/scripts/cf_log.py create mode 100644 src/core/code-flow/scripts/cf_log.py diff --git a/.code-flow/scripts/cf_inject_hook.py b/.code-flow/scripts/cf_inject_hook.py index ac822db..0f89a64 100644 --- a/.code-flow/scripts/cf_inject_hook.py +++ b/.code-flow/scripts/cf_inject_hook.py @@ -21,9 +21,10 @@ save_inject_state, select_specs_tiered, ) - - - +from cf_log import ( + reset_stdout, + cleanup_none_logfile, +) def main() -> None: @@ -32,6 +33,11 @@ def main() -> None: if not raw.strip(): return data = json.loads(raw) + + # log all inject data + sid = resolve_session_id(data) + reset_stdout("hook_inject_" + sid) + tool_name = data.get("tool_name", "") tool_input = data.get("tool_input") or {} file_path = tool_input.get("file_path", "") @@ -60,7 +66,6 @@ def main() -> None: domains = match_domains(rel_path, effective_mapping) # Load state with session isolation (fix #10) - sid = resolve_session_id(data) state = load_inject_state(project_root) state_sid = state.get("session_id", "") if state_sid != sid: @@ -141,6 +146,7 @@ def main() -> None: "matched_specs": [s["path"] for s in selected], } sys.stdout.write(json.dumps(payload)) + cleanup_none_logfile() except Exception as exc: # Fix #9: log errors to stderr instead of silently swallowing _log(f"cf_inject_hook error: {exc}") diff --git a/.code-flow/scripts/cf_log.py b/.code-flow/scripts/cf_log.py new file mode 100644 index 0000000..78034c2 --- /dev/null +++ b/.code-flow/scripts/cf_log.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +# -*- encoding=utf-8 -*- +import datetime +import io +import os +import sys + + +LOG_ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), "../../logs")) +os.makedirs(LOG_ROOT, exist_ok=True) + +_log_path = None + + +def get_log_path(prefix): + try: + if not prefix or not isinstance(prefix, str): + log_path = os.path.join(LOG_ROOT, datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + ".log") + log_path = os.path.join(LOG_ROOT, prefix, datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + ".log") + os.makedirs(os.path.dirname(log_path), exist_ok=True) + except Exception as e: + raise e + return log_path + + +class LogTee: + def __init__(self, stream, filepath): + self._stream = stream + self._file = open(filepath, "w", encoding="utf-8") + + def write(self, data): + self._stream.write(data) + self._file.write(data) + + def flush(self): + self._stream.flush() + self._file.flush() + + def close(self): + self._file.close() + + +def reset_stdout(prefix=None): + """ + 重建 stdout + :param prefix: + :return: + """ + global _log_path + _log_path = get_log_path(prefix) + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8") + sys.stdout = LogTee(sys.stdout, _log_path) + + +def cleanup_none_logfile(): + """ + 清理空的日志文件 + """ + global _log_path + if _log_path and isinstance(_log_path, str): + try: + if not os.path.isfile(_log_path): + return + with open(_log_path, "r") as f: + content = f.read().strip() + if not content: + os.remove(_log_path) + except Exception: + pass diff --git a/src/core/code-flow/scripts/cf_inject_hook.py b/src/core/code-flow/scripts/cf_inject_hook.py index ac822db..0f89a64 100644 --- a/src/core/code-flow/scripts/cf_inject_hook.py +++ b/src/core/code-flow/scripts/cf_inject_hook.py @@ -21,9 +21,10 @@ save_inject_state, select_specs_tiered, ) - - - +from cf_log import ( + reset_stdout, + cleanup_none_logfile, +) def main() -> None: @@ -32,6 +33,11 @@ def main() -> None: if not raw.strip(): return data = json.loads(raw) + + # log all inject data + sid = resolve_session_id(data) + reset_stdout("hook_inject_" + sid) + tool_name = data.get("tool_name", "") tool_input = data.get("tool_input") or {} file_path = tool_input.get("file_path", "") @@ -60,7 +66,6 @@ def main() -> None: domains = match_domains(rel_path, effective_mapping) # Load state with session isolation (fix #10) - sid = resolve_session_id(data) state = load_inject_state(project_root) state_sid = state.get("session_id", "") if state_sid != sid: @@ -141,6 +146,7 @@ def main() -> None: "matched_specs": [s["path"] for s in selected], } sys.stdout.write(json.dumps(payload)) + cleanup_none_logfile() except Exception as exc: # Fix #9: log errors to stderr instead of silently swallowing _log(f"cf_inject_hook error: {exc}") diff --git a/src/core/code-flow/scripts/cf_log.py b/src/core/code-flow/scripts/cf_log.py new file mode 100644 index 0000000..78034c2 --- /dev/null +++ b/src/core/code-flow/scripts/cf_log.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +# -*- encoding=utf-8 -*- +import datetime +import io +import os +import sys + + +LOG_ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), "../../logs")) +os.makedirs(LOG_ROOT, exist_ok=True) + +_log_path = None + + +def get_log_path(prefix): + try: + if not prefix or not isinstance(prefix, str): + log_path = os.path.join(LOG_ROOT, datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + ".log") + log_path = os.path.join(LOG_ROOT, prefix, datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + ".log") + os.makedirs(os.path.dirname(log_path), exist_ok=True) + except Exception as e: + raise e + return log_path + + +class LogTee: + def __init__(self, stream, filepath): + self._stream = stream + self._file = open(filepath, "w", encoding="utf-8") + + def write(self, data): + self._stream.write(data) + self._file.write(data) + + def flush(self): + self._stream.flush() + self._file.flush() + + def close(self): + self._file.close() + + +def reset_stdout(prefix=None): + """ + 重建 stdout + :param prefix: + :return: + """ + global _log_path + _log_path = get_log_path(prefix) + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8") + sys.stdout = LogTee(sys.stdout, _log_path) + + +def cleanup_none_logfile(): + """ + 清理空的日志文件 + """ + global _log_path + if _log_path and isinstance(_log_path, str): + try: + if not os.path.isfile(_log_path): + return + with open(_log_path, "r") as f: + content = f.read().strip() + if not content: + os.remove(_log_path) + except Exception: + pass From ce483d81303925e7df51cf45cd6596510a1f35cc Mon Sep 17 00:00:00 2001 From: xn <3395884215@qq.com> Date: Fri, 17 Apr 2026 12:18:08 +0800 Subject: [PATCH 2/5] =?UTF-8?q?fix.=20=E4=BF=AE=E5=A4=8D=20unicode=20?= =?UTF-8?q?=E7=A0=81=E7=82=B9=E4=BD=9C=E4=B8=BA=E4=B8=8A=E4=B8=8B=E6=96=87?= =?UTF-8?q?=E6=B3=A8=E5=85=A5=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .code-flow/scripts/cf_inject_hook.py | 2 +- src/core/code-flow/scripts/cf_inject_hook.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.code-flow/scripts/cf_inject_hook.py b/.code-flow/scripts/cf_inject_hook.py index 0f89a64..399488e 100644 --- a/.code-flow/scripts/cf_inject_hook.py +++ b/.code-flow/scripts/cf_inject_hook.py @@ -145,7 +145,7 @@ def main() -> None: "context_tags": sorted(context_tags), "matched_specs": [s["path"] for s in selected], } - sys.stdout.write(json.dumps(payload)) + sys.stdout.write(json.dumps(payload, ensure_ascii=False)) cleanup_none_logfile() except Exception as exc: # Fix #9: log errors to stderr instead of silently swallowing diff --git a/src/core/code-flow/scripts/cf_inject_hook.py b/src/core/code-flow/scripts/cf_inject_hook.py index 0f89a64..399488e 100644 --- a/src/core/code-flow/scripts/cf_inject_hook.py +++ b/src/core/code-flow/scripts/cf_inject_hook.py @@ -145,7 +145,7 @@ def main() -> None: "context_tags": sorted(context_tags), "matched_specs": [s["path"] for s in selected], } - sys.stdout.write(json.dumps(payload)) + sys.stdout.write(json.dumps(payload, ensure_ascii=False)) cleanup_none_logfile() except Exception as exc: # Fix #9: log errors to stderr instead of silently swallowing From 9cc50dcaf7586c54c3731a91a72349cbe5a441c4 Mon Sep 17 00:00:00 2001 From: xn <3395884215@qq.com> Date: Fri, 17 Apr 2026 15:34:45 +0800 Subject: [PATCH 3/5] =?UTF-8?q?update.=20=E4=BF=AE=E6=94=B9=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E5=AE=A1=E6=9F=A5=E9=97=AE=E9=A2=98=E3=80=81=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=20log=20=E5=BC=80=E5=85=B3=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .code-flow/config.yml | 1 + .code-flow/scripts/cf_inject_hook.py | 20 ++--- .code-flow/scripts/cf_log.py | 85 +++++++++----------- src/core/code-flow/config.yml | 1 + src/core/code-flow/scripts/cf_inject_hook.py | 20 ++--- src/core/code-flow/scripts/cf_log.py | 85 +++++++++----------- 6 files changed, 98 insertions(+), 114 deletions(-) diff --git a/.code-flow/config.yml b/.code-flow/config.yml index eec3c2a..b730389 100644 --- a/.code-flow/config.yml +++ b/.code-flow/config.yml @@ -8,6 +8,7 @@ budget: inject: auto: true + log: false code_extensions: - ".py" - ".ts" diff --git a/.code-flow/scripts/cf_inject_hook.py b/.code-flow/scripts/cf_inject_hook.py index 399488e..0424308 100644 --- a/.code-flow/scripts/cf_inject_hook.py +++ b/.code-flow/scripts/cf_inject_hook.py @@ -21,10 +21,7 @@ save_inject_state, select_specs_tiered, ) -from cf_log import ( - reset_stdout, - cleanup_none_logfile, -) +from cf_log import reset_stdout def main() -> None: @@ -34,9 +31,17 @@ def main() -> None: return data = json.loads(raw) - # log all inject data + # Resolve session id early (needed for both state and logging) sid = resolve_session_id(data) - reset_stdout("hook_inject_" + sid) + + # Load config to check if logging is enabled + project_root = os.getcwd() + config = load_config(project_root) + inject_config = config.get("inject") or {} if config else {} + + # Enable logging if configured (default off) + if inject_config.get("log") is True: + reset_stdout("hook_inject_" + sid) tool_name = data.get("tool_name", "") tool_input = data.get("tool_input") or {} @@ -46,13 +51,11 @@ def main() -> None: if not isinstance(file_path, str) or not file_path: return - project_root = os.getcwd() abs_path = file_path if not os.path.isabs(abs_path): abs_path = os.path.join(project_root, file_path) rel_path = os.path.relpath(abs_path, project_root) - config = load_config(project_root) if not config: return inject_config = config.get("inject") or {} @@ -146,7 +149,6 @@ def main() -> None: "matched_specs": [s["path"] for s in selected], } sys.stdout.write(json.dumps(payload, ensure_ascii=False)) - cleanup_none_logfile() except Exception as exc: # Fix #9: log errors to stderr instead of silently swallowing _log(f"cf_inject_hook error: {exc}") diff --git a/.code-flow/scripts/cf_log.py b/.code-flow/scripts/cf_log.py index 78034c2..bfec92a 100644 --- a/.code-flow/scripts/cf_log.py +++ b/.code-flow/scripts/cf_log.py @@ -1,69 +1,58 @@ #!/usr/bin/env python3 -# -*- encoding=utf-8 -*- +# coding: utf-8 +import atexit import datetime -import io import os +import random import sys +LOG_ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), "../logs")) -LOG_ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), "../../logs")) -os.makedirs(LOG_ROOT, exist_ok=True) -_log_path = None +def get_log_path(prefix: str | None) -> str: + now = datetime.datetime.now() + timestamp = now.strftime("%Y%m%d_%H%M%S") + f"{now.microsecond // 1000:03d}" + rand_suffix = f"{random.randint(0, 999):03d}" + log_file_name = f"{timestamp}_{rand_suffix}.log" + if not prefix: + log_path = os.path.join(LOG_ROOT, log_file_name) + else: + log_path = os.path.join(LOG_ROOT, prefix, log_file_name) -def get_log_path(prefix): - try: - if not prefix or not isinstance(prefix, str): - log_path = os.path.join(LOG_ROOT, datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + ".log") - log_path = os.path.join(LOG_ROOT, prefix, datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + ".log") - os.makedirs(os.path.dirname(log_path), exist_ok=True) - except Exception as e: - raise e + os.makedirs(os.path.dirname(log_path), exist_ok=True) return log_path -class LogTee: - def __init__(self, stream, filepath): - self._stream = stream - self._file = open(filepath, "w", encoding="utf-8") +class StdoutLogTee: + def __init__(self, prefix: str = None) -> None: + self._file_path = get_log_path(prefix) + self._stream = sys.stdout + self._content = "" + self._file = open(self._file_path, "a", encoding="utf-8") + # 注册退出时自动清理空日志文件 + atexit.register(self.close) - def write(self, data): + def write(self, data: str) -> int: self._stream.write(data) - self._file.write(data) + self._content += data + return self._file.write(data) - def flush(self): + def flush(self) -> None: self._stream.flush() self._file.flush() - def close(self): - self._file.close() - + def err_log(self, err_msg): + print(err_msg, file=sys.stderr) -def reset_stdout(prefix=None): - """ - 重建 stdout - :param prefix: - :return: - """ - global _log_path - _log_path = get_log_path(prefix) - sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8") - sys.stdout = LogTee(sys.stdout, _log_path) + def close(self): + try: + self._file.close() + if not self._content.strip(): + os.remove(self._file_path) + except Exception as err: + self.err_log("close error: {}".format(err)) -def cleanup_none_logfile(): - """ - 清理空的日志文件 - """ - global _log_path - if _log_path and isinstance(_log_path, str): - try: - if not os.path.isfile(_log_path): - return - with open(_log_path, "r") as f: - content = f.read().strip() - if not content: - os.remove(_log_path) - except Exception: - pass +def reset_stdout(prefix: str | None = None): + sys.stdout = StdoutLogTee(prefix) diff --git a/src/core/code-flow/config.yml b/src/core/code-flow/config.yml index 20ac493..1ac96c1 100644 --- a/src/core/code-flow/config.yml +++ b/src/core/code-flow/config.yml @@ -8,6 +8,7 @@ budget: inject: auto: true + log: false code_extensions: - ".py" - ".ts" diff --git a/src/core/code-flow/scripts/cf_inject_hook.py b/src/core/code-flow/scripts/cf_inject_hook.py index 399488e..0424308 100644 --- a/src/core/code-flow/scripts/cf_inject_hook.py +++ b/src/core/code-flow/scripts/cf_inject_hook.py @@ -21,10 +21,7 @@ save_inject_state, select_specs_tiered, ) -from cf_log import ( - reset_stdout, - cleanup_none_logfile, -) +from cf_log import reset_stdout def main() -> None: @@ -34,9 +31,17 @@ def main() -> None: return data = json.loads(raw) - # log all inject data + # Resolve session id early (needed for both state and logging) sid = resolve_session_id(data) - reset_stdout("hook_inject_" + sid) + + # Load config to check if logging is enabled + project_root = os.getcwd() + config = load_config(project_root) + inject_config = config.get("inject") or {} if config else {} + + # Enable logging if configured (default off) + if inject_config.get("log") is True: + reset_stdout("hook_inject_" + sid) tool_name = data.get("tool_name", "") tool_input = data.get("tool_input") or {} @@ -46,13 +51,11 @@ def main() -> None: if not isinstance(file_path, str) or not file_path: return - project_root = os.getcwd() abs_path = file_path if not os.path.isabs(abs_path): abs_path = os.path.join(project_root, file_path) rel_path = os.path.relpath(abs_path, project_root) - config = load_config(project_root) if not config: return inject_config = config.get("inject") or {} @@ -146,7 +149,6 @@ def main() -> None: "matched_specs": [s["path"] for s in selected], } sys.stdout.write(json.dumps(payload, ensure_ascii=False)) - cleanup_none_logfile() except Exception as exc: # Fix #9: log errors to stderr instead of silently swallowing _log(f"cf_inject_hook error: {exc}") diff --git a/src/core/code-flow/scripts/cf_log.py b/src/core/code-flow/scripts/cf_log.py index 78034c2..bfec92a 100644 --- a/src/core/code-flow/scripts/cf_log.py +++ b/src/core/code-flow/scripts/cf_log.py @@ -1,69 +1,58 @@ #!/usr/bin/env python3 -# -*- encoding=utf-8 -*- +# coding: utf-8 +import atexit import datetime -import io import os +import random import sys +LOG_ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), "../logs")) -LOG_ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), "../../logs")) -os.makedirs(LOG_ROOT, exist_ok=True) -_log_path = None +def get_log_path(prefix: str | None) -> str: + now = datetime.datetime.now() + timestamp = now.strftime("%Y%m%d_%H%M%S") + f"{now.microsecond // 1000:03d}" + rand_suffix = f"{random.randint(0, 999):03d}" + log_file_name = f"{timestamp}_{rand_suffix}.log" + if not prefix: + log_path = os.path.join(LOG_ROOT, log_file_name) + else: + log_path = os.path.join(LOG_ROOT, prefix, log_file_name) -def get_log_path(prefix): - try: - if not prefix or not isinstance(prefix, str): - log_path = os.path.join(LOG_ROOT, datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + ".log") - log_path = os.path.join(LOG_ROOT, prefix, datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + ".log") - os.makedirs(os.path.dirname(log_path), exist_ok=True) - except Exception as e: - raise e + os.makedirs(os.path.dirname(log_path), exist_ok=True) return log_path -class LogTee: - def __init__(self, stream, filepath): - self._stream = stream - self._file = open(filepath, "w", encoding="utf-8") +class StdoutLogTee: + def __init__(self, prefix: str = None) -> None: + self._file_path = get_log_path(prefix) + self._stream = sys.stdout + self._content = "" + self._file = open(self._file_path, "a", encoding="utf-8") + # 注册退出时自动清理空日志文件 + atexit.register(self.close) - def write(self, data): + def write(self, data: str) -> int: self._stream.write(data) - self._file.write(data) + self._content += data + return self._file.write(data) - def flush(self): + def flush(self) -> None: self._stream.flush() self._file.flush() - def close(self): - self._file.close() - + def err_log(self, err_msg): + print(err_msg, file=sys.stderr) -def reset_stdout(prefix=None): - """ - 重建 stdout - :param prefix: - :return: - """ - global _log_path - _log_path = get_log_path(prefix) - sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8") - sys.stdout = LogTee(sys.stdout, _log_path) + def close(self): + try: + self._file.close() + if not self._content.strip(): + os.remove(self._file_path) + except Exception as err: + self.err_log("close error: {}".format(err)) -def cleanup_none_logfile(): - """ - 清理空的日志文件 - """ - global _log_path - if _log_path and isinstance(_log_path, str): - try: - if not os.path.isfile(_log_path): - return - with open(_log_path, "r") as f: - content = f.read().strip() - if not content: - os.remove(_log_path) - except Exception: - pass +def reset_stdout(prefix: str | None = None): + sys.stdout = StdoutLogTee(prefix) From 34416710fba77b832d37220b15eb559da6756546 Mon Sep 17 00:00:00 2001 From: xn <3395884215@qq.com> Date: Tue, 21 Apr 2026 19:38:33 +0800 Subject: [PATCH 4/5] =?UTF-8?q?update.=20=E7=BB=A7=E7=BB=AD=E4=BC=98?= =?UTF-8?q?=E5=8C=96=20cf=20log=20=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .code-flow/scripts/cf_log.py | 71 ++++++++++++++++------------ src/core/code-flow/scripts/cf_log.py | 71 ++++++++++++++++------------ 2 files changed, 84 insertions(+), 58 deletions(-) diff --git a/.code-flow/scripts/cf_log.py b/.code-flow/scripts/cf_log.py index bfec92a..d0defed 100644 --- a/.code-flow/scripts/cf_log.py +++ b/.code-flow/scripts/cf_log.py @@ -6,53 +6,66 @@ import random import sys -LOG_ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), "../logs")) - - -def get_log_path(prefix: str | None) -> str: - now = datetime.datetime.now() - timestamp = now.strftime("%Y%m%d_%H%M%S") + f"{now.microsecond // 1000:03d}" - rand_suffix = f"{random.randint(0, 999):03d}" - log_file_name = f"{timestamp}_{rand_suffix}.log" - - if not prefix: - log_path = os.path.join(LOG_ROOT, log_file_name) - else: - log_path = os.path.join(LOG_ROOT, prefix, log_file_name) - - os.makedirs(os.path.dirname(log_path), exist_ok=True) - return log_path +LOG_ROOT = os.path.normpath( + os.path.join(os.path.dirname(os.path.abspath(__file__)), "../logs") +) class StdoutLogTee: - def __init__(self, prefix: str = None) -> None: - self._file_path = get_log_path(prefix) + def __init__(self, prefix: str | None = None) -> None: + self._file_path = self.init_log_path(prefix) self._stream = sys.stdout - self._content = "" + self._has_content = False + self._closed = False self._file = open(self._file_path, "a", encoding="utf-8") - # 注册退出时自动清理空日志文件 atexit.register(self.close) def write(self, data: str) -> int: + self._has_content = True + written = self._file.write(data) self._stream.write(data) - self._content += data - return self._file.write(data) + return written def flush(self) -> None: self._stream.flush() self._file.flush() - def err_log(self, err_msg): + def isatty(self) -> bool: + return self._stream.isatty() + + def err_log(self, err_msg: str) -> None: print(err_msg, file=sys.stderr) - def close(self): + def close(self) -> None: + if self._closed: + return + self._closed = True try: self._file.close() - if not self._content.strip(): - os.remove(self._file_path) except Exception as err: - self.err_log("close error: {}".format(err)) + self.err_log(f"close error: {err}") + if not self._has_content and os.path.isfile(self._file_path): + try: + os.remove(self._file_path) + except Exception as err: + self.err_log(f"remove empty logfile error: {err}") + + def init_log_path(self, prefix: str | None) -> str: + now = datetime.datetime.now() + timestamp = now.strftime("%Y%m%d_%H%M%S") + f"{now.microsecond // 1000:03d}" + rand_suffix = f"{random.randint(0, 999):03d}" + log_file_name = f"{timestamp}_{rand_suffix}.log" + + if not prefix: + log_path = os.path.join(LOG_ROOT, log_file_name) + else: + log_path = os.path.join(LOG_ROOT, prefix, log_file_name) + + os.makedirs(os.path.dirname(log_path), exist_ok=True) + return log_path -def reset_stdout(prefix: str | None = None): - sys.stdout = StdoutLogTee(prefix) +def reset_stdout(prefix: str | None = None) -> "StdoutLogTee": + tee = StdoutLogTee(prefix) + sys.stdout = tee + return tee diff --git a/src/core/code-flow/scripts/cf_log.py b/src/core/code-flow/scripts/cf_log.py index bfec92a..d0defed 100644 --- a/src/core/code-flow/scripts/cf_log.py +++ b/src/core/code-flow/scripts/cf_log.py @@ -6,53 +6,66 @@ import random import sys -LOG_ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), "../logs")) - - -def get_log_path(prefix: str | None) -> str: - now = datetime.datetime.now() - timestamp = now.strftime("%Y%m%d_%H%M%S") + f"{now.microsecond // 1000:03d}" - rand_suffix = f"{random.randint(0, 999):03d}" - log_file_name = f"{timestamp}_{rand_suffix}.log" - - if not prefix: - log_path = os.path.join(LOG_ROOT, log_file_name) - else: - log_path = os.path.join(LOG_ROOT, prefix, log_file_name) - - os.makedirs(os.path.dirname(log_path), exist_ok=True) - return log_path +LOG_ROOT = os.path.normpath( + os.path.join(os.path.dirname(os.path.abspath(__file__)), "../logs") +) class StdoutLogTee: - def __init__(self, prefix: str = None) -> None: - self._file_path = get_log_path(prefix) + def __init__(self, prefix: str | None = None) -> None: + self._file_path = self.init_log_path(prefix) self._stream = sys.stdout - self._content = "" + self._has_content = False + self._closed = False self._file = open(self._file_path, "a", encoding="utf-8") - # 注册退出时自动清理空日志文件 atexit.register(self.close) def write(self, data: str) -> int: + self._has_content = True + written = self._file.write(data) self._stream.write(data) - self._content += data - return self._file.write(data) + return written def flush(self) -> None: self._stream.flush() self._file.flush() - def err_log(self, err_msg): + def isatty(self) -> bool: + return self._stream.isatty() + + def err_log(self, err_msg: str) -> None: print(err_msg, file=sys.stderr) - def close(self): + def close(self) -> None: + if self._closed: + return + self._closed = True try: self._file.close() - if not self._content.strip(): - os.remove(self._file_path) except Exception as err: - self.err_log("close error: {}".format(err)) + self.err_log(f"close error: {err}") + if not self._has_content and os.path.isfile(self._file_path): + try: + os.remove(self._file_path) + except Exception as err: + self.err_log(f"remove empty logfile error: {err}") + + def init_log_path(self, prefix: str | None) -> str: + now = datetime.datetime.now() + timestamp = now.strftime("%Y%m%d_%H%M%S") + f"{now.microsecond // 1000:03d}" + rand_suffix = f"{random.randint(0, 999):03d}" + log_file_name = f"{timestamp}_{rand_suffix}.log" + + if not prefix: + log_path = os.path.join(LOG_ROOT, log_file_name) + else: + log_path = os.path.join(LOG_ROOT, prefix, log_file_name) + + os.makedirs(os.path.dirname(log_path), exist_ok=True) + return log_path -def reset_stdout(prefix: str | None = None): - sys.stdout = StdoutLogTee(prefix) +def reset_stdout(prefix: str | None = None) -> "StdoutLogTee": + tee = StdoutLogTee(prefix) + sys.stdout = tee + return tee From bff2aedc496c61e57c64739df49fb383322dadeb Mon Sep 17 00:00:00 2001 From: xn <3395884215@qq.com> Date: Tue, 21 Apr 2026 21:39:57 +0800 Subject: [PATCH 5/5] =?UTF-8?q?fix.=20=E7=BB=A7=E7=BB=AD=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=AE=A1=E6=9F=A5=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .code-flow/scripts/cf_inject_hook.py | 4 +- .code-flow/scripts/cf_log.py | 65 ++++++++++++------- .code-flow/scripts/cf_user_prompt_hook.py | 9 ++- src/core/code-flow/scripts/cf_inject_hook.py | 4 +- src/core/code-flow/scripts/cf_log.py | 65 ++++++++++++------- .../code-flow/scripts/cf_user_prompt_hook.py | 9 ++- 6 files changed, 108 insertions(+), 48 deletions(-) diff --git a/.code-flow/scripts/cf_inject_hook.py b/.code-flow/scripts/cf_inject_hook.py index 53e0a6f..da27916 100644 --- a/.code-flow/scripts/cf_inject_hook.py +++ b/.code-flow/scripts/cf_inject_hook.py @@ -23,6 +23,8 @@ ) from cf_log import reset_stdout +HOOK_TYPE = "hook_inject" + def main() -> None: try: @@ -41,7 +43,7 @@ def main() -> None: # Enable logging if configured (default off) if inject_config.get("log") is True: - reset_stdout("hook_inject_" + sid) + reset_stdout(HOOK_TYPE + sid) tool_name = data.get("tool_name", "") tool_input = data.get("tool_input") or {} diff --git a/.code-flow/scripts/cf_log.py b/.code-flow/scripts/cf_log.py index d0defed..f8ec9a1 100644 --- a/.code-flow/scripts/cf_log.py +++ b/.code-flow/scripts/cf_log.py @@ -10,21 +10,30 @@ os.path.join(os.path.dirname(os.path.abspath(__file__)), "../logs") ) +# fix#2: module-level save of original stdout, enables restore_stdout() rollback +_original_stdout = sys.stdout + class StdoutLogTee: def __init__(self, prefix: str | None = None) -> None: - self._file_path = self.init_log_path(prefix) - self._stream = sys.stdout - self._has_content = False + self._file_path = self._init_log_path(prefix) # fix#5: private method + # fix#2: bind to real stdout, prevent chain tee on repeated calls + self._stream = _original_stdout + # fix#3: track actual bytes, replace bool _has_content + self._bytes_written = 0 self._closed = False self._file = open(self._file_path, "a", encoding="utf-8") atexit.register(self.close) def write(self, data: str) -> int: - self._has_content = True - written = self._file.write(data) - self._stream.write(data) - return written + # fix#1: stdout write first; file write failure degrades to stderr warning, does not block main flow + n = self._stream.write(data) + self._bytes_written += len(data) # fix#3 + try: + self._file.write(data) + except OSError as err: + self._err_log(f"log write error: {err}") + return n def flush(self) -> None: self._stream.flush() @@ -33,35 +42,42 @@ def flush(self) -> None: def isatty(self) -> bool: return self._stream.isatty() - def err_log(self, err_msg: str) -> None: + def _err_log(self, err_msg: str) -> None: + # fix#5: private method print(err_msg, file=sys.stderr) def close(self) -> None: if self._closed: return self._closed = True + # fix#4: unregister on manual close, prevent handler accumulation + atexit.unregister(self.close) try: self._file.close() - except Exception as err: - self.err_log(f"close error: {err}") - if not self._has_content and os.path.isfile(self._file_path): - try: - os.remove(self._file_path) - except Exception as err: - self.err_log(f"remove empty logfile error: {err}") - - def init_log_path(self, prefix: str | None) -> str: + except OSError as err: + self._err_log(f"close error: {err}") + finally: + # fix#4: inside finally, empty-file cleanup runs even if file.close() raises + # fix#3: use _bytes_written, avoid false positive from write("") + if not self._bytes_written and os.path.isfile(self._file_path): + try: + os.remove(self._file_path) + except OSError as err: + self._err_log(f"remove empty logfile error: {err}") + + def _init_log_path(self, prefix: str | None) -> str: + if not prefix: + prefix = "default" now = datetime.datetime.now() timestamp = now.strftime("%Y%m%d_%H%M%S") + f"{now.microsecond // 1000:03d}" rand_suffix = f"{random.randint(0, 999):03d}" log_file_name = f"{timestamp}_{rand_suffix}.log" - if not prefix: - log_path = os.path.join(LOG_ROOT, log_file_name) - else: - log_path = os.path.join(LOG_ROOT, prefix, log_file_name) + log_dir = os.path.join(LOG_ROOT, prefix) + log_path = os.path.join(log_dir, log_file_name) - os.makedirs(os.path.dirname(log_path), exist_ok=True) + # makr sure log_dir + os.makedirs(log_dir, exist_ok=True) return log_path @@ -69,3 +85,8 @@ def reset_stdout(prefix: str | None = None) -> "StdoutLogTee": tee = StdoutLogTee(prefix) sys.stdout = tee return tee + + +def restore_stdout() -> None: + # fix#2: new, roll back to original stdout + sys.stdout = _original_stdout diff --git a/.code-flow/scripts/cf_user_prompt_hook.py b/.code-flow/scripts/cf_user_prompt_hook.py index 73ad8cf..3c3bbb2 100644 --- a/.code-flow/scripts/cf_user_prompt_hook.py +++ b/.code-flow/scripts/cf_user_prompt_hook.py @@ -34,6 +34,10 @@ save_inject_state, select_specs_tiered, ) +from cf_log import reset_stdout + + +HOOK_TYPE = "hook_user_prompt" # Match bare paths, @-prefixed paths, and backtick-quoted paths _PATH_RE = re.compile(r'[@`]?([a-zA-Z0-9_.][a-zA-Z0-9_./\-]*\.[a-zA-Z]{1,6})\b') @@ -79,6 +83,9 @@ def main() -> None: if inject_config.get("auto") is False: return compress_enabled = resolve_compress(inject_config) + # Enable logging if configured (default off) + if inject_config.get("log") is True: + reset_stdout(HOOK_TYPE + sid) mapping = config.get("path_mapping") or {} effective_mapping = build_effective_mapping(project_root, mapping) @@ -165,7 +172,7 @@ def main() -> None: "matched_specs": [s["path"] for s in selected], } - sys.stdout.write(json.dumps(payload)) + sys.stdout.write(json.dumps(payload, ensure_ascii=False)) except Exception as exc: _log(f"cf_user_prompt_hook error: {exc}") diff --git a/src/core/code-flow/scripts/cf_inject_hook.py b/src/core/code-flow/scripts/cf_inject_hook.py index 53e0a6f..da27916 100644 --- a/src/core/code-flow/scripts/cf_inject_hook.py +++ b/src/core/code-flow/scripts/cf_inject_hook.py @@ -23,6 +23,8 @@ ) from cf_log import reset_stdout +HOOK_TYPE = "hook_inject" + def main() -> None: try: @@ -41,7 +43,7 @@ def main() -> None: # Enable logging if configured (default off) if inject_config.get("log") is True: - reset_stdout("hook_inject_" + sid) + reset_stdout(HOOK_TYPE + sid) tool_name = data.get("tool_name", "") tool_input = data.get("tool_input") or {} diff --git a/src/core/code-flow/scripts/cf_log.py b/src/core/code-flow/scripts/cf_log.py index d0defed..5921a3e 100644 --- a/src/core/code-flow/scripts/cf_log.py +++ b/src/core/code-flow/scripts/cf_log.py @@ -10,21 +10,30 @@ os.path.join(os.path.dirname(os.path.abspath(__file__)), "../logs") ) +# fix#2: module-level save of original stdout, enables restore_stdout() rollback +_original_stdout = sys.stdout + class StdoutLogTee: def __init__(self, prefix: str | None = None) -> None: - self._file_path = self.init_log_path(prefix) - self._stream = sys.stdout - self._has_content = False + self._file_path = self._init_log_path(prefix) # fix#5: private method + # fix#2: bind to real stdout, prevent chain tee on repeated calls + self._stream = _original_stdout + # fix#3: track actual bytes, replace bool _has_content + self._bytes_written = 0 self._closed = False self._file = open(self._file_path, "a", encoding="utf-8") atexit.register(self.close) def write(self, data: str) -> int: - self._has_content = True - written = self._file.write(data) - self._stream.write(data) - return written + # fix#1: stdout write first; file write failure degrades to stderr warning, does not block main flow + n = self._stream.write(data) + self._bytes_written += len(data) # fix#3 + try: + self._file.write(data) + except OSError as err: + self._err_log(f"log write error: {err}") + return n def flush(self) -> None: self._stream.flush() @@ -33,35 +42,42 @@ def flush(self) -> None: def isatty(self) -> bool: return self._stream.isatty() - def err_log(self, err_msg: str) -> None: + def _err_log(self, err_msg: str) -> None: + # fix#5: private method print(err_msg, file=sys.stderr) def close(self) -> None: if self._closed: return self._closed = True + # fix#4: unregister on manual close, prevent handler accumulation + atexit.unregister(self.close) try: self._file.close() - except Exception as err: - self.err_log(f"close error: {err}") - if not self._has_content and os.path.isfile(self._file_path): - try: - os.remove(self._file_path) - except Exception as err: - self.err_log(f"remove empty logfile error: {err}") - - def init_log_path(self, prefix: str | None) -> str: + except OSError as err: + self._err_log(f"close error: {err}") + finally: + # fix#4: inside finally, empty-file cleanup runs even if file.close() raises + # fix#3: use _bytes_written, avoid false positive from write("") + if not self._bytes_written and os.path.isfile(self._file_path): + try: + os.remove(self._file_path) + except OSError as err: + self._err_log(f"remove empty logfile error: {err}") + + def _init_log_path(self, prefix: str | None) -> str: + if not prefix: + prefix = "default" now = datetime.datetime.now() timestamp = now.strftime("%Y%m%d_%H%M%S") + f"{now.microsecond // 1000:03d}" rand_suffix = f"{random.randint(0, 999):03d}" log_file_name = f"{timestamp}_{rand_suffix}.log" - if not prefix: - log_path = os.path.join(LOG_ROOT, log_file_name) - else: - log_path = os.path.join(LOG_ROOT, prefix, log_file_name) + log_path = os.path.join(LOG_ROOT, prefix, log_file_name) + log_dir = os.path.join(LOG_ROOT, prefix) - os.makedirs(os.path.dirname(log_path), exist_ok=True) + # makr sure log_dir + os.makedirs(log_dir, exist_ok=True) return log_path @@ -69,3 +85,8 @@ def reset_stdout(prefix: str | None = None) -> "StdoutLogTee": tee = StdoutLogTee(prefix) sys.stdout = tee return tee + + +def restore_stdout() -> None: + # fix#2: new, roll back to original stdout + sys.stdout = _original_stdout diff --git a/src/core/code-flow/scripts/cf_user_prompt_hook.py b/src/core/code-flow/scripts/cf_user_prompt_hook.py index 73ad8cf..3c3bbb2 100644 --- a/src/core/code-flow/scripts/cf_user_prompt_hook.py +++ b/src/core/code-flow/scripts/cf_user_prompt_hook.py @@ -34,6 +34,10 @@ save_inject_state, select_specs_tiered, ) +from cf_log import reset_stdout + + +HOOK_TYPE = "hook_user_prompt" # Match bare paths, @-prefixed paths, and backtick-quoted paths _PATH_RE = re.compile(r'[@`]?([a-zA-Z0-9_.][a-zA-Z0-9_./\-]*\.[a-zA-Z]{1,6})\b') @@ -79,6 +83,9 @@ def main() -> None: if inject_config.get("auto") is False: return compress_enabled = resolve_compress(inject_config) + # Enable logging if configured (default off) + if inject_config.get("log") is True: + reset_stdout(HOOK_TYPE + sid) mapping = config.get("path_mapping") or {} effective_mapping = build_effective_mapping(project_root, mapping) @@ -165,7 +172,7 @@ def main() -> None: "matched_specs": [s["path"] for s in selected], } - sys.stdout.write(json.dumps(payload)) + sys.stdout.write(json.dumps(payload, ensure_ascii=False)) except Exception as exc: _log(f"cf_user_prompt_hook error: {exc}")