From 321dc57e30ae5d76f466a55cbda3e44edf0ce4ea Mon Sep 17 00:00:00 2001 From: Zhengshou Lai Date: Mon, 6 Jul 2026 21:11:23 +0800 Subject: [PATCH] =?UTF-8?q?refactor(config):=20=E7=BB=9F=E4=B8=80=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E5=AD=98=E5=82=A8=E4=B8=BA=20~/.mytoolkit/config.json?= =?UTF-8?q?=20(keys.*)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Config 类改用 config.json + keys.* 格式,废弃 env.json (自动迁移) - 新增 resolve_key() / write_key_to_mytoolkit() 共享工具函数 - env CLI 文本更新 (vars → keys) - 更新 CLAUDE.md / README.md 配置描述 --- CLAUDE.md | 29 ++++-- README.md | 4 +- mytoolkit/commands/env.py | 10 +- mytoolkit/config.py | 189 ++++++++++++++++++++++++++++---------- 4 files changed, 164 insertions(+), 68 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 5deaee3..beb9ee4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -43,16 +43,25 @@ skill 或其他脚本应通过上述命令定位资源,避免硬编码 `~/work --- -## 2. 语音合成 (TTS) +## 2. 配置管理 + +通过 `mytoolkit env` 管理,存储于 `~/.mytoolkit/config.json`(`keys.*` 格式)。 + +```bash +mytoolkit env set apikey_ark +mytoolkit env set volc_appid +mytoolkit env set volc_access_token +mytoolkit env list # 查看所有 key +mytoolkit env export # 导出为 shell export 语句 +``` + +API key 读取顺序(所有 skill 脚本统一):环境变量 → `~/.mytoolkit/config.json`。共享调用为 `mytoolkit.config.resolve_key()`。 + +## 3. 语音合成 (TTS) 通过火山引擎(豆包)WebSocket API 实现,命令为 `mytoolkit voice tts`。 -### 配置 - -```bash -mytoolkit env set volc_appid -mytoolkit env set volc_access_token -``` +配置方式见上方 §2 配置管理。 ### 用法 @@ -70,7 +79,7 @@ mytoolkit voice tts "文本" -v zh_male_wennuanahu_moon_bigtts --format mp3 --sp --- -## 3. LaTeX 论文脚手架与投稿模板 +## 4. LaTeX 论文脚手架与投稿模板 ### 新建论文项目 @@ -108,7 +117,7 @@ mytoolkit init journal springer mydir --- -## 4. 依赖管理 +## 5. 依赖管理 项目使用 `uv` 管理依赖。修改 `pyproject.toml` 后运行 `uv sync` 同步。 @@ -118,7 +127,7 @@ uv sync --- -## 5. 命名规范 +## 6. 命名规范 - **文件与文件夹**:英文统一使用 kebab-case,如 `review-comments.docx` - **代码中的函数与变量**:统一使用 snake_case,如 `review_comments()` diff --git a/README.md b/README.md index aa90a14..34ca210 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ All user-level config lives under `~/.mytoolkit/` (override with `$MYTOOLKIT_HOM | File | Owner | Purpose | |------|-------|---------| -| `env.json` | `mytoolkit env` | API keys, paths, feishu credentials (flat key/value) | +| `config.json` | `mytoolkit env` | API keys, paths, feishu credentials (keys.* namespace) | | `mail.json` | `mytoolkit mail` | IMAP/SMTP server settings | | `templates.json` | `mytoolkit templates` | Pointer to external template root | @@ -131,7 +131,7 @@ mytoolkit env export # print MYCLI_FOO=... export statements eval "$(mytoolkit env export)" # load into current shell ``` -Legacy `bin/config.json` (in the source dir) is auto-migrated to `~/.mytoolkit/env.json` on first run after upgrade. +Legacy `~/.mytoolkit/env.json` is auto-migrated into `~/.mytoolkit/config.json` (keys.* format) on first load after upgrade. ## Uninstall diff --git a/mytoolkit/commands/env.py b/mytoolkit/commands/env.py index 4905634..069a906 100644 --- a/mytoolkit/commands/env.py +++ b/mytoolkit/commands/env.py @@ -13,14 +13,14 @@ def env_cmd(): @env_cmd.command("list") def env_list(): - """List all vars.""" + """List all keys.""" click.echo(f"Storage: {CONFIG_PATH}") - vars = config.get_all() - if not vars: - click.echo("No vars configured.") + keys = config.get_all() + if not keys: + click.echo("No keys configured.") return click.echo() - for name, value in sorted(vars.items()): + for name, value in sorted(keys.items()): click.echo(f"{name:20} = {value}") diff --git a/mytoolkit/config.py b/mytoolkit/config.py index 98347ac..4176d49 100644 --- a/mytoolkit/config.py +++ b/mytoolkit/config.py @@ -1,94 +1,181 @@ -"""Configuration management for bin.""" +"""Configuration management for mytoolkit. + +Unified config file: ~/.mytoolkit/config.json (keys.* namespace) +Legacy: ~/.mytoolkit/env.json (migrated on first load) +""" import json import os from pathlib import Path _MYTOOLKIT_HOME = Path(os.environ.get("MYTOOLKIT_HOME", Path.home() / ".mytoolkit")) -CONFIG_PATH = _MYTOOLKIT_HOME / "env.json" -_LEGACY_PATH = Path(__file__).parent / "config.json" +CONFIG_PATH = _MYTOOLKIT_HOME / "config.json" +_LEGACY_MODULE_PATH = Path(__file__).parent / "config.json" +_LEGACY_ENV_PATH = _MYTOOLKIT_HOME / "env.json" class Config: - """Simple config manager for environment variables.""" + """Simple config manager for mytoolkit. + + Stores in ~/.mytoolkit/config.json under the "keys" key:: + + {"keys": {"apikey_ark": "...", "volc_appid": "...", "path_study": "...", ...}} + """ def __init__(self): self._config_path = CONFIG_PATH self._migrate_legacy() self._data = self._load() + # ------------------------------------------------------------------ + # Migration + # ------------------------------------------------------------------ + def _migrate_legacy(self) -> None: - """One-shot move of legacy config files into env.json.""" + """One-shot: merge legacy env.json into config.json (keys.* format).""" + self._config_path.parent.mkdir(parents=True, exist_ok=True) + + # Load any existing config.json + existing: dict = {} if self._config_path.exists(): - return - - migrated = False - - # 1) Legacy bundled config.json next to this module. - if _LEGACY_PATH.exists(): - self._config_path.parent.mkdir(parents=True, exist_ok=True) - _LEGACY_PATH.rename(self._config_path) - migrated = True - - # 2) Old ~/.mytoolkit/config.json (sectioned layout) -> flat env.json vars. - legacy_user = self._config_path.parent / "config.json" - if not migrated and legacy_user.exists(): try: - with open(legacy_user, encoding="utf-8") as f: - data = json.load(f) + existing = json.loads(self._config_path.read_text()) except (json.JSONDecodeError, OSError): - data = {} - flat_vars: dict[str, str] = {} - for section in ("paths", "keys", "tokens", "other"): - section_data = data.get(section) - if isinstance(section_data, dict): - flat_vars.update(section_data) - if flat_vars: - self._data = {"vars": flat_vars} - self.save() - legacy_user.unlink() - migrated = True + existing = {} + + keys = existing.get("keys", {}) + + # 1) Legacy ~/.mytoolkit/env.json (vars.* → keys.*) + migrated_env = False + if _LEGACY_ENV_PATH.exists(): + try: + legacy = json.loads(_LEGACY_ENV_PATH.read_text()) + legacy_vars = legacy.get("vars", {}) + if legacy_vars: + keys.update(legacy_vars) + migrated_env = True + except (json.JSONDecodeError, OSError): + pass + + if migrated_env: + existing["keys"] = dict(sorted(keys.items())) + self._config_path.write_text( + json.dumps(existing, indent=2, ensure_ascii=False) + "\n" + ) + # Rename env.json out of the way so we don't re-migrate + backup = _LEGACY_ENV_PATH.with_name("env.json.migrated") + if not backup.exists(): + _LEGACY_ENV_PATH.rename(backup) + + # 2) Very old bundled config.json next to this module (early mytoolkit) + if not self._config_path.exists() and _LEGACY_MODULE_PATH.exists(): + try: + _LEGACY_MODULE_PATH.rename(self._config_path) + except OSError: + pass + + # ------------------------------------------------------------------ + # Load / Save + # ------------------------------------------------------------------ def _load(self) -> dict: if self._config_path.exists(): - with open(self._config_path, encoding="utf-8") as f: - return json.load(f) - return {"vars": {}} + try: + return json.loads(self._config_path.read_text()) + except (json.JSONDecodeError, OSError): + pass + return {"keys": {}} def save(self): - """Save config to file.""" self._config_path.parent.mkdir(parents=True, exist_ok=True) - with open(self._config_path, "w", encoding="utf-8") as f: - json.dump(self._data, f, indent=2, ensure_ascii=False) - f.write("\n") + self._config_path.write_text( + json.dumps(self._data, indent=2, ensure_ascii=False) + "\n" + ) + + # ------------------------------------------------------------------ + # Accessors + # ------------------------------------------------------------------ def get(self, name: str) -> str | None: - """Get var by name.""" - return self._data.get("vars", {}).get(name) + """Get a key by name.""" + return self._data.get("keys", {}).get(name) def get_all(self) -> dict[str, str]: - """Get all vars.""" - return self._data.get("vars", {}).copy() + """Return all keys as a flat dict.""" + return self._data.get("keys", {}).copy() def set(self, name: str, value: str): - """Set a var.""" - if "vars" not in self._data: - self._data["vars"] = {} - self._data["vars"][name] = value + """Set a key and persist.""" + if "keys" not in self._data: + self._data["keys"] = {} + self._data["keys"][name] = value self.save() def remove(self, name: str) -> bool: - """Remove a var. Returns True if existed.""" - if name in self._data.get("vars", {}): - del self._data["vars"][name] + """Remove a key. Returns True if existed.""" + if name in self._data.get("keys", {}): + del self._data["keys"][name] self.save() return True return False def export(self) -> dict[str, str]: - """Get all vars with env-compatible names.""" + """Return all keys with MYCLI_ prefix for shell eval.""" return {f"MYCLI_{k.upper()}": v for k, v in self.get_all().items()} -# Global instance +# Global singleton config = Config() + + +# ------------------------------------------------------------------ +# Shared utility: 2-layer key resolution for skill scripts +# ------------------------------------------------------------------ + +def resolve_key(name: str) -> str: + """Two-layer key resolution. + + Priority: + 1. Environment variable (checks ``MYCLI_`` first, then ````) + 2. ``~/.mytoolkit/config.json`` → ``keys.`` + + Returns the key value, or ``""`` if not found. + """ + # 1. Env var (MYCLI_ prefix first, then bare) + env_name = name.upper() + for var in (f"MYCLI_{env_name}", env_name): + val = os.environ.get(var, "") + if val: + return val + + # 2. ~/.mytoolkit/config.json + try: + cfg_path = Path.home() / ".mytoolkit" / "config.json" + if cfg_path.exists(): + data = json.loads(cfg_path.read_text()) + return data.get("keys", {}).get(name, "") + except (json.JSONDecodeError, OSError): + pass + + return "" + + +def write_key_to_mytoolkit(name: str, value: str) -> None: + """Write a key into ~/.mytoolkit/config.json (persistent). + + Called by skills when a key is missing and the user provides it. + """ + cfg_path = Path.home() / ".mytoolkit" / "config.json" + cfg_path.parent.mkdir(parents=True, exist_ok=True) + try: + if cfg_path.exists(): + data = json.loads(cfg_path.read_text()) + else: + data = {} + if "keys" not in data: + data["keys"] = {} + data["keys"][name] = value + cfg_path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n") + except (json.JSONDecodeError, OSError): + data = {"keys": {name: value}} + cfg_path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n")