mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-27 21:50:36 +00:00
fix(tui): keep every word of an unquoted --print/--json message (#6636)
* fix(tui): keep every word of an unquoted --print/--json message --print and --json took a single optional value and never read the positional words, so `deerflow --print summarize this repo` sent only "summarize", `deerflow summarize this repo --print` exited 2 asking for a message on a terminal, and with piped stdin the typed words were ignored. Both flags now take every following word and fall back to words given before them, as --cli does; a typed message wins over piped stdin. A message split around the flag is refused with exit 2, because argparse loses the word order between the two parts. * docs(changelog): reference #6636 * fix(tui): refuse --print and --json together Only one headless mode runs, so the other flag's words were dropped: '--print hello --json world' ran with 'hello', and '--print --json hello' asked for a message. Put both flags in a mutually exclusive group so argparse exits 2 for every combination. --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
parent
cb4393879d
commit
5116434712
12
CHANGELOG.md
12
CHANGELOG.md
@ -4156,6 +4156,17 @@ This release closes that milestone with **439 merged pull requests**.
|
||||
missing; `verify-versions.yml` installs the uv version the backend image pins.
|
||||
([#6588])
|
||||
|
||||
- **tui:** `deerflow --print` and `--json` no longer drop words of an unquoted
|
||||
message. Each flag took a single value and never read the positional words,
|
||||
so `deerflow --print summarize this repo` sent only `summarize`,
|
||||
`deerflow summarize this repo --print` exited 2 asking for a message on a
|
||||
terminal, and with piped stdin the typed words were silently ignored. Both flags now take every word
|
||||
that follows them and fall back to words given before them, as `--cli` does;
|
||||
a typed message wins over piped stdin. A message split around the flag
|
||||
(`deerflow hello --print there`) exits 2 with a hint to quote it, since its
|
||||
word order cannot be recovered. Passing both `--print` and `--json` now exits
|
||||
2 instead of running `--print` and dropping the `--json` message. ([#6636])
|
||||
|
||||
### Security
|
||||
|
||||
- **authz:** Enforce permission checks on routes that only had authentication, and
|
||||
@ -9565,3 +9576,4 @@ with **180 merged pull requests** since the first 2.0 milestone tag.
|
||||
[#6611]: https://github.com/bytedance/deer-flow/pull/6611
|
||||
[#6613]: https://github.com/bytedance/deer-flow/pull/6613
|
||||
[#6635]: https://github.com/bytedance/deer-flow/pull/6635
|
||||
[#6636]: https://github.com/bytedance/deer-flow/pull/6636
|
||||
|
||||
@ -3193,6 +3193,15 @@
|
||||
规范化交给 uv,`2.1.0-rc0` 仍与 `2.1.0rc0` 匹配),缺少 `uv` 时直接失败;
|
||||
`verify-versions.yml` 会安装与 backend 镜像相同的固定 uv 版本。([#6588])
|
||||
|
||||
- **TUI:** `deerflow --print` 和 `--json` 不再丢弃未加引号消息中的词。此前这两个
|
||||
选项只接收一个值,也从不读取位置参数,因此 `deerflow --print summarize this repo`
|
||||
只会发送 `summarize`,`deerflow summarize this repo --print` 在终端中会以退出码 2
|
||||
要求提供消息,而有管道 stdin 时命令行上输入的词会被悄悄忽略。现在两个选项会接收
|
||||
其后的所有词,并像 `--cli` 一样在没有值时使用选项之前的词;命令行上输入的消息
|
||||
优先于管道 stdin。消息被选项分隔在两侧时(`deerflow hello --print there`),
|
||||
由于词序无法还原,会以退出码 2 退出并提示加引号。同时传入 `--print` 和 `--json`
|
||||
现在会以退出码 2 退出,而不是运行 `--print` 并丢弃 `--json` 的消息。([#6636])
|
||||
|
||||
### 安全
|
||||
|
||||
- **鉴权:** 仅有认证而无权限校验的路由现在会强制执行权限检查,且
|
||||
@ -7690,3 +7699,4 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子
|
||||
[#6611]: https://github.com/bytedance/deer-flow/pull/6611
|
||||
[#6613]: https://github.com/bytedance/deer-flow/pull/6613
|
||||
[#6635]: https://github.com/bytedance/deer-flow/pull/6635
|
||||
[#6636]: https://github.com/bytedance/deer-flow/pull/6636
|
||||
|
||||
@ -33,6 +33,12 @@ Launch modes:
|
||||
| `DEER_FLOW_TUI=1 deerflow` | Force the TUI via environment |
|
||||
| `DEER_FLOW_TUI_TRANSPARENT=1 deerflow` | Persist terminal-background rendering via environment |
|
||||
|
||||
`--print` and `--json` keep every word of an unquoted message, and words before
|
||||
a bare flag (`deerflow summarize this repo --print`) are the message, as with
|
||||
`--cli`. A message given on the command line wins over piped stdin. Words on
|
||||
both sides of the flag are rejected with exit 2, since their order cannot be
|
||||
recovered. `--print` and `--json` cannot be combined; pick one output mode.
|
||||
|
||||
If no TTY is available and no headless flag is given, `deerflow` prints guidance
|
||||
instead of hanging.
|
||||
|
||||
|
||||
@ -55,21 +55,21 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
epilog="Extension management: deerflow extensions --help",
|
||||
add_help=True,
|
||||
)
|
||||
parser.add_argument("message", nargs="*", help="initial prompt for the TUI, or message in --cli mode")
|
||||
parser.add_argument(
|
||||
parser.add_argument("message", nargs="*", help="initial prompt for the TUI, or the message for --print, --json or --cli")
|
||||
# One headless mode runs per invocation; refuse both so neither message is dropped.
|
||||
headless = parser.add_mutually_exclusive_group()
|
||||
headless.add_argument(
|
||||
"--print",
|
||||
dest="print",
|
||||
nargs="?",
|
||||
const=None,
|
||||
nargs="*",
|
||||
default=_UNSET,
|
||||
metavar="MESSAGE",
|
||||
help="headless one-shot: print the final answer and exit (reads stdin if no MESSAGE)",
|
||||
)
|
||||
parser.add_argument(
|
||||
headless.add_argument(
|
||||
"--json",
|
||||
dest="json",
|
||||
nargs="?",
|
||||
const=None,
|
||||
nargs="*",
|
||||
default=_UNSET,
|
||||
metavar="MESSAGE",
|
||||
help="headless streaming: emit newline-delimited JSON StreamEvents and exit",
|
||||
@ -100,6 +100,18 @@ def _strip_chat(argv: Sequence[str]) -> list[str]:
|
||||
return argv
|
||||
|
||||
|
||||
def _headless_message(parser: argparse.ArgumentParser, flag: str, values: list[str], positional: str | None) -> str | None:
|
||||
"""Return the words after a ``--print``/``--json`` flag, or else the positional words.
|
||||
|
||||
argparse loses the order between the flag's words and positional words, so a
|
||||
message split around the flag is refused instead of reassembled by guess.
|
||||
"""
|
||||
message = " ".join(values).strip() or None
|
||||
if message and positional:
|
||||
parser.error(f'{flag}: pass the message in one place, e.g. deerflow {flag} "your question"')
|
||||
return message or positional
|
||||
|
||||
|
||||
def _truthy(value: object) -> bool:
|
||||
return isinstance(value, str) and value.strip().lower() in {"1", "true", "yes", "on"}
|
||||
|
||||
@ -122,7 +134,7 @@ def plan_launch(
|
||||
parser.error("--recursion-limit requires --print, --json, or --cli")
|
||||
|
||||
if args.print is not _UNSET:
|
||||
message = args.print if isinstance(args.print, str) else None
|
||||
message = _headless_message(parser, "--print", args.print, positional)
|
||||
if message is None and stdin_isatty:
|
||||
return LaunchPlan(mode="headless-help", reason="--print needs a MESSAGE argument or piped stdin.")
|
||||
return LaunchPlan(
|
||||
@ -135,7 +147,7 @@ def plan_launch(
|
||||
)
|
||||
|
||||
if args.json is not _UNSET:
|
||||
message = args.json if isinstance(args.json, str) else None
|
||||
message = _headless_message(parser, "--json", args.json, positional)
|
||||
if message is None and stdin_isatty:
|
||||
return LaunchPlan(mode="headless-help", reason="--json needs a MESSAGE argument or piped stdin.")
|
||||
return LaunchPlan(
|
||||
|
||||
@ -67,6 +67,74 @@ def test_json_mode():
|
||||
assert p.message == "hello"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", ["--print", "--json"])
|
||||
def test_headless_unquoted_message_keeps_every_word(mode):
|
||||
"""An unquoted message must not be truncated to its first word."""
|
||||
p = plan([mode, "summarize", "this", "repo"])
|
||||
assert p.mode == mode.removeprefix("--")
|
||||
assert p.message == "summarize this repo"
|
||||
assert p.read_stdin is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", ["--print", "--json"])
|
||||
def test_headless_message_before_the_flag_is_used(mode):
|
||||
"""Words before a bare --print/--json are the message, as with --cli."""
|
||||
p = plan(["summarize", "this", "repo", mode])
|
||||
assert p.mode == mode.removeprefix("--")
|
||||
assert p.message == "summarize this repo"
|
||||
assert p.read_stdin is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", ["--print", "--json"])
|
||||
def test_headless_message_wins_over_piped_stdin(mode):
|
||||
p = plan(["hello", mode], stdin_tty=False)
|
||||
assert p.message == "hello"
|
||||
assert p.read_stdin is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"argv",
|
||||
[
|
||||
["hello", "--print", "there"],
|
||||
["--print", "hello", "--continue", "there"],
|
||||
["hello", "--json", "there"],
|
||||
],
|
||||
)
|
||||
def test_headless_message_split_around_the_flag_is_rejected(argv, capsys):
|
||||
"""Word order across the flag is lost by argparse, so refuse rather than guess."""
|
||||
with pytest.raises(SystemExit) as exc:
|
||||
plan(argv)
|
||||
assert exc.value.code == 2
|
||||
assert "pass the message in one place" in capsys.readouterr().err
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"argv",
|
||||
[
|
||||
["--print", "hello", "--json", "world"],
|
||||
["--print", "--json", "hello"],
|
||||
["--json", "hello", "--print"],
|
||||
["hello", "--print", "--json"],
|
||||
],
|
||||
)
|
||||
def test_print_and_json_together_are_rejected(argv, capsys):
|
||||
"""Only one headless mode runs, so the other flag's message must not vanish."""
|
||||
with pytest.raises(SystemExit) as exc:
|
||||
plan(argv, stdin_tty=False)
|
||||
assert exc.value.code == 2
|
||||
assert "not allowed with argument" in capsys.readouterr().err
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", ["--print", "--json"])
|
||||
def test_headless_blank_message_is_treated_as_missing(mode):
|
||||
p = plan([mode, " "])
|
||||
assert p.mode == "headless-help"
|
||||
|
||||
|
||||
def test_headless_help_documents_multi_word_messages():
|
||||
assert "--print [MESSAGE ...]" in build_parser().format_help()
|
||||
|
||||
|
||||
def test_force_tui_even_without_tty():
|
||||
p = plan(["--tui"], stdin_tty=False, stdout_tty=False)
|
||||
assert p.mode == "tui"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user