fix(telegram): handle forwarded uploads and show overrides (#149)

This commit is contained in:
banteg
2026-01-16 02:03:39 +04:00
committed by GitHub
parent 70737cb9c9
commit 246680a62c
5 changed files with 127 additions and 5 deletions
@@ -38,6 +38,7 @@ This line is parsed from replies and takes precedence over new directives.
| `/agent` | Show/set the default agent for the current scope. | | `/agent` | Show/set the default agent for the current scope. |
| `/model` | Show/set the model override for the current scope. | | `/model` | Show/set the model override for the current scope. |
| `/reasoning` | Show/set the reasoning override for the current scope. | | `/reasoning` | Show/set the reasoning override for the current scope. |
| `/trigger` | Show/set trigger mode (mentions-only vs all). |
| `/file put <path>` | Upload a document into the repo/worktree (requires file transfer enabled). | | `/file put <path>` | Upload a document into the repo/worktree (requires file transfer enabled). |
| `/file get <path>` | Fetch a file or directory back into Telegram. | | `/file get <path>` | Fetch a file or directory back into Telegram. |
| `/topic <project> @branch` | Create/bind a topic (topics enabled). | | `/topic <project> @branch` | Create/bind a topic (topics enabled). |
+40 -1
View File
@@ -6,6 +6,7 @@ from ...context import RunContext
from ...directives import DirectiveError from ...directives import DirectiveError
from ..chat_prefs import ChatPrefsStore from ..chat_prefs import ChatPrefsStore
from ..engine_defaults import resolve_engine_for_message from ..engine_defaults import resolve_engine_for_message
from ..engine_overrides import resolve_override_value
from ..files import split_command_args from ..files import split_command_args
from ..topic_state import TopicStateStore from ..topic_state import TopicStateStore
from ..topics import _topic_key from ..topics import _topic_key
@@ -89,6 +90,40 @@ async def _handle_agent_command(
"global_default": "global default", "global_default": "global default",
} }
agent_line = f"agent: {selection.engine} ({source_labels[selection.source]})" agent_line = f"agent: {selection.engine} ({source_labels[selection.source]})"
topic_override = None
if tkey is not None and topic_store is not None:
topic_override = await topic_store.get_engine_override(
tkey[0], tkey[1], selection.engine
)
chat_override = None
if chat_prefs is not None:
chat_override = await chat_prefs.get_engine_override(
msg.chat_id, selection.engine
)
override_labels = {
"topic_override": "topic override",
"chat_default": "chat default",
"default": "no override",
}
model_resolution = resolve_override_value(
topic_override=topic_override,
chat_override=chat_override,
field="model",
)
reasoning_resolution = resolve_override_value(
topic_override=topic_override,
chat_override=chat_override,
field="reasoning",
)
model_value = model_resolution.value or "default"
model_line = (
f"model: {model_value} ({override_labels[model_resolution.source]})"
)
reasoning_value = reasoning_resolution.value or "default"
reasoning_line = (
"reasoning: "
f"{reasoning_value} ({override_labels[reasoning_resolution.source]})"
)
topic_default = selection.topic_default or "none" topic_default = selection.topic_default or "none"
if tkey is None: if tkey is None:
topic_default = "none" topic_default = "none"
@@ -110,7 +145,11 @@ async def _handle_agent_command(
) )
available = ", ".join(cfg.runtime.engine_ids) available = ", ".join(cfg.runtime.engine_ids)
available_line = f"available: {available}" available_line = f"available: {available}"
await reply(text="\n\n".join([agent_line, defaults_line, available_line])) await reply(
text="\n\n".join(
[agent_line, model_line, reasoning_line, defaults_line, available_line]
)
)
return return
if action == "set": if action == "set":
+1 -3
View File
@@ -8,7 +8,6 @@ import msgspec
OverrideSource = Literal["topic_override", "chat_default", "default"] OverrideSource = Literal["topic_override", "chat_default", "default"]
REASONING_LEVELS: tuple[str, ...] = ("minimal", "low", "medium", "high", "xhigh") REASONING_LEVELS: tuple[str, ...] = ("minimal", "low", "medium", "high", "xhigh")
OPENCODE_REASONING_LEVELS: tuple[str, ...] = ("none", *REASONING_LEVELS)
REASONING_SUPPORTED_ENGINES = frozenset({"codex"}) REASONING_SUPPORTED_ENGINES = frozenset({"codex"})
@@ -98,8 +97,7 @@ def resolve_override_value(
def allowed_reasoning_levels(engine: str) -> tuple[str, ...]: def allowed_reasoning_levels(engine: str) -> tuple[str, ...]:
if engine == "opencode": _ = engine
return OPENCODE_REASONING_LEVELS
return REASONING_LEVELS return REASONING_LEVELS
+7 -1
View File
@@ -1283,7 +1283,13 @@ async def run_main_loop(
reply = make_reply(cfg, msg) reply = make_reply(cfg, msg)
text = msg.text text = msg.text
is_voice_transcribed = False is_voice_transcribed = False
if _is_forwarded(msg.raw): is_forward_candidate = (
_is_forwarded(msg.raw)
and msg.document is None
and msg.voice is None
and msg.media_group_id is None
)
if is_forward_candidate:
_attach_forward(msg) _attach_forward(msg)
continue continue
forward_key = _forward_key(msg) forward_key = _forward_key(msg)
+78
View File
@@ -2372,6 +2372,84 @@ async def test_run_main_loop_ignores_forwarded_without_prompt() -> None:
assert runner.calls == [] assert runner.calls == []
@pytest.mark.anyio
async def test_run_main_loop_forwarded_document_still_uploads(
tmp_path: Path,
) -> None:
payload = b"hello"
class _UploadBot(_FakeBot):
async def get_file(self, file_id: str) -> File | None:
_ = file_id
return File(file_path="files/hello.txt")
async def download_file(self, file_path: str) -> bytes | None:
_ = file_path
return payload
runner = ScriptRunner([Return(answer="ok")], engine=CODEX_ENGINE)
projects = ProjectsConfig(
projects={
"proj": ProjectConfig(
alias="proj",
path=tmp_path,
worktrees_dir=Path(".worktrees"),
)
},
default_project="proj",
)
runtime = TransportRuntime(router=_make_router(runner), projects=projects)
transport = _FakeTransport()
exec_cfg = ExecBridgeConfig(
transport=transport,
presenter=MarkdownPresenter(),
final_notify=True,
)
cfg = TelegramBridgeConfig(
bot=_UploadBot(),
runtime=runtime,
chat_id=123,
startup_msg="",
exec_cfg=exec_cfg,
forward_coalesce_s=FAST_FORWARD_COALESCE_S,
media_group_debounce_s=FAST_MEDIA_GROUP_DEBOUNCE_S,
files=TelegramFilesSettings(
enabled=True,
auto_put=True,
auto_put_mode="prompt",
),
)
async def poller(_cfg: TelegramBridgeConfig):
yield TelegramIncomingMessage(
transport="telegram",
chat_id=123,
message_id=1,
text="do thing",
reply_to_message_id=None,
reply_to_text=None,
sender_id=123,
chat_type="private",
document=TelegramDocument(
file_id="doc-1",
file_name="hello.txt",
mime_type="text/plain",
file_size=len(payload),
raw={"file_id": "doc-1"},
),
raw={"forward_origin": {"type": "user"}},
)
await run_main_loop(cfg, poller)
saved_path = tmp_path / "incoming" / "hello.txt"
assert saved_path.read_bytes() == payload
assert runner.calls
prompt_text, _ = runner.calls[0]
assert prompt_text.startswith("do thing")
assert "[uploaded file: incoming/hello.txt]" in prompt_text
@pytest.mark.anyio @pytest.mark.anyio
async def test_run_main_loop_prompt_upload_auto_resumes_chat_sessions( async def test_run_main_loop_prompt_upload_auto_resumes_chat_sessions(
tmp_path: Path, tmp_path: Path,