Auto-select workspace from Telegram prompt
This commit is contained in:
@@ -43,6 +43,11 @@ On Synology/NAS Docker deployments, keep `CODEX_WRITE_SANDBOX=danger-full-access
|
|||||||
for `/work`. The stricter `workspace-write` sandbox may fail with bubblewrap
|
for `/work`. The stricter `workspace-write` sandbox may fail with bubblewrap
|
||||||
namespace errors inside the container.
|
namespace errors inside the container.
|
||||||
|
|
||||||
|
If no `/repo` is set, the bridge tries to auto-select a workspace folder when a
|
||||||
|
Telegram prompt mentions an exact folder name such as `card_game` or
|
||||||
|
`nas_connection`. Use `/repo <name>` when you want to pin the workspace
|
||||||
|
explicitly.
|
||||||
|
|
||||||
`/settings`, `/models`, `/schedules`, `/records`, and `/sessions` return inline
|
`/settings`, `/models`, `/schedules`, `/records`, and `/sessions` return inline
|
||||||
buttons, so you can change speed/model/reasoning settings, delete schedules,
|
buttons, so you can change speed/model/reasoning settings, delete schedules,
|
||||||
manage personal records, and switch or archive sessions without copying ids.
|
manage personal records, and switch or archive sessions without copying ids.
|
||||||
|
|||||||
46
app/bot.py
46
app/bot.py
@@ -945,6 +945,9 @@ class TelegramCodexBot:
|
|||||||
return
|
return
|
||||||
|
|
||||||
selected_sandbox = sandbox or self.config.readonly_sandbox
|
selected_sandbox = sandbox or self.config.readonly_sandbox
|
||||||
|
auto_repo_path = self._auto_select_repo_for_prompt(chat_id, prompt)
|
||||||
|
if auto_repo_path is not None:
|
||||||
|
await self._close_client_for_chat(chat_id)
|
||||||
preview = _preview_prompt(prompt)
|
preview = _preview_prompt(prompt)
|
||||||
task = asyncio.create_task(
|
task = asyncio.create_task(
|
||||||
self._run_codex_job(
|
self._run_codex_job(
|
||||||
@@ -961,11 +964,13 @@ class TelegramCodexBot:
|
|||||||
sandbox=selected_sandbox,
|
sandbox=selected_sandbox,
|
||||||
)
|
)
|
||||||
mode = "쓰기 작업" if selected_sandbox != self.config.readonly_sandbox else "조회 작업"
|
mode = "쓰기 작업" if selected_sandbox != self.config.readonly_sandbox else "조회 작업"
|
||||||
|
repo_note = f"\n자동 작업폴더: {auto_repo_path}" if auto_repo_path else ""
|
||||||
await self._reply(
|
await self._reply(
|
||||||
update,
|
update,
|
||||||
f"{mode}을 시작했습니다.\n"
|
f"{mode}을 시작했습니다.\n"
|
||||||
f"요청: {preview}\n"
|
f"요청: {preview}\n"
|
||||||
"완료되면 결과를 보내드릴게요. 진행 중에는 /status로 확인할 수 있습니다.",
|
"완료되면 결과를 보내드릴게요. 진행 중에는 /status로 확인할 수 있습니다."
|
||||||
|
f"{repo_note}",
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _run_codex_job(
|
async def _run_codex_job(
|
||||||
@@ -1348,6 +1353,40 @@ class TelegramCodexBot:
|
|||||||
return False
|
return False
|
||||||
return sandbox == self.config.readonly_sandbox
|
return sandbox == self.config.readonly_sandbox
|
||||||
|
|
||||||
|
def _auto_select_repo_for_prompt(self, chat_id: int, prompt: str) -> Path | None:
|
||||||
|
chat_state = self.state.get_chat(chat_id)
|
||||||
|
if chat_state.repo_path:
|
||||||
|
return None
|
||||||
|
repo_path = self._detect_workspace_repo(prompt)
|
||||||
|
if repo_path is None:
|
||||||
|
return None
|
||||||
|
self.state.set_repo(chat_id, repo_path)
|
||||||
|
return repo_path
|
||||||
|
|
||||||
|
def _detect_workspace_repo(self, prompt: str) -> Path | None:
|
||||||
|
root = self.config.workspace_root.resolve()
|
||||||
|
if not root.exists():
|
||||||
|
return None
|
||||||
|
tokens = _repo_name_tokens(prompt)
|
||||||
|
if not tokens:
|
||||||
|
return None
|
||||||
|
children: dict[str, Path] = {}
|
||||||
|
try:
|
||||||
|
for child in root.iterdir():
|
||||||
|
if child.is_dir():
|
||||||
|
children[child.name.casefold()] = child.resolve()
|
||||||
|
except OSError:
|
||||||
|
return None
|
||||||
|
for token in tokens:
|
||||||
|
candidate = children.get(token.casefold())
|
||||||
|
if candidate and (candidate / ".git").exists():
|
||||||
|
return candidate
|
||||||
|
for token in tokens:
|
||||||
|
candidate = children.get(token.casefold())
|
||||||
|
if candidate:
|
||||||
|
return candidate
|
||||||
|
return None
|
||||||
|
|
||||||
def _resolve_workspace_path(self, requested: str) -> Path:
|
def _resolve_workspace_path(self, requested: str) -> Path:
|
||||||
root = self.config.workspace_root.resolve()
|
root = self.config.workspace_root.resolve()
|
||||||
path = Path(requested)
|
path = Path(requested)
|
||||||
@@ -1388,6 +1427,11 @@ def _preview_prompt(prompt: str, limit: int = 120) -> str:
|
|||||||
return preview[: limit - 3] + "..."
|
return preview[: limit - 3] + "..."
|
||||||
|
|
||||||
|
|
||||||
|
def _repo_name_tokens(prompt: str) -> list[str]:
|
||||||
|
tokens = re.findall(r"[A-Za-z0-9][A-Za-z0-9_.-]{1,79}", prompt)
|
||||||
|
return _dedupe(tokens)
|
||||||
|
|
||||||
|
|
||||||
def _checked_label(label: str, checked: bool) -> str:
|
def _checked_label(label: str, checked: bool) -> str:
|
||||||
return f"✓ {label}" if checked else label
|
return f"✓ {label}" if checked else label
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user