From 30c6532f2307011242de948855b86e5b0a26984a Mon Sep 17 00:00:00 2001 From: dark Date: Thu, 4 Jun 2026 13:59:55 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=84=E7=90=86prompt=5Ftoolkit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- packaging/README_packaged_agent.md | 2 +- packaging/build_linux_self_contained.sh | 9 ++++++++ pam_deploy_graph/interactive.py | 28 ++++++++++++++++++------- tests/test_interactive_cli.py | 13 ++++++++++++ 5 files changed, 44 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 4493e2c..8ac21a9 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ packaging/ - 本地已安装 `langgraph` 和 `mcp`,并完成 LangGraph fake 全局流程 smoke。 - CLI `analyze` 输出已做敏感字段脱敏。 - 增加 `chat` 常驻式 CLI 对话框,支持自然语言分析、参数设置、执行确认、显式回滚、状态查看、事件查看、checkpoint 选择和续跑。 -- chat 在开发环境可选启用 `rich` / `prompt_toolkit`;PyInstaller 打包环境默认使用普通文本输入,避免交互兼容问题。 +- chat 在开发环境和默认发布包中都会优先启用 `rich` / `prompt_toolkit`;如果增强输入初始化失败,会自动降级到普通 `input()`。 - chat 执行前会归一化参数并展示实际写入脚本配置的值;`script_only` / `hybrid_node_mcp` 会提前检查 `ZIP_FILE_PATH` 是否存在。 - chat 执行中会播报每个 action 的开始、完成或失败;action 执行失败会停在当前 checkpoint,不再误报 LangGraph 不可用。 - 每个 action 完成后都会进入一次 LLM/规则审核;如果审核建议停止,流程会暂停并给出建议,等待用户 `resume`。 diff --git a/packaging/README_packaged_agent.md b/packaging/README_packaged_agent.md index 478b2c8..71e49ec 100644 --- a/packaging/README_packaged_agent.md +++ b/packaging/README_packaged_agent.md @@ -34,7 +34,7 @@ pam-deploy-agent-linux-x86_64/ ./run.sh run-deploy --help ``` -发布包默认使用普通文本输入,避免 PyInstaller 环境下 `prompt_toolkit` 兼容性问题;输出仍会在可用时使用 `rich` 做更清晰的文本展示。 +发布包默认会优先使用 `prompt_toolkit` 增强输入,支持更稳定的退格、历史记录和补全;如果增强输入初始化失败,会自动降级到普通 `input()`。输出仍会在可用时使用 `rich` 做更清晰的文本展示。 逐 IP action 失败后会保存 checkpoint 并暂停;修复外部环境后输入 `resume` 会从失败 action 重试。回滚不再属于主 workflow 自动分支,需要时在 chat 内输入 `rollback [IP]` 显式执行。 chat 会在执行前归一化并展示实际写入脚本配置的参数;`script_only` / `hybrid_node_mcp` 会先检查 `ZIP_FILE_PATH` 是否存在,避免脚本运行后才用默认路径失败。执行过程中每个 action 都会输出开始、完成或失败状态;每个 action 完成后还会自动进入一次 LLM/规则审核,并播报审核开始和审核结果。 diff --git a/packaging/build_linux_self_contained.sh b/packaging/build_linux_self_contained.sh index fe86bd6..0083886 100644 --- a/packaging/build_linux_self_contained.sh +++ b/packaging/build_linux_self_contained.sh @@ -44,6 +44,14 @@ else python -m pip install -e . fi +PYINSTALLER_EXTRA_ARGS=() +if python -c "import importlib.util; raise SystemExit(0 if importlib.util.find_spec('prompt_toolkit') else 1)"; then + PYINSTALLER_EXTRA_ARGS+=(--collect-submodules prompt_toolkit --collect-data prompt_toolkit) +fi +if python -c "import importlib.util; raise SystemExit(0 if importlib.util.find_spec('rich') else 1)"; then + PYINSTALLER_EXTRA_ARGS+=(--collect-submodules rich) +fi + echo "==> 使用 PyInstaller 生成自带 Python 运行时的可执行目录" python -m PyInstaller \ --clean \ @@ -57,6 +65,7 @@ python -m PyInstaller \ --collect-submodules pam_deploy_graph \ --collect-submodules langgraph \ --hidden-import pam_deploy_graph.cli \ + "${PYINSTALLER_EXTRA_ARGS[@]}" \ packaging/pyinstaller_entry.py echo "==> 组装发布目录" diff --git a/pam_deploy_graph/interactive.py b/pam_deploy_graph/interactive.py index ce02c9c..adba645 100644 --- a/pam_deploy_graph/interactive.py +++ b/pam_deploy_graph/interactive.py @@ -8,7 +8,6 @@ import shlex import builtins import logging import os -import sys from dataclasses import asdict from pathlib import Path from typing import Any, Callable @@ -1058,14 +1057,12 @@ def _build_prompt_input(input_func: InputFunc) -> InputFunc: """如果安装了 prompt_toolkit,则启用历史记录和命令补全。""" if input_func is not builtins.input: return input_func - if getattr(sys, "frozen", False): - return input_func try: from prompt_toolkit import PromptSession from prompt_toolkit.completion import WordCompleter from prompt_toolkit.history import FileHistory except ImportError: - return input_func + return _build_readline_input(input_func) commands = [ "help", @@ -1099,13 +1096,28 @@ def _build_prompt_input(input_func: InputFunc) -> InputFunc: except OSError: history = None - session = PromptSession( - history=history, - completer=WordCompleter(commands, ignore_case=True, sentence=True), - ) + try: + session = PromptSession( + history=history, + completer=WordCompleter(commands, ignore_case=True, sentence=True), + ) + except Exception: + logger.exception("chat prompt_toolkit 初始化失败,降级为普通 input") + return _build_readline_input(input_func) return session.prompt +def _build_readline_input(input_func: InputFunc) -> InputFunc: + """在没有 prompt_toolkit 时尽量启用 GNU readline,改善 Linux 终端退格键兼容。""" + if input_func is not builtins.input or os.name == "nt": + return input_func + try: + import readline # noqa: F401 + except ImportError: + logger.debug("chat readline 不可用,使用普通 input") + return input_func + + def _build_output_func(output_func: OutputFunc) -> OutputFunc: """如果安装了 rich,则使用 rich 输出;否则保持原输出函数。""" if output_func is not builtins.print: diff --git a/tests/test_interactive_cli.py b/tests/test_interactive_cli.py index 0cc742a..aaa4a9f 100644 --- a/tests/test_interactive_cli.py +++ b/tests/test_interactive_cli.py @@ -1,4 +1,5 @@ import builtins +import sys from pathlib import Path import pytest @@ -370,3 +371,15 @@ def test_prompt_history_creates_runtime_dir(tmp_path: Path, monkeypatch): assert callable(prompt) assert (tmp_path / "runtime").is_dir() + + +def test_prompt_toolkit_enabled_when_frozen(tmp_path: Path, monkeypatch): + pytest.importorskip("prompt_toolkit") + monkeypatch.chdir(tmp_path) + monkeypatch.setattr(sys, "frozen", True, raising=False) + + prompt = _build_prompt_input(builtins.input) + + assert callable(prompt) + assert prompt is not builtins.input + assert (tmp_path / "runtime").is_dir()