From e1090e13438e94d850f6aa9a6e3a4e45bc125956 Mon Sep 17 00:00:00 2001
From: ttiee <469784630@qq.com>
Date: Thu, 12 Mar 2026 09:33:42 +0800
Subject: [PATCH 1/2] docs(repo): add contribution guidance and issue templates
Add repository-level agent guidance, GitHub issue/PR templates, and tracked planning/maintenance docs for the current follow-up work.
Refs #1
---
.github/ISSUE_TEMPLATE/bug.md | 31 ----
.github/ISSUE_TEMPLATE/bug_report.yml | 122 ++++++---------
.github/ISSUE_TEMPLATE/config.yml | 6 +
.github/ISSUE_TEMPLATE/feature.md | 26 ---
.github/ISSUE_TEMPLATE/feature_request.yml | 59 ++++---
.github/pull_request_template.md | 21 +++
AGENTS.md | 55 +++++++
.../2026-03-12-repository-follow-ups.md | 55 +++++++
...03-12-repository-standardization-design.md | 117 ++++++++++++++
.../2026-03-12-repository-standardization.md | 148 ++++++++++++++++++
10 files changed, 488 insertions(+), 152 deletions(-)
delete mode 100644 .github/ISSUE_TEMPLATE/bug.md
create mode 100644 .github/ISSUE_TEMPLATE/config.yml
delete mode 100644 .github/ISSUE_TEMPLATE/feature.md
create mode 100644 .github/pull_request_template.md
create mode 100644 AGENTS.md
create mode 100644 docs/maintenance/2026-03-12-repository-follow-ups.md
create mode 100644 docs/plans/2026-03-12-repository-standardization-design.md
create mode 100644 docs/plans/2026-03-12-repository-standardization.md
diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md
deleted file mode 100644
index ab5ee96..0000000
--- a/.github/ISSUE_TEMPLATE/bug.md
+++ /dev/null
@@ -1,31 +0,0 @@
----
-name: Bug 反馈 (经典模板)
-about: 有关 bug 的报告
-title: "[Bug]"
-labels: bug, triage
-assignees: ""
----
-
-## 请确认:
-
-* [ ] 问题的标题明确
-* [ ] 我翻阅过其他的issue并且找不到类似的问题
-
-# Bug
-
-## 问题
-
-
-## 如何复现
-
-
-## 预期行为
-
-
-## 使用环境:
-- Python 版本:
-- Nonebot2 版本:
-- 插件 版本:
-
-## 日志/截图
-
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index 3047b1f..2cbdae1 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -1,108 +1,78 @@
-name: Bug 反馈
-title: "[Bug]: "
-description: 提交 Bug 反馈以帮助我们改进代码
-labels: ["bug"]
+name: Bug Report
+description: Report a confirmed defect or regression
+title: "bug: "
+labels:
+ - bug
body:
- - type: checkboxes
- id: ensure
+ - type: markdown
attributes:
- label: 确认项
- description: 请确认以下选项
- options:
- - label: 问题的标题明确
- required: true
- - label: 我翻阅过其他的 issue 并且找不到类似的问题
- required: true
- - label: 我已经尝试过在最新的代码中修复这个问题
- required: false
- - type: dropdown
- id: env-os
- attributes:
- label: 操作系统
- description: 选择运行 NoneBot 的系统
- options:
- - Windows
- - MacOS
- - Linux
- - Other
- validations:
- required: true
+ value: |
+ Provide a reproducible report. Include exact commands, config, and observed behavior.
- type: input
- id: env-python-ver
+ id: summary
attributes:
- label: Python 版本
- description: 填写运行 NoneBot 的 Python 版本
- placeholder: e.g. 3.11.0
+ label: Summary
+ description: One-sentence description of the bug
+ placeholder: codex_workdir is ignored by /home and the directory browser Home action
validations:
required: true
- - type: input
- id: env-nb-ver
- attributes:
- label: NoneBot 版本
- description: 填写 NoneBot 版本
- placeholder: e.g. 2.3.0
- validations:
- required: true
-
- - type: input
- id: env-adapter
+ - type: textarea
+ id: environment
attributes:
- label: 适配器
- description: 填写 NoneBot 加载使用的适配器
- placeholder: e.g. OneBot V11
+ label: Environment
+ description: Python, PDM, NoneBot, adapter, OS, and any relevant Codex setup details
+ placeholder: |
+ Python:
+ PDM:
+ NoneBot:
+ Adapter:
+ OS:
+ Codex CLI:
validations:
required: true
- - type: input
- id: env-plugin
+ - type: textarea
+ id: reproduce
attributes:
- label: 插件版本
- description: 填写使用的插件版本
- placeholder: e.g. 0.1.0
+ label: Steps To Reproduce
+ description: Provide exact steps or commands
+ placeholder: |
+ 1. Configure codex_workdir = "/tmp/work"
+ 2. Start the bot
+ 3. Run /home
+ 4. Observe the reported workdir
validations:
required: true
- type: textarea
- id: describe
+ id: expected
attributes:
- label: 描述问题
- description: 清晰简洁地说明问题是什么
+ label: Expected Behavior
validations:
required: true
- type: textarea
- id: reproduction
+ id: actual
attributes:
- label: 复现步骤
- description: 提供能复现此问题的详细操作步骤
- placeholder: |
- 1. 首先……
- 2. 然后……
- 3. 发生……
+ label: Actual Behavior
validations:
required: true
- type: textarea
- id: expected
+ id: evidence
attributes:
- label: 期望的结果
- description: 清晰简洁地描述你期望发生的事情
+ label: Verification Evidence
+ description: Include failing tests, logs, screenshots, or command output summaries
- type: textarea
- id: logs
+ id: affected
attributes:
- label: 截图或日志
- description: 提供有助于诊断问题的任何日志和截图
-
- - type: textarea
- id: config
- attributes:
- label: Nonebot 配置项
- description: Nonebot 配置项 (如果你的配置文件中包含敏感信息,请自行删除)
- render: dotenv
+ label: Affected Files Or Commands
placeholder: |
- # e.g.
- # KEY=VALUE
- # KEY2=VALUE2
\ No newline at end of file
+ src/nonebot_plugin_codex/service.py
+ src/nonebot_plugin_codex/telegram.py
+ /home
+ /cd
+
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..1b2eefc
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,6 @@
+blank_issues_enabled: false
+contact_links:
+ - name: Repository Plans
+ url: https://github.com/ttiee/nonebot-plugin-codex/tree/main/docs/plans
+ about: Review existing design and implementation planning documents before opening broad-scoped requests.
+
diff --git a/.github/ISSUE_TEMPLATE/feature.md b/.github/ISSUE_TEMPLATE/feature.md
deleted file mode 100644
index 3b6b103..0000000
--- a/.github/ISSUE_TEMPLATE/feature.md
+++ /dev/null
@@ -1,26 +0,0 @@
----
-name: Feature 特性请求 (经典模板)
-about: 为本插件加份菜
-title: "[Feature] "
-labels: enhancement, triage
-assignees: ""
----
-
-## 请确认:
-
-* [ ] 新特性的目的明确
-
-
-## Feature
-### 概要
-
-
-
-### 是否已有相关实现
-
-暂无
-
-
-### 其他内容
-
-暂无
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
index f9201cc..b79d9c0 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.yml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -1,30 +1,51 @@
-name: 功能建议
-title: "[Feature]: "
-description: 提出关于项目新功能的想法
-labels: ["enhancement"]
+name: Feature Request
+description: Propose an improvement or new capability
+title: "feat: "
+labels:
+ - enhancement
body:
- - type: checkboxes
- id: ensure
+ - type: markdown
attributes:
- label: 确认项
- description: 请确认以下选项
- options:
- - label: 新特性的目的明确
- required: true
- - label: 我已经使用过该插件并且了解其功能
- required: true
+ value: |
+ Describe the user problem first, then the proposed solution.
+
+ - type: input
+ id: summary
+ attributes:
+ label: Summary
+ placeholder: Add button panels for mode/model/effort/permission commands
+ validations:
+ required: true
+
- type: textarea
id: problem
attributes:
- label: 希望能解决的问题
- description: 在使用中遇到什么问题而需要新的功能?
+ label: Problem Statement
+ description: What is hard, missing, or error-prone today?
validations:
required: true
- type: textarea
- id: feature
+ id: proposal
attributes:
- label: 描述所需要的功能
- description: 请说明需要的功能或解决方法
+ label: Proposed Solution
validations:
- required: true
\ No newline at end of file
+ required: true
+
+ - type: textarea
+ id: alternatives
+ attributes:
+ label: Alternatives Considered
+
+ - type: textarea
+ id: scope
+ attributes:
+ label: Scope And Constraints
+ description: Compatibility, migration, UX, or testing constraints that matter
+
+ - type: textarea
+ id: verification
+ attributes:
+ label: Verification Plan
+ description: Tests, manual checks, or rollout validation you expect
+
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 0000000..74cd470
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,21 @@
+## Summary
+
+-
+
+## Linked Issue
+
+Closes #
+
+## What Changed
+
+-
+
+## Verification
+
+- [ ] `pdm run pytest -q`
+- [ ] `pdm run ruff check .`
+
+## Risks / Follow-Ups
+
+-
+
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..cb994e2
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,55 @@
+# Repository Guidelines
+
+## Project Scope
+
+`nonebot-plugin-codex` is a NoneBot plugin that exposes local Codex CLI workflows through Telegram. The repository is a Python package using a `src/` layout and PDM for dependency management.
+
+## Repository Layout
+
+- `src/nonebot_plugin_codex/`: plugin source code
+- `tests/`: pytest-based automated tests
+- `docs/plans/`: design and implementation planning documents
+- `docs/maintenance/`: tracked maintenance items and issue-ready notes
+
+## Working Norms
+
+- Keep changes aligned with the current plugin architecture and configuration compatibility.
+- Prefer small, reviewable changes with explicit tests for behavior changes.
+- Do not silently change documented config semantics; update docs and tests together.
+- Preserve backward compatibility for user-facing commands unless the change is explicitly intentional and documented.
+
+## Development Commands
+
+- Install dependencies: `pdm sync -G:all`
+- Run tests: `pdm run pytest -q`
+- Run focused tests: `pdm run pytest tests/test_service.py tests/test_telegram_handlers.py -q`
+- Run lint: `pdm run ruff check .`
+
+Use `pdm run ...` for repository commands so the project environment and `src/` import path are configured correctly.
+
+## Code Change Expectations
+
+- Follow TDD for behavior changes: write or update the failing test first, verify the failure, then implement the minimum fix.
+- Reuse existing callback/state patterns before adding new interaction mechanisms.
+- Keep repository files ASCII unless an existing file already uses non-ASCII content and it is appropriate to match it.
+- Avoid unrelated refactors while fixing a bug or delivering a scoped improvement.
+
+## Issue Expectations
+
+- Every non-trivial change should be traceable to an issue or a documented maintenance note.
+- Issue reports should include reproduction steps, expected behavior, actual behavior, and the affected files or commands.
+- Use `docs/maintenance/` markdown files as durable issue drafts when preparing GitHub issues from local work.
+
+## Pull Request Expectations
+
+- Link the PR to the relevant issue.
+- Summarize user-visible behavior changes and internal refactors separately.
+- Include fresh verification evidence, at minimum:
+ - `pdm run pytest -q`
+ - `pdm run ruff check .`
+- Call out any unverified areas or follow-up work explicitly.
+
+## Safety Notes
+
+- Respect persisted compatibility paths such as `data/codex_bridge/preferences.json` and `~/.codex/*` unless the change explicitly updates migration behavior.
+- Do not remove or rewrite user-authored documents under `docs/` unless the task requires it.
diff --git a/docs/maintenance/2026-03-12-repository-follow-ups.md b/docs/maintenance/2026-03-12-repository-follow-ups.md
new file mode 100644
index 0000000..ccbfc96
--- /dev/null
+++ b/docs/maintenance/2026-03-12-repository-follow-ups.md
@@ -0,0 +1,55 @@
+# Repository Follow-Ups
+
+## Summary
+
+The repository has a confirmed configuration bug around `codex_workdir`, several selector commands that still rely on manual argument entry instead of button-driven Telegram UX, and missing repository-level collaboration templates.
+
+## Confirmed Problems
+
+### 1. `codex_workdir` does not actually drive runtime defaults
+
+The README documents `codex_workdir` as:
+
+- the default workdir
+- the base for relative `/cd`
+- the Home target in the directory browser
+
+Current runtime behavior does not match that contract:
+
+- default chat preferences fall back to the OS home directory
+- `/home` resets to the OS home directory
+- directory browser `Home` jumps to the OS home directory
+
+Affected files:
+
+- `src/nonebot_plugin_codex/service.py`
+- `src/nonebot_plugin_codex/telegram.py`
+- `README.md`
+
+### 2. Selection-heavy commands still require manual text input
+
+The repository already uses inline keyboard flows for `/cd` and `/sessions`, but these commands still depend on remembering arguments:
+
+- `/mode`
+- `/model`
+- `/effort`
+- `/permission`
+
+This is inconsistent with the current UX direction and increases input friction on Telegram.
+
+### 3. Repository collaboration standards are not yet codified
+
+The project does not currently provide repository-level contribution guidance or GitHub issue/PR templates aligned with the existing stack and verification commands.
+
+## Proposed Follow-Up Work
+
+1. Add `AGENTS.md` and GitHub issue/PR templates.
+2. Fix `codex_workdir` so the configured path is the runtime default home/workdir baseline.
+3. Convert `/mode`, `/model`, `/effort`, and `/permission` to button panels while keeping existing text arguments compatible.
+4. Add regression tests for all of the above behavior.
+
+## Verification Targets
+
+- `pdm run pytest -q`
+- `pdm run ruff check .`
+- focused tests covering service and Telegram handler behavior for the new workdir and selector panel flows
diff --git a/docs/plans/2026-03-12-repository-standardization-design.md b/docs/plans/2026-03-12-repository-standardization-design.md
new file mode 100644
index 0000000..15843e7
--- /dev/null
+++ b/docs/plans/2026-03-12-repository-standardization-design.md
@@ -0,0 +1,117 @@
+# Repository Standardization Design
+
+## Background
+
+This repository already has working code and a basic test suite, but it lacks repository-level contribution guidance and reusable GitHub issue/PR templates. The current Telegram UX also mixes button-driven flows (`/cd`, `/sessions`) with manual argument entry (`/mode`, `/model`, `/effort`, `/permission`), and the documented `codex_workdir` setting does not actually drive runtime defaults.
+
+## Goals
+
+- Add repository-level collaboration guidance with a focused `AGENTS.md`.
+- Add GitHub issue and PR templates so maintenance work is repeatable and reviewable.
+- Write a tracked markdown backlog item for the currently identified problems, and open a GitHub issue from it if the CLI environment allows.
+- Fix `codex_workdir` so documented behavior matches runtime behavior.
+- Convert `/mode`, `/model`, `/effort`, and `/permission` from manual-argument commands into button-driven panels, consistent with the existing directory/session browsers.
+- Add regression tests that lock in the new behavior.
+
+## Non-Goals
+
+- No broad redesign of the plugin command set beyond the four selection-heavy commands above.
+- No new external dependencies.
+- No addition of broader community docs such as `CONTRIBUTING.md` in this pass.
+
+## Approach Options
+
+### Option 1: Minimal fixes only
+
+Add the missing docs, fix `codex_workdir`, and leave the command UX unchanged.
+
+- Pros: smallest diff
+- Cons: leaves obvious UX inconsistency in place
+
+### Option 2: Standardize workflow and command selection UX
+
+Add repository/process docs, file the issue, fix `codex_workdir`, and convert the four selector commands to button panels built on the existing callback pattern.
+
+- Pros: matches the user's stated goal, keeps architecture consistent, and improves both maintainer workflow and Telegram usability
+- Cons: touches both docs and interaction code in one change set
+
+### Option 3: Full maintainer-doc expansion
+
+Do Option 2 plus add `CONTRIBUTING.md`, labels guidance, and a broader governance layer.
+
+- Pros: more complete project scaffolding
+- Cons: wider scope than necessary for the current repository size
+
+## Recommended Design
+
+Use Option 2.
+
+### Repository Guidance
+
+Add a top-level `AGENTS.md` aimed at repository contributors and AI agents. It should define:
+
+- project purpose and stack
+- where code lives
+- required verification commands
+- expectations for tests, issue references, and PR descriptions
+- guardrails around touching user data/config-compatible paths
+
+### GitHub Templates
+
+Add:
+
+- `.github/ISSUE_TEMPLATE/bug_report.yml`
+- `.github/ISSUE_TEMPLATE/feature_request.yml`
+- `.github/ISSUE_TEMPLATE/config.yml`
+- `.github/pull_request_template.md`
+
+The templates should bias toward reproducibility, expected/actual behavior, verification evidence, and linked issues.
+
+### Backlog And Issue Workflow
+
+Create a markdown file under `docs/` that captures the identified bugs and improvements in repository language. Use that as the source of truth for a GitHub issue body and then create the issue with `gh issue create`.
+
+### `codex_workdir` Fix
+
+The service currently defaults to `Path.home()` in places where the documented configured workdir should be used. The fix is to make the configured `CodexBridgeSettings.workdir` the single default "workspace home" used by:
+
+- new chat preferences
+- `/home`
+- directory browser `Home`
+- any fallback that currently assumes the OS home directory
+
+This should remain compatible with explicit per-chat overrides saved in preferences.
+
+### Button Panels For Selection Commands
+
+Reuse the existing callback-driven browser pattern rather than inventing a second interaction system.
+
+Add a lightweight settings browser state that can render and handle button panels for:
+
+- mode
+- model
+- effort
+- permission
+
+The panel should show current values and let the user change them by tapping buttons. Existing text-argument usage should continue to work for backward compatibility.
+
+### Testing Strategy
+
+Use TDD:
+
+- write failing tests for `codex_workdir` default/home behavior
+- write failing tests for the new button-panel rendering and callback handling
+- then implement the minimum service and handler changes
+
+Tests should cover both service-level behavior and Telegram handler-level interaction where appropriate.
+
+## Risks And Mitigations
+
+- Risk: callback state becomes more fragmented.
+ Mitigation: follow the existing browser/history callback structure and naming conventions.
+
+- Risk: fixing `codex_workdir` breaks persisted preference behavior.
+ Mitigation: only use configured workdir for defaults/fallbacks, not to overwrite explicit stored chat preferences.
+
+- Risk: issue/PR automation may fail due to repo permissions or branch state.
+ Mitigation: keep the markdown source file regardless of CLI outcome and report any GitHub-side blocker explicitly.
diff --git a/docs/plans/2026-03-12-repository-standardization.md b/docs/plans/2026-03-12-repository-standardization.md
new file mode 100644
index 0000000..012d891
--- /dev/null
+++ b/docs/plans/2026-03-12-repository-standardization.md
@@ -0,0 +1,148 @@
+# Repository Standardization Implementation Plan
+
+> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
+
+**Goal:** Standardize repository collaboration flow, file a tracked maintenance issue, fix `codex_workdir`, and convert selector commands to button-driven Telegram panels with regression tests.
+
+**Architecture:** Reuse the existing Telegram callback/browser architecture already used by directory and history browsing. Keep text commands backward-compatible while adding button panels and make `CodexBridgeSettings.workdir` the single configured home/default path used across service and handler flows.
+
+**Tech Stack:** Python 3.10+, NoneBot2, nonebot-adapter-telegram, pytest, GitHub CLI
+
+---
+
+### Task 1: Add Repository Collaboration Files
+
+**Files:**
+- Create: `AGENTS.md`
+- Create: `.github/ISSUE_TEMPLATE/bug_report.yml`
+- Create: `.github/ISSUE_TEMPLATE/feature_request.yml`
+- Create: `.github/ISSUE_TEMPLATE/config.yml`
+- Create: `.github/pull_request_template.md`
+
+**Step 1: Add the repository-level guidance files**
+
+Write concise repository instructions covering structure, test/lint commands, issue/PR expectations, and change safety.
+
+**Step 2: Review templates for repository fit**
+
+Check that templates reference the actual commands and project structure in this repo.
+
+**Step 3: Verify the files exist and are readable**
+
+Run: `rg --files AGENTS.md .github`
+Expected: new repository guidance and GitHub template files are listed
+
+### Task 2: Capture Backlog And Open GitHub Issue
+
+**Files:**
+- Create: `docs/maintenance/2026-03-12-repository-follow-ups.md`
+
+**Step 1: Write the backlog markdown**
+
+Capture the confirmed bugs and UX improvements in issue-ready wording.
+
+**Step 2: Attempt GitHub issue creation**
+
+Run: `gh issue create --title "
" --body-file docs/maintenance/2026-03-12-repository-follow-ups.md`
+Expected: returns an issue URL
+
+**Step 3: If creation fails, record the blocker**
+
+Do not drop the markdown file; use it as the fallback deliverable.
+
+### Task 3: Write Failing Tests For `codex_workdir`
+
+**Files:**
+- Modify: `tests/test_service.py`
+- Modify: `tests/test_telegram_handlers.py`
+
+**Step 1: Add tests for configured workdir defaults**
+
+Add tests asserting:
+- default preferences use `CodexBridgeSettings.workdir`
+- directory browser `home` action uses configured workdir
+- `/home` uses configured workdir rather than OS home
+
+**Step 2: Run targeted tests and confirm failure**
+
+Run: `pdm run pytest tests/test_service.py tests/test_telegram_handlers.py -q`
+Expected: new tests fail because current code still uses `Path.home()`
+
+### Task 4: Implement `codex_workdir` Runtime Fix
+
+**Files:**
+- Modify: `src/nonebot_plugin_codex/service.py`
+- Modify: `src/nonebot_plugin_codex/telegram.py`
+
+**Step 1: Replace hard-coded `Path.home()` defaults with configured workdir**
+
+Keep stored per-chat overrides intact; only change defaults and "home" semantics.
+
+**Step 2: Re-run the targeted tests**
+
+Run: `pdm run pytest tests/test_service.py tests/test_telegram_handlers.py -q`
+Expected: the new workdir tests pass
+
+### Task 5: Write Failing Tests For Button Panels
+
+**Files:**
+- Modify: `tests/test_service.py`
+- Modify: `tests/test_telegram_handlers.py`
+
+**Step 1: Add service/handler tests for new selector panels**
+
+Cover:
+- panel rendering for mode/model/effort/permission
+- callback handling updates the right preference
+- legacy text arguments still work
+
+**Step 2: Run focused tests and confirm failure**
+
+Run: `pdm run pytest tests/test_service.py tests/test_telegram_handlers.py -q`
+Expected: failures because selector panel state and callbacks do not exist yet
+
+### Task 6: Implement Button-Driven Selector Panels
+
+**Files:**
+- Modify: `src/nonebot_plugin_codex/service.py`
+- Modify: `src/nonebot_plugin_codex/telegram.py`
+- Modify: `src/nonebot_plugin_codex/__init__.py`
+
+**Step 1: Add callback encoding/state/rendering for selector panels**
+
+Follow the same general pattern as directory/history browsers.
+
+**Step 2: Update handlers to open panels when no argument is supplied**
+
+Keep argument-based command handling intact for compatibility.
+
+**Step 3: Add callback routing for selector interactions**
+
+Ensure stale-panel handling mirrors existing callback flows.
+
+**Step 4: Re-run focused tests**
+
+Run: `pdm run pytest tests/test_service.py tests/test_telegram_handlers.py -q`
+Expected: selector panel tests pass
+
+### Task 7: Full Verification And GitHub PR
+
+**Files:**
+- Modify: any files touched above as needed
+
+**Step 1: Run full verification**
+
+Run: `pdm run pytest -q`
+Expected: all tests pass
+
+Run: `pdm run ruff check .`
+Expected: all checks pass
+
+**Step 2: Review diff and create a branch if needed**
+
+Ensure the issue reference appears in commit/PR metadata.
+
+**Step 3: Create PR linked to the issue**
+
+Run: `gh pr create --fill --body-file `
+Expected: returns a PR URL linked to the created issue
From df19f35ab7330ca1d25e52ae2f439529eaf530a3 Mon Sep 17 00:00:00 2001
From: ttiee <469784630@qq.com>
Date: Thu, 12 Mar 2026 09:33:59 +0800
Subject: [PATCH 2/2] feat(telegram): honor codex workdir and add settings
panels
Use the configured codex_workdir as the runtime home/default workdir and add inline button panels for mode, model, effort, and permission selection.
Refs #1
---
src/nonebot_plugin_codex/__init__.py | 10 ++
src/nonebot_plugin_codex/service.py | 251 ++++++++++++++++++++++++++-
src/nonebot_plugin_codex/telegram.py | 154 +++++++++++-----
tests/test_service.py | 75 ++++++++
tests/test_telegram_handlers.py | 121 ++++++++++++-
5 files changed, 565 insertions(+), 46 deletions(-)
diff --git a/src/nonebot_plugin_codex/__init__.py b/src/nonebot_plugin_codex/__init__.py
index e5c55d0..850db64 100644
--- a/src/nonebot_plugin_codex/__init__.py
+++ b/src/nonebot_plugin_codex/__init__.py
@@ -82,6 +82,12 @@
block=True,
rule=handlers.is_history_callback,
)
+ setting_callback = on_type(
+ CallbackQueryEvent,
+ priority=10,
+ block=True,
+ rule=handlers.is_setting_callback,
+ )
@codex_cmd.handle()
async def _handle_codex(
@@ -159,6 +165,10 @@ async def _handle_browser_callback(bot: Bot, event: CallbackQueryEvent) -> None:
async def _handle_history_callback(bot: Bot, event: CallbackQueryEvent) -> None:
await handlers.handle_history_callback(bot, event)
+ @setting_callback.handle()
+ async def _handle_setting_callback(bot: Bot, event: CallbackQueryEvent) -> None:
+ await handlers.handle_setting_callback(bot, event)
+
@follow_up.handle()
async def _handle_follow_up(bot: Bot, event: MessageEvent) -> None:
await handlers.handle_follow_up(bot, event)
diff --git a/src/nonebot_plugin_codex/service.py b/src/nonebot_plugin_codex/service.py
index c3ab491..9399b60 100644
--- a/src/nonebot_plugin_codex/service.py
+++ b/src/nonebot_plugin_codex/service.py
@@ -34,6 +34,9 @@
HISTORY_CALLBACK_PREFIX = "chs"
HISTORY_PAGE_SIZE = 6
HISTORY_STALE_MESSAGE = "历史会话面板已失效,请重新执行 /sessions"
+SETTING_CALLBACK_PREFIX = "csp"
+SETTING_STALE_MESSAGE = "设置面板已失效,请重新执行对应命令"
+SUPPORTED_SETTING_PANELS = {"mode", "model", "effort", "permission"}
@dataclass(slots=True)
@@ -165,6 +168,15 @@ class DirectoryBrowserState:
message_id: int | None = None
+@dataclass(slots=True)
+class SettingPanelState:
+ chat_key: str
+ kind: str
+ token: str
+ version: int
+ message_id: int | None = None
+
+
def build_chat_key(chat_type: str, chat_id: int) -> str:
if chat_type == "private":
return f"private_{chat_id}"
@@ -276,6 +288,30 @@ def decode_history_callback(payload: str) -> tuple[str, int, str, int | None]:
return token, version, action, index
+def encode_setting_callback(
+ token: str,
+ version: int,
+ action: str,
+ value: str | None = None,
+) -> str:
+ suffix = "" if value is None else f":{value}"
+ return f"{SETTING_CALLBACK_PREFIX}:{token}:{version}:{action}{suffix}"
+
+
+def decode_setting_callback(payload: str) -> tuple[str, int, str, str | None]:
+ parts = payload.split(":")
+ if len(parts) not in {4, 5} or parts[0] != SETTING_CALLBACK_PREFIX:
+ raise ValueError("无效的设置回调。")
+ token = parts[1]
+ try:
+ version = int(parts[2])
+ except ValueError as exc:
+ raise ValueError("无效的设置回调。") from exc
+ action = parts[3]
+ value = parts[4] if len(parts) == 5 else None
+ return token, version, action, value
+
+
def parse_event_line(line: str) -> dict[str, Any] | None:
try:
payload = json.loads(line)
@@ -446,9 +482,17 @@ def __init__(
self.preference_overrides = self._load_preferences()
self.directory_browsers: dict[str, DirectoryBrowserState] = {}
self.history_browsers: dict[str, HistoryBrowserState] = {}
+ self.setting_panels: dict[str, SettingPanelState] = {}
self._native_history_entries: list[HistoricalSessionSummary] = []
self._native_history_loaded = False
+ def _configured_workdir(self) -> str:
+ configured = Path(self.settings.workdir).expanduser()
+ try:
+ return str(configured.resolve())
+ except OSError:
+ return str(configured)
+
def _spawn_native_client(self) -> Any:
if self.native_client is None:
return None
@@ -1342,6 +1386,205 @@ async def apply_history_session(self, chat_key: str, token: str, version: int) -
notice_lines.append("下一条普通消息会继续该会话。")
return "\n".join(notice_lines)
+ def _replace_setting_panel_state(
+ self,
+ chat_key: str,
+ kind: str,
+ *,
+ previous: SettingPanelState | None = None,
+ ) -> SettingPanelState:
+ if kind not in SUPPORTED_SETTING_PANELS:
+ raise ValueError("未知设置面板。")
+ state = SettingPanelState(
+ chat_key=chat_key,
+ kind=kind,
+ token=previous.token if previous else self._make_browser_token(),
+ version=(previous.version + 1) if previous else 1,
+ message_id=previous.message_id if previous else None,
+ )
+ self.setting_panels[chat_key] = state
+ return state
+
+ def open_setting_panel(self, chat_key: str, kind: str) -> SettingPanelState:
+ self._ensure_not_running(chat_key)
+ self.get_preferences(chat_key)
+ return self._replace_setting_panel_state(chat_key, kind)
+
+ def get_setting_panel(
+ self,
+ chat_key: str,
+ token: str | None = None,
+ version: int | None = None,
+ ) -> SettingPanelState:
+ state = self.setting_panels.get(chat_key)
+ if state is None:
+ raise ValueError(SETTING_STALE_MESSAGE)
+ if token is not None and state.token != token:
+ raise ValueError(SETTING_STALE_MESSAGE)
+ if version is not None and state.version != version:
+ raise ValueError(SETTING_STALE_MESSAGE)
+ return state
+
+ def remember_setting_panel_message(
+ self,
+ chat_key: str,
+ token: str,
+ message_id: int | None,
+ ) -> None:
+ if message_id is None:
+ return
+ panel = self.get_setting_panel(chat_key, token=token)
+ panel.message_id = message_id
+
+ def close_setting_panel(self, chat_key: str, token: str, version: int) -> None:
+ self.get_setting_panel(chat_key, token=token, version=version)
+ self.setting_panels.pop(chat_key, None)
+
+ def navigate_setting_panel(
+ self,
+ chat_key: str,
+ token: str,
+ version: int,
+ action: str,
+ ) -> SettingPanelState:
+ panel = self.get_setting_panel(chat_key, token=token, version=version)
+ if action != "refresh":
+ raise ValueError("未知设置操作。")
+ return self._replace_setting_panel_state(chat_key, panel.kind, previous=panel)
+
+ def render_setting_panel(self, chat_key: str) -> tuple[str, InlineKeyboardMarkup]:
+ panel = self.get_setting_panel(chat_key)
+ preferences = self.get_preferences(chat_key)
+ lines: list[str]
+ options: list[tuple[str, str]]
+
+ if panel.kind == "mode":
+ session = self.sessions.get(chat_key)
+ active_mode = session.active_mode if session else preferences.default_mode
+ lines = [
+ "模式设置",
+ f"当前默认模式:{preferences.default_mode}",
+ f"当前活跃模式:{active_mode}",
+ ]
+ options = [
+ (
+ "resume",
+ "✓ resume" if preferences.default_mode == "resume" else "resume",
+ ),
+ ("exec", "✓ exec" if preferences.default_mode == "exec" else "exec"),
+ ]
+ elif panel.kind == "model":
+ lines = [
+ "模型设置",
+ f"当前设置:{format_preferences_summary(preferences)}",
+ ]
+ options = [
+ (
+ model.slug,
+ f"{'✓ ' if model.slug == preferences.model else ''}{model.slug}",
+ )
+ for model in self.list_models()
+ ]
+ elif panel.kind == "effort":
+ supported = self.get_supported_efforts(preferences.model)
+ lines = [
+ "推理强度设置",
+ f"当前模型:{preferences.model}",
+ f"当前推理强度:{preferences.reasoning_effort}",
+ f"支持:{' / '.join(supported)}",
+ ]
+ options = [
+ (
+ effort,
+ (
+ f"✓ {effort}"
+ if effort == preferences.reasoning_effort
+ else effort
+ ),
+ )
+ for effort in supported
+ ]
+ elif panel.kind == "permission":
+ lines = [
+ "权限模式设置",
+ f"当前权限模式:{preferences.permission_mode}",
+ "safe = workspace-write",
+ "danger = 绕过审批与沙箱",
+ ]
+ options = [
+ (
+ "safe",
+ "✓ safe" if preferences.permission_mode == "safe" else "safe",
+ ),
+ (
+ "danger",
+ (
+ "✓ danger"
+ if preferences.permission_mode == "danger"
+ else "danger"
+ ),
+ ),
+ ]
+ else:
+ raise ValueError("未知设置面板。")
+
+ keyboard = [
+ [
+ InlineKeyboardButton(
+ text=label,
+ callback_data=encode_setting_callback(
+ panel.token,
+ panel.version,
+ "set",
+ value,
+ ),
+ )
+ ]
+ for value, label in options
+ ]
+ keyboard.append(
+ [
+ InlineKeyboardButton(
+ text="刷新",
+ callback_data=encode_setting_callback(
+ panel.token,
+ panel.version,
+ "refresh",
+ ),
+ ),
+ InlineKeyboardButton(
+ text="关闭",
+ callback_data=encode_setting_callback(
+ panel.token,
+ panel.version,
+ "close",
+ ),
+ ),
+ ]
+ )
+ return "\n".join(lines), InlineKeyboardMarkup(inline_keyboard=keyboard)
+
+ async def apply_setting_panel_selection(
+ self,
+ chat_key: str,
+ token: str,
+ version: int,
+ value: str,
+ ) -> str:
+ panel = self.get_setting_panel(chat_key, token=token, version=version)
+ if panel.kind == "mode":
+ notice = await self.update_default_mode(chat_key, value)
+ elif panel.kind == "model":
+ notice = await self.update_model(chat_key, value)
+ elif panel.kind == "effort":
+ notice = await self.update_reasoning_effort(chat_key, value)
+ elif panel.kind == "permission":
+ notice = await self.update_permission_mode(chat_key, value)
+ else:
+ raise ValueError("未知设置面板。")
+ self._replace_setting_panel_state(chat_key, panel.kind, previous=panel)
+ return notice
+
def load_models(self) -> dict[str, ModelInfo]:
try:
payload = json.loads(
@@ -1419,7 +1662,9 @@ def _load_preferences(self) -> dict[str, ChatPreferences]:
reasoning_effort=reasoning_effort,
permission_mode=permission_mode,
workdir=(
- workdir if isinstance(workdir, str) and workdir else str(Path.home())
+ workdir
+ if isinstance(workdir, str) and workdir
+ else self._configured_workdir()
),
default_mode=(
default_mode
@@ -1490,7 +1735,7 @@ def _default_preferences(self) -> ChatPreferences:
model=model.slug,
reasoning_effort=effort,
permission_mode="safe",
- workdir=str(Path.home()),
+ workdir=self._configured_workdir(),
default_mode="resume",
)
@@ -1717,7 +1962,7 @@ def navigate_directory_browser(
if action == "home":
return self._replace_browser_state(
chat_key,
- str(Path.home()),
+ self._configured_workdir(),
page=0,
previous=browser,
)
diff --git a/src/nonebot_plugin_codex/telegram.py b/src/nonebot_plugin_codex/telegram.py
index beb77bd..d66e401 100644
--- a/src/nonebot_plugin_codex/telegram.py
+++ b/src/nonebot_plugin_codex/telegram.py
@@ -4,7 +4,6 @@
import time
import asyncio
from typing import Any
-from pathlib import Path
from nonebot.adapters.telegram import Bot
from nonebot.adapters.telegram.message import Message
@@ -16,14 +15,16 @@
HISTORY_STALE_MESSAGE,
BROWSER_CALLBACK_PREFIX,
HISTORY_CALLBACK_PREFIX,
+ SETTING_STALE_MESSAGE,
+ SETTING_CALLBACK_PREFIX,
CodexBridgeService,
chunk_text,
build_chat_key,
format_result_text,
decode_browser_callback,
decode_history_callback,
+ decode_setting_callback,
should_forward_follow_up,
- format_preferences_summary,
)
RETRY_AFTER_PATTERN = re.compile(r"retry after (\d+(?:\.\d+)?)", re.IGNORECASE)
@@ -281,6 +282,11 @@ async def is_history_callback(self, event: CallbackQueryEvent) -> bool:
f"{HISTORY_CALLBACK_PREFIX}:"
)
+ async def is_setting_callback(self, event: CallbackQueryEvent) -> bool:
+ return isinstance(event.data, str) and event.data.startswith(
+ f"{SETTING_CALLBACK_PREFIX}:"
+ )
+
def callback_message_id(self, event: CallbackQueryEvent) -> int | None:
message = getattr(event, "message", None)
return getattr(message, "message_id", None)
@@ -306,6 +312,22 @@ async def send_history_browser(
getattr(message, "message_id", None),
)
+ async def send_setting_panel(
+ self,
+ bot: Bot,
+ event: MessageEvent,
+ chat_key: str,
+ kind: str,
+ ) -> None:
+ panel = self.service.open_setting_panel(chat_key, kind)
+ text, markup = self.service.render_setting_panel(chat_key)
+ message = await self.send_event_message(bot, event, text, reply_markup=markup)
+ self.service.remember_setting_panel_message(
+ chat_key,
+ panel.token,
+ getattr(message, "message_id", None),
+ )
+
async def edit_or_resend_browser(
self,
bot: Bot,
@@ -370,6 +392,37 @@ async def edit_or_resend_history_browser(
getattr(message, "message_id", None),
)
+ async def edit_or_resend_setting_panel(
+ self,
+ bot: Bot,
+ event: CallbackQueryEvent,
+ chat_key: str,
+ ) -> None:
+ panel = self.service.get_setting_panel(chat_key)
+ text, markup = self.service.render_setting_panel(chat_key)
+ message_id = self.callback_message_id(event) or panel.message_id
+ chat_id = self.event_chat(event).id
+ try:
+ if message_id is None:
+ raise ValueError("missing message id")
+ await self.edit_message(
+ bot,
+ chat_id=chat_id,
+ message_id=message_id,
+ text=text,
+ reply_markup=markup,
+ )
+ self.service.remember_setting_panel_message(chat_key, panel.token, message_id)
+ except Exception:
+ message = await self.send_chat_message(
+ bot, chat_id, text, reply_markup=markup
+ )
+ self.service.remember_setting_panel_message(
+ chat_key,
+ panel.token,
+ getattr(message, "message_id", None),
+ )
+
async def handle_codex(self, bot: Bot, event: MessageEvent, args: Message) -> None:
chat_key = self.chat_key(event)
session = self.service.activate_chat(chat_key)
@@ -399,19 +452,13 @@ async def handle_codex(self, bot: Bot, event: MessageEvent, args: Message) -> No
async def handle_mode(self, bot: Bot, event: MessageEvent, args: Message) -> None:
chat_key = self.chat_key(event)
mode = args.extract_plain_text().strip()
- if not mode:
- preferences = self.service.get_preferences(chat_key)
- session = self.service.get_session(chat_key)
- await self.send_event_message(
- bot,
- event,
- f"当前默认模式:{preferences.default_mode}\n当前活跃模式:{session.active_mode}",
- )
- return
try:
+ if not mode:
+ await self.send_setting_panel(bot, event, chat_key, "mode")
+ return
notice = await self.service.update_default_mode(chat_key, mode)
await self.send_event_message(bot, event, notice)
- except (ValueError, RuntimeError) as exc:
+ except (FileNotFoundError, ValueError, RuntimeError) as exc:
await self.send_event_message(bot, event, self.error_text(exc))
async def handle_exec(self, bot: Bot, event: MessageEvent, args: Message) -> None:
@@ -448,17 +495,8 @@ async def handle_model(self, bot: Bot, event: MessageEvent, args: Message) -> No
chat_key = self.chat_key(event)
slug = args.extract_plain_text().strip()
try:
- preferences = self.service.get_preferences(chat_key)
if not slug:
- efforts = "/".join(self.service.get_supported_efforts(preferences.model))
- await self.send_event_message(
- bot,
- event,
- (
- f"当前设置:{format_preferences_summary(preferences)}\n"
- f"当前模型支持推理强度:{efforts}"
- ),
- )
+ await self.send_setting_panel(bot, event, chat_key, "model")
return
notice = await self.service.update_model(chat_key, slug)
await self.send_event_message(bot, event, notice)
@@ -469,17 +507,8 @@ async def handle_effort(self, bot: Bot, event: MessageEvent, args: Message) -> N
chat_key = self.chat_key(event)
effort = args.extract_plain_text().strip()
try:
- preferences = self.service.get_preferences(chat_key)
- supported = "/".join(self.service.get_supported_efforts(preferences.model))
if not effort:
- await self.send_event_message(
- bot,
- event,
- (
- f"当前推理强度:{preferences.reasoning_effort}\n"
- f"当前模型 `{preferences.model}` 支持:{supported}"
- ),
- )
+ await self.send_setting_panel(bot, event, chat_key, "effort")
return
notice = await self.service.update_reasoning_effort(chat_key, effort)
await self.send_event_message(bot, event, notice)
@@ -492,16 +521,8 @@ async def handle_permission(
chat_key = self.chat_key(event)
permission = args.extract_plain_text().strip()
try:
- preferences = self.service.get_preferences(chat_key)
if not permission:
- await self.send_event_message(
- bot,
- event,
- (
- f"当前权限模式:{preferences.permission_mode}\n"
- "safe = workspace-write,danger = 绕过审批与沙箱。"
- ),
- )
+ await self.send_setting_panel(bot, event, chat_key, "permission")
return
notice = await self.service.update_permission_mode(chat_key, permission)
await self.send_event_message(bot, event, notice)
@@ -529,7 +550,8 @@ async def handle_cd(self, bot: Bot, event: MessageEvent, args: Message) -> None:
async def handle_home(self, bot: Bot, event: MessageEvent) -> None:
try:
notice = await self.service.update_workdir(
- self.chat_key(event), str(Path.home())
+ self.chat_key(event),
+ self.service.settings.workdir,
)
await self.send_event_message(bot, event, notice)
except (ValueError, RuntimeError) as exc:
@@ -636,6 +658,54 @@ async def handle_history_callback(self, bot: Bot, event: CallbackQueryEvent) ->
event.id, text=self.error_text(exc), show_alert=True
)
+ async def handle_setting_callback(self, bot: Bot, event: CallbackQueryEvent) -> None:
+ if not isinstance(event.data, str):
+ await bot.answer_callback_query(
+ event.id, text=SETTING_STALE_MESSAGE, show_alert=True
+ )
+ return
+
+ try:
+ chat_key = self.chat_key(event)
+ chat_id = self.event_chat(event).id
+ token, version, action, value = decode_setting_callback(event.data)
+ if action == "set":
+ if not value:
+ raise ValueError("设置值无效。")
+ await self.service.apply_setting_panel_selection(
+ chat_key, token, version, value
+ )
+ await self.edit_or_resend_setting_panel(bot, event, chat_key)
+ await bot.answer_callback_query(event.id, text="已更新。")
+ return
+ if action == "close":
+ self.service.close_setting_panel(chat_key, token, version)
+ message_id = self.callback_message_id(event)
+ if message_id is not None:
+ await self.edit_message(
+ bot,
+ chat_id=chat_id,
+ message_id=message_id,
+ text="设置面板已关闭。",
+ reply_markup=None,
+ )
+ await bot.answer_callback_query(event.id, text="已关闭。")
+ return
+ self.service.navigate_setting_panel(chat_key, token, version, action)
+ await self.edit_or_resend_setting_panel(bot, event, chat_key)
+ await bot.answer_callback_query(event.id)
+ except ValueError as exc:
+ text = str(exc) or SETTING_STALE_MESSAGE
+ await bot.answer_callback_query(
+ event.id,
+ text=text,
+ show_alert=text == SETTING_STALE_MESSAGE,
+ )
+ except RuntimeError as exc:
+ await bot.answer_callback_query(
+ event.id, text=self.error_text(exc), show_alert=True
+ )
+
async def handle_follow_up(self, bot: Bot, event: MessageEvent) -> None:
chat_key = self.chat_key(event)
session = self.service.get_session(chat_key)
diff --git a/tests/test_service.py b/tests/test_service.py
index 7441192..b6e8af4 100644
--- a/tests/test_service.py
+++ b/tests/test_service.py
@@ -69,6 +69,38 @@ def test_build_exec_argv_for_safe_and_resume_mode() -> None:
assert argv[-2:] == ["thread-1", "hello"]
+def test_default_preferences_use_configured_workdir(
+ tmp_path: Path, model_cache_file: Path
+) -> None:
+ service = make_service(tmp_path, model_cache_file)
+
+ preferences = service.get_preferences("private_1")
+
+ assert preferences.workdir == str(tmp_path.resolve())
+
+
+def test_directory_browser_home_uses_configured_workdir(
+ tmp_path: Path, model_cache_file: Path
+) -> None:
+ service = make_service(tmp_path, model_cache_file)
+ outside_dir = tmp_path.parent
+
+ browser = service._replace_browser_state( # noqa: SLF001
+ "private_1",
+ str(outside_dir),
+ page=0,
+ )
+
+ browser = service.navigate_directory_browser(
+ "private_1",
+ browser.token,
+ browser.version,
+ "home",
+ )
+
+ assert browser.current_path == str(tmp_path.resolve())
+
+
@pytest.mark.asyncio
async def test_update_default_mode_persists_preference_and_switches_active_mode(
tmp_path: Path, model_cache_file: Path
@@ -162,3 +194,46 @@ async def test_apply_history_session_uses_existing_cwd_when_original_missing(
assert "原工作目录不存在,已保留当前工作目录。" in notice
assert f"当前工作目录:{current_dir.resolve()}" in notice
+
+
+@pytest.mark.parametrize(
+ ("kind", "expected_heading"),
+ [
+ ("mode", "模式设置"),
+ ("model", "模型设置"),
+ ("effort", "推理强度设置"),
+ ("permission", "权限模式设置"),
+ ],
+)
+def test_render_setting_panels_show_expected_headings(
+ tmp_path: Path,
+ model_cache_file: Path,
+ kind: str,
+ expected_heading: str,
+) -> None:
+ service = make_service(tmp_path, model_cache_file)
+ service.activate_chat("private_1")
+
+ service.open_setting_panel("private_1", kind)
+ text, markup = service.render_setting_panel("private_1")
+
+ assert expected_heading in text
+ assert markup.inline_keyboard
+
+
+@pytest.mark.asyncio
+async def test_apply_permission_setting_panel_updates_preference(
+ tmp_path: Path, model_cache_file: Path
+) -> None:
+ service = make_service(tmp_path, model_cache_file)
+ panel = service.open_setting_panel("private_1", "permission")
+
+ notice = await service.apply_setting_panel_selection(
+ "private_1",
+ panel.token,
+ panel.version,
+ "danger",
+ )
+
+ assert "danger" in notice
+ assert service.get_preferences("private_1").permission_mode == "danger"
diff --git a/tests/test_telegram_handlers.py b/tests/test_telegram_handlers.py
index e386e27..0e180d4 100644
--- a/tests/test_telegram_handlers.py
+++ b/tests/test_telegram_handlers.py
@@ -78,14 +78,20 @@ async def answer_callback_query(self, callback_id: str, **kwargs: Any) -> None:
class FakeService:
def __init__(self) -> None:
self.session = ChatSession()
- self.settings = SimpleNamespace(chunk_size=3500)
+ self.settings = SimpleNamespace(chunk_size=3500, workdir="/tmp/configured-home")
self.browser_text = "目录浏览"
self.history_text = "Codex 历史会话"
+ self.setting_text = "模式设置"
self.default_mode = "resume"
self.execute_calls: list[tuple[str, str | None]] = []
self.browser_token = "token"
self.browser_version = 1
self.browser_applied = False
+ self.setting_token = "setting"
+ self.setting_version = 1
+ self.setting_kind = "mode"
+ self.setting_updates: list[str] = []
+ self.updated_workdirs: list[str] = []
def get_session(self, chat_key: str) -> ChatSession:
return self.session
@@ -134,6 +140,7 @@ def remember_browser_message(
return None
async def update_workdir(self, chat_key: str, target: str) -> str:
+ self.updated_workdirs.append(target)
return f"当前工作目录:{target}"
def get_browser(self, chat_key: str) -> SimpleNamespace:
@@ -174,6 +181,58 @@ def remember_history_browser_message(
) -> None:
return None
+ def open_setting_panel(self, chat_key: str, kind: str) -> SimpleNamespace:
+ self.setting_kind = kind
+ return SimpleNamespace(token=self.setting_token)
+
+ def render_setting_panel(self, chat_key: str) -> tuple[str, None]:
+ texts = {
+ "mode": "模式设置",
+ "model": "模型设置",
+ "effort": "推理强度设置",
+ "permission": "权限模式设置",
+ }
+ return texts[self.setting_kind], None
+
+ def remember_setting_panel_message(
+ self, chat_key: str, token: str, message_id: int | None
+ ) -> None:
+ return None
+
+ def get_setting_panel(self, chat_key: str) -> SimpleNamespace:
+ return SimpleNamespace(
+ token=self.setting_token,
+ version=self.setting_version,
+ kind=self.setting_kind,
+ message_id=1,
+ )
+
+ def navigate_setting_panel(
+ self,
+ chat_key: str,
+ token: str,
+ version: int,
+ action: str,
+ ) -> None:
+ return None
+
+ async def apply_setting_panel_selection(
+ self,
+ chat_key: str,
+ token: str,
+ version: int,
+ value: str,
+ ) -> str:
+ self.setting_updates.append(value)
+ return f"当前设置:{value}"
+
+ def close_setting_panel(self, chat_key: str, token: str, version: int) -> None:
+ return None
+
+ async def update_default_mode(self, chat_key: str, mode: str) -> str:
+ self.setting_updates.append(mode)
+ return f"当前默认模式:{mode}"
+
@pytest.mark.asyncio
async def test_handle_codex_without_prompt_sends_status_message() -> None:
@@ -244,3 +303,63 @@ async def test_handle_browser_callback_apply_updates_directory() -> None:
assert service.browser_applied is True
assert bot.answered[0]["text"] == "工作目录已更新。"
+
+
+@pytest.mark.asyncio
+async def test_handle_home_uses_configured_workdir() -> None:
+ service = FakeService()
+ handlers = TelegramHandlers(service)
+ bot = FakeBot()
+
+ await handlers.handle_home(bot, FakeEvent(""))
+
+ assert service.updated_workdirs == [service.settings.workdir]
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ ("handler_name", "kind", "expected_text"),
+ [
+ ("handle_mode", "mode", "模式设置"),
+ ("handle_model", "model", "模型设置"),
+ ("handle_effort", "effort", "推理强度设置"),
+ ("handle_permission", "permission", "权限模式设置"),
+ ],
+)
+async def test_selection_commands_without_argument_open_setting_panels(
+ handler_name: str,
+ kind: str,
+ expected_text: str,
+) -> None:
+ service = FakeService()
+ handlers = TelegramHandlers(service)
+ bot = FakeBot()
+
+ await getattr(handlers, handler_name)(bot, FakeEvent(""), FakeMessage(""))
+
+ assert service.setting_kind == kind
+ assert bot.sent[0]["text"] == expected_text
+
+
+@pytest.mark.asyncio
+async def test_handle_mode_with_argument_still_updates_default_mode() -> None:
+ service = FakeService()
+ handlers = TelegramHandlers(service)
+ bot = FakeBot()
+
+ await handlers.handle_mode(bot, FakeEvent(""), FakeMessage("exec"))
+
+ assert service.setting_updates == ["exec"]
+
+
+@pytest.mark.asyncio
+async def test_handle_setting_callback_updates_setting() -> None:
+ service = FakeService()
+ handlers = TelegramHandlers(service)
+ bot = FakeBot()
+ event = FakeCallbackEvent("csp:setting:1:set:danger")
+
+ await handlers.handle_setting_callback(bot, event)
+
+ assert service.setting_updates == ["danger"]
+ assert bot.answered[0]["text"] == "已更新。"