diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 2cbdae1..c7f973f 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,78 +1,77 @@ -name: Bug Report -description: Report a confirmed defect or regression -title: "bug: " +name: 问题反馈 +description: 报告已确认的缺陷或回归问题 +title: "问题:" labels: - bug body: - type: markdown attributes: value: | - Provide a reproducible report. Include exact commands, config, and observed behavior. + 请尽量提供可复现的信息,包括命令、配置、实际表现和预期结果。 - type: input id: summary attributes: - label: Summary - description: One-sentence description of the bug - placeholder: codex_workdir is ignored by /home and the directory browser Home action + label: 问题概述 + description: 用一句话描述你遇到的问题 + placeholder: codex_workdir 在 /home 和目录浏览器的 Home 按钮中没有生效 validations: required: true - type: textarea id: environment attributes: - label: Environment - description: Python, PDM, NoneBot, adapter, OS, and any relevant Codex setup details + label: 运行环境 + description: 填写 Python、PDM、NoneBot、适配器、操作系统以及相关 Codex 环境信息 placeholder: | - Python: - PDM: - NoneBot: - Adapter: - OS: - Codex CLI: + Python 版本: + PDM 版本: + NoneBot 版本: + Adapter 版本: + 操作系统: + Codex CLI: validations: required: true - type: textarea id: reproduce attributes: - label: Steps To Reproduce - description: Provide exact steps or commands + label: 复现步骤 + description: 提供可以稳定复现问题的步骤或命令 placeholder: | - 1. Configure codex_workdir = "/tmp/work" - 2. Start the bot - 3. Run /home - 4. Observe the reported workdir + 1. 配置 codex_workdir = "/tmp/work" + 2. 启动机器人 + 3. 执行 /home + 4. 观察返回的工作目录 validations: required: true - type: textarea id: expected attributes: - label: Expected Behavior + label: 预期行为 validations: required: true - type: textarea id: actual attributes: - label: Actual Behavior + label: 实际行为 validations: required: true - type: textarea id: evidence attributes: - label: Verification Evidence - description: Include failing tests, logs, screenshots, or command output summaries + label: 补充信息 + description: 可填写失败测试、日志、截图或命令输出摘要 - type: textarea id: affected attributes: - label: Affected Files Or Commands + label: 相关文件或命令 placeholder: | 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 index 1b2eefc..1e80377 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,6 +1,5 @@ blank_issues_enabled: false contact_links: - - name: Repository Plans + - name: 仓库规划文档 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. - + about: 在提交范围较大的请求前,请先查看现有设计文档和实现计划。 diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index b79d9c0..2e184c2 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -1,51 +1,50 @@ -name: Feature Request -description: Propose an improvement or new capability -title: "feat: " +name: 功能请求 +description: 提议一个改进或新能力 +title: "功能:" labels: - enhancement body: - type: markdown attributes: 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 + label: 功能概述 + placeholder: 为 /mode、/model、/effort、/permission 增加按钮面板 validations: required: true - type: textarea id: problem attributes: - label: Problem Statement - description: What is hard, missing, or error-prone today? + label: 问题背景 + description: 当前使用上有什么困难、缺失或容易出错的地方? validations: required: true - type: textarea id: proposal attributes: - label: Proposed Solution + label: 建议方案 validations: required: true - type: textarea id: alternatives attributes: - label: Alternatives Considered + label: 备选方案 - type: textarea id: scope attributes: - label: Scope And Constraints - description: Compatibility, migration, UX, or testing constraints that matter + label: 范围与约束 + description: 这里填写兼容性、迁移、交互或测试方面的约束 - type: textarea id: verification attributes: - label: Verification Plan - description: Tests, manual checks, or rollout validation you expect - + label: 验证计划 + description: 希望如何验证这个功能,例如测试、手工检查或上线确认 diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 74cd470..04bfa2d 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,21 +1,20 @@ -## Summary +## 变更摘要 - -## Linked Issue +## 关联 Issue Closes # -## What Changed +## 具体改动 - -## Verification +## 验证 - [ ] `pdm run pytest -q` - [ ] `pdm run ruff check .` -## Risks / Follow-Ups +## 风险 / 后续项 - - diff --git a/docs/plans/2026-03-12-model-cache-and-cn-templates-design.md b/docs/plans/2026-03-12-model-cache-and-cn-templates-design.md new file mode 100644 index 0000000..8ede9ae --- /dev/null +++ b/docs/plans/2026-03-12-model-cache-and-cn-templates-design.md @@ -0,0 +1,99 @@ +# Model Cache Fallback And Chinese Templates Design + +## Background + +The repository still has one confirmed runtime defect: if the configured model cache file is missing, `get_preferences()` currently tries to build default preferences via `load_models()`, which makes non-model commands fail early. At the same time, the repository's new GitHub issue templates and PR template are still written in English, while the project's user-facing language and maintainer preference are Chinese. + +## Goals + +- Fix the remaining bug where non-model commands fail when the model cache file is missing. +- Keep model-specific commands honest: they may still require model metadata, but generic commands should not. +- Localize the GitHub issue templates into Chinese. +- Localize the PR template into Chinese as well for consistency. + +## Non-Goals + +- Do not redesign the Telegram command flow again. +- Do not add external configuration discovery beyond the current Codex config and existing settings. +- Do not change issue labels or broader repo governance. + +## Root Cause + +`CodexBridgeService.get_preferences()` lazily creates default preferences through `_default_preferences()`. That method currently loads the model cache first, even though many call sites only need a summary of current settings or the current workdir. As a result, commands like `/pwd`, `/codex` without a prompt, and `/new` can fail with `FileNotFoundError` before the user reaches any model-specific operation. + +## Approach Options + +### Option 1: Catch FileNotFoundError in handlers + +Wrap more handlers in `FileNotFoundError` handling and keep the service logic unchanged. + +- Pros: very small code diff +- Cons: symptom fix only; the service still couples generic preference creation to model metadata + +### Option 2: Decouple default preferences from model cache + +Make `_default_preferences()` prefer `~/.codex/config.toml` values first, then safe fallback defaults, and only consult the model cache when available for normalization. + +- Pros: fixes the root cause at the service layer, keeps generic commands usable, preserves existing behavior when metadata exists +- Cons: needs careful tests so model-specific commands still behave intentionally + +### Option 3: Allow missing model in preferences + +Let preferences hold `model=None` and push the requirement down to execution-time paths only. + +- Pros: semantically explicit +- Cons: much wider type and formatting churn than necessary + +## Recommended Design + +Use Option 2. + +### Service Behavior + +- `_default_preferences()` should stop hard-requiring `load_models()`. +- It should: + - read `codex_config_path` first + - if the model cache is available, preserve the current normalization behavior + - if the model cache is missing or invalid, fall back to the configured model/effort from `config.toml` + - if even that is missing, use a small internal default such as `gpt-5` and `high` + +This keeps generic preference creation stable while avoiding a large `None`-propagation refactor. + +### Command Expectations + +- These should work even if the model cache file is absent: + - `/pwd` + - `/codex` without prompt + - `/new` +- These may still require model metadata and can continue to error clearly if it is missing: + - `/models` + - `/model` + - `/effort` + - model and effort selector panels + +### Template Localization + +Translate the current GitHub-facing templates to Chinese: + +- `.github/ISSUE_TEMPLATE/bug_report.yml` +- `.github/ISSUE_TEMPLATE/feature_request.yml` +- `.github/ISSUE_TEMPLATE/config.yml` +- `.github/pull_request_template.md` + +The structure should remain the same; only the repository-facing language and helper copy need localization. + +### Testing + +Add failing tests first for: + +- service default preferences without a model cache +- handler paths that should stay usable without a model cache +- presence of Chinese wording in the issue and PR templates + +## Risks And Mitigations + +- Risk: fallback defaults may diverge from an individual user's actual Codex defaults. + Mitigation: prefer `codex_config_path` values before any hardcoded fallback. + +- Risk: a broad fallback could accidentally hide model-metadata errors everywhere. + Mitigation: keep `load_models()`-dependent commands unchanged; only decouple generic preference creation. diff --git a/docs/plans/2026-03-12-model-cache-and-cn-templates.md b/docs/plans/2026-03-12-model-cache-and-cn-templates.md new file mode 100644 index 0000000..0e2a6c7 --- /dev/null +++ b/docs/plans/2026-03-12-model-cache-and-cn-templates.md @@ -0,0 +1,96 @@ +# Model Cache Fallback And Chinese Templates Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Fix the model-cache-dependent command failure path and localize the repository's GitHub issue and PR templates into Chinese. + +**Architecture:** Remove the root-cause dependency between generic preference creation and model metadata by making default preferences derive from Codex config plus safe fallback values. Keep model-specific commands dependent on explicit model metadata, and update the GitHub-facing templates in place without changing their structural purpose. + +**Tech Stack:** Python 3.10+, NoneBot2, nonebot-adapter-telegram, pytest, GitHub issue/PR templates + +--- + +### Task 1: Add Failing Tests For Model Cache Fallback + +**Files:** +- Modify: `tests/test_service.py` +- Modify: `tests/test_telegram_handlers.py` + +**Step 1: Write the failing tests** + +Add tests asserting: +- `get_preferences()` works without a model cache when `config.toml` provides model defaults +- `/pwd` works without a model cache +- `/codex` without a prompt works without a model cache + +**Step 2: Run the focused tests to verify failure** + +Run: `pdm run pytest tests/test_service.py tests/test_telegram_handlers.py -q` +Expected: FAIL on the new no-model-cache cases + +### Task 2: Implement The Root-Cause Fix + +**Files:** +- Modify: `src/nonebot_plugin_codex/service.py` + +**Step 1: Implement minimal fallback logic** + +Change default preference construction so it: +- prefers `codex_config_path` +- uses model cache normalization only when metadata is available +- falls back to internal defaults when both model cache and config are absent + +**Step 2: Re-run the focused tests** + +Run: `pdm run pytest tests/test_service.py tests/test_telegram_handlers.py -q` +Expected: PASS for the new fallback tests + +### Task 3: Add Failing Checks For Chinese Templates + +**Files:** +- Modify: `tests/test_plugin_meta.py` + +**Step 1: Add tests or file-content assertions for template wording** + +Assert the issue and PR templates contain Chinese labels/headings expected by the new repository standard. + +**Step 2: Run the focused tests to verify failure** + +Run: `pdm run pytest tests/test_plugin_meta.py -q` +Expected: FAIL because templates are still English + +### Task 4: Localize GitHub Templates + +**Files:** +- Modify: `.github/ISSUE_TEMPLATE/bug_report.yml` +- Modify: `.github/ISSUE_TEMPLATE/feature_request.yml` +- Modify: `.github/ISSUE_TEMPLATE/config.yml` +- Modify: `.github/pull_request_template.md` + +**Step 1: Translate the templates to Chinese** + +Keep field structure and intent, but localize headings, descriptions, placeholders, and helper text. + +**Step 2: Re-run the focused tests** + +Run: `pdm run pytest tests/test_plugin_meta.py -q` +Expected: PASS + +### Task 5: Full Verification + +**Files:** +- Modify: any touched files above as needed + +**Step 1: Run the full test suite** + +Run: `pdm run pytest -q` +Expected: all tests pass + +**Step 2: Run lint** + +Run: `pdm run ruff check .` +Expected: all checks pass + +**Step 3: Update GitHub artifacts if needed** + +If the template language change should be reflected in the existing issue/PR descriptions, update them after code verification. diff --git a/src/nonebot_plugin_codex/service.py b/src/nonebot_plugin_codex/service.py index 9399b60..cfc6bcf 100644 --- a/src/nonebot_plugin_codex/service.py +++ b/src/nonebot_plugin_codex/service.py @@ -27,6 +27,8 @@ VISIBLE_MODEL = "list" SUPPORTED_EFFORT_COMMANDS = {"high", "xhigh"} SUPPORTED_PERMISSION_MODES = {"safe", "danger"} +FALLBACK_MODEL = "gpt-5" +FALLBACK_REASONING_EFFORT = "high" BROWSER_CALLBACK_PREFIX = "cdb" BROWSER_PAGE_SIZE = 8 BROWSER_FILE_SUMMARY_LIMIT = 10 @@ -1727,12 +1729,18 @@ def _normalize_effort(self, model: ModelInfo, effort: str | None) -> str: return model.default_reasoning_level def _default_preferences(self) -> ChatPreferences: - models = self.load_models() - model = self._pick_default_model(models) - _, configured_effort = self._load_codex_defaults() - effort = self._normalize_effort(model, configured_effort) + configured_model, configured_effort = self._load_codex_defaults() + try: + models = self.load_models() + except (FileNotFoundError, ValueError): + model_slug = configured_model or FALLBACK_MODEL + effort = configured_effort or FALLBACK_REASONING_EFFORT + else: + model = self._pick_default_model(models) + effort = self._normalize_effort(model, configured_effort) + model_slug = model.slug return ChatPreferences( - model=model.slug, + model=model_slug, reasoning_effort=effort, permission_mode="safe", workdir=self._configured_workdir(), diff --git a/tests/test_plugin_meta.py b/tests/test_plugin_meta.py index 498a3ad..81a4077 100644 --- a/tests/test_plugin_meta.py +++ b/tests/test_plugin_meta.py @@ -1,8 +1,32 @@ from __future__ import annotations +from pathlib import Path + from nonebot_plugin_codex import __plugin_meta__ def test_plugin_metadata_uses_string_adapter_names() -> None: assert __plugin_meta__.homepage == "https://github.com/ttiee/nonebot-plugin-codex" assert __plugin_meta__.supported_adapters == {"~telegram"} + + +def test_issue_templates_are_localized_in_chinese() -> None: + repo_root = Path(__file__).resolve().parents[1] + bug_template_path = repo_root / ".github" / "ISSUE_TEMPLATE" / "bug_report.yml" + bug_template = bug_template_path.read_text(encoding="utf-8") + feature_template = ( + repo_root / ".github" / "ISSUE_TEMPLATE" / "feature_request.yml" + ).read_text(encoding="utf-8") + + assert "问题反馈" in bug_template + assert "功能请求" in feature_template + + +def test_pull_request_template_is_localized_in_chinese() -> None: + repo_root = Path(__file__).resolve().parents[1] + template = (repo_root / ".github" / "pull_request_template.md").read_text( + encoding="utf-8" + ) + + assert "变更摘要" in template + assert "验证" in template diff --git a/tests/test_service.py b/tests/test_service.py index b6e8af4..a4c906b 100644 --- a/tests/test_service.py +++ b/tests/test_service.py @@ -52,6 +52,24 @@ def make_service( ) +def make_service_without_model_cache(tmp_path: Path) -> CodexBridgeService: + codex_config = tmp_path / "config.toml" + codex_config.write_text('model = "gpt-5"\nmodel_reasoning_effort = "xhigh"\n') + return CodexBridgeService( + CodexBridgeSettings( + binary="codex", + workdir=str(tmp_path), + models_cache_path=tmp_path / "missing-models.json", + codex_config_path=codex_config, + preferences_path=tmp_path / "data" / "codex_bridge" / "preferences.json", + session_index_path=tmp_path / ".codex" / "session_index.jsonl", + sessions_dir=tmp_path / ".codex" / "sessions", + archived_sessions_dir=tmp_path / ".codex" / "archived_sessions", + ), + which_resolver=lambda _: "/usr/bin/codex", + ) + + def test_build_exec_argv_for_safe_and_resume_mode() -> None: argv = build_exec_argv( "codex", @@ -79,6 +97,18 @@ def test_default_preferences_use_configured_workdir( assert preferences.workdir == str(tmp_path.resolve()) +def test_default_preferences_use_codex_config_when_model_cache_is_missing( + tmp_path: Path, +) -> None: + service = make_service_without_model_cache(tmp_path) + + preferences = service.get_preferences("private_1") + + assert preferences.model == "gpt-5" + assert preferences.reasoning_effort == "xhigh" + assert preferences.workdir == str(tmp_path.resolve()) + + def test_directory_browser_home_uses_configured_workdir( tmp_path: Path, model_cache_file: Path ) -> None: diff --git a/tests/test_telegram_handlers.py b/tests/test_telegram_handlers.py index 0e180d4..163c647 100644 --- a/tests/test_telegram_handlers.py +++ b/tests/test_telegram_handlers.py @@ -1,5 +1,6 @@ from __future__ import annotations +from pathlib import Path from typing import Any from types import SimpleNamespace from dataclasses import field, dataclass @@ -7,7 +8,12 @@ import pytest from nonebot_plugin_codex.telegram import TelegramHandlers -from nonebot_plugin_codex.service import ChatSession, encode_browser_callback +from nonebot_plugin_codex.service import ( + ChatSession, + CodexBridgeService, + CodexBridgeSettings, + encode_browser_callback, +) @dataclass @@ -234,6 +240,24 @@ async def update_default_mode(self, chat_key: str, mode: str) -> str: return f"当前默认模式:{mode}" +def make_real_service_without_model_cache(tmp_path: Path) -> CodexBridgeService: + codex_config = tmp_path / "config.toml" + codex_config.write_text('model = "gpt-5"\nmodel_reasoning_effort = "xhigh"\n') + return CodexBridgeService( + CodexBridgeSettings( + binary="codex", + workdir=str(tmp_path), + models_cache_path=tmp_path / "missing-models.json", + codex_config_path=codex_config, + preferences_path=tmp_path / "data" / "codex_bridge" / "preferences.json", + session_index_path=tmp_path / ".codex" / "session_index.jsonl", + sessions_dir=tmp_path / ".codex" / "sessions", + archived_sessions_dir=tmp_path / ".codex" / "archived_sessions", + ), + which_resolver=lambda _: "/usr/bin/codex", + ) + + @pytest.mark.asyncio async def test_handle_codex_without_prompt_sends_status_message() -> None: service = FakeService() @@ -363,3 +387,29 @@ async def test_handle_setting_callback_updates_setting() -> None: assert service.setting_updates == ["danger"] assert bot.answered[0]["text"] == "已更新。" + + +@pytest.mark.asyncio +async def test_handle_pwd_works_when_model_cache_is_missing(tmp_path: Path) -> None: + service = make_real_service_without_model_cache(tmp_path) + handlers = TelegramHandlers(service) + bot = FakeBot() + + await handlers.handle_pwd(bot, FakeEvent("")) + + assert "当前工作目录" in bot.sent[0]["text"] + assert "模型: gpt-5" in bot.sent[0]["text"] + + +@pytest.mark.asyncio +async def test_handle_codex_without_prompt_works_when_model_cache_is_missing( + tmp_path: Path, +) -> None: + service = make_real_service_without_model_cache(tmp_path) + handlers = TelegramHandlers(service) + bot = FakeBot() + + await handlers.handle_codex(bot, FakeEvent(""), FakeMessage("")) + + assert "Codex 已连接" in bot.sent[0]["text"] + assert "模型: gpt-5" in bot.sent[0]["text"]